aboutsummaryrefslogtreecommitdiffstats
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
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>
-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
-rw-r--r--yarn.lock26
12 files changed, 183 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
diff --git a/yarn.lock b/yarn.lock
index 5a844c8..768f3c0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1297,6 +1297,7 @@ __metadata:
1297 mobx-state-tree: ^5.1.3 1297 mobx-state-tree: ^5.1.3
1298 ms: ^2.1.3 1298 ms: ^2.1.3
1299 nanoid: ^3.3.2 1299 nanoid: ^3.3.2
1300 os-locale: ^6.0.2
1300 os-name: ^5.0.1 1301 os-name: ^5.0.1
1301 slug: ^5.3.0 1302 slug: ^5.3.0
1302 source-map-support: ^0.5.21 1303 source-map-support: ^0.5.21
@@ -5219,6 +5220,13 @@ __metadata:
5219 languageName: node 5220 languageName: node
5220 linkType: hard 5221 linkType: hard
5221 5222
5223"invert-kv@npm:^3.0.0":
5224 version: 3.0.1
5225 resolution: "invert-kv@npm:3.0.1"
5226 checksum: 782c44c97f8b693006f5ba0995301754bf68d2160ec98fc34d96b266e2c28cc0c91d86c341ca058fe993bc3dd91f104f776a40f04b6c75254a9a1a0d716ac814
5227 languageName: node
5228 linkType: hard
5229
5222"ip@npm:^1.1.5": 5230"ip@npm:^1.1.5":
5223 version: 1.1.5 5231 version: 1.1.5
5224 resolution: "ip@npm:1.1.5" 5232 resolution: "ip@npm:1.1.5"
@@ -6421,6 +6429,15 @@ __metadata:
6421 languageName: node 6429 languageName: node
6422 linkType: hard 6430 linkType: hard
6423 6431
6432"lcid@npm:^3.1.1":
6433 version: 3.1.1
6434 resolution: "lcid@npm:3.1.1"
6435 dependencies:
6436 invert-kv: ^3.0.0
6437 checksum: 7ebab7a2696a3cc6c6c9f25d957ef81dd2a8a2f48b7e2a9185e4bbcfc36d70cb633acf5fa5c9508f3d30badf23a303b1b6afe0bba8f0bb7d353d0f5d59c9ec1b
6438 languageName: node
6439 linkType: hard
6440
6424"leven@npm:^3.1.0": 6441"leven@npm:^3.1.0":
6425 version: 3.1.0 6442 version: 3.1.0
6426 resolution: "leven@npm:3.1.0" 6443 resolution: "leven@npm:3.1.0"
@@ -7147,6 +7164,15 @@ __metadata:
7147 languageName: node 7164 languageName: node
7148 linkType: hard 7165 linkType: hard
7149 7166
7167"os-locale@npm:^6.0.2":
7168 version: 6.0.2
7169 resolution: "os-locale@npm:6.0.2"
7170 dependencies:
7171 lcid: ^3.1.1
7172 checksum: 812d73334c8773b971bf7fd257b84d2ce7b85d5d2184370f2875fe0e51451f530d6f7c272de1faa0b9ff02d0d10dafd665b6425ed85489271705ab5738691a43
7173 languageName: node
7174 linkType: hard
7175
7150"os-name@npm:^5.0.1": 7176"os-name@npm:^5.0.1":
7151 version: 5.0.1 7177 version: 5.0.1
7152 resolution: "os-name@npm:5.0.1" 7178 resolution: "os-name@npm:5.0.1"