Skip to content

Commit 9840ef0

Browse files
fix: formatting remove territory for unified formatting (#26623)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** We are removing the locale territory (e.g. 'en-US', 'en-GB') so we can have a unified currency experience. Will need to sync with the ramps team who are using these shared utils. I've also added a small test harness to prevent this regression in the future. ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: fix: formatting remove territory for unified formatting ## **Related issues** Fixes: #26428 https://consensyssoftware.atlassian.net/browse/ASSETS-2767 ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <img width="441" height="950" alt="Screenshot 2026-02-26 at 10 53 56" src="https://github.com/user-attachments/assets/6229d6f2-f018-4e3f-95cd-2789d85904ae" /> ### **After** <img width="811" height="940" alt="Screenshot 2026-02-26 at 12 08 56" src="https://github.com/user-attachments/assets/78dc2765-4ebf-48d0-853b-eab998ecf5e2" /> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes locale handling for all callers of `useFormatters`, which can affect currency/number formatting across the app; impact is broad but the logic change is small and covered by a new regression test. > > **Overview** > Forces `useFormatters` to *drop the locale territory* (e.g., `en-US` -> `en`) before creating formatters, to unify currency formatting output. > > Adds a hook test that mocks `I18n.locale` across several English locales and asserts `formatCurrency` for USD uses the `$` prefix (and never `US$`). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6e0cf303a8611a738fda26268abd2ef317a75e2e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent a801664 commit 9840ef0

4 files changed

Lines changed: 105 additions & 5 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { renderHook } from '@testing-library/react-hooks';
2+
import { useFormatters } from './useFormatters';
3+
import I18n from '../../../locales/i18n';
4+
5+
jest.mock('../../../locales/i18n', () => ({
6+
__esModule: true,
7+
default: { locale: 'en' },
8+
}));
9+
10+
const mockI18n = jest.mocked(I18n);
11+
12+
describe('useFormatters', () => {
13+
beforeEach(() => {
14+
mockI18n.locale = 'en';
15+
});
16+
17+
describe('formatCurrency', () => {
18+
const enLocales = ['en', 'en-US', 'en-GB', 'en-CA', 'en-AU', 'en-NZ'];
19+
const tests = [
20+
{ value: 100, currency: 'USD', expected: '$100.00' },
21+
{ value: 0.5, currency: 'USD', expected: '$0.50' },
22+
{ value: 1234.56, currency: 'USD', expected: '$1,234.56' },
23+
];
24+
const localeTestCases = enLocales.flatMap((locale) =>
25+
tests.map((testCase) => ({ ...testCase, locale })),
26+
);
27+
28+
it.each(localeTestCases)(
29+
'returns $ prefix for $value $currency, never US$ (using locale $locale)',
30+
({ value, currency, expected, locale }) => {
31+
mockI18n.locale = locale;
32+
const { result } = renderHook(() => useFormatters());
33+
34+
const formatted = result.current.formatCurrency(value, currency);
35+
36+
expect(formatted).toBe(expected);
37+
expect(formatted).not.toContain('US$');
38+
},
39+
);
40+
});
41+
});

app/components/hooks/useFormatters.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { useMemo } from 'react';
22
import { createFormatters } from '@metamask/assets-controllers';
33
import I18n from '../../../locales/i18n';
44

5+
export const getLocaleLanguageCode = () => I18n.locale.split('-')[0];
6+
57
export function useFormatters() {
6-
const locale = I18n.locale;
8+
const locale = getLocaleLanguageCode();
79
return useMemo(() => createFormatters({ locale }), [locale]);
810
}

app/selectors/assets/assets-list.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ import {
1616
selectSortedAssetsBySelectedAccountGroup,
1717
selectTronResourcesBySelectedAccountGroup,
1818
} from './assets-list';
19+
import I18n from '../../../locales/i18n';
20+
21+
jest.mock('../../../locales/i18n', () => ({
22+
__esModule: true,
23+
default: { locale: 'en' },
24+
}));
25+
const mockI18n = jest.mocked(I18n);
1926

2027
const mockState = ({
2128
filterNetwork,
@@ -683,6 +690,10 @@ describe('selectSortedAssetsBySelectedAccountGroup', () => {
683690
});
684691

685692
describe('selectAsset', () => {
693+
beforeEach(() => {
694+
mockI18n.locale = 'en';
695+
});
696+
686697
it('returns formatted evm native asset based on filter criteria', () => {
687698
const state = mockState();
688699
const result = selectAsset(state, {
@@ -974,6 +985,46 @@ describe('selectAsset', () => {
974985
expect(result?.isStaked).toBe(false);
975986
expect(result?.isStaked).not.toBeUndefined();
976987
});
988+
989+
describe('balanceFiat locale formatting', () => {
990+
beforeEach(() => {
991+
const actualNumberFormat = Intl.NumberFormat;
992+
jest.spyOn(Intl, 'NumberFormat').mockImplementation((locale, options) => {
993+
// Mobile (Android & IOS do not support currencyDisplay and are unable to use 'narrowSymbol')
994+
// We can remove these mocks once this becomes supported.
995+
delete options?.currencyDisplay;
996+
return actualNumberFormat(locale, options);
997+
});
998+
});
999+
1000+
afterEach(() => {
1001+
jest.restoreAllMocks();
1002+
});
1003+
1004+
const enLocaleTestCases = [
1005+
'en',
1006+
'en-US',
1007+
'en-GB',
1008+
'en-CA',
1009+
'en-AU',
1010+
'en-NZ',
1011+
];
1012+
it.each(enLocaleTestCases.map((locale) => [locale]))(
1013+
'returns $24,000.00, never US$ (locale: %s)',
1014+
(locale) => {
1015+
mockI18n.locale = locale;
1016+
const state = mockState();
1017+
const result = selectAsset(state, {
1018+
address: '0x0000000000000000000000000000000000000000',
1019+
chainId: '0x1',
1020+
isStaked: false,
1021+
});
1022+
1023+
expect(result?.balanceFiat).toBe('$24,000.00');
1024+
expect(result?.balanceFiat).not.toContain('US$');
1025+
},
1026+
);
1027+
});
9771028
});
9781029

9791030
describe('selectTronResourcesBySelectedAccountGroup', () => {

app/selectors/assets/assets-list.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { sortAssetsWithPriority } from '../../components/UI/Tokens/util/sortAsse
3535
import { selectAllTokens } from '../tokensController';
3636
import { selectSelectedInternalAccountAddress } from '../accountsController';
3737
import { selectSelectedInternalAccountByScope } from '../multichainAccounts/accounts';
38+
import { getLocaleLanguageCode } from '../../components/hooks/useFormatters';
3839

3940
/**
4041
* Structured map of Tron resources for efficient access.
@@ -396,10 +397,15 @@ function assetToToken(
396397
},
397398
),
398399
balanceFiat: asset.fiat
399-
? formatWithThreshold(asset.fiat.balance, oneHundredths, I18n.locale, {
400-
style: 'currency',
401-
currency: asset.fiat.currency,
402-
})
400+
? formatWithThreshold(
401+
asset.fiat.balance,
402+
oneHundredths,
403+
getLocaleLanguageCode(),
404+
{
405+
style: 'currency',
406+
currency: asset.fiat.currency,
407+
},
408+
)
403409
: undefined,
404410
logo:
405411
asset.accountType.startsWith('eip155') && asset.isNative

0 commit comments

Comments
 (0)