diff options
Diffstat (limited to 'packages/main')
-rw-r--r-- | packages/main/esbuild.config.js | 12 | ||||
-rw-r--r-- | packages/main/package.json | 1 | ||||
-rw-r--r-- | packages/main/src/i18n/loadLocalization.ts | 11 | ||||
-rw-r--r-- | packages/main/src/i18n/synchronizeLocalizationSettings.ts | 99 | ||||
-rw-r--r-- | packages/main/src/initReactions.ts | 41 | ||||
-rw-r--r-- | packages/main/src/stores/SharedStore.ts | 3 | ||||
-rw-r--r-- | packages/main/types/importMeta.d.ts | 1 |
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 @@ | |||
1 | import { readdir } from 'node:fs/promises'; | ||
2 | import path from 'node:path'; | ||
3 | |||
1 | import getRepoInfo from 'git-repo-info'; | 4 | import getRepoInfo from 'git-repo-info'; |
2 | 5 | ||
3 | import { node } from '../../config/buildConstants.js'; | 6 | import { node } from '../../config/buildConstants.js'; |
4 | import fileUrlToDirname from '../../config/fileUrlToDirname.js'; | 7 | import fileUrlToDirname from '../../config/fileUrlToDirname.js'; |
5 | import getEsbuildConfig from '../../config/getEsbuildConfig.js'; | 8 | import getEsbuildConfig from '../../config/getEsbuildConfig.js'; |
6 | 9 | ||
10 | /** @type {string} */ | ||
11 | const thisDir = fileUrlToDirname(import.meta.url); | ||
12 | |||
7 | const externalPackages = ['electron']; | 13 | const externalPackages = ['electron']; |
8 | 14 | ||
9 | if (process.env.MODE !== 'development') { | 15 | if (process.env.MODE !== 'development') { |
@@ -12,9 +18,12 @@ if (process.env.MODE !== 'development') { | |||
12 | 18 | ||
13 | const gitInfo = getRepoInfo(); | 19 | const gitInfo = getRepoInfo(); |
14 | 20 | ||
21 | /** @type {string[]} */ | ||
22 | const locales = await readdir(path.join(thisDir, '../../locales')); | ||
23 | |||
15 | export default getEsbuildConfig( | 24 | export 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 | ||
21 | import { fallbackLng } from '@sophie/shared'; | 21 | import { FALLBACK_LOCALE } from '@sophie/shared'; |
22 | import i18next from 'i18next'; | 22 | import i18next from 'i18next'; |
23 | import { autorun } from 'mobx'; | 23 | import { autorun } from 'mobx'; |
24 | import { addDisposer } from 'mobx-state-tree'; | 24 | import { addDisposer } from 'mobx-state-tree'; |
25 | 25 | ||
26 | import type Resources from '../infrastructure/resources/Resources'; | ||
27 | import type MainStore from '../stores/MainStore'; | 26 | import type MainStore from '../stores/MainStore'; |
28 | import { getLogger } from '../utils/log'; | 27 | import { getLogger } from '../utils/log'; |
29 | 28 | ||
30 | import I18nStore from './I18nStore'; | 29 | import I18nStore from './I18nStore'; |
30 | import LocatlizationRepository from './LocalizationRepository'; | ||
31 | import RepositoryBasedI18nBackend from './RepositoryBasedI18nBackend'; | 31 | import RepositoryBasedI18nBackend from './RepositoryBasedI18nBackend'; |
32 | import i18nLog from './i18nLog'; | 32 | import i18nLog from './i18nLog'; |
33 | import LocalizationFiles from './impl/LocaltizationFiles'; | ||
34 | 33 | ||
35 | const log = getLogger('loadLocationzation'); | 34 | const log = getLogger('loadLocationzation'); |
36 | 35 | ||
37 | export default async function loadLocalization( | 36 | export 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 | |||
21 | import { FALLBACK_LOCALE, SYSTEM_LOCALE } from '@sophie/shared'; | ||
22 | import { reaction } from 'mobx'; | ||
23 | import { addDisposer } from 'mobx-state-tree'; | ||
24 | |||
25 | import type MainStore from '../stores/MainStore'; | ||
26 | import { getLogger } from '../utils/log'; | ||
27 | |||
28 | const log = getLogger('synchronizeLocalizationSettings'); | ||
29 | |||
30 | export 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 | */ | ||
44 | function 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 | |||
63 | export 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 | ||
21 | import { app, session } from 'electron'; | 21 | import { app, session } from 'electron'; |
22 | import { osLocale } from 'os-locale'; | ||
22 | 23 | ||
24 | import LocalizationFiles from './i18n/impl/LocaltizationFiles'; | ||
23 | import loadLocalization from './i18n/loadLocalization'; | 25 | import loadLocalization from './i18n/loadLocalization'; |
26 | import { synchronizeLocalizationSettings } from './i18n/synchronizeLocalizationSettings'; | ||
24 | import ConfigFile from './infrastructure/config/impl/ConfigFile'; | 27 | import ConfigFile from './infrastructure/config/impl/ConfigFile'; |
25 | import UserAgents from './infrastructure/electron/UserAgents'; | 28 | import UserAgents from './infrastructure/electron/UserAgents'; |
26 | import ElectronViewFactory from './infrastructure/electron/impl/ElectronViewFactory'; | 29 | import 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 | } |