aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-04-03 02:05:40 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-05-16 00:54:59 +0200
commit49c622189cf1d2c489b963d9be2f7493543afa3a (patch)
tree87636e0c08ce6ee2258566e3e0879707c9a51ea7 /packages
parentbuild: Allow command line arguments to watch (diff)
downloadsophie-49c622189cf1d2c489b963d9be2f7493543afa3a.tar.gz
sophie-49c622189cf1d2c489b963d9be2f7493543afa3a.tar.zst
sophie-49c622189cf1d2c489b963d9be2f7493543afa3a.zip
feat(main): Language setting in config file
Load localization according to either the environment or the configuration file from the list of supported locales. Ideally, we would also set the chromium locale with --lang, but by the time we have read the config file (to known which locale to set), electron has already initialized the chromium resource bundle. So the chromium localization will always be auto-detected by chromium. Also makes startup hopefully a bit faster by doing more things concurrently while the localization and the main window is being loaded. Signed-off-by: Kristóf Marussy <kristof@marussy.com>
Diffstat (limited to 'packages')
-rw-r--r--packages/main/esbuild.config.js12
-rw-r--r--packages/main/package.json1
-rw-r--r--packages/main/src/i18n/loadLocalization.ts11
-rw-r--r--packages/main/src/i18n/synchronizeLocalizationSettings.ts99
-rw-r--r--packages/main/src/initReactions.ts41
-rw-r--r--packages/main/src/stores/SharedStore.ts3
-rw-r--r--packages/main/types/importMeta.d.ts1
-rw-r--r--packages/renderer/src/i18n/loadRendererLoalization.ts5
-rw-r--r--packages/shared/src/index.ts4
-rw-r--r--packages/shared/src/stores/GlobalSettingsBase.ts3
-rw-r--r--packages/shared/src/stores/SharedStoreBase.ts4
11 files changed, 157 insertions, 27 deletions
diff --git a/packages/main/esbuild.config.js b/packages/main/esbuild.config.js
index 996ec5a..ae8565d 100644
--- a/packages/main/esbuild.config.js
+++ b/packages/main/esbuild.config.js
@@ -1,9 +1,15 @@
1import { readdir } from 'node:fs/promises';
2import path from 'node:path';
3
1import getRepoInfo from 'git-repo-info'; 4import getRepoInfo from 'git-repo-info';
2 5
3import { node } from '../../config/buildConstants.js'; 6import { node } from '../../config/buildConstants.js';
4import fileUrlToDirname from '../../config/fileUrlToDirname.js'; 7import fileUrlToDirname from '../../config/fileUrlToDirname.js';
5import getEsbuildConfig from '../../config/getEsbuildConfig.js'; 8import getEsbuildConfig from '../../config/getEsbuildConfig.js';
6 9
10/** @type {string} */
11const thisDir = fileUrlToDirname(import.meta.url);
12
7const externalPackages = ['electron']; 13const externalPackages = ['electron'];
8 14
9if (process.env.MODE !== 'development') { 15if (process.env.MODE !== 'development') {
@@ -12,9 +18,12 @@ if (process.env.MODE !== 'development') {
12 18
13const gitInfo = getRepoInfo(); 19const gitInfo = getRepoInfo();
14 20
21/** @type {string[]} */
22const locales = await readdir(path.join(thisDir, '../../locales'));
23
15export default getEsbuildConfig( 24export default getEsbuildConfig(
16 { 25 {
17 absWorkingDir: fileUrlToDirname(import.meta.url), 26 absWorkingDir: thisDir,
18 entryPoints: ['src/index.ts'], 27 entryPoints: ['src/index.ts'],
19 outfile: 'dist/index.cjs', 28 outfile: 'dist/index.cjs',
20 format: 'cjs', 29 format: 'cjs',
@@ -27,5 +36,6 @@ export default getEsbuildConfig(
27 GIT_SHA: gitInfo.abbreviatedSha, 36 GIT_SHA: gitInfo.abbreviatedSha,
28 GIT_BRANCH: gitInfo.branch, 37 GIT_BRANCH: gitInfo.branch,
29 BUILD_DATE: Date.now(), 38 BUILD_DATE: Date.now(),
39 SUPPORTED_LOCALES: locales,
30 }, 40 },
31); 41);
diff --git a/packages/main/package.json b/packages/main/package.json
index 491e83a..c84816d 100644
--- a/packages/main/package.json
+++ b/packages/main/package.json
@@ -23,6 +23,7 @@
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",
26 "os-name": "^5.0.1", 27 "os-name": "^5.0.1",
27 "slug": "^5.3.0" 28 "slug": "^5.3.0"
28 }, 29 },
diff --git a/packages/main/src/i18n/loadLocalization.ts b/packages/main/src/i18n/loadLocalization.ts
index ec3cf84..0413373 100644
--- a/packages/main/src/i18n/loadLocalization.ts
+++ b/packages/main/src/i18n/loadLocalization.ts
@@ -18,33 +18,31 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import { fallbackLng } from '@sophie/shared'; 21import { FALLBACK_LOCALE } from '@sophie/shared';
22import i18next from 'i18next'; 22import i18next from 'i18next';
23import { autorun } from 'mobx'; 23import { autorun } from 'mobx';
24import { addDisposer } from 'mobx-state-tree'; 24import { addDisposer } from 'mobx-state-tree';
25 25
26import type Resources from '../infrastructure/resources/Resources';
27import type MainStore from '../stores/MainStore'; 26import type MainStore from '../stores/MainStore';
28import { getLogger } from '../utils/log'; 27import { getLogger } from '../utils/log';
29 28
30import I18nStore from './I18nStore'; 29import I18nStore from './I18nStore';
30import LocatlizationRepository from './LocalizationRepository';
31import RepositoryBasedI18nBackend from './RepositoryBasedI18nBackend'; 31import RepositoryBasedI18nBackend from './RepositoryBasedI18nBackend';
32import i18nLog from './i18nLog'; 32import i18nLog from './i18nLog';
33import LocalizationFiles from './impl/LocaltizationFiles';
34 33
35const log = getLogger('loadLocationzation'); 34const log = getLogger('loadLocationzation');
36 35
37export default async function loadLocalization( 36export default async function loadLocalization(
38 store: MainStore, 37 store: MainStore,
39 resources: Resources, 38 repository: LocatlizationRepository,
40 devMode: boolean, 39 devMode: boolean,
41): Promise<void> { 40): Promise<void> {
42 const repository = new LocalizationFiles(resources);
43 const backend = new RepositoryBasedI18nBackend(repository, devMode); 41 const backend = new RepositoryBasedI18nBackend(repository, devMode);
44 const i18n = i18next 42 const i18n = i18next
45 .createInstance({ 43 .createInstance({
46 lng: store.shared.language, 44 lng: store.shared.language,
47 fallbackLng, 45 fallbackLng: [FALLBACK_LOCALE],
48 debug: devMode, 46 debug: devMode,
49 saveMissing: devMode, 47 saveMissing: devMode,
50 }) 48 })
@@ -58,6 +56,7 @@ export default async function loadLocalization(
58 shared: { language }, 56 shared: { language },
59 } = store; 57 } = store;
60 if (i18n.language !== language) { 58 if (i18n.language !== language) {
59 log.debug('Setting language', language);
61 i18n.changeLanguage(language).catch((error) => { 60 i18n.changeLanguage(language).catch((error) => {
62 log.error('Failed to change language', error); 61 log.error('Failed to change language', error);
63 }); 62 });
diff --git a/packages/main/src/i18n/synchronizeLocalizationSettings.ts b/packages/main/src/i18n/synchronizeLocalizationSettings.ts
new file mode 100644
index 0000000..971a593
--- /dev/null
+++ b/packages/main/src/i18n/synchronizeLocalizationSettings.ts
@@ -0,0 +1,99 @@
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 cdff551..b383c8f 100644
--- a/packages/main/src/initReactions.ts
+++ b/packages/main/src/initReactions.ts
@@ -19,8 +19,11 @@
19 */ 19 */
20 20
21import { app, session } from 'electron'; 21import { app, session } from 'electron';
22import { osLocale } from 'os-locale';
22 23
24import LocalizationFiles from './i18n/impl/LocaltizationFiles';
23import loadLocalization from './i18n/loadLocalization'; 25import loadLocalization from './i18n/loadLocalization';
26import { synchronizeLocalizationSettings } from './i18n/synchronizeLocalizationSettings';
24import ConfigFile from './infrastructure/config/impl/ConfigFile'; 27import ConfigFile from './infrastructure/config/impl/ConfigFile';
25import UserAgents from './infrastructure/electron/UserAgents'; 28import UserAgents from './infrastructure/electron/UserAgents';
26import ElectronViewFactory from './infrastructure/electron/impl/ElectronViewFactory'; 29import ElectronViewFactory from './infrastructure/electron/impl/ElectronViewFactory';
@@ -44,28 +47,36 @@ export default async function initReactions(
44 store.shared, 47 store.shared,
45 configRepository, 48 configRepository,
46 ); 49 );
50 const resources = getDistResources(devMode);
51 const localizationLoaded = (async () => {
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.
47 await app.whenReady(); 62 await app.whenReady();
48 const disposeNativeThemeController = synchronizeNativeTheme(store.shared); 63 const disposeNativeThemeController = synchronizeNativeTheme(store.shared);
49 const resources = getDistResources(devMode);
50 hardenSession(resources, devMode, session.defaultSession); 64 hardenSession(resources, devMode, session.defaultSession);
51 if (devMode) {
52 await installDevToolsExtensions();
53 }
54 const userAgents = new UserAgents(app.userAgentFallback); 65 const userAgents = new UserAgents(app.userAgentFallback);
55 app.userAgentFallback = userAgents.fallbackUserAgent(devMode); 66 app.userAgentFallback = userAgents.fallbackUserAgent(devMode);
56 const localizeInterface = async () => { 67 const devToolsLoaded = devMode
57 await loadLocalization(store, resources, devMode); 68 ? installDevToolsExtensions()
58 setApplicationMenu(store, devMode, isMac); 69 : Promise.resolve();
59 };
60 const localization = localizeInterface();
61 const viewFactory = new ElectronViewFactory(userAgents, resources, devMode); 70 const viewFactory = new ElectronViewFactory(userAgents, resources, devMode);
62 const [mainWindow] = await Promise.all([ 71 const mainWindow = (async () => {
63 viewFactory.createMainWindow(store), 72 await localizationLoaded;
64 viewFactory.loadServiceInject(), 73 setApplicationMenu(store, devMode, isMac);
65 ]); 74 await devToolsLoaded;
66 store.setMainWindow(mainWindow); 75 return viewFactory.createMainWindow(store);
76 })();
77 await viewFactory.loadServiceInject();
67 loadServices(store, viewFactory); 78 loadServices(store, viewFactory);
68 await localization; 79 store.setMainWindow(await mainWindow);
69 return () => { 80 return () => {
70 disposeNativeThemeController(); 81 disposeNativeThemeController();
71 disposeConfigController(); 82 disposeConfigController();
diff --git a/packages/main/src/stores/SharedStore.ts b/packages/main/src/stores/SharedStore.ts
index 67d58d6..960b65e 100644
--- a/packages/main/src/stores/SharedStore.ts
+++ b/packages/main/src/stores/SharedStore.ts
@@ -64,6 +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 {
68 self.language = langauge;
69 },
67 setShouldUseDarkColors(shouldUseDarkColors: boolean): void { 70 setShouldUseDarkColors(shouldUseDarkColors: boolean): void {
68 self.shouldUseDarkColors = shouldUseDarkColors; 71 self.shouldUseDarkColors = shouldUseDarkColors;
69 }, 72 },
diff --git a/packages/main/types/importMeta.d.ts b/packages/main/types/importMeta.d.ts
index 7426961..9818ca1 100644
--- a/packages/main/types/importMeta.d.ts
+++ b/packages/main/types/importMeta.d.ts
@@ -7,5 +7,6 @@ interface ImportMeta {
7 GIT_SHA: string; 7 GIT_SHA: string;
8 GIT_BRANCH: string; 8 GIT_BRANCH: string;
9 BUILD_DATE: number; 9 BUILD_DATE: number;
10 SUPPORTED_LOCALES: string[];
10 }; 11 };
11} 12}
diff --git a/packages/renderer/src/i18n/loadRendererLoalization.ts b/packages/renderer/src/i18n/loadRendererLoalization.ts
index 19d1e2d..b078aeb 100644
--- a/packages/renderer/src/i18n/loadRendererLoalization.ts
+++ b/packages/renderer/src/i18n/loadRendererLoalization.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 { fallbackLng, SophieRenderer } from '@sophie/shared'; 21import { FALLBACK_LOCALE, SophieRenderer } from '@sophie/shared';
22import i18next from 'i18next'; 22import i18next from 'i18next';
23import { autorun } from 'mobx'; 23import { autorun } from 'mobx';
24import { addDisposer } from 'mobx-state-tree'; 24import { addDisposer } from 'mobx-state-tree';
@@ -40,7 +40,7 @@ export default function loadRendererLocalization(
40 const i18n = i18next 40 const i18n = i18next
41 .createInstance({ 41 .createInstance({
42 lng: store.shared.language, 42 lng: store.shared.language,
43 fallbackLng, 43 fallbackLng: [FALLBACK_LOCALE],
44 interpolation: { 44 interpolation: {
45 escapeValue: false, // Not needed for react 45 escapeValue: false, // Not needed for react
46 }, 46 },
@@ -73,6 +73,7 @@ export default function loadRendererLocalization(
73 shared: { language }, 73 shared: { language },
74 } = store; 74 } = store;
75 if (i18n.language !== language) { 75 if (i18n.language !== language) {
76 log.debug('Setting language', language);
76 i18n.changeLanguage(language).catch((error) => { 77 i18n.changeLanguage(language).catch((error) => {
77 log.error('Failed to change language', error); 78 log.error('Failed to change language', error);
78 }); 79 });
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
index 51f9f06..c4de885 100644
--- a/packages/shared/src/index.ts
+++ b/packages/shared/src/index.ts
@@ -18,8 +18,6 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21export const fallbackLng = ['en'];
22
23export type { default as SophieRenderer } from './contextBridge/SophieRenderer'; 21export type { default as SophieRenderer } from './contextBridge/SophieRenderer';
24 22
25export { MainToRendererIpcMessage, RendererToMainIpcMessage } from './ipc'; 23export { MainToRendererIpcMessage, RendererToMainIpcMessage } from './ipc';
@@ -44,6 +42,7 @@ export type {
44export { 42export {
45 default as GlobalSettingsBase, 43 default as GlobalSettingsBase,
46 defineGlobalSettingsModel, 44 defineGlobalSettingsModel,
45 SYSTEM_LOCALE,
47} from './stores/GlobalSettingsBase'; 46} from './stores/GlobalSettingsBase';
48 47
49export { default as Profile } from './stores/Profile'; 48export { default as Profile } from './stores/Profile';
@@ -79,4 +78,5 @@ export type {
79export { 78export {
80 default as SharedStoreBase, 79 default as SharedStoreBase,
81 defineSharedStoreModel, 80 defineSharedStoreModel,
81 FALLBACK_LOCALE,
82} from './stores/SharedStoreBase'; 82} from './stores/SharedStoreBase';
diff --git a/packages/shared/src/stores/GlobalSettingsBase.ts b/packages/shared/src/stores/GlobalSettingsBase.ts
index 1bd0628..c74c822 100644
--- a/packages/shared/src/stores/GlobalSettingsBase.ts
+++ b/packages/shared/src/stores/GlobalSettingsBase.ts
@@ -30,10 +30,13 @@ import { ThemeSource } from '../schemas/ThemeSource';
30 30
31import ServiceBase from './ServiceBase'; 31import ServiceBase from './ServiceBase';
32 32
33export const SYSTEM_LOCALE = 'system';
34
33export function defineGlobalSettingsModel<TS extends IAnyModelType>( 35export function defineGlobalSettingsModel<TS extends IAnyModelType>(
34 service: TS, 36 service: TS,
35) { 37) {
36 return types.model('GlobalSettings', { 38 return types.model('GlobalSettings', {
39 language: SYSTEM_LOCALE,
37 themeSource: types.optional( 40 themeSource: types.optional(
38 types.enumeration(ThemeSource.options), 41 types.enumeration(ThemeSource.options),
39 'system', 42 'system',
diff --git a/packages/shared/src/stores/SharedStoreBase.ts b/packages/shared/src/stores/SharedStoreBase.ts
index 86bd0fc..a576a0e 100644
--- a/packages/shared/src/stores/SharedStoreBase.ts
+++ b/packages/shared/src/stores/SharedStoreBase.ts
@@ -31,6 +31,8 @@ import GlobalSettingsBase from './GlobalSettingsBase';
31import ProfileBase from './Profile'; 31import ProfileBase from './Profile';
32import ServiceBase from './ServiceBase'; 32import ServiceBase from './ServiceBase';
33 33
34export const FALLBACK_LOCALE = 'en';
35
34export function defineSharedStoreModel< 36export function defineSharedStoreModel<
35 TG extends IAnyModelType, 37 TG extends IAnyModelType,
36 TP extends IAnyModelType, 38 TP extends IAnyModelType,
@@ -43,7 +45,7 @@ export function defineSharedStoreModel<
43 servicesById: types.map(service), 45 servicesById: types.map(service),
44 services: types.array(types.reference(service)), 46 services: types.array(types.reference(service)),
45 shouldUseDarkColors: false, 47 shouldUseDarkColors: false,
46 language: 'en', 48 language: FALLBACK_LOCALE,
47 }); 49 });
48} 50}
49 51