aboutsummaryrefslogtreecommitdiffstats
path: root/packages/main/src/i18n
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-04-03 18:56:00 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-05-16 00:54:59 +0200
commit7a926c4b223c8be46a1defab4a86104d617eaaf9 (patch)
tree5e50a787397ff72268c22c6dffc67432dc76e184 /packages/main/src/i18n
parentfix(main): Inconsistent RendererBridge snapshot (diff)
downloadsophie-7a926c4b223c8be46a1defab4a86104d617eaaf9.tar.gz
sophie-7a926c4b223c8be46a1defab4a86104d617eaaf9.tar.zst
sophie-7a926c4b223c8be46a1defab4a86104d617eaaf9.zip
refactor: Use i18next for language resolution
Due to https://github.com/i18next/i18next/issues/1564 we still have to implement our own language resolution, but we can rely on resolvedLanguage to determine which language to pass through to the renderer. We will use the language detected by chromium as the system locale, so there is no need to use os-locale for detection any more. We use i18next in the main process do resolve the language, then set the resolve (not requested!) language in the renderer process to avoid doing resolution twice. This avoids the need in the renderer process to know the list of supported languages. We set the language and the writing direction in HTML in the renderer. Signed-off-by: Kristóf Marussy <kristof@marussy.com>
Diffstat (limited to 'packages/main/src/i18n')
-rw-r--r--packages/main/src/i18n/loadLocalization.ts93
-rw-r--r--packages/main/src/i18n/synchronizeLocalizationSettings.ts99
2 files changed, 76 insertions, 116 deletions
diff --git a/packages/main/src/i18n/loadLocalization.ts b/packages/main/src/i18n/loadLocalization.ts
index 0413373..507075d 100644
--- a/packages/main/src/i18n/loadLocalization.ts
+++ b/packages/main/src/i18n/loadLocalization.ts
@@ -18,49 +18,108 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import { FALLBACK_LOCALE } from '@sophie/shared'; 21import { FALLBACK_LOCALE, SYSTEM_LOCALE } from '@sophie/shared';
22import i18next from 'i18next'; 22import i18next, { i18n } from 'i18next';
23import { autorun } from 'mobx'; 23import { reaction } from 'mobx';
24import { addDisposer } from 'mobx-state-tree'; 24import { addDisposer } from 'mobx-state-tree';
25 25
26import type MainStore from '../stores/MainStore'; 26import type MainStore from '../stores/MainStore';
27import { getLogger } from '../utils/log'; 27import { getLogger } from '../utils/log';
28 28
29import I18nStore from './I18nStore'; 29import I18nStore from './I18nStore';
30import LocatlizationRepository from './LocalizationRepository'; 30import type LocatlizationRepository from './LocalizationRepository';
31import RepositoryBasedI18nBackend from './RepositoryBasedI18nBackend'; 31import RepositoryBasedI18nBackend from './RepositoryBasedI18nBackend';
32import i18nLog from './i18nLog'; 32import i18nLog from './i18nLog';
33 33
34const log = getLogger('loadLocationzation'); 34const log = getLogger('loadLocationzation');
35 35
36const TEST_LOCALE = 'cimode';
37
38function getLanguage(
39 language: string,
40 systemLocale: string,
41 supportedLanguages: string[],
42): string {
43 const selectedLanguage = language === SYSTEM_LOCALE ? systemLocale : language;
44 if (selectedLanguage === TEST_LOCALE) {
45 return selectedLanguage;
46 }
47 // Even though i18next has a `supportedLngs` array from which it can pick a supported language,
48 // we still have to do this ourselves to avoid spurious warnings like
49 // https://github.com/i18next/i18next/issues/1564
50 if (supportedLanguages.includes(selectedLanguage)) {
51 return selectedLanguage;
52 }
53 if (selectedLanguage.includes('-')) {
54 const iso639 = selectedLanguage.split('-')[0];
55 if (supportedLanguages.includes(iso639)) {
56 return iso639;
57 }
58 }
59 return FALLBACK_LOCALE;
60}
61
62function updateSharedStoreLanguage(store: MainStore, i18nInstance: i18n): void {
63 const resolvedLanguage =
64 i18nInstance.language === TEST_LOCALE
65 ? TEST_LOCALE
66 : i18nInstance.resolvedLanguage;
67 const dir = i18nInstance.dir();
68 // We do not want to pass the list of supported languages to the renderer process,
69 // so we extract the resolved languages from `i18n` as pass only that to the renderer.
70 // Thus, the renderer always selects a language that is actually supported.
71 store.shared.setLanguage(resolvedLanguage, dir);
72 log.debug('Loaded language', resolvedLanguage, 'with direction', dir);
73}
74
36export default async function loadLocalization( 75export default async function loadLocalization(
37 store: MainStore, 76 store: MainStore,
77 systemLocale: string,
78 supportedLanguages: string[],
38 repository: LocatlizationRepository, 79 repository: LocatlizationRepository,
39 devMode: boolean, 80 devMode: boolean,
40): Promise<void> { 81): Promise<void> {
41 const backend = new RepositoryBasedI18nBackend(repository, devMode); 82 const backend = new RepositoryBasedI18nBackend(repository, devMode);
42 const i18n = i18next 83 const i18nInstance = i18next
43 .createInstance({ 84 .createInstance({
44 lng: store.shared.language, 85 lng: getLanguage(
86 store.settings.language,
87 systemLocale,
88 supportedLanguages,
89 ),
90 supportedLngs: supportedLanguages,
45 fallbackLng: [FALLBACK_LOCALE], 91 fallbackLng: [FALLBACK_LOCALE],
46 debug: devMode, 92 debug: devMode,
47 saveMissing: devMode, 93 saveMissing: devMode,
48 }) 94 })
49 .use(backend) 95 .use(backend)
50 .use(i18nLog); 96 .use(i18nLog);
51 const i18nStore = new I18nStore(i18n); 97 const i18nStore = new I18nStore(i18nInstance);
52 store.setI18n(i18nStore); 98 store.setI18n(i18nStore);
53 await i18n.init(); 99
54 const disposeChangeLanguage = autorun(() => { 100 await i18nInstance.init();
55 const { 101 updateSharedStoreLanguage(store, i18nInstance);
56 shared: { language }, 102
57 } = store; 103 const disposeChangeLanguage = reaction(
58 if (i18n.language !== language) { 104 () => store.settings.language,
59 log.debug('Setting language', language); 105 (languageSetting) => {
60 i18n.changeLanguage(language).catch((error) => { 106 (async () => {
107 const languageToSet = getLanguage(
108 languageSetting,
109 systemLocale,
110 supportedLanguages,
111 );
112 if (i18nInstance.language !== languageToSet) {
113 await i18nInstance.changeLanguage(languageToSet);
114 updateSharedStoreLanguage(store, i18nInstance);
115 }
116 })().catch((error) => {
61 log.error('Failed to change language', error); 117 log.error('Failed to change language', error);
62 }); 118 });
63 } 119 },
64 }); 120 {
121 fireImmediately: true,
122 },
123 );
65 addDisposer(store, disposeChangeLanguage); 124 addDisposer(store, disposeChangeLanguage);
66} 125}
diff --git a/packages/main/src/i18n/synchronizeLocalizationSettings.ts b/packages/main/src/i18n/synchronizeLocalizationSettings.ts
deleted file mode 100644
index 971a593..0000000
--- a/packages/main/src/i18n/synchronizeLocalizationSettings.ts
+++ /dev/null
@@ -1,99 +0,0 @@
1/*
2 * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import { FALLBACK_LOCALE, SYSTEM_LOCALE } from '@sophie/shared';
22import { reaction } from 'mobx';
23import { addDisposer } from 'mobx-state-tree';
24
25import type MainStore from '../stores/MainStore';
26import { getLogger } from '../utils/log';
27
28const log = getLogger('synchronizeLocalizationSettings');
29
30export const TEST_LOCALE = 'cimode';
31
32/**
33 * Finds the closes requested supported language for the requested one.
34 *
35 * If `language` is supported, this function will return it.
36 * Otherwise, it returns a supported language with the same ISO639 code but
37 * no country code (e.g., `en` for `en-GB`) if it exists.
38 * If no supported language matches, `FALLBACK_LOCALE` will be returned.
39 *
40 * @param language The requested language.
41 * @param supportedLanguages The set of supported languages.
42 * @returns The language to load.
43 */
44function getMatchingLocale(
45 language: string,
46 supportedLanguages: Set<string>,
47): string {
48 // Also let the test locale (i.e., show localization keys directly) through.
49 if (language === TEST_LOCALE || supportedLanguages.has(language)) {
50 return language;
51 }
52 const separatorIndex = language.indexOf('-');
53 if (separatorIndex < 0) {
54 return FALLBACK_LOCALE;
55 }
56 const iso639 = language.slice(0, Math.max(0, separatorIndex));
57 if (supportedLanguages.has(iso639)) {
58 return iso639;
59 }
60 return FALLBACK_LOCALE;
61}
62
63export async function synchronizeLocalizationSettings(
64 store: MainStore,
65 supportedLanguages: string[],
66 osLocale: () => Promise<string>,
67): Promise<void> {
68 const { settings, shared } = store;
69 const supportedLangaugesSet = new Set(supportedLanguages);
70 const setLanguageAsync = async (languageSetting: string) => {
71 const requestedLanguage =
72 languageSetting === SYSTEM_LOCALE ? await osLocale() : languageSetting;
73 const matchingLanguage = getMatchingLocale(
74 requestedLanguage,
75 supportedLangaugesSet,
76 );
77 log.debug(
78 'Setting language',
79 matchingLanguage,
80 'for requested language',
81 requestedLanguage,
82 );
83 shared.setLanguage(matchingLanguage);
84 };
85 const disposer = reaction(
86 () => settings.language,
87 (languageSetting) => {
88 setLanguageAsync(languageSetting).catch((error) => {
89 log.error('Failed to update language', error);
90 });
91 },
92 {
93 fireImmediately: false,
94 },
95 );
96 addDisposer(store, disposer);
97 // Make sure that the language is already set when we resolve.
98 await setLanguageAsync(settings.language);
99}