aboutsummaryrefslogtreecommitdiffstats
path: root/packages/main
diff options
context:
space:
mode:
Diffstat (limited to 'packages/main')
-rw-r--r--packages/main/package.json1
-rw-r--r--packages/main/src/i18n/loadLocalization.ts93
-rw-r--r--packages/main/src/i18n/synchronizeLocalizationSettings.ts99
-rw-r--r--packages/main/src/initReactions.ts21
-rw-r--r--packages/main/src/stores/SharedStore.ts5
5 files changed, 88 insertions, 131 deletions
diff --git a/packages/main/package.json b/packages/main/package.json
index c84816d..491e83a 100644
--- a/packages/main/package.json
+++ b/packages/main/package.json
@@ -23,7 +23,6 @@
23 "mobx-state-tree": "^5.1.3", 23 "mobx-state-tree": "^5.1.3",
24 "ms": "^2.1.3", 24 "ms": "^2.1.3",
25 "nanoid": "^3.3.2", 25 "nanoid": "^3.3.2",
26 "os-locale": "^6.0.2",
27 "os-name": "^5.0.1", 26 "os-name": "^5.0.1",
28 "slug": "^5.3.0" 27 "slug": "^5.3.0"
29 }, 28 },
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}
diff --git a/packages/main/src/initReactions.ts b/packages/main/src/initReactions.ts
index b383c8f..94b1f06 100644
--- a/packages/main/src/initReactions.ts
+++ b/packages/main/src/initReactions.ts
@@ -19,11 +19,9 @@
19 */ 19 */
20 20
21import { app, session } from 'electron'; 21import { app, session } from 'electron';
22import { osLocale } from 'os-locale';
23 22
24import LocalizationFiles from './i18n/impl/LocaltizationFiles'; 23import LocalizationFiles from './i18n/impl/LocaltizationFiles';
25import loadLocalization from './i18n/loadLocalization'; 24import loadLocalization from './i18n/loadLocalization';
26import { synchronizeLocalizationSettings } from './i18n/synchronizeLocalizationSettings';
27import ConfigFile from './infrastructure/config/impl/ConfigFile'; 25import ConfigFile from './infrastructure/config/impl/ConfigFile';
28import UserAgents from './infrastructure/electron/UserAgents'; 26import UserAgents from './infrastructure/electron/UserAgents';
29import ElectronViewFactory from './infrastructure/electron/impl/ElectronViewFactory'; 27import ElectronViewFactory from './infrastructure/electron/impl/ElectronViewFactory';
@@ -48,18 +46,17 @@ export default async function initReactions(
48 configRepository, 46 configRepository,
49 ); 47 );
50 const resources = getDistResources(devMode); 48 const resources = getDistResources(devMode);
51 const localizationLoaded = (async () => { 49 // Ideally, we would the the chromium `--lang` according to the settings store here,
52 const localizationFiles = new LocalizationFiles(resources);
53 await synchronizeLocalizationSettings(
54 store,
55 import.meta.env.SUPPORTED_LOCALES,
56 osLocale,
57 );
58 await loadLocalization(store, localizationFiles, devMode);
59 })();
60 // Ideally, we would the the chromium `--lang` here,
61 // but `app.isReady()` is often already `true`, so we're too late to do that. 50 // but `app.isReady()` is often already `true`, so we're too late to do that.
62 await app.whenReady(); 51 await app.whenReady();
52 const localizationFiles = new LocalizationFiles(resources);
53 const localizationLoaded = loadLocalization(
54 store,
55 app.getLocale(),
56 import.meta.env.SUPPORTED_LOCALES,
57 localizationFiles,
58 devMode,
59 );
63 const disposeNativeThemeController = synchronizeNativeTheme(store.shared); 60 const disposeNativeThemeController = synchronizeNativeTheme(store.shared);
64 hardenSession(resources, devMode, session.defaultSession); 61 hardenSession(resources, devMode, session.defaultSession);
65 const userAgents = new UserAgents(app.userAgentFallback); 62 const userAgents = new UserAgents(app.userAgentFallback);
diff --git a/packages/main/src/stores/SharedStore.ts b/packages/main/src/stores/SharedStore.ts
index 960b65e..3aa8c73 100644
--- a/packages/main/src/stores/SharedStore.ts
+++ b/packages/main/src/stores/SharedStore.ts
@@ -18,7 +18,7 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import { defineSharedStoreModel } from '@sophie/shared'; 21import { defineSharedStoreModel, WritingDirection } from '@sophie/shared';
22import { getSnapshot, Instance } from 'mobx-state-tree'; 22import { getSnapshot, Instance } from 'mobx-state-tree';
23 23
24import { getLogger } from '../utils/log'; 24import { getLogger } from '../utils/log';
@@ -64,8 +64,9 @@ const SharedStore = defineSharedStoreModel(GlobalSettings, Profile, Service)
64 loadConfig(config: Config): void { 64 loadConfig(config: Config): void {
65 loadConfig(self, config); 65 loadConfig(self, config);
66 }, 66 },
67 setLanguage(langauge: string): void { 67 setLanguage(langauge: string, writingDirection: WritingDirection): void {
68 self.language = langauge; 68 self.language = langauge;
69 self.writingDirection = writingDirection;
69 }, 70 },
70 setShouldUseDarkColors(shouldUseDarkColors: boolean): void { 71 setShouldUseDarkColors(shouldUseDarkColors: boolean): void {
71 self.shouldUseDarkColors = shouldUseDarkColors; 72 self.shouldUseDarkColors = shouldUseDarkColors;