aboutsummaryrefslogtreecommitdiffstats
path: root/packages/main
diff options
context:
space:
mode:
Diffstat (limited to 'packages/main')
-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
7 files changed, 146 insertions, 22 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}