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.ts104
1 files changed, 104 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..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}