1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-22 11:22:43 +02:00
metamask-extension/shared/modules/i18n.ts
2023-06-20 13:44:11 +01:00

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();
}