aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-03-30 01:36:22 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-05-16 00:54:57 +0200
commit7af01713180066b6dc1061dae930840e48c60fec (patch)
tree604a52eb8f167caf3400dad7589aaa5be2d06c8d
parentfeat: Add custom menubar (diff)
downloadsophie-7af01713180066b6dc1061dae930840e48c60fec.tar.gz
sophie-7af01713180066b6dc1061dae930840e48c60fec.tar.zst
sophie-7af01713180066b6dc1061dae930840e48c60fec.zip
feat(main): Add localization support
Add i18next with a custom backend to the main process to load localization from file. Missing localizations are written to a missing localizations file in debug mode, but silently fall back in production mode. We will also need to add a custom backend for the renderer process that communicates with the main process. (i18next-fs-electron-backend is not applicable here, because we need localizations both in the main and renderer processes.) Signed-off-by: Kristóf Marussy <kristof@marussy.com>
-rw-r--r--.electron-builder.config.cjs1
-rw-r--r--locales/en/translation.json20
-rw-r--r--package.json5
-rw-r--r--packages/main/package.json1
-rw-r--r--packages/main/src/i18n/I18nStore.ts104
-rw-r--r--packages/main/src/i18n/LocalizationRepository.ts39
-rw-r--r--packages/main/src/i18n/RepositoryBasedI18nBackend.ts224
-rw-r--r--packages/main/src/i18n/i18nLog.ts41
-rw-r--r--packages/main/src/i18n/impl/LocaltizationFiles.ts93
-rw-r--r--packages/main/src/i18n/loadLocalization.ts50
-rw-r--r--packages/main/src/infrastructure/config/impl/ConfigFile.ts5
-rw-r--r--packages/main/src/infrastructure/electron/impl/setApplicationMenu.ts29
-rw-r--r--packages/main/src/infrastructure/resources/Resources.ts2
-rw-r--r--packages/main/src/infrastructure/resources/impl/getDistResources.ts4
-rw-r--r--packages/main/src/initReactions.ts8
-rw-r--r--packages/main/src/stores/MainStore.ts9
-rw-r--r--packages/main/src/utils/isErrno.ts28
-rw-r--r--yarn.lock133
18 files changed, 666 insertions, 130 deletions
diff --git a/.electron-builder.config.cjs b/.electron-builder.config.cjs
index aa7d9e0..4340623 100644
--- a/.electron-builder.config.cjs
+++ b/.electron-builder.config.cjs
@@ -17,6 +17,7 @@ const config = {
17 'packages/renderer/dist/**', 17 'packages/renderer/dist/**',
18 'packages/service-inject/dist/**', 18 'packages/service-inject/dist/**',
19 'packages/service-preload/dist/**', 19 'packages/service-preload/dist/**',
20 'locales/**',
20 // Do not ship with source maps. 21 // Do not ship with source maps.
21 '!**/*.map', 22 '!**/*.map',
22 ], 23 ],
diff --git a/locales/en/translation.json b/locales/en/translation.json
new file mode 100644
index 0000000..2cd7959
--- /dev/null
+++ b/locales/en/translation.json
@@ -0,0 +1,20 @@
1{
2 "menu": {
3 "servicesMenu": "Services",
4 "view": {
5 "showLocationBar": "Show Location Bar",
6 "reload": "Reload",
7 "forceReload": "Force Reload",
8 "toggleDeveloperTools": "Toggle Developer Tools",
9 "reloadSophie": "Reload Sophie",
10 "toggleSophieDeveloperTools": "Toggle Sophie Developer Tools"
11 },
12 "services": {
13 "nextService": "Next Service",
14 "previousService": "Previous Service"
15 },
16 "help": {
17 "gitlab": "Gitlab"
18 }
19 }
20}
diff --git a/package.json b/package.json
index fcba86e..7f0d9f9 100644
--- a/package.json
+++ b/package.json
@@ -94,8 +94,5 @@
94 "typescript": "^4.5.5", 94 "typescript": "^4.5.5",
95 "vite": "^2.8.4" 95 "vite": "^2.8.4"
96 }, 96 },
97 "packageManager": "yarn@3.1.1", 97 "packageManager": "yarn@3.1.1"
98 "dependencies": {
99 "preload": "^0.1.0"
100 }
101} 98}
diff --git a/packages/main/package.json b/packages/main/package.json
index 4ba6d06..85f9b12 100644
--- a/packages/main/package.json
+++ b/packages/main/package.json
@@ -14,6 +14,7 @@
14 "deep-equal": "^2.0.5", 14 "deep-equal": "^2.0.5",
15 "electron": "17.1.0", 15 "electron": "17.1.0",
16 "fs-extra": "^10.0.1", 16 "fs-extra": "^10.0.1",
17 "i18next": "^21.6.14",
17 "json5": "^2.2.0", 18 "json5": "^2.2.0",
18 "lodash-es": "^4.17.21", 19 "lodash-es": "^4.17.21",
19 "loglevel": "^1.8.0", 20 "loglevel": "^1.8.0",
diff --git a/packages/main/src/i18n/I18nStore.ts b/packages/main/src/i18n/I18nStore.ts
new file mode 100644
index 0000000..d833e8a
--- /dev/null
+++ b/packages/main/src/i18n/I18nStore.ts
@@ -0,0 +1,104 @@
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 type { i18n, TFunction } from 'i18next';
22import { IAtom, createAtom } from 'mobx';
23
24import { getLogger } from '../utils/log';
25
26const log = getLogger('I18nStore');
27
28export type UseTranslationResult =
29 | { ready: true; i18n: i18n; t: TFunction }
30 | { ready: false };
31
32export default class I18nStore {
33 private readonly languageChangedAtom: IAtom;
34
35 private readonly namespaceLoadedAtoms: Map<string, IAtom> = new Map();
36
37 private readonly notifyLanguageChange = () =>
38 this.languageChangedAtom.reportObserved();
39
40 constructor(private readonly i18next: i18n) {
41 this.languageChangedAtom = createAtom(
42 'i18next',
43 () => i18next.on('languageChanged', this.notifyLanguageChange),
44 () => i18next.off('languageChanged', this.notifyLanguageChange),
45 );
46 }
47
48 useTranslation(ns?: string): UseTranslationResult {
49 const observed = this.languageChangedAtom.reportObserved();
50 const namespaceToLoad =
51 ns ?? this.i18next.options.defaultNS ?? 'translation';
52 if (
53 this.i18next.isInitialized &&
54 this.i18next.hasLoadedNamespace(namespaceToLoad)
55 ) {
56 return {
57 ready: true,
58 i18n: this.i18next,
59 // eslint-disable-next-line unicorn/no-null -- `i18next` API requires `null`.
60 t: this.i18next.getFixedT(null, namespaceToLoad),
61 };
62 }
63 if (observed) {
64 this.loadNamespace(namespaceToLoad);
65 }
66 return { ready: false };
67 }
68
69 private loadNamespace(ns: string): void {
70 const existingAtom = this.namespaceLoadedAtoms.get(ns);
71 if (existingAtom !== undefined) {
72 existingAtom.reportObserved();
73 return;
74 }
75 const atom = createAtom(`i18next-${ns}`);
76 this.namespaceLoadedAtoms.set(ns, atom);
77 atom.reportObserved();
78
79 const loaded = () => {
80 this.namespaceLoadedAtoms.delete(ns);
81 atom.reportChanged();
82 };
83
84 const loadAsync = async () => {
85 await this.i18next.loadNamespaces([ns]);
86 if (this.i18next.isInitialized) {
87 setImmediate(loaded);
88 return;
89 }
90 const initialized = () => {
91 setImmediate(() => {
92 this.i18next.off('initialized', initialized);
93 loaded();
94 });
95 };
96 this.i18next.on('initialized', initialized);
97 };
98
99 loadAsync().catch((error) => {
100 log.error('Failed to load translations for namespace', ns, error);
101 setImmediate(loaded);
102 });
103 }
104}
diff --git a/packages/main/src/i18n/LocalizationRepository.ts b/packages/main/src/i18n/LocalizationRepository.ts
new file mode 100644
index 0000000..041449d
--- /dev/null
+++ b/packages/main/src/i18n/LocalizationRepository.ts
@@ -0,0 +1,39 @@
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 type { ResourceKey } from 'i18next';
22
23export default interface LocatlizationRepository {
24 getResourceContents(
25 language: string,
26 namespace: string,
27 ): Promise<ResourceKey>;
28
29 getMissingResourceContents(
30 language: string,
31 namespace: string,
32 ): Promise<ResourceKey>;
33
34 setMissingResourceContents(
35 language: string,
36 namespace: string,
37 data: ResourceKey,
38 ): Promise<void>;
39}
diff --git a/packages/main/src/i18n/RepositoryBasedI18nBackend.ts b/packages/main/src/i18n/RepositoryBasedI18nBackend.ts
new file mode 100644
index 0000000..4d87ce9
--- /dev/null
+++ b/packages/main/src/i18n/RepositoryBasedI18nBackend.ts
@@ -0,0 +1,224 @@
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 type {
22 BackendModule,
23 InitOptions,
24 ReadCallback,
25 Services,
26} from 'i18next';
27import { debounce } from 'lodash-es';
28import ms from 'ms';
29
30import { getLogger } from '../utils/log';
31
32import type LocatlizationRepository from './LocalizationRepository';
33
34const MISSING_ENTRIES_DEBOUNCE_TIME = ms('1s');
35
36const log = getLogger('RepositoryBasedI18nBackend');
37
38/**
39 * I18next backend for loading translations from a file.
40 *
41 * Loosely based on [i18next-fs-backend](https://github.com/i18next/i18next-fs-backend),
42 * but we shrink bundle size by omitting JSON5 or YAML translation handling.
43 * Direct file IO is replaced by the `LocalizationRepository`,
44 * which handles platform-specific concerns.
45 */
46export default class RepositoryBasedI18nBackend
47 implements BackendModule<unknown>
48{
49 type = 'backend' as const;
50
51 private writeQueue: Map<string, Map<string, Map<string, string>>> = new Map();
52
53 private keySeparator: string | false = '.';
54
55 private readonly flushWriteQueueImmediately = () => {
56 const savedWriteQueue = this.writeQueue;
57 this.writeQueue = new Map();
58 savedWriteQueue.forEach((languagesMap, namespace) => {
59 languagesMap.forEach((keysMap, language) => {
60 this.flushChanges(language, namespace, keysMap).catch((error) => {
61 log.error(
62 'Cannot write missing',
63 language,
64 'translations for namespace',
65 namespace,
66 error,
67 );
68 });
69 });
70 });
71 };
72
73 private readonly flushWriteQueue = debounce(
74 this.flushWriteQueueImmediately,
75 MISSING_ENTRIES_DEBOUNCE_TIME,
76 );
77
78 constructor(
79 private readonly repository: LocatlizationRepository,
80 private readonly devMode = false,
81 ) {}
82
83 init(
84 _services: Services,
85 _backendOptions: unknown,
86 { keySeparator }: InitOptions,
87 ) {
88 if (keySeparator !== undefined) {
89 this.keySeparator = keySeparator;
90 }
91 }
92
93 read(language: string, namespace: string, callback: ReadCallback): void {
94 const readAsync = async () => {
95 const translations = await this.repository.getResourceContents(
96 language,
97 namespace,
98 );
99 // eslint-disable-next-line unicorn/no-null -- `i18next` API requires `null`.
100 setImmediate(() => callback(null, translations));
101 };
102
103 readAsync().catch((error) => {
104 log.error(
105 'Error while loading',
106 language,
107 'translations for namespace',
108 namespace,
109 error,
110 );
111 const callbackError =
112 error instanceof Error
113 ? error
114 : new Error(`Unknown error: ${JSON.stringify(error)}`);
115 /*
116 eslint-disable-next-line promise/no-callback-in-promise, unicorn/no-null --
117 Converting from promise based API to a callback. `i18next` API requires `null`.
118 */
119 setImmediate(() => callback(callbackError, null));
120 });
121 }
122
123 create(
124 languages: string[],
125 namespace: string,
126 key: string,
127 fallbackValue: string,
128 ): void {
129 if (!this.devMode) {
130 throw new Error(
131 'Refusing to write missing translations in production mode',
132 );
133 }
134 let languagesMapOrUndefined = this.writeQueue.get(namespace);
135 if (languagesMapOrUndefined === undefined) {
136 languagesMapOrUndefined = new Map();
137 this.writeQueue.set(namespace, languagesMapOrUndefined);
138 }
139 const languagesMap = languagesMapOrUndefined;
140 languages.forEach((language) => {
141 let keysMap = languagesMap.get(language);
142 if (keysMap === undefined) {
143 keysMap = new Map();
144 languagesMap.set(language, keysMap);
145 }
146 keysMap.set(key, fallbackValue);
147 });
148 this.flushWriteQueue();
149 }
150
151 /**
152 * Flushes new translations to the missing translations file.
153 *
154 * Will always fails if `devMode` is `false`.
155 * If no changes are made to the translations, the file won't be rewritten.
156 *
157 * @param language The language to add translations for.
158 * @param namespace The namespace to add translations for.
159 * @param entries The translations to add.
160 * @returns A promise that resolves when changes were flushed successfully.
161 */
162 private async flushChanges(
163 language: string,
164 namespace: string,
165 entries: Map<string, string>,
166 ): Promise<void> {
167 if (!this.devMode) {
168 throw new Error(
169 'Refusing to write missing translations in production mode',
170 );
171 }
172 const resourceContents = await this.repository.getMissingResourceContents(
173 language,
174 namespace,
175 );
176 const translations =
177 typeof resourceContents === 'object' ? resourceContents : {};
178 let changed = false;
179 entries.forEach((value, key) => {
180 const splitKey = this.keySeparator ? key.split(this.keySeparator) : [key];
181 let obj = translations;
182 /*
183 eslint-disable @typescript-eslint/no-unsafe-assignment,
184 security/detect-object-injection --
185 We need to modify raw objects, because this is the storage format for i18next.
186 To mitigate the prototype injection security risk,
187 we make sure to only ever run this in development mode.
188 */
189 for (let i = 0; i < splitKey.length - 1; i += 1) {
190 let nextObj = obj[splitKey[i]];
191 if (typeof nextObj !== 'object') {
192 nextObj = {};
193 obj[splitKey[i]] = nextObj;
194 }
195 obj = nextObj;
196 }
197 const lastKey = splitKey[splitKey.length - 1];
198 if (obj[lastKey] !== value) {
199 obj[lastKey] = value;
200 changed = true;
201 }
202 /*
203 eslint-enable @typescript-eslint/no-unsafe-assignment,
204 security/detect-object-injection
205 */
206 });
207 if (!changed) {
208 return;
209 }
210 await this.repository.setMissingResourceContents(
211 language,
212 namespace,
213 translations,
214 );
215 log.debug(
216 'Wrote',
217 entries.size,
218 'missing',
219 language,
220 'translations for namespace',
221 namespace,
222 );
223 }
224}
diff --git a/packages/main/src/i18n/i18nLog.ts b/packages/main/src/i18n/i18nLog.ts
new file mode 100644
index 0000000..33a8c4e
--- /dev/null
+++ b/packages/main/src/i18n/i18nLog.ts
@@ -0,0 +1,41 @@
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 type { LoggerModule } from 'i18next';
22
23import { getLogger } from '../utils/log';
24
25const log = getLogger('i18nLog');
26
27/*
28 eslint-disable-next-line @typescript-eslint/unbound-method --
29 loglevel log methods don't rely on `this`, so this is safe,
30 and also keeps stacktraces smaller.
31*/
32const { debug, warn, error } = log;
33
34const i18nLog: LoggerModule = {
35 type: 'logger',
36 log: debug,
37 warn,
38 error,
39};
40
41export default i18nLog;
diff --git a/packages/main/src/i18n/impl/LocaltizationFiles.ts b/packages/main/src/i18n/impl/LocaltizationFiles.ts
new file mode 100644
index 0000000..73a769e
--- /dev/null
+++ b/packages/main/src/i18n/impl/LocaltizationFiles.ts
@@ -0,0 +1,93 @@
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 { readFile, writeFile } from 'node:fs/promises';
22
23import type { ResourceKey } from 'i18next';
24
25import type Resources from '../../infrastructure/resources/Resources';
26import isErrno from '../../utils/isErrno';
27import { getLogger } from '../../utils/log';
28import type LocatlizationRepository from '../LocalizationRepository';
29
30const log = getLogger('LocalizationFiles');
31
32export default class LocalizationFiles implements LocatlizationRepository {
33 constructor(private readonly resources: Resources) {}
34
35 async getResourceContents(
36 language: string,
37 namespace: string,
38 ): Promise<ResourceKey> {
39 const fileName = this.resources.getLocalizationPath(
40 language,
41 `${namespace}.json`,
42 );
43 const contents = await readFile(fileName, 'utf8');
44 log.info('Read localization file', fileName);
45 // The contents of the file should come from a signed archive during production,
46 // so we don't need to (and can't) validate them.
47 return JSON.parse(contents) as ResourceKey;
48 }
49
50 private getMissingLocalizationPath(
51 language: string,
52 namespace: string,
53 ): string {
54 return this.resources.getLocalizationPath(
55 language,
56 `${namespace}.missing.json`,
57 );
58 }
59
60 async getMissingResourceContents(
61 language: string,
62 namespace: string,
63 ): Promise<ResourceKey> {
64 const fileName = this.getMissingLocalizationPath(language, namespace);
65 try {
66 const contents = await readFile(fileName, 'utf8');
67 // The contents of the file are only used during development,
68 // so we don't need to (and can't) validate them.
69 return JSON.parse(contents) as ResourceKey;
70 } catch (error) {
71 if (isErrno(error, 'ENOENT')) {
72 log.debug(
73 'No missing translations file',
74 language,
75 'for namespace',
76 namespace,
77 );
78 return {};
79 }
80 throw error;
81 }
82 }
83
84 setMissingResourceContents(
85 language: string,
86 namespace: string,
87 data: ResourceKey,
88 ): Promise<void> {
89 const fileName = this.getMissingLocalizationPath(language, namespace);
90 const contents = JSON.stringify(data, undefined, 2);
91 return writeFile(fileName, contents, 'utf8');
92 }
93}
diff --git a/packages/main/src/i18n/loadLocalization.ts b/packages/main/src/i18n/loadLocalization.ts
new file mode 100644
index 0000000..1408a30
--- /dev/null
+++ b/packages/main/src/i18n/loadLocalization.ts
@@ -0,0 +1,50 @@
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 i18next from 'i18next';
22
23import type Resources from '../infrastructure/resources/Resources';
24import type MainStore from '../stores/MainStore';
25
26import I18nStore from './I18nStore';
27import RepositoryBasedI18nBackend from './RepositoryBasedI18nBackend';
28import i18nLog from './i18nLog';
29import LocalizationFiles from './impl/LocaltizationFiles';
30
31export default async function loadLocalization(
32 store: MainStore,
33 resources: Resources,
34 devMode: boolean,
35): Promise<void> {
36 const repository = new LocalizationFiles(resources);
37 const backend = new RepositoryBasedI18nBackend(repository, devMode);
38 const i18n = i18next
39 .createInstance({
40 lng: 'en',
41 fallbackLng: ['en'],
42 debug: devMode,
43 saveMissing: devMode,
44 })
45 .use(backend)
46 .use(i18nLog);
47 await i18n.init();
48 const i18nStore = new I18nStore(i18n);
49 store.setI18n(i18nStore);
50}
diff --git a/packages/main/src/infrastructure/config/impl/ConfigFile.ts b/packages/main/src/infrastructure/config/impl/ConfigFile.ts
index e8237b4..4ad0fcc 100644
--- a/packages/main/src/infrastructure/config/impl/ConfigFile.ts
+++ b/packages/main/src/infrastructure/config/impl/ConfigFile.ts
@@ -27,6 +27,7 @@ import { throttle } from 'lodash-es';
27 27
28import type Config from '../../../stores/config/Config'; 28import type Config from '../../../stores/config/Config';
29import type Disposer from '../../../utils/Disposer'; 29import type Disposer from '../../../utils/Disposer';
30import isErrno from '../../../utils/isErrno';
30import { getLogger } from '../../../utils/log'; 31import { getLogger } from '../../../utils/log';
31import type ConfigRepository from '../ConfigRepository'; 32import type ConfigRepository from '../ConfigRepository';
32import type { ReadConfigResult } from '../ConfigRepository'; 33import type { ReadConfigResult } from '../ConfigRepository';
@@ -55,7 +56,7 @@ export default class ConfigFile implements ConfigRepository {
55 try { 56 try {
56 configStr = await readFile(this.#configFilePath, 'utf8'); 57 configStr = await readFile(this.#configFilePath, 'utf8');
57 } catch (error) { 58 } catch (error) {
58 if ((error as NodeJS.ErrnoException).code === 'ENOENT') { 59 if (isErrno(error, 'ENOENT')) {
59 log.debug('Config file', this.#configFilePath, 'was not found'); 60 log.debug('Config file', this.#configFilePath, 'was not found');
60 return { found: false }; 61 return { found: false };
61 } 62 }
@@ -94,7 +95,7 @@ export default class ConfigFile implements ConfigRepository {
94 mtime = stats.mtime; 95 mtime = stats.mtime;
95 log.trace('Config file last modified at', mtime); 96 log.trace('Config file last modified at', mtime);
96 } catch (error) { 97 } catch (error) {
97 if ((error as NodeJS.ErrnoException).code === 'ENOENT') { 98 if (isErrno(error, 'ENOENT')) {
98 log.debug( 99 log.debug(
99 'Config file', 100 'Config file',
100 this.#configFilePath, 101 this.#configFilePath,
diff --git a/packages/main/src/infrastructure/electron/impl/setApplicationMenu.ts b/packages/main/src/infrastructure/electron/impl/setApplicationMenu.ts
index 5166719..49bfbfd 100644
--- a/packages/main/src/infrastructure/electron/impl/setApplicationMenu.ts
+++ b/packages/main/src/infrastructure/electron/impl/setApplicationMenu.ts
@@ -30,6 +30,12 @@ export default function setApplicationMenu(
30 isMac: boolean, 30 isMac: boolean,
31): void { 31): void {
32 const dispose = autorun(() => { 32 const dispose = autorun(() => {
33 const translation = store.useTranslation();
34 if (!translation.ready) {
35 return;
36 }
37 const { t } = translation;
38
33 const { settings, shared, visibleService } = store; 39 const { settings, shared, visibleService } = store;
34 const { showLocationBar, selectedService } = settings; 40 const { showLocationBar, selectedService } = settings;
35 const { canSwitchServices, services } = shared; 41 const { canSwitchServices, services } = shared;
@@ -42,7 +48,7 @@ export default function setApplicationMenu(
42 role: 'viewMenu', 48 role: 'viewMenu',
43 submenu: [ 49 submenu: [
44 { 50 {
45 label: 'Show Location Bar', 51 label: t<string>('menu.view.showLocationBar'),
46 accelerator: 'CommandOrControl+Shift+L', 52 accelerator: 'CommandOrControl+Shift+L',
47 type: 'checkbox', 53 type: 'checkbox',
48 checked: showLocationBar, 54 checked: showLocationBar,
@@ -52,7 +58,7 @@ export default function setApplicationMenu(
52 }, 58 },
53 { type: 'separator' }, 59 { type: 'separator' },
54 { 60 {
55 label: 'Reload', 61 label: t<string>('menu.view.reload'),
56 accelerator: 'CommandOrControl+R', 62 accelerator: 'CommandOrControl+R',
57 enabled: selectedService !== undefined, 63 enabled: selectedService !== undefined,
58 click() { 64 click() {
@@ -60,7 +66,7 @@ export default function setApplicationMenu(
60 }, 66 },
61 }, 67 },
62 { 68 {
63 label: 'Force Reload', 69 label: t<string>('menu.view.forceReload'),
64 accelerator: 'CommandOrControl+Shift+R', 70 accelerator: 'CommandOrControl+Shift+R',
65 enabled: selectedService !== undefined, 71 enabled: selectedService !== undefined,
66 click() { 72 click() {
@@ -68,7 +74,7 @@ export default function setApplicationMenu(
68 }, 74 },
69 }, 75 },
70 { 76 {
71 label: 'Toggle Developer Tools', 77 label: t<string>('menu.view.toggleDeveloperTools'),
72 accelerator: 'CommandOrControl+Shift+I', 78 accelerator: 'CommandOrControl+Shift+I',
73 enabled: visibleService !== undefined, 79 enabled: visibleService !== undefined,
74 click() { 80 click() {
@@ -80,12 +86,15 @@ export default function setApplicationMenu(
80 ? ([ 86 ? ([
81 { 87 {
82 role: 'forceReload', 88 role: 'forceReload',
83 label: 'Reload Sophie', 89 label: t<string>('menu.view.reloadSophie'),
84 accelerator: 'CommandOrControl+Shift+Alt+R', 90 accelerator: 'CommandOrControl+Shift+Alt+R',
85 }, 91 },
86 { 92 {
87 role: 'toggleDevTools', 93 role: 'toggleDevTools',
88 label: 'Toggle Sophie Developer Tools', 94 label: t<string>(
95 'menu.view.toggleSophieDeveloperTools',
96 'Toggle Sophie Developer Tools',
97 ),
89 accelerator: 'CommandOrControl+Shift+Alt+I', 98 accelerator: 'CommandOrControl+Shift+Alt+I',
90 }, 99 },
91 { type: 'separator' }, 100 { type: 'separator' },
@@ -95,10 +104,10 @@ export default function setApplicationMenu(
95 ], 104 ],
96 }, 105 },
97 { 106 {
98 label: 'Services', 107 label: t<string>('menu.servicesMenu'),
99 submenu: [ 108 submenu: [
100 { 109 {
101 label: 'Next Service', 110 label: t<string>('menu.services.nextService'),
102 accelerator: 'CommandOrControl+Tab', 111 accelerator: 'CommandOrControl+Tab',
103 enabled: canSwitchServices, 112 enabled: canSwitchServices,
104 click() { 113 click() {
@@ -106,7 +115,7 @@ export default function setApplicationMenu(
106 }, 115 },
107 }, 116 },
108 { 117 {
109 label: 'Previous Service', 118 label: t<string>('menu.services.previousService'),
110 accelerator: 'CommandOrControl+Shift+Tab', 119 accelerator: 'CommandOrControl+Shift+Tab',
111 enabled: canSwitchServices, 120 enabled: canSwitchServices,
112 click() { 121 click() {
@@ -140,7 +149,7 @@ export default function setApplicationMenu(
140 role: 'help', 149 role: 'help',
141 submenu: [ 150 submenu: [
142 { 151 {
143 label: 'Gitlab', 152 label: t<string>('menu.help.gitlab'),
144 click() { 153 click() {
145 store.openWebpageInBrowser(); 154 store.openWebpageInBrowser();
146 }, 155 },
diff --git a/packages/main/src/infrastructure/resources/Resources.ts b/packages/main/src/infrastructure/resources/Resources.ts
index 269c838..0e90c6b 100644
--- a/packages/main/src/infrastructure/resources/Resources.ts
+++ b/packages/main/src/infrastructure/resources/Resources.ts
@@ -21,6 +21,8 @@
21export default interface Resources { 21export default interface Resources {
22 getPath(packageName: string, relativePathInPackage: string): string; 22 getPath(packageName: string, relativePathInPackage: string): string;
23 23
24 getLocalizationPath(language: string, fileName: string): string;
25
24 getFileURL(packageName: string, relativePathInPackage: string): string; 26 getFileURL(packageName: string, relativePathInPackage: string): string;
25 27
26 getRendererURL(relativePathInRendererPackage: string): string; 28 getRendererURL(relativePathInRendererPackage: string): string;
diff --git a/packages/main/src/infrastructure/resources/impl/getDistResources.ts b/packages/main/src/infrastructure/resources/impl/getDistResources.ts
index f3c3f7b..3c1ffb3 100644
--- a/packages/main/src/infrastructure/resources/impl/getDistResources.ts
+++ b/packages/main/src/infrastructure/resources/impl/getDistResources.ts
@@ -33,6 +33,7 @@ export default function getDistResources(
33 devServerURL = import.meta.env?.VITE_DEV_SERVER_URL, 33 devServerURL = import.meta.env?.VITE_DEV_SERVER_URL,
34): Resources { 34): Resources {
35 const packagesRoot = path.join(thisDir, '..', '..'); 35 const packagesRoot = path.join(thisDir, '..', '..');
36 const localizationRoot = path.join(packagesRoot, '..', 'locales');
36 37
37 function getPath(packageName: string, relativePathInPackage: string): string { 38 function getPath(packageName: string, relativePathInPackage: string): string {
38 return path.join(packagesRoot, packageName, 'dist', relativePathInPackage); 39 return path.join(packagesRoot, packageName, 'dist', relativePathInPackage);
@@ -48,6 +49,9 @@ export default function getDistResources(
48 49
49 return { 50 return {
50 getPath, 51 getPath,
52 getLocalizationPath(language, fileName) {
53 return path.join(localizationRoot, language, fileName);
54 },
51 getFileURL, 55 getFileURL,
52 getRendererURL: 56 getRendererURL:
53 devMode && devServerURL !== undefined 57 devMode && devServerURL !== undefined
diff --git a/packages/main/src/initReactions.ts b/packages/main/src/initReactions.ts
index 9c49fc5..cdff551 100644
--- a/packages/main/src/initReactions.ts
+++ b/packages/main/src/initReactions.ts
@@ -20,6 +20,7 @@
20 20
21import { app, session } from 'electron'; 21import { app, session } from 'electron';
22 22
23import loadLocalization from './i18n/loadLocalization';
23import ConfigFile from './infrastructure/config/impl/ConfigFile'; 24import ConfigFile from './infrastructure/config/impl/ConfigFile';
24import UserAgents from './infrastructure/electron/UserAgents'; 25import UserAgents from './infrastructure/electron/UserAgents';
25import ElectronViewFactory from './infrastructure/electron/impl/ElectronViewFactory'; 26import ElectronViewFactory from './infrastructure/electron/impl/ElectronViewFactory';
@@ -52,7 +53,11 @@ export default async function initReactions(
52 } 53 }
53 const userAgents = new UserAgents(app.userAgentFallback); 54 const userAgents = new UserAgents(app.userAgentFallback);
54 app.userAgentFallback = userAgents.fallbackUserAgent(devMode); 55 app.userAgentFallback = userAgents.fallbackUserAgent(devMode);
55 setApplicationMenu(store, devMode, isMac); 56 const localizeInterface = async () => {
57 await loadLocalization(store, resources, devMode);
58 setApplicationMenu(store, devMode, isMac);
59 };
60 const localization = localizeInterface();
56 const viewFactory = new ElectronViewFactory(userAgents, resources, devMode); 61 const viewFactory = new ElectronViewFactory(userAgents, resources, devMode);
57 const [mainWindow] = await Promise.all([ 62 const [mainWindow] = await Promise.all([
58 viewFactory.createMainWindow(store), 63 viewFactory.createMainWindow(store),
@@ -60,6 +65,7 @@ export default async function initReactions(
60 ]); 65 ]);
61 store.setMainWindow(mainWindow); 66 store.setMainWindow(mainWindow);
62 loadServices(store, viewFactory); 67 loadServices(store, viewFactory);
68 await localization;
63 return () => { 69 return () => {
64 disposeNativeThemeController(); 70 disposeNativeThemeController();
65 disposeConfigController(); 71 disposeConfigController();
diff --git a/packages/main/src/stores/MainStore.ts b/packages/main/src/stores/MainStore.ts
index 86fadcb..873f998 100644
--- a/packages/main/src/stores/MainStore.ts
+++ b/packages/main/src/stores/MainStore.ts
@@ -21,6 +21,8 @@
21import type { Action, BrowserViewBounds } from '@sophie/shared'; 21import type { Action, BrowserViewBounds } from '@sophie/shared';
22import { applySnapshot, Instance, types } from 'mobx-state-tree'; 22import { applySnapshot, Instance, types } from 'mobx-state-tree';
23 23
24import type I18nStore from '../i18n/I18nStore';
25import type { UseTranslationResult } from '../i18n/I18nStore';
24import type { MainWindow } from '../infrastructure/electron/types'; 26import type { MainWindow } from '../infrastructure/electron/types';
25import { getLogger } from '../utils/log'; 27import { getLogger } from '../utils/log';
26 28
@@ -44,6 +46,7 @@ const MainStore = types
44 {}, 46 {},
45 ), 47 ),
46 shared: types.optional(SharedStore, {}), 48 shared: types.optional(SharedStore, {}),
49 i18n: types.frozen<I18nStore | undefined>(),
47 }) 50 })
48 .views((self) => ({ 51 .views((self) => ({
49 get settings(): GlobalSettings { 52 get settings(): GlobalSettings {
@@ -61,6 +64,9 @@ const MainStore = types
61 ? selectedService 64 ? selectedService
62 : undefined; 65 : undefined;
63 }, 66 },
67 useTranslation(ns?: string): UseTranslationResult {
68 return self.i18n?.useTranslation(ns) ?? { ready: false };
69 },
64 })) 70 }))
65 .volatile( 71 .volatile(
66 (): { 72 (): {
@@ -124,6 +130,9 @@ const MainStore = types
124 beforeDestroy(): void { 130 beforeDestroy(): void {
125 self.mainWindow?.dispose(); 131 self.mainWindow?.dispose();
126 }, 132 },
133 setI18n(i18n: I18nStore): void {
134 self.i18n = i18n;
135 },
127 })); 136 }));
128 137
129/* 138/*
diff --git a/packages/main/src/utils/isErrno.ts b/packages/main/src/utils/isErrno.ts
new file mode 100644
index 0000000..8adbc33
--- /dev/null
+++ b/packages/main/src/utils/isErrno.ts
@@ -0,0 +1,28 @@
1/*
2 * Copyright (C) 2021-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
21export default function isErrno(error: unknown, code: string): boolean {
22 return (
23 typeof error === 'object' &&
24 error !== null &&
25 'code' in error &&
26 (error as NodeJS.ErrnoException).code === code
27 );
28}
diff --git a/yarn.lock b/yarn.lock
index 320d129..b0afd2d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -448,6 +448,15 @@ __metadata:
448 languageName: node 448 languageName: node
449 linkType: hard 449 linkType: hard
450 450
451"@babel/runtime@npm:^7.17.2":
452 version: 7.17.8
453 resolution: "@babel/runtime@npm:7.17.8"
454 dependencies:
455 regenerator-runtime: ^0.13.4
456 checksum: 68d195c1630bb91ac20e86635d292a17ebab7f361cfe79406b3f5a6cc2e59fa283ae5006568899abf869312c2b35b744bd407aea8ffdb650f1a68d07785d47e9
457 languageName: node
458 linkType: hard
459
451"@babel/template@npm:^7.16.7, @babel/template@npm:^7.3.3": 460"@babel/template@npm:^7.16.7, @babel/template@npm:^7.3.3":
452 version: 7.16.7 461 version: 7.16.7
453 resolution: "@babel/template@npm:7.16.7" 462 resolution: "@babel/template@npm:7.16.7"
@@ -1286,6 +1295,7 @@ __metadata:
1286 esbuild: ^0.14.23 1295 esbuild: ^0.14.23
1287 fs-extra: ^10.0.1 1296 fs-extra: ^10.0.1
1288 git-repo-info: ^2.1.1 1297 git-repo-info: ^2.1.1
1298 i18next: ^21.6.14
1289 jest: ^27.5.1 1299 jest: ^27.5.1
1290 jest-mock: ^27.5.1 1300 jest-mock: ^27.5.1
1291 json5: ^2.2.0 1301 json5: ^2.2.0
@@ -3231,13 +3241,6 @@ __metadata:
3231 languageName: node 3241 languageName: node
3232 linkType: hard 3242 linkType: hard
3233 3243
3234"deep-equal@npm:~0.0.0":
3235 version: 0.0.0
3236 resolution: "deep-equal@npm:0.0.0"
3237 checksum: 024cf06cfe917e2f2debf531891537fa21a859a8e5696085d97aefc89e20c66ac20f98dd472fcbe0001f235a3b978a1f5bbbfd661ca795d87e5832b3831bd01a
3238 languageName: node
3239 linkType: hard
3240
3241"deep-extend@npm:^0.6.0": 3244"deep-extend@npm:^0.6.0":
3242 version: 0.6.0 3245 version: 0.6.0
3243 resolution: "deep-extend@npm:0.6.0" 3246 resolution: "deep-extend@npm:0.6.0"
@@ -3275,13 +3278,6 @@ __metadata:
3275 languageName: node 3278 languageName: node
3276 linkType: hard 3279 linkType: hard
3277 3280
3278"defined@npm:~0.0.0":
3279 version: 0.0.0
3280 resolution: "defined@npm:0.0.0"
3281 checksum: 037f80bb4d8b92e72ca99f1b24d1d9d683741b28b9cb9c7b5f4a4c913826fd7f4b103440a623a3882f2218ec3d87649fbf86c65c45e6999cf1124b9a8924fca5
3282 languageName: node
3283 linkType: hard
3284
3285"delayed-stream@npm:~1.0.0": 3281"delayed-stream@npm:~1.0.0":
3286 version: 1.0.0 3282 version: 1.0.0
3287 resolution: "delayed-stream@npm:1.0.0" 3283 resolution: "delayed-stream@npm:1.0.0"
@@ -4820,16 +4816,6 @@ __metadata:
4820 languageName: node 4816 languageName: node
4821 linkType: hard 4817 linkType: hard
4822 4818
4823"global@npm:~2.0.7":
4824 version: 2.0.7
4825 resolution: "global@npm:2.0.7"
4826 dependencies:
4827 min-document: ~0.2.2
4828 process: ~0.5.1
4829 checksum: f2ae2bfd251c014cd8273680b4be221f4c20eb07ad9bfce21f6de7bddbcb5d58b393488e8ebf119ede54addb561c648aa47b2407a630c3a69188c8d11c5b0d75
4830 languageName: node
4831 linkType: hard
4832
4833"globals@npm:^11.1.0": 4819"globals@npm:^11.1.0":
4834 version: 11.12.0 4820 version: 11.12.0
4835 resolution: "globals@npm:11.12.0" 4821 resolution: "globals@npm:11.12.0"
@@ -5067,6 +5053,15 @@ __metadata:
5067 languageName: node 5053 languageName: node
5068 linkType: hard 5054 linkType: hard
5069 5055
5056"i18next@npm:^21.6.14":
5057 version: 21.6.14
5058 resolution: "i18next@npm:21.6.14"
5059 dependencies:
5060 "@babel/runtime": ^7.17.2
5061 checksum: bc6e117874d9b69a39d6ad322851d25f75908c7fa977c8771b98ba7b0273aceba96e82326ed0855b8db098b1490c5a0decbe62b4f61dac84fdc677c2fdc52bb8
5062 languageName: node
5063 linkType: hard
5064
5070"iconv-corefoundation@npm:^1.1.7": 5065"iconv-corefoundation@npm:^1.1.7":
5071 version: 1.1.7 5066 version: 1.1.7
5072 resolution: "iconv-corefoundation@npm:1.1.7" 5067 resolution: "iconv-corefoundation@npm:1.1.7"
@@ -5183,7 +5178,7 @@ __metadata:
5183 languageName: node 5178 languageName: node
5184 linkType: hard 5179 linkType: hard
5185 5180
5186"inherits@npm:2, inherits@npm:^2.0.3, inherits@npm:~2.0.1, inherits@npm:~2.0.3": 5181"inherits@npm:2, inherits@npm:^2.0.3, inherits@npm:~2.0.3":
5187 version: 2.0.4 5182 version: 2.0.4
5188 resolution: "inherits@npm:2.0.4" 5183 resolution: "inherits@npm:2.0.4"
5189 checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 5184 checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1
@@ -5204,13 +5199,6 @@ __metadata:
5204 languageName: node 5199 languageName: node
5205 linkType: hard 5200 linkType: hard
5206 5201
5207"insert-css@npm:0.0.0":
5208 version: 0.0.0
5209 resolution: "insert-css@npm:0.0.0"
5210 checksum: d0441ad656674fe971a594dcf6f53f53cdb8d469668344a7dd15dfa7f2805829d553f3cf799d967256cead4d960f884ffcd27e45df9732916a3c8b7763a16f7b
5211 languageName: node
5212 linkType: hard
5213
5214"internal-slot@npm:^1.0.3": 5202"internal-slot@npm:^1.0.3":
5215 version: 1.0.3 5203 version: 1.0.3
5216 resolution: "internal-slot@npm:1.0.3" 5204 resolution: "internal-slot@npm:1.0.3"
@@ -6356,13 +6344,6 @@ __metadata:
6356 languageName: node 6344 languageName: node
6357 linkType: hard 6345 linkType: hard
6358 6346
6359"jsonify@npm:~0.0.0":
6360 version: 0.0.0
6361 resolution: "jsonify@npm:0.0.0"
6362 checksum: d8d4ed476c116e6987a460dcb82f22284686caae9f498ac87b0502c1765ac1522f4f450a4cad4cc368d202fd3b27a3860735140a82867fc6d558f5f199c38bce
6363 languageName: node
6364 linkType: hard
6365
6366"jsx-ast-utils@npm:^2.4.1 || ^3.0.0, jsx-ast-utils@npm:^3.2.1": 6347"jsx-ast-utils@npm:^2.4.1 || ^3.0.0, jsx-ast-utils@npm:^3.2.1":
6367 version: 3.2.1 6348 version: 3.2.1
6368 resolution: "jsx-ast-utils@npm:3.2.1" 6349 resolution: "jsx-ast-utils@npm:3.2.1"
@@ -6692,15 +6673,6 @@ __metadata:
6692 languageName: node 6673 languageName: node
6693 linkType: hard 6674 linkType: hard
6694 6675
6695"min-document@npm:~0.2.2":
6696 version: 0.2.8
6697 resolution: "min-document@npm:0.2.8"
6698 dependencies:
6699 tape: ~1.0.2
6700 checksum: 6f861c6169d5b89c8592dafb1247e962319491099b676276e07663be7d4e30d17fc7bcd3576e18dd4e08c5ea6a3a87db33317d813303bdecba0b562a3fb70adc
6701 languageName: node
6702 linkType: hard
6703
6704"min-indent@npm:^1.0.0": 6676"min-indent@npm:^1.0.0":
6705 version: 1.0.1 6677 version: 1.0.1
6706 resolution: "min-indent@npm:1.0.1" 6678 resolution: "min-indent@npm:1.0.1"
@@ -7122,20 +7094,6 @@ __metadata:
7122 languageName: node 7094 languageName: node
7123 linkType: hard 7095 linkType: hard
7124 7096
7125"once@npm:~1.1.1":
7126 version: 1.1.1
7127 resolution: "once@npm:1.1.1"
7128 checksum: c5384f8fc8b973476afe84433ed7202cc5fdbf749abc06908716d706caca1ab951209de38f008ce8f5abca8896edec8aff83c97438a7b6808b5582e01b12864f
7129 languageName: node
7130 linkType: hard
7131
7132"once@npm:~1.2.0":
7133 version: 1.2.0
7134 resolution: "once@npm:1.2.0"
7135 checksum: 4e0b4908bd34dd1db7c83a262e66bcfd077ac4eff94d0499b52878346d1371770147d4d9ae6cc47f0f3a3370570b6b79289daee9ea4cfad143d19caf04849259
7136 languageName: node
7137 linkType: hard
7138
7139"onetime@npm:^5.1.2": 7097"onetime@npm:^5.1.2":
7140 version: 5.1.2 7098 version: 5.1.2
7141 resolution: "onetime@npm:5.1.2" 7099 resolution: "onetime@npm:5.1.2"
@@ -7410,18 +7368,6 @@ __metadata:
7410 languageName: node 7368 languageName: node
7411 linkType: hard 7369 linkType: hard
7412 7370
7413"preload@npm:^0.1.0":
7414 version: 0.1.0
7415 resolution: "preload@npm:0.1.0"
7416 dependencies:
7417 inherits: ~2.0.1
7418 insert-css: 0.0.0
7419 once: ~1.2.0
7420 xhr: ~1.2.3
7421 checksum: 992aa3b9f6c3b4bd40c87bfbdac389337b046d877258d6fc7e4cfc674b60d72eb3fe83ead47665bc76787c6c2ed1a41ff1dedb073f890f098607df1877e5b639
7422 languageName: node
7423 linkType: hard
7424
7425"prelude-ls@npm:^1.2.1": 7371"prelude-ls@npm:^1.2.1":
7426 version: 1.2.1 7372 version: 1.2.1
7427 resolution: "prelude-ls@npm:1.2.1" 7373 resolution: "prelude-ls@npm:1.2.1"
@@ -7479,13 +7425,6 @@ __metadata:
7479 languageName: node 7425 languageName: node
7480 linkType: hard 7426 linkType: hard
7481 7427
7482"process@npm:~0.5.1":
7483 version: 0.5.2
7484 resolution: "process@npm:0.5.2"
7485 checksum: 613505ec6d518654fc9c677881c6b8c871179dbf4bfe23f5a7a15defdb58efb1b75bfc96576adc118ba0bca1b76c359687a13f7d43950af0b7d5caa27efbe9e3
7486 languageName: node
7487 linkType: hard
7488
7489"progress@npm:^2.0.3": 7428"progress@npm:^2.0.3":
7490 version: 2.0.3 7429 version: 2.0.3
7491 resolution: "progress@npm:2.0.3" 7430 resolution: "progress@npm:2.0.3"
@@ -8299,7 +8238,6 @@ __metadata:
8299 jest: ^27.5.1 8238 jest: ^27.5.1
8300 jest-junit: ^13.0.0 8239 jest-junit: ^13.0.0
8301 nano-staged: ^0.6.0 8240 nano-staged: ^0.6.0
8302 preload: ^0.1.0
8303 prettier: ^2.5.1 8241 prettier: ^2.5.1
8304 rimraf: ^3.0.2 8242 rimraf: ^3.0.2
8305 simple-git-hooks: ^2.7.0 8243 simple-git-hooks: ^2.7.0
@@ -8614,20 +8552,6 @@ __metadata:
8614 languageName: node 8552 languageName: node
8615 linkType: hard 8553 linkType: hard
8616 8554
8617"tape@npm:~1.0.2":
8618 version: 1.0.4
8619 resolution: "tape@npm:1.0.4"
8620 dependencies:
8621 deep-equal: ~0.0.0
8622 defined: ~0.0.0
8623 jsonify: ~0.0.0
8624 through: ~2.3.4
8625 bin:
8626 tape: ./bin/tape
8627 checksum: c8a3e7b0dbb8450d3b18bf8d82a5ac393d0d25ed26a3fa5c1f3205e263f8828b744c6a61cd7d35fa93c4948d2da88232994bf41451186cd9caf77ed87e47871d
8628 languageName: node
8629 linkType: hard
8630
8631"tar@npm:^6.0.2, tar@npm:^6.1.2": 8555"tar@npm:^6.0.2, tar@npm:^6.1.2":
8632 version: 6.1.11 8556 version: 6.1.11
8633 resolution: "tar@npm:6.1.11" 8557 resolution: "tar@npm:6.1.11"
@@ -8687,13 +8611,6 @@ __metadata:
8687 languageName: node 8611 languageName: node
8688 linkType: hard 8612 linkType: hard
8689 8613
8690"through@npm:~2.3.4":
8691 version: 2.3.8
8692 resolution: "through@npm:2.3.8"
8693 checksum: a38c3e059853c494af95d50c072b83f8b676a9ba2818dcc5b108ef252230735c54e0185437618596c790bbba8fcdaef5b290405981ffa09dce67b1f1bf190cbd
8694 languageName: node
8695 linkType: hard
8696
8697"tmp-promise@npm:^3.0.2": 8614"tmp-promise@npm:^3.0.2":
8698 version: 3.0.3 8615 version: 3.0.3
8699 resolution: "tmp-promise@npm:3.0.3" 8616 resolution: "tmp-promise@npm:3.0.3"
@@ -9401,16 +9318,6 @@ __metadata:
9401 languageName: node 9318 languageName: node
9402 linkType: hard 9319 linkType: hard
9403 9320
9404"xhr@npm:~1.2.3":
9405 version: 1.2.4
9406 resolution: "xhr@npm:1.2.4"
9407 dependencies:
9408 global: ~2.0.7
9409 once: ~1.1.1
9410 checksum: 55840633fc96bfee99a9c7c839aa8db670f061fc13c45a4056f66c1194b0525f511dbcb5f01ba6ea2f02908f600fd19af13e6d24622ba26b168afbfd94f9211a
9411 languageName: node
9412 linkType: hard
9413
9414"xml-name-validator@npm:^3.0.0": 9321"xml-name-validator@npm:^3.0.0":
9415 version: 3.0.0 9322 version: 3.0.0
9416 resolution: "xml-name-validator@npm:3.0.0" 9323 resolution: "xml-name-validator@npm:3.0.0"