Skip to content

Commit b247f27

Browse files
Support translation in background code (#19650)
1 parent 3bbfe87 commit b247f27

File tree

20 files changed

+872
-361
lines changed

20 files changed

+872
-361
lines changed

.storybook/i18n.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { Component, createContext, useMemo } from 'react';
22
import PropTypes from 'prop-types';
3-
import { getMessage } from '../ui/helpers/utils/i18n-helper';
3+
import { getMessage } from '../shared/modules/i18n';
44
import { I18nContext } from '../ui/contexts/i18n';
55

66
export { I18nContext };

app/_locales/en/messages.json

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/scripts/metamask-controller.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ import {
205205
} from './controllers/permissions';
206206
import createRPCMethodTrackingMiddleware from './lib/createRPCMethodTrackingMiddleware';
207207
import { securityProviderCheck } from './lib/security-provider-helpers';
208+
import { updateCurrentLocale } from './translate';
208209

209210
export const METAMASK_CONTROLLER_EVENTS = {
210211
// Fired after state changes that impact the extension badge (unapproved msg count)
@@ -360,6 +361,10 @@ export default class MetamaskController extends EventEmitter {
360361
///: END:ONLY_INCLUDE_IN
361362
});
362363

364+
this.preferencesController.store.subscribe(async ({ currentLocale }) => {
365+
await updateCurrentLocale(currentLocale);
366+
});
367+
363368
this.tokensController = new TokensController({
364369
chainId: this.networkController.store.getState().providerConfig.chainId,
365370
onPreferencesStateChange: this.preferencesController.store.subscribe.bind(

app/scripts/platforms/extension.js

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getEnvironmentType } from '../lib/util';
66
import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app';
77
import { TransactionStatus } from '../../../shared/constants/transaction';
88
import { getURLHostName } from '../../../ui/helpers/utils/util';
9+
import { t } from '../translate';
910

1011
export default class ExtensionPlatform {
1112
//
@@ -181,22 +182,30 @@ export default class ExtensionPlatform {
181182
toLower(getURLHostName(url).replace(/([.]\w+)$/u, '')),
182183
);
183184

184-
const title = 'Confirmed transaction';
185-
const message = `Transaction ${nonce} confirmed! ${
186-
url.length ? `View on ${view}` : ''
187-
}`;
185+
const title = t('notificationTransactionSuccessTitle');
186+
let message = t('notificationTransactionSuccessMessage', nonce);
187+
188+
if (url.length) {
189+
message += ` ${t('notificationTransactionSuccessView', view)}`;
190+
}
191+
188192
await this._showNotification(title, message, url);
189193
}
190194

191195
async _showFailedTransaction(txMeta, errorMessage) {
192196
const nonce = parseInt(txMeta.txParams.nonce, 16);
193-
const title = 'Failed transaction';
194-
let message = `Transaction ${nonce} failed! ${
195-
errorMessage || txMeta.err.message
196-
}`;
197+
const title = t('notificationTransactionFailedTitle');
198+
let message = t(
199+
'notificationTransactionFailedMessage',
200+
nonce,
201+
errorMessage || txMeta.err.message,
202+
);
197203
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
198204
if (isNaN(nonce)) {
199-
message = `Transaction failed! ${errorMessage || txMeta.err.message}`;
205+
message = t(
206+
'notificationTransactionFailedMessageMMI',
207+
errorMessage || txMeta.err.message,
208+
);
200209
}
201210
///: END:ONLY_INCLUDE_IN
202211
await this._showNotification(title, message);

app/scripts/translate.test.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import {
2+
getMessage,
3+
fetchLocale,
4+
FALLBACK_LOCALE,
5+
} from '../../shared/modules/i18n';
6+
import { t, updateCurrentLocale } from './translate';
7+
8+
const localeCodeMock = 'te';
9+
const keyMock = 'testKey';
10+
const substitutionsMock = ['a1', 'b2'];
11+
const messageMock = 'testMessage';
12+
const messageMock2 = 'testMessage2';
13+
const alternateLocaleDataMock = { [keyMock]: { message: messageMock2 } };
14+
15+
jest.mock('../../shared/modules/i18n');
16+
jest.mock('../_locales/en/messages.json', () => ({
17+
[keyMock]: { message: messageMock },
18+
}));
19+
20+
describe('Translate', () => {
21+
const getMessageMock = getMessage as jest.MockedFunction<typeof getMessage>;
22+
const fetchLocaleMock = fetchLocale as jest.MockedFunction<
23+
typeof fetchLocale
24+
>;
25+
26+
beforeEach(async () => {
27+
jest.resetAllMocks();
28+
await updateCurrentLocale(FALLBACK_LOCALE);
29+
});
30+
31+
describe('updateCurrentLocale', () => {
32+
it('retrieves locale data from shared module', async () => {
33+
await updateCurrentLocale(localeCodeMock);
34+
35+
expect(fetchLocale).toHaveBeenCalledTimes(1);
36+
expect(fetchLocale).toHaveBeenCalledWith(localeCodeMock);
37+
});
38+
39+
it('does not retrieve locale data if same locale already set', async () => {
40+
await updateCurrentLocale(localeCodeMock);
41+
await updateCurrentLocale(localeCodeMock);
42+
43+
expect(fetchLocale).toHaveBeenCalledTimes(1);
44+
expect(fetchLocale).toHaveBeenCalledWith(localeCodeMock);
45+
});
46+
47+
it('does not retrieve locale data if fallback locale set', async () => {
48+
await updateCurrentLocale(localeCodeMock);
49+
await updateCurrentLocale(FALLBACK_LOCALE);
50+
51+
expect(fetchLocale).toHaveBeenCalledTimes(1);
52+
expect(fetchLocale).toHaveBeenCalledWith(localeCodeMock);
53+
});
54+
});
55+
56+
describe('t', () => {
57+
it('returns value from shared module', () => {
58+
getMessageMock.mockReturnValue(messageMock);
59+
60+
expect(t(keyMock, ...substitutionsMock)).toStrictEqual(messageMock);
61+
});
62+
63+
it('uses en locale by default', () => {
64+
getMessageMock.mockReturnValue(messageMock);
65+
66+
t(keyMock, ...substitutionsMock);
67+
68+
expect(getMessage).toHaveBeenCalledTimes(1);
69+
expect(getMessage).toHaveBeenCalledWith(
70+
FALLBACK_LOCALE,
71+
{ [keyMock]: { message: messageMock } },
72+
keyMock,
73+
substitutionsMock,
74+
);
75+
});
76+
77+
it('uses locale passed to updateCurrentLocale if called', async () => {
78+
(getMessage as jest.MockedFunction<typeof getMessage>).mockReturnValue(
79+
messageMock,
80+
);
81+
82+
fetchLocaleMock.mockResolvedValueOnce(alternateLocaleDataMock);
83+
await updateCurrentLocale(localeCodeMock);
84+
85+
t(keyMock, ...substitutionsMock);
86+
87+
expect(getMessage).toHaveBeenCalledTimes(1);
88+
expect(getMessage).toHaveBeenCalledWith(
89+
localeCodeMock,
90+
alternateLocaleDataMock,
91+
keyMock,
92+
substitutionsMock,
93+
);
94+
});
95+
96+
it('returns value from en locale as fallback if current locale returns null', async () => {
97+
(
98+
getMessage as jest.MockedFunction<typeof getMessage>
99+
).mockReturnValueOnce(null);
100+
101+
(
102+
getMessage as jest.MockedFunction<typeof getMessage>
103+
).mockReturnValueOnce(messageMock2);
104+
105+
fetchLocaleMock.mockResolvedValueOnce(alternateLocaleDataMock);
106+
await updateCurrentLocale(localeCodeMock);
107+
108+
expect(t(keyMock, ...substitutionsMock)).toStrictEqual(messageMock2);
109+
110+
expect(getMessage).toHaveBeenCalledTimes(2);
111+
expect(getMessage).toHaveBeenCalledWith(
112+
FALLBACK_LOCALE,
113+
{ [keyMock]: { message: messageMock } },
114+
keyMock,
115+
substitutionsMock,
116+
);
117+
expect(getMessage).toHaveBeenCalledWith(
118+
localeCodeMock,
119+
alternateLocaleDataMock,
120+
keyMock,
121+
substitutionsMock,
122+
);
123+
});
124+
});
125+
});

app/scripts/translate.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import enTranslations from '../_locales/en/messages.json';
2+
import {
3+
FALLBACK_LOCALE,
4+
I18NMessageDict,
5+
fetchLocale,
6+
getMessage,
7+
} from '../../shared/modules/i18n';
8+
9+
let currentLocale: string = FALLBACK_LOCALE;
10+
let translations: I18NMessageDict = enTranslations;
11+
12+
export async function updateCurrentLocale(locale: string): Promise<void> {
13+
if (currentLocale === locale) {
14+
return;
15+
}
16+
17+
if (locale === FALLBACK_LOCALE) {
18+
translations = enTranslations;
19+
} else {
20+
translations = await fetchLocale(locale);
21+
}
22+
23+
currentLocale = locale;
24+
}
25+
26+
export function t(key: string, ...substitutions: string[]): string | null {
27+
return (
28+
getMessage(currentLocale, translations, key, substitutions) ||
29+
getMessage(FALLBACK_LOCALE, enTranslations, key, substitutions)
30+
);
31+
}

development/verify-locale-strings.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ async function verifyEnglishLocale() {
187187
'shared/**/*.ts',
188188
'app/scripts/constants/**/*.js',
189189
'app/scripts/constants/**/*.ts',
190+
'app/scripts/platforms/**/*.js',
190191
],
191192
{
192193
ignore: [...globsToStrictSearch, testGlob],

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ module.exports = {
5151
'<rootDir>/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js',
5252
'<rootDir>/app/scripts/migrations/*.test.(js|ts)',
5353
'<rootDir>/app/scripts/platforms/*.test.js',
54+
'<rootDir>/app/scripts/translate.test.ts',
5455
'<rootDir>/shared/**/*.test.(js|ts)',
5556
'<rootDir>/ui/**/*.test.(js|ts|tsx)',
5657
'<rootDir>/development/fitness-functions/**/*.test.(js|ts|tsx)',

shared/lib/error-utils.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ import browser from 'webextension-polyfill';
33
///: END:ONLY_INCLUDE_IN
44
import { memoize } from 'lodash';
55
import getFirstPreferredLangCode from '../../app/scripts/lib/get-first-preferred-lang-code';
6-
import {
7-
fetchLocale,
8-
loadRelativeTimeFormatLocaleData,
9-
} from '../../ui/helpers/utils/i18n-helper';
6+
import { fetchLocale, loadRelativeTimeFormatLocaleData } from '../modules/i18n';
107
///: BEGIN:ONLY_INCLUDE_IN(desktop)
118
import { renderDesktopError } from '../../ui/pages/desktop-error/render-desktop-error';
129
import { EXTENSION_ERROR_PAGE_TYPES } from '../constants/desktop';

shared/lib/error-utils.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import browser from 'webextension-polyfill';
2-
import { fetchLocale } from '../../ui/helpers/utils/i18n-helper';
2+
import { fetchLocale } from '../modules/i18n';
33
import { SUPPORT_LINK } from './ui-utils';
44
import {
55
downloadDesktopApp,
@@ -12,7 +12,7 @@ import {
1212
} from './error-utils';
1313
import { openCustomProtocol } from './deep-linking';
1414

15-
jest.mock('../../ui/helpers/utils/i18n-helper', () => ({
15+
jest.mock('../modules/i18n', () => ({
1616
fetchLocale: jest.fn(),
1717
loadRelativeTimeFormatLocaleData: jest.fn(),
1818
}));

0 commit comments

Comments
 (0)