aboutsummaryrefslogtreecommitdiffstats
path: root/packages/main/src/i18n/I18nStore.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/main/src/i18n/I18nStore.ts')
-rw-r--r--packages/main/src/i18n/I18nStore.ts145
1 files changed, 145 insertions, 0 deletions
diff --git a/packages/main/src/i18n/I18nStore.ts b/packages/main/src/i18n/I18nStore.ts
new file mode 100644
index 0000000..4c77322
--- /dev/null
+++ b/packages/main/src/i18n/I18nStore.ts
@@ -0,0 +1,145 @@
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, ResourceKey, TFunction } from 'i18next';
22import { IAtom, createAtom } from 'mobx';
23
24import getLogger from '../utils/getLogger.js';
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 try {
86 await this.i18next.loadNamespaces([ns]);
87 } catch (error) {
88 setImmediate(loaded);
89 throw error;
90 }
91 if (this.i18next.isInitialized) {
92 setImmediate(loaded);
93 return;
94 }
95 const initialized = () => {
96 setImmediate(() => {
97 this.i18next.off('initialized', initialized);
98 loaded();
99 });
100 };
101 this.i18next.on('initialized', initialized);
102 };
103
104 loadAsync().catch((error) => {
105 log.error('Failed to load translations for namespace', ns, error);
106 });
107 }
108
109 async reloadTranslations(): Promise<void> {
110 await this.i18next.reloadResources();
111 setImmediate(() => {
112 this.languageChangedAtom.reportChanged();
113 });
114 log.debug('Reloaded translations');
115 }
116
117 async getTranslation(
118 language: string,
119 namespace: string,
120 ): Promise<ResourceKey> {
121 if (!this.i18next.hasResourceBundle(language, namespace)) {
122 await this.i18next.loadLanguages([language]);
123 await this.i18next.loadNamespaces([namespace]);
124 }
125 const bundle = this.i18next.getResourceBundle(
126 language,
127 namespace,
128 ) as unknown;
129 if (typeof bundle !== 'object' || bundle === null) {
130 throw new Error(
131 `Failed to load ${namespace} resource bundle for language ${language}`,
132 );
133 }
134 return bundle as ResourceKey;
135 }
136
137 addMissingTranslation(
138 languages: string[],
139 namespace: string,
140 key: string,
141 value: string,
142 ): void {
143 this.i18next.modules.backend?.create?.(languages, namespace, key, value);
144 }
145}