mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-01 13:47:06 +01:00
224 lines
6.0 KiB
TypeScript
224 lines
6.0 KiB
TypeScript
|
import log from 'loglevel';
|
||
|
import { Json } from '@metamask/utils';
|
||
|
import getFetchWithTimeout from './fetch-with-timeout';
|
||
|
|
||
|
const fetchWithTimeout = getFetchWithTimeout();
|
||
|
|
||
|
// From app/_locales folders there is a messages.json file such as app/_locales/en, comes with key and translated results
|
||
|
// and we use as t('reject') to get the translated message in the codebase
|
||
|
// and in i18n lib, the translated message is an object (I18NMessage) with message & description -
|
||
|
// message is the string that will replace the translationKey, and that message may contain replacement variables such as $1, $2, etc.
|
||
|
// Description is key describing the usage of the message.
|
||
|
export interface I18NMessage {
|
||
|
message: string;
|
||
|
description?: string;
|
||
|
}
|
||
|
|
||
|
// The overall translation file is made of same entries
|
||
|
// translationKey (string) and the I18NMessage as the value.
|
||
|
export interface I18NMessageDict {
|
||
|
[translationKey: string]: I18NMessage;
|
||
|
}
|
||
|
|
||
|
export type I18NSubstitution = string | (() => any) | object;
|
||
|
|
||
|
// A parameterized type (or generic type) of maps that use the same structure (translationKey) key
|
||
|
interface I18NMessageDictMap<R> {
|
||
|
[translationKey: string]: R;
|
||
|
}
|
||
|
|
||
|
export const FALLBACK_LOCALE = 'en';
|
||
|
|
||
|
const warned: { [localeCode: string]: I18NMessageDictMap<boolean> } = {};
|
||
|
|
||
|
const missingMessageErrors: I18NMessageDictMap<Error> = {};
|
||
|
|
||
|
const missingSubstitutionErrors: {
|
||
|
[localeCode: string]: I18NMessageDictMap<boolean>;
|
||
|
} = {};
|
||
|
|
||
|
const relativeTimeFormatLocaleData = new Set();
|
||
|
|
||
|
/**
|
||
|
* Returns a localized message for the given key
|
||
|
*
|
||
|
* @param localeCode - The code for the current locale
|
||
|
* @param localeMessages - The map of messages for the current locale
|
||
|
* @param key - The message key
|
||
|
* @param substitutions - A list of message substitution replacements can replace $n in given message
|
||
|
* @param onError - An optional callback to provide additional processing on any errors
|
||
|
* @param join - An optional callback to join the substituted parts using custom logic
|
||
|
* @returns The localized message
|
||
|
*/
|
||
|
export const getMessage = <T>(
|
||
|
localeCode: string,
|
||
|
localeMessages: I18NMessageDict,
|
||
|
key: string,
|
||
|
substitutions?: I18NSubstitution[],
|
||
|
onError?: (error: Error) => void,
|
||
|
join?: (substitutedParts: I18NSubstitution[]) => T,
|
||
|
): T | string | null => {
|
||
|
if (!localeMessages) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
const message = localeMessages[key];
|
||
|
|
||
|
if (!message) {
|
||
|
missingKeyError(key, localeCode, onError);
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
const text = message.message;
|
||
|
|
||
|
const parts = hasSubstitutions(substitutions)
|
||
|
? applySubstitutions(
|
||
|
text,
|
||
|
substitutions as I18NSubstitution[],
|
||
|
key,
|
||
|
localeCode,
|
||
|
onError,
|
||
|
)
|
||
|
: [text];
|
||
|
|
||
|
return join ? join(parts) : parts.join('');
|
||
|
};
|
||
|
|
||
|
export async function fetchLocale(
|
||
|
localeCode: string,
|
||
|
): Promise<I18NMessageDict> {
|
||
|
try {
|
||
|
const response = await fetchWithTimeout(
|
||
|
`./_locales/${localeCode}/messages.json`,
|
||
|
);
|
||
|
return await response.json();
|
||
|
} catch (error) {
|
||
|
log.error(`failed to fetch ${localeCode} locale because of ${error}`);
|
||
|
return {};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export async function loadRelativeTimeFormatLocaleData(
|
||
|
localeCode: string,
|
||
|
): Promise<void> {
|
||
|
const languageTag = localeCode.split('_')[0];
|
||
|
if (
|
||
|
Intl.RelativeTimeFormat &&
|
||
|
typeof (Intl.RelativeTimeFormat as any).__addLocaleData === 'function' &&
|
||
|
!relativeTimeFormatLocaleData.has(languageTag)
|
||
|
) {
|
||
|
const localeData = await fetchRelativeTimeFormatData(languageTag);
|
||
|
(Intl.RelativeTimeFormat as any).__addLocaleData(localeData);
|
||
|
relativeTimeFormatLocaleData.add(languageTag);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export function clearCaches() {
|
||
|
Object.keys(warned).forEach((key) => {
|
||
|
delete warned[key];
|
||
|
});
|
||
|
|
||
|
Object.keys(missingMessageErrors).forEach((key) => {
|
||
|
delete missingMessageErrors[key];
|
||
|
});
|
||
|
|
||
|
Object.keys(missingSubstitutionErrors).forEach((key) => {
|
||
|
delete missingSubstitutionErrors[key];
|
||
|
});
|
||
|
|
||
|
relativeTimeFormatLocaleData.clear();
|
||
|
}
|
||
|
|
||
|
function applySubstitutions(
|
||
|
message: string,
|
||
|
substitutions: I18NSubstitution[],
|
||
|
key: string,
|
||
|
localeCode: string,
|
||
|
onError?: (error: Error) => void,
|
||
|
): I18NSubstitution[] {
|
||
|
const parts = message.split(/(\$\d)/gu);
|
||
|
|
||
|
return parts.map((part: string) => {
|
||
|
const subMatch = part.match(/\$(\d)/u);
|
||
|
|
||
|
if (!subMatch) {
|
||
|
return part;
|
||
|
}
|
||
|
|
||
|
const substituteIndex = Number(subMatch[1]) - 1;
|
||
|
const substitution = substitutions[substituteIndex];
|
||
|
|
||
|
if (substitution === null || substitution === undefined) {
|
||
|
missingSubstitutionError(key, localeCode, onError);
|
||
|
}
|
||
|
|
||
|
return substitutions?.[substituteIndex];
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function missingKeyError(
|
||
|
key: string,
|
||
|
localeCode: string,
|
||
|
onError?: (error: Error) => void,
|
||
|
) {
|
||
|
if (localeCode === FALLBACK_LOCALE && !missingMessageErrors[key]) {
|
||
|
const error = new Error(
|
||
|
`Unable to find value of key "${key}" for locale "${localeCode}"`,
|
||
|
);
|
||
|
|
||
|
missingMessageErrors[key] = error;
|
||
|
|
||
|
onError?.(error);
|
||
|
log.error(error);
|
||
|
|
||
|
if (process.env.IN_TEST) {
|
||
|
throw error;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (localeCode === FALLBACK_LOCALE || warned[localeCode]?.[key]) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
warned[localeCode] = warned[localeCode] ?? {};
|
||
|
warned[localeCode][key] = true;
|
||
|
|
||
|
log.warn(
|
||
|
`Translator - Unable to find value of key "${key}" for locale "${localeCode}"`,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function missingSubstitutionError(
|
||
|
key: string,
|
||
|
localeCode: string,
|
||
|
onError?: (error: Error) => void,
|
||
|
) {
|
||
|
if (missingSubstitutionErrors[localeCode]?.[key]) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
missingSubstitutionErrors[localeCode] =
|
||
|
missingSubstitutionErrors[localeCode] ?? {};
|
||
|
|
||
|
missingSubstitutionErrors[localeCode][key] = true;
|
||
|
|
||
|
const error = new Error(
|
||
|
`Insufficient number of substitutions for key "${key}" with locale "${localeCode}"`,
|
||
|
);
|
||
|
|
||
|
log.error(error);
|
||
|
|
||
|
onError?.(error);
|
||
|
}
|
||
|
|
||
|
function hasSubstitutions(substitutions?: I18NSubstitution[]) {
|
||
|
return (substitutions?.length ?? 0) > 0;
|
||
|
}
|
||
|
|
||
|
async function fetchRelativeTimeFormatData(languageTag: string): Promise<Json> {
|
||
|
const response = await fetchWithTimeout(
|
||
|
`./intl/${languageTag}/relative-time-format-data.json`,
|
||
|
);
|
||
|
return await response.json();
|
||
|
}
|