You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
315 lines
8.8 KiB
TypeScript
315 lines
8.8 KiB
TypeScript
// this file is a weird one as it is used by both sides of electron at the same time
|
|
|
|
import {
|
|
FormatDistanceStrictOptions,
|
|
FormatDistanceToNowStrictOptions,
|
|
formatDistanceStrict,
|
|
formatDistanceToNowStrict,
|
|
} from 'date-fns';
|
|
import timeLocales from 'date-fns/locale';
|
|
import { isUndefined } from 'lodash';
|
|
import {
|
|
GetMessageArgs,
|
|
LocalizerDictionary,
|
|
LocalizerToken,
|
|
PluralKey,
|
|
PluralString,
|
|
} from '../types/Localizer';
|
|
import { LOCALE_DEFAULTS } from '../session/constants';
|
|
import { updateLocale } from '../state/ducks/dictionary';
|
|
|
|
export function loadDictionary(locale: Locale) {
|
|
return import(`../../_locales/${locale}/messages.json`) as Promise<LocalizerDictionary>;
|
|
}
|
|
|
|
const timeLocaleMap = {
|
|
ar: timeLocales.ar,
|
|
be: timeLocales.be,
|
|
bg: timeLocales.bg,
|
|
ca: timeLocales.ca,
|
|
cs: timeLocales.cs,
|
|
da: timeLocales.da,
|
|
de: timeLocales.de,
|
|
el: timeLocales.el,
|
|
en: timeLocales.enUS,
|
|
eo: timeLocales.eo,
|
|
es: timeLocales.es,
|
|
/** TODO - Check this */
|
|
es_419: timeLocales.es,
|
|
et: timeLocales.et,
|
|
fa: timeLocales.faIR,
|
|
fi: timeLocales.fi,
|
|
/** TODO - Check this */
|
|
fil: timeLocales.fi,
|
|
fr: timeLocales.fr,
|
|
he: timeLocales.he,
|
|
hi: timeLocales.hi,
|
|
hr: timeLocales.hr,
|
|
hu: timeLocales.hu,
|
|
/** TODO - Check this */
|
|
'hy-AM': timeLocales.hy,
|
|
id: timeLocales.id,
|
|
it: timeLocales.it,
|
|
ja: timeLocales.ja,
|
|
ka: timeLocales.ka,
|
|
km: timeLocales.km,
|
|
/** TODO - Check this */
|
|
kmr: timeLocales.km,
|
|
kn: timeLocales.kn,
|
|
ko: timeLocales.ko,
|
|
lt: timeLocales.lt,
|
|
lv: timeLocales.lv,
|
|
mk: timeLocales.mk,
|
|
nb: timeLocales.nb,
|
|
nl: timeLocales.nl,
|
|
/** TODO - Find this this */
|
|
no: timeLocales.enUS,
|
|
/** TODO - Find this this */
|
|
pa: timeLocales.enUS,
|
|
pl: timeLocales.pl,
|
|
pt_BR: timeLocales.ptBR,
|
|
pt_PT: timeLocales.pt,
|
|
ro: timeLocales.ro,
|
|
ru: timeLocales.ru,
|
|
/** TODO - Find this this */
|
|
si: timeLocales.enUS,
|
|
sk: timeLocales.sk,
|
|
sl: timeLocales.sl,
|
|
sq: timeLocales.sq,
|
|
sr: timeLocales.sr,
|
|
sv: timeLocales.sv,
|
|
ta: timeLocales.ta,
|
|
th: timeLocales.th,
|
|
/** TODO - Find this this */
|
|
tl: timeLocales.enUS,
|
|
tr: timeLocales.tr,
|
|
uk: timeLocales.uk,
|
|
uz: timeLocales.uz,
|
|
vi: timeLocales.vi,
|
|
zh_CN: timeLocales.zhCN,
|
|
zh_TW: timeLocales.zhTW,
|
|
};
|
|
|
|
export type Locale = keyof typeof timeLocaleMap;
|
|
|
|
const enPluralFormRegex = /\{(\w+), plural, one \{(\w+)\} other \{(\w+)\}\}/;
|
|
|
|
const cardinalPluralFormRegex = /(zero|one|two|few|many|other) \{([^}]*)\}/g;
|
|
|
|
const cardinalPluralRegex: Record<Intl.LDMLPluralRule, RegExp> = {
|
|
zero: /(zero) \{([^}]*)\}/,
|
|
one: /(one) \{([^}]*)\}/,
|
|
two: /(two) \{([^}]*)\}/,
|
|
few: /(few) \{([^}]*)\}/,
|
|
many: /(many) \{([^}]*)\}/,
|
|
other: /(other) \{([^}]*)\}/,
|
|
};
|
|
|
|
function getPluralKey(string: PluralString): PluralKey | undefined {
|
|
const match = string.match(enPluralFormRegex);
|
|
return match ? match[1] : undefined;
|
|
}
|
|
|
|
function getStringForCardinalRule(
|
|
localizedString: string,
|
|
cardinalRule: Intl.LDMLPluralRule
|
|
): string | undefined {
|
|
const match = localizedString.match(cardinalPluralRegex[cardinalRule]);
|
|
return match ? match[2] : undefined;
|
|
}
|
|
|
|
const isPluralForm = (localizedString: string): localizedString is PluralString =>
|
|
enPluralFormRegex.test(localizedString);
|
|
|
|
/**
|
|
* Logs an i18n message to the console.
|
|
* @param message - The message to log.
|
|
*
|
|
* TODO - Replace this logging method when the new logger is created
|
|
*/
|
|
function i18nLog(message: string) {
|
|
// eslint:disable: no-console
|
|
// eslint-disable-next-line no-console
|
|
(window?.log?.error ?? console.log)(message);
|
|
}
|
|
|
|
/**
|
|
* Sets up the i18n function with the provided locale and messages.
|
|
*
|
|
* @param locale - The locale to use for translations.
|
|
* @param dictionary - A dictionary of localized messages.
|
|
*
|
|
* @returns A function that retrieves a localized message string, substituting variables where necessary.
|
|
*/
|
|
export const setupi18n = (locale: Locale, dictionary: LocalizerDictionary) => {
|
|
if (!locale) {
|
|
throw new Error('i18n: locale parameter is required');
|
|
}
|
|
|
|
if (!dictionary) {
|
|
throw new Error('i18n: messages parameter is required');
|
|
}
|
|
|
|
if (window.inboxStore) {
|
|
window.inboxStore.dispatch(updateLocale(locale));
|
|
window.log.info('Loaded dictionary dispatch');
|
|
}
|
|
window.log.info('i18n setup');
|
|
|
|
/**
|
|
* Retrieves a localized message string, substituting variables where necessary.
|
|
*
|
|
* @param token - The token identifying the message to retrieve.
|
|
* @param args - An optional record of substitution variables and their replacement values. This is required if the string has dynamic variables.
|
|
*
|
|
* @returns The localized message string with substitutions applied.
|
|
*
|
|
* @example
|
|
* // The string greeting is 'Hello, {name}!' in the current locale
|
|
* window.i18n('greeting', { name: 'Alice' });
|
|
* // => 'Hello, Alice!'
|
|
*/
|
|
function getMessage<T extends LocalizerToken, R extends LocalizerDictionary[T]>(
|
|
...[token, args]: GetMessageArgs<T>
|
|
): R {
|
|
try {
|
|
const {
|
|
inboxStore,
|
|
sessionFeatureFlags: { replaceLocalizedStringsWithKeys },
|
|
} = window;
|
|
|
|
if (replaceLocalizedStringsWithKeys) {
|
|
return token as R;
|
|
}
|
|
|
|
const storedDictionary =
|
|
inboxStore && 'getState' in inboxStore && typeof inboxStore.getState === 'function'
|
|
? (inboxStore.getState().dictionary.dictionary as LocalizerDictionary)
|
|
: undefined;
|
|
|
|
if (!storedDictionary) {
|
|
i18nLog(`i18n: Stored dictionary not found, using setup dictionary as fallback`);
|
|
}
|
|
|
|
const localizedDictionary = storedDictionary ?? dictionary;
|
|
|
|
let localizedString = localizedDictionary[token] as R;
|
|
|
|
if (!localizedString) {
|
|
i18nLog(`i18n: Attempted to get translation for nonexistent key '${token}'`);
|
|
return token as R;
|
|
}
|
|
|
|
/** If a localized string does not have any arguments to substitute it is returned with no changes */
|
|
if (!args) {
|
|
return localizedString;
|
|
}
|
|
|
|
if (isPluralForm(localizedString)) {
|
|
const pluralKey = getPluralKey(localizedString) as keyof typeof args;
|
|
|
|
if (!pluralKey) {
|
|
i18nLog(
|
|
`i18n: Attempted to nonexistent pluralKey for plural form string '${localizedString}'`
|
|
);
|
|
} else {
|
|
const num = args[pluralKey] ?? 0;
|
|
|
|
const cardinalRule = new Intl.PluralRules(locale).select(num);
|
|
|
|
const pluralString = getStringForCardinalRule(localizedString, cardinalRule);
|
|
|
|
if (!pluralString) {
|
|
i18nLog(
|
|
`i18n: Plural string not found for cardinal '${cardinalRule}': '${localizedString}'`
|
|
);
|
|
return token as R;
|
|
}
|
|
|
|
localizedString = pluralString.replaceAll('#', num) as R;
|
|
}
|
|
}
|
|
|
|
/** Find and replace the dynamic variables in a localized string and substitute the variables with the provided values */
|
|
return localizedString.replace(/\{(\w+)\}/g, (match, arg: keyof typeof args) => {
|
|
const substitution = args[arg];
|
|
|
|
if (isUndefined(substitution)) {
|
|
const defaultSubstitution = LOCALE_DEFAULTS[arg as keyof typeof LOCALE_DEFAULTS];
|
|
|
|
return isUndefined(defaultSubstitution) ? match : defaultSubstitution;
|
|
}
|
|
|
|
return substitution.toString();
|
|
}) as R;
|
|
} catch (error) {
|
|
i18nLog(`i18n: ${error.message}`);
|
|
return token as R;
|
|
}
|
|
}
|
|
|
|
window.getLocale = () => locale;
|
|
|
|
return getMessage;
|
|
};
|
|
|
|
// eslint-disable-next-line import/no-mutable-exports
|
|
export let langNotSupportedMessageShown = false;
|
|
|
|
export const loadEmojiPanelI18n = async () => {
|
|
if (!window) {
|
|
return undefined;
|
|
}
|
|
|
|
const lang = window.getLocale();
|
|
if (lang !== 'en') {
|
|
try {
|
|
const langData = await import(`@emoji-mart/data/i18n/${lang}.json`);
|
|
return langData;
|
|
} catch (err) {
|
|
if (!langNotSupportedMessageShown) {
|
|
window?.log?.warn(
|
|
'Language is not supported by emoji-mart package. See https://github.com/missive/emoji-mart/tree/main/packages/emoji-mart-data/i18n'
|
|
);
|
|
langNotSupportedMessageShown = true;
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
export const formatTimeDistance = (
|
|
date: Date,
|
|
baseDate: Date,
|
|
options?: Omit<FormatDistanceStrictOptions, 'locale'>
|
|
) => {
|
|
const locale = window.getLocale();
|
|
return formatDistanceStrict(date, baseDate, {
|
|
locale: timeLocaleMap[locale],
|
|
...options,
|
|
});
|
|
};
|
|
|
|
export const formatTimeDistanceToNow = (
|
|
date: Date,
|
|
options?: Omit<FormatDistanceToNowStrictOptions, 'locale'>
|
|
) => {
|
|
const locale = window.getLocale();
|
|
return formatDistanceToNowStrict(date, {
|
|
locale: timeLocaleMap[locale],
|
|
...options,
|
|
});
|
|
};
|
|
|
|
// RTL Support
|
|
|
|
export type HTMLDirection = 'ltr' | 'rtl';
|
|
|
|
export function isRtlBody(): boolean {
|
|
const body = document.getElementsByTagName('body').item(0);
|
|
|
|
return body?.classList.contains('rtl') || false;
|
|
}
|
|
|
|
export const useHTMLDirection = (): HTMLDirection => (isRtlBody() ? 'rtl' : 'ltr');
|