fix: time&date format are not dependent on crowdin supported locales

pull/3207/head
Audric Ackermann 7 months ago
parent 1582474a11
commit b36e18b6f4
No known key found for this signature in database

@ -7,11 +7,11 @@ const os = require('os');
const { setupI18n } = require('./ts/util/i18n/i18n');
const config = url.parse(window.location.toString(), true).query;
const { dictionary, locale } = ipcRenderer.sendSync('locale-data');
const { dictionary, crowdinLocale } = ipcRenderer.sendSync('locale-data');
window.theme = config.theme;
window.i18n = setupI18n({
locale,
crowdinLocale,
translationDictionary: dictionary,
});

@ -6,13 +6,14 @@ const url = require('url');
const { setupI18n } = require('./ts/util/i18n/i18n');
const config = url.parse(window.location.toString(), true).query;
const { dictionary, locale } = ipcRenderer.sendSync('locale-data');
const { dictionary, crowdinLocale } = ipcRenderer.sendSync('locale-data');
// If the app is locked we can't access the database to check the theme.
window.theme = 'classic-dark';
window.primaryColor = 'green';
window.i18n = setupI18n({
locale,
crowdinLocale,
translationDictionary: dictionary,
});

@ -11,12 +11,12 @@ const _ = require('lodash');
const { setupI18n } = require('./ts/util/i18n/i18n');
const { dictionary, locale } = ipc.sendSync('locale-data');
const { dictionary, crowdinLocale } = ipc.sendSync('locale-data');
const config = url.parse(window.location.toString(), true).query;
const configAny = config;
window.i18n = setupI18n({ locale, translationDictionary: dictionary });
window.i18n = setupI18n({ crowdinLocale, translationDictionary: dictionary });
let title = config.name;
if (config.environment !== 'production') {

@ -22,7 +22,7 @@ import { THEME_GLOBALS } from '../../themes/globals';
import { SessionWrapperModal } from '../SessionWrapperModal';
import { SessionIcon, SessionIconButton } from '../icon';
import { SessionSpinner } from '../loading';
import { getLocale } from '../../util/i18n/shared';
import { getCrowdinLocale } from '../../util/i18n/shared';
export type StatusLightType = {
glowStartDelay: number;
@ -136,9 +136,9 @@ const OnionPathModalInner = () => {
<Flex container={true} flexDirection="column" alignItems="flex-start">
{nodes.map((snode: Snode | any) => {
const country = reader?.get(snode.ip || '0.0.0.0')?.country;
const locale = getLocale();
const locale = getCrowdinLocale();
// typescript complains that the [] operator cannot be used with the 'string' coming from getLocale()
// typescript complains that the [] operator cannot be used with the 'string' coming from getCrowdinLocale()
const countryNamesAsAny = country?.names as any;
const countryName =
snode.label || // to take care of the "Device" case

@ -6,3 +6,89 @@ export enum LOCALE_DEFAULTS {
}
export const rtlLocales = ['ar', 'fa', 'he', 'ps', 'ur'];
export const crowdinLocales = [
'en',
'af',
'ar',
'az',
'bal',
'be',
'bg',
'bn',
'ca',
'cs',
'cy',
'da',
'de',
'el',
'eo',
'es-419',
'es',
'et',
'eu',
'fa',
'fi',
'fil',
'fr',
'gl',
'ha',
'he',
'hi',
'hr',
'hu',
'hy-AM',
'id',
'it',
'ja',
'ka',
'km',
'kmr',
'kn',
'ko',
'ku',
'lg',
'lo',
'lt',
'lv',
'mk',
'mn',
'ms',
'my',
'nb',
'ne',
'nl',
'nn',
'no',
'ny',
'pa',
'pl',
'ps',
'pt-BR',
'pt-PT',
'ro',
'ru',
'sh',
'si',
'sk',
'sl',
'sq',
'sr',
'sr',
'sv',
'sw',
'ta',
'te',
'th',
'tl',
'tr',
'uk',
'ur',
'uz',
'vi',
'xh',
'zh-CN',
'zh-TW',
] as const;
export type CrowdinLocale = (typeof crowdinLocales)[number];

@ -162,8 +162,12 @@ import { setLatestRelease } from '../node/latest_desktop_release';
import { isDevProd, isTestIntegration } from '../shared/env_vars';
import { classicDark } from '../themes';
import type { SetupI18nReturnType } from '../types/localizer';
import { getTranslationDictionary } from '../util/i18n/shared';
import { getLocale, isLocaleSet, type Locale } from '../util/i18n/shared';
import {
isSessionLocaleSet,
getTranslationDictionary,
getCrowdinLocale,
} from '../util/i18n/shared';
import { loadLocalizedDictionary } from '../node/locale';
// Both of these will be set after app fires the 'ready' event
@ -184,7 +188,7 @@ function prepareURL(pathSegments: Array<string>, moreKeys?: { theme: any }) {
slashes: true,
query: {
name: packageJson.productName,
locale: getLocale(),
locale: getCrowdinLocale(),
version: app.getVersion(),
commitHash: config.get('commitHash'),
environment: (config as any).environment,
@ -717,11 +721,12 @@ app.on('ready', async () => {
logger = getLogger();
assertLogger().info('app ready');
assertLogger().info(`starting version ${packageJson.version}`);
if (!isLocaleSet()) {
const appLocale = (process.env.LANGUAGE || app.getLocale() || 'en') as Locale;
if (!isSessionLocaleSet()) {
const appLocale = process.env.LANGUAGE || app.getLocale() || 'en';
const loadedLocale = loadLocalizedDictionary({ appLocale, logger });
i18n = loadedLocale.i18n;
assertLogger().info(`locale is ${loadedLocale.locale}`);
assertLogger().info(`appLocale is ${appLocale}`);
assertLogger().info(`crowdin locale is ${loadedLocale.crowdinLocale}`);
}
const key = getDefaultSQLKey();
@ -907,7 +912,7 @@ ipc.on('locale-data', event => {
// eslint-disable-next-line no-param-reassign
event.returnValue = {
dictionary: getTranslationDictionary(),
locale: getLocale(),
crowdinLocale: getCrowdinLocale(),
};
});

@ -29,7 +29,7 @@ import { Notifications } from '../util/notifications';
import { Registration } from '../util/registration';
import { Storage, isSignInByLinking } from '../util/storage';
import { getOppositeTheme, isThemeMismatched } from '../util/theme';
import { getLocale } from '../util/i18n/shared';
import { getCrowdinLocale } from '../util/i18n/shared';
import { rtlLocales } from '../localization/constants';
// Globally disable drag and drop
@ -292,7 +292,7 @@ async function start() {
});
function switchBodyToRtlIfNeeded() {
const loc = getLocale();
const loc = getCrowdinLocale();
if (rtlLocales.includes(loc) && !document.getElementById('body')?.classList.contains('rtl')) {
document.getElementById('body')?.classList.add('rtl');
}

@ -1,12 +1,13 @@
import fs from 'fs';
import path from 'path';
import { isEmpty } from 'lodash';
import type { LocalizerDictionary, SetupI18nReturnType } from '../types/localizer';
import { getAppRootPath } from './getRootPath';
import type { Locale } from '../util/i18n/shared';
import { en } from '../localization/locales';
import { setupI18n } from '../util/i18n/i18n';
import { CrowdinLocale } from '../localization/constants';
function normalizeLocaleName(locale: string) {
export function normalizeLocaleName(locale: string) {
const dashedLocale = locale.replaceAll('_', '-');
// Note: this is a pain, but we somehow needs to keep in sync this logic and the LOCALE_PATH_MAPPING from
@ -46,8 +47,11 @@ function getLocaleMessages(locale: string): LocalizerDictionary {
export function loadLocalizedDictionary({
appLocale,
logger,
}: { appLocale?: Locale; logger?: any } = {}): {
locale: Locale;
}: {
appLocale: string;
logger?: any;
}): {
crowdinLocale: CrowdinLocale;
i18n: SetupI18nReturnType;
} {
if (!appLocale) {
@ -63,26 +67,28 @@ export function loadLocalizedDictionary({
//
// possible locales:
// https://github.com/electron/electron/blob/master/docs/api/locales.md
let locale = normalizeLocaleName(appLocale) as Locale;
let crowdinLocale = normalizeLocaleName(appLocale) as CrowdinLocale;
let translationDictionary;
try {
translationDictionary = getLocaleMessages(locale);
translationDictionary = getLocaleMessages(crowdinLocale);
} catch (e) {
logger.error(`Problem loading messages for locale ${locale} ${e.stack}`);
logger.error(`Problem loading messages for locale ${crowdinLocale} ${e.stack}`);
logger.error('Falling back to en locale');
}
locale = 'en';
if (!translationDictionary || isEmpty(translationDictionary)) {
translationDictionary = en;
crowdinLocale = 'en';
}
const i18n = setupI18n({
locale,
crowdinLocale,
translationDictionary,
});
return {
locale,
crowdinLocale,
i18n,
};
}

@ -11,10 +11,11 @@ export const setup = (browserWindow: BrowserWindow, i18n: SetupI18nReturnType) =
const userLocale = process.env.LANGUAGE
? process.env.LANGUAGE
: osLocaleSync().replace(/_/g, '-');
const userLocales = [userLocale, userLocale.split('-')[0]];
const userLocales = [userLocale, userLocale.split('-')[0], userLocale.split('_')[0]];
const available = session.availableSpellCheckerLanguages;
const languages = userLocales.filter(l => available.includes(l));
console.log(`spellcheck: userLocales: ${userLocales}`);
console.log(`spellcheck: user locale: ${userLocale}`);
console.log('spellcheck: available spellchecker languages: ', available);
console.log('spellcheck: setting languages to: ', languages);

@ -12,5 +12,8 @@ export const testDictionary = {
} as const;
export function initI18n(dictionary: Record<string, string> = en) {
return setupI18n({ locale: 'en', translationDictionary: dictionary as LocalizerDictionary });
return setupI18n({
crowdinLocale: 'en',
translationDictionary: dictionary as LocalizerDictionary,
});
}

@ -1,4 +1,4 @@
import { getLocale } from './shared';
import { getCrowdinLocale } from './shared';
let langNotSupportedMessageShown = false;
@ -7,7 +7,7 @@ export const loadEmojiPanelI18n = async () => {
return undefined;
}
const triedLocales: Array<string> = [];
const lang = getLocale();
const lang = getCrowdinLocale();
if (lang !== 'en') {
try {
triedLocales.push(lang);

@ -8,7 +8,12 @@ import type {
PluralKey,
PluralString,
} from '../../../types/localizer';
import { getTranslationDictionary, getLocale, getStringForCardinalRule, i18nLog } from '../shared';
import {
getTranslationDictionary,
getStringForCardinalRule,
i18nLog,
getCrowdinLocale,
} from '../shared';
function getPluralKey<R extends PluralKey | undefined>(string: PluralString): R {
const match = /{(\w+), plural, one \[.+\] other \[.+\]}/g.exec(string);
@ -80,7 +85,7 @@ export function getRawMessage<
} else {
const num = args?.[pluralKey as keyof typeof args] ?? 0;
const currentLocale = getLocale();
const currentLocale = getCrowdinLocale();
const cardinalRule = new Intl.PluralRules(currentLocale).select(num);
const pluralString = getStringForCardinalRule(localizedString, cardinalRule);

@ -3,35 +3,36 @@
import { isEmpty } from 'lodash';
import type { LocalizerDictionary, SetupI18nReturnType } from '../../types/localizer';
import { getMessage } from './functions/getMessage';
import { i18nLog, Locale, setInitialLocale } from './shared';
import { i18nLog, setInitialLocale } from './shared';
import { CrowdinLocale } from '../../localization/constants';
/**
* Sets up the i18n function with the provided locale and messages.
*
* @param params - An object containing optional parameters.
* @param params.locale - The locale to use for translations
* @param params.translationDictionary - A dictionary of localized messages. Defaults to {@link en}.
* @param params.crowdinLocale - The locale to use for translations (crowdin)
* @param params.translationDictionary - A dictionary of localized messages
*
* @returns A function that retrieves a localized message string, substituting variables where necessary.
*/
export const setupI18n = ({
locale,
crowdinLocale,
translationDictionary,
}: {
locale: Locale;
crowdinLocale: CrowdinLocale;
translationDictionary: LocalizerDictionary;
}): SetupI18nReturnType => {
if (!locale) {
throw new Error(`locale not provided in i18n setup`);
if (!crowdinLocale) {
throw new Error(`crowdinLocale not provided in i18n setup`);
}
if (!translationDictionary || isEmpty(translationDictionary)) {
throw new Error('translationDictionary was not provided');
}
setInitialLocale(locale, translationDictionary);
setInitialLocale(crowdinLocale, translationDictionary);
i18nLog(`Setup Complete with locale: ${locale}`);
i18nLog(`Setup Complete with crowdinLocale: ${crowdinLocale}`);
return getMessage;
};

@ -1,10 +1,10 @@
import { en } from '../../localization/locales';
import {
getLocale,
getStringForCardinalRule,
getFallbackDictionary,
getTranslationDictionary,
i18nLog,
getCrowdinLocale,
} from './shared';
import { LOCALE_DEFAULTS } from '../../localization/constants';
import { deSanitizeHtmlTags, sanitizeArgs } from '../../components/basic/Localizer';
@ -236,7 +236,7 @@ export class LocalizedStringBuilder<
}
}
const currentLocale = getLocale();
const currentLocale = getCrowdinLocale();
const cardinalRule = new Intl.PluralRules(currentLocale).select(num);
let pluralString = getStringForCardinalRule(str, cardinalRule);

@ -1,10 +1,11 @@
import { Locale } from 'date-fns';
import { CrowdinLocale } from '../../localization/constants';
import { en } from '../../localization/locales';
import type { LocalizerDictionary } from '../../types/localizer';
import { timeLocaleMap } from './timeLocaleMap';
let mappedBrowserLocaleDisplayed = false;
let initialLocale: Locale | undefined;
let crowdinLocale: CrowdinLocale | undefined;
let translationDictionary: LocalizerDictionary | undefined;
/**
@ -12,7 +13,7 @@ let translationDictionary: LocalizerDictionary | undefined;
*/
export function resetLocaleAndTranslationDict() {
translationDictionary = undefined;
initialLocale = undefined;
crowdinLocale = undefined;
}
/**
@ -46,32 +47,34 @@ export function i18nLog(message: string) {
}
}
export type Locale = keyof typeof timeLocaleMap;
export function getTimeLocaleDictionary() {
return timeLocaleMap[getLocale()];
return (timeLocaleMap as Record<string, Locale>)[getBrowserLocale()] || timeLocaleMap.en;
}
/**
* Returns the current locale.
* Returns the current locale as supported by Session (i.e. one generated by crowdin)
*/
export function getLocale(): Locale {
if (!initialLocale) {
i18nLog(`getLocale: using initialLocale: ${initialLocale}`);
export function getCrowdinLocale(): CrowdinLocale {
if (!crowdinLocale) {
i18nLog(`getCrowdinLocale: ${crowdinLocale}`);
throw new Error('initialLocale is unset');
throw new Error('crowdinLocale is unset');
}
return initialLocale;
return crowdinLocale;
}
/**
* Returns the closest supported locale by the browser.
*/
export function getBrowserLocale() {
const userLocaleDashed = getLocale();
const browserLocale = process.env.LANGUAGE || getCrowdinLocale() || 'en';
// supportedLocalesOf will throw if the locales has a '_' instead of a '-' in it.
const userLocaleDashed = browserLocale.replaceAll('_', '-');
const matchinglocales = Intl.DateTimeFormat.supportedLocalesOf(userLocaleDashed);
const mappingTo = matchinglocales?.[0] || 'en';
const matchingLocales = Intl.DateTimeFormat.supportedLocalesOf(userLocaleDashed);
const mappingTo = matchingLocales?.[0] || 'en';
if (!mappedBrowserLocaleDisplayed) {
mappedBrowserLocaleDisplayed = true;
@ -81,16 +84,16 @@ export function getBrowserLocale() {
return mappingTo;
}
export function setInitialLocale(locale: Locale, dictionary: LocalizerDictionary) {
if (translationDictionary) {
throw new Error('setInitialLocale: translationDictionary or initialLocale is already init');
export function setInitialLocale(crowdinLocaleArg: CrowdinLocale, dictionary: LocalizerDictionary) {
if (translationDictionary || crowdinLocale) {
throw new Error('setInitialLocale: translationDictionary or crowdinLocale is already init');
}
translationDictionary = dictionary;
initialLocale = locale;
crowdinLocale = crowdinLocaleArg;
}
export function isLocaleSet() {
return initialLocale !== undefined;
export function isSessionLocaleSet() {
return !!crowdinLocale;
}
export function getStringForCardinalRule(

@ -1,66 +1,37 @@
import timeLocales from 'date-fns/locale';
import * as supportedByDateFns from 'date-fns/locale';
import { Locale } from 'date-fns';
import { CrowdinLocale, crowdinLocales } from '../../localization/constants';
type MappedToEnType = { [K in CrowdinLocale]: Locale };
/**
* Map every locales supported by Crowdin to english first.
* Then we overwrite those values with what we have support for from date-fns and what we need to overwrite
*/
const mappedToEn: MappedToEnType = crowdinLocales.reduce((acc, key) => {
acc[key] = supportedByDateFns.enUS;
return acc;
}, {} as MappedToEnType);
// Note: to find new mapping you can use:
// https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
export 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,
'es-419': timeLocales.es,
et: timeLocales.et,
fa: timeLocales.faIR,
fi: timeLocales.fi,
fil: timeLocales.fi,
fr: timeLocales.fr,
he: timeLocales.he,
hi: timeLocales.hi,
hr: timeLocales.hr,
hu: timeLocales.hu,
'hy-AM': timeLocales.hy,
id: timeLocales.id,
it: timeLocales.it,
ja: timeLocales.ja,
ka: timeLocales.ka,
km: timeLocales.km,
kmr: timeLocales.km, // central khmer, mapped to date-fns khmer: km
kn: timeLocales.kn,
ko: timeLocales.ko,
lt: timeLocales.lt,
lv: timeLocales.lv,
mk: timeLocales.mk,
nb: timeLocales.nb, // Norwegian Bokmål, mapped to date-fns "Norwegian Bokmål": nb
nl: timeLocales.nl, // dutch/flemish
no: timeLocales.nb, // norwegian, mapped to date-fns "Norwegian Bokmål": nb
pa: timeLocales.hi, // punjabi: not supported by date-fns, mapped to Hindi: hi
pl: timeLocales.pl,
'pt-BR': timeLocales.ptBR,
'pt-PT': timeLocales.pt,
ro: timeLocales.ro,
ru: timeLocales.ru,
si: timeLocales.enUS, // sinhala, not suported by date-fns, mapped to english for now
sk: timeLocales.sk,
sl: timeLocales.sl,
sq: timeLocales.sq,
sr: timeLocales.sr,
sv: timeLocales.sv,
ta: timeLocales.ta,
th: timeLocales.th,
tl: timeLocales.enUS, // tagalog, not suported by date-fns, mapped to english for now
tr: timeLocales.tr,
uk: timeLocales.uk,
uz: timeLocales.uz,
vi: timeLocales.vi,
'zh-CN': timeLocales.zhCN,
'zh-TW': timeLocales.zhTW,
export const timeLocaleMap: Record<CrowdinLocale, Locale> = {
...mappedToEn,
...supportedByDateFns,
en: supportedByDateFns.enUS,
// then overwrite anything that we don't agree with or need to support specifically.
'es-419': supportedByDateFns.es,
fa: supportedByDateFns.faIR,
fil: supportedByDateFns.fi,
'hy-AM': supportedByDateFns.hy,
kmr: supportedByDateFns.km, // Central khmer
'pt-BR': supportedByDateFns.ptBR,
'pt-PT': supportedByDateFns.pt,
'zh-CN': supportedByDateFns.zhCN,
'zh-TW': supportedByDateFns.zhTW,
};
export function getForcedEnglishTimeLocale() {

Loading…
Cancel
Save