diff options
Diffstat (limited to 'packages/main')
-rw-r--r-- | packages/main/src/i18n/I18nStore.ts | 31 | ||||
-rw-r--r-- | packages/main/src/i18n/loadLocalization.ts | 23 | ||||
-rw-r--r-- | packages/main/src/infrastructure/electron/impl/ElectronMainWindow.ts | 25 | ||||
-rw-r--r-- | packages/main/src/infrastructure/electron/impl/setApplicationMenu.ts | 5 | ||||
-rw-r--r-- | packages/main/src/infrastructure/electron/types.ts | 2 | ||||
-rw-r--r-- | packages/main/src/stores/MainStore.ts | 71 |
6 files changed, 129 insertions, 28 deletions
diff --git a/packages/main/src/i18n/I18nStore.ts b/packages/main/src/i18n/I18nStore.ts index c364f0e..54c3d20 100644 --- a/packages/main/src/i18n/I18nStore.ts +++ b/packages/main/src/i18n/I18nStore.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 | ||
21 | import type { i18n, TFunction } from 'i18next'; | 21 | import type { i18n, ResourceKey, TFunction } from 'i18next'; |
22 | import { IAtom, createAtom } from 'mobx'; | 22 | import { IAtom, createAtom } from 'mobx'; |
23 | 23 | ||
24 | import { getLogger } from '../utils/log'; | 24 | import { getLogger } from '../utils/log'; |
@@ -113,4 +113,33 @@ export default class I18nStore { | |||
113 | }); | 113 | }); |
114 | log.debug('Reloaded translations'); | 114 | log.debug('Reloaded translations'); |
115 | } | 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 | } | ||
116 | } | 145 | } |
diff --git a/packages/main/src/i18n/loadLocalization.ts b/packages/main/src/i18n/loadLocalization.ts index 1408a30..ec3cf84 100644 --- a/packages/main/src/i18n/loadLocalization.ts +++ b/packages/main/src/i18n/loadLocalization.ts | |||
@@ -18,16 +18,22 @@ | |||
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 i18next from 'i18next'; | 22 | import i18next from 'i18next'; |
23 | import { autorun } from 'mobx'; | ||
24 | import { addDisposer } from 'mobx-state-tree'; | ||
22 | 25 | ||
23 | import type Resources from '../infrastructure/resources/Resources'; | 26 | import type Resources from '../infrastructure/resources/Resources'; |
24 | import type MainStore from '../stores/MainStore'; | 27 | import type MainStore from '../stores/MainStore'; |
28 | import { getLogger } from '../utils/log'; | ||
25 | 29 | ||
26 | import I18nStore from './I18nStore'; | 30 | import I18nStore from './I18nStore'; |
27 | import RepositoryBasedI18nBackend from './RepositoryBasedI18nBackend'; | 31 | import RepositoryBasedI18nBackend from './RepositoryBasedI18nBackend'; |
28 | import i18nLog from './i18nLog'; | 32 | import i18nLog from './i18nLog'; |
29 | import LocalizationFiles from './impl/LocaltizationFiles'; | 33 | import LocalizationFiles from './impl/LocaltizationFiles'; |
30 | 34 | ||
35 | const log = getLogger('loadLocationzation'); | ||
36 | |||
31 | export default async function loadLocalization( | 37 | export default async function loadLocalization( |
32 | store: MainStore, | 38 | store: MainStore, |
33 | resources: Resources, | 39 | resources: Resources, |
@@ -37,14 +43,25 @@ export default async function loadLocalization( | |||
37 | const backend = new RepositoryBasedI18nBackend(repository, devMode); | 43 | const backend = new RepositoryBasedI18nBackend(repository, devMode); |
38 | const i18n = i18next | 44 | const i18n = i18next |
39 | .createInstance({ | 45 | .createInstance({ |
40 | lng: 'en', | 46 | lng: store.shared.language, |
41 | fallbackLng: ['en'], | 47 | fallbackLng, |
42 | debug: devMode, | 48 | debug: devMode, |
43 | saveMissing: devMode, | 49 | saveMissing: devMode, |
44 | }) | 50 | }) |
45 | .use(backend) | 51 | .use(backend) |
46 | .use(i18nLog); | 52 | .use(i18nLog); |
47 | await i18n.init(); | ||
48 | const i18nStore = new I18nStore(i18n); | 53 | const i18nStore = new I18nStore(i18n); |
49 | store.setI18n(i18nStore); | 54 | store.setI18n(i18nStore); |
55 | await i18n.init(); | ||
56 | const disposeChangeLanguage = autorun(() => { | ||
57 | const { | ||
58 | shared: { language }, | ||
59 | } = store; | ||
60 | if (i18n.language !== language) { | ||
61 | i18n.changeLanguage(language).catch((error) => { | ||
62 | log.error('Failed to change language', error); | ||
63 | }); | ||
64 | } | ||
65 | }); | ||
66 | addDisposer(store, disposeChangeLanguage); | ||
50 | } | 67 | } |
diff --git a/packages/main/src/infrastructure/electron/impl/ElectronMainWindow.ts b/packages/main/src/infrastructure/electron/impl/ElectronMainWindow.ts index cff7957..6144d89 100644 --- a/packages/main/src/infrastructure/electron/impl/ElectronMainWindow.ts +++ b/packages/main/src/infrastructure/electron/impl/ElectronMainWindow.ts | |||
@@ -22,6 +22,7 @@ import { | |||
22 | Action, | 22 | Action, |
23 | MainToRendererIpcMessage, | 23 | MainToRendererIpcMessage, |
24 | RendererToMainIpcMessage, | 24 | RendererToMainIpcMessage, |
25 | Translation, | ||
25 | } from '@sophie/shared'; | 26 | } from '@sophie/shared'; |
26 | import { BrowserWindow, ipcMain, IpcMainEvent } from 'electron'; | 27 | import { BrowserWindow, ipcMain, IpcMainEvent } from 'electron'; |
27 | import type { IJsonPatch } from 'mobx-state-tree'; | 28 | import type { IJsonPatch } from 'mobx-state-tree'; |
@@ -96,6 +97,24 @@ export default class ElectronMainWindow implements MainWindow { | |||
96 | return this.bridge.snapshot; | 97 | return this.bridge.snapshot; |
97 | }); | 98 | }); |
98 | 99 | ||
100 | ipcMain.handle( | ||
101 | RendererToMainIpcMessage.GetTranslation, | ||
102 | (event, translation) => { | ||
103 | const { id } = event.sender; | ||
104 | if (id !== webContents.id) { | ||
105 | log.warn( | ||
106 | 'Unexpected', | ||
107 | RendererToMainIpcMessage.GetTranslation, | ||
108 | 'from webContents', | ||
109 | id, | ||
110 | ); | ||
111 | throw new Error('Invalid IPC call'); | ||
112 | } | ||
113 | const { language, namespace } = Translation.parse(translation); | ||
114 | return store.getTranslation(language, namespace); | ||
115 | }, | ||
116 | ); | ||
117 | |||
99 | this.bridge = new RendererBridge(store, (patch) => { | 118 | this.bridge = new RendererBridge(store, (patch) => { |
100 | webContents.send(MainToRendererIpcMessage.SharedStorePatch, patch); | 119 | webContents.send(MainToRendererIpcMessage.SharedStorePatch, patch); |
101 | }); | 120 | }); |
@@ -142,6 +161,12 @@ export default class ElectronMainWindow implements MainWindow { | |||
142 | ); | 161 | ); |
143 | } | 162 | } |
144 | 163 | ||
164 | reloadTranslations(): void { | ||
165 | this.browserWindow.webContents.send( | ||
166 | MainToRendererIpcMessage.ReloadTranslations, | ||
167 | ); | ||
168 | } | ||
169 | |||
145 | dispose() { | 170 | dispose() { |
146 | this.bridge.dispose(); | 171 | this.bridge.dispose(); |
147 | this.browserWindow.destroy(); | 172 | this.browserWindow.destroy(); |
diff --git a/packages/main/src/infrastructure/electron/impl/setApplicationMenu.ts b/packages/main/src/infrastructure/electron/impl/setApplicationMenu.ts index 49bfbfd..8e10383 100644 --- a/packages/main/src/infrastructure/electron/impl/setApplicationMenu.ts +++ b/packages/main/src/infrastructure/electron/impl/setApplicationMenu.ts | |||
@@ -91,10 +91,7 @@ export default function setApplicationMenu( | |||
91 | }, | 91 | }, |
92 | { | 92 | { |
93 | role: 'toggleDevTools', | 93 | role: 'toggleDevTools', |
94 | label: t<string>( | 94 | label: t<string>('menu.view.toggleSophieDeveloperTools'), |
95 | 'menu.view.toggleSophieDeveloperTools', | ||
96 | 'Toggle Sophie Developer Tools', | ||
97 | ), | ||
98 | accelerator: 'CommandOrControl+Shift+Alt+I', | 95 | accelerator: 'CommandOrControl+Shift+Alt+I', |
99 | }, | 96 | }, |
100 | { type: 'separator' }, | 97 | { type: 'separator' }, |
diff --git a/packages/main/src/infrastructure/electron/types.ts b/packages/main/src/infrastructure/electron/types.ts index 4716f0b..e5b0fd6 100644 --- a/packages/main/src/infrastructure/electron/types.ts +++ b/packages/main/src/infrastructure/electron/types.ts | |||
@@ -41,6 +41,8 @@ export interface MainWindow { | |||
41 | 41 | ||
42 | setServiceView(serviceView: ServiceView | undefined): void; | 42 | setServiceView(serviceView: ServiceView | undefined): void; |
43 | 43 | ||
44 | reloadTranslations(): void; | ||
45 | |||
44 | dispose(): void; | 46 | dispose(): void; |
45 | } | 47 | } |
46 | 48 | ||
diff --git a/packages/main/src/stores/MainStore.ts b/packages/main/src/stores/MainStore.ts index 93cabb9..d717bed 100644 --- a/packages/main/src/stores/MainStore.ts +++ b/packages/main/src/stores/MainStore.ts | |||
@@ -19,7 +19,8 @@ | |||
19 | */ | 19 | */ |
20 | 20 | ||
21 | import type { Action, BrowserViewBounds } from '@sophie/shared'; | 21 | import type { Action, BrowserViewBounds } from '@sophie/shared'; |
22 | import { applySnapshot, Instance, types } from 'mobx-state-tree'; | 22 | import type { ResourceKey } from 'i18next'; |
23 | import { applySnapshot, flow, Instance, types } from 'mobx-state-tree'; | ||
23 | 24 | ||
24 | import type I18nStore from '../i18n/I18nStore'; | 25 | import type I18nStore from '../i18n/I18nStore'; |
25 | import type { UseTranslationResult } from '../i18n/I18nStore'; | 26 | import type { UseTranslationResult } from '../i18n/I18nStore'; |
@@ -67,6 +68,12 @@ const MainStore = types | |||
67 | useTranslation(ns?: string): UseTranslationResult { | 68 | useTranslation(ns?: string): UseTranslationResult { |
68 | return self.i18n?.useTranslation(ns) ?? { ready: false }; | 69 | return self.i18n?.useTranslation(ns) ?? { ready: false }; |
69 | }, | 70 | }, |
71 | getTranslation(language: string, namespace: string): Promise<ResourceKey> { | ||
72 | if (self.i18n === undefined) { | ||
73 | throw new Error('i18next has not been set'); | ||
74 | } | ||
75 | return self.i18n.getTranslation(language, namespace); | ||
76 | }, | ||
70 | })) | 77 | })) |
71 | .volatile( | 78 | .volatile( |
72 | (): { | 79 | (): { |
@@ -79,10 +86,43 @@ const MainStore = types | |||
79 | setBrowserViewBounds(bounds: BrowserViewBounds): void { | 86 | setBrowserViewBounds(bounds: BrowserViewBounds): void { |
80 | applySnapshot(self.browserViewBounds, bounds); | 87 | applySnapshot(self.browserViewBounds, bounds); |
81 | }, | 88 | }, |
89 | setMainWindow(mainWindow: MainWindow | undefined): void { | ||
90 | self.mainWindow = mainWindow; | ||
91 | }, | ||
92 | openWebpageInBrowser() { | ||
93 | getEnv(self).openURLInExternalBrowser( | ||
94 | 'https://gitlab.com/say-hi-to-sophie/shophie', | ||
95 | ); | ||
96 | }, | ||
97 | openAboutDialog() { | ||
98 | getEnv(self).openAboutDialog(); | ||
99 | }, | ||
100 | beforeDestroy(): void { | ||
101 | self.mainWindow?.dispose(); | ||
102 | }, | ||
103 | setI18n(i18n: I18nStore): void { | ||
104 | self.i18n = i18n; | ||
105 | }, | ||
106 | addMissingTranslation( | ||
107 | languages: string[], | ||
108 | namespace: string, | ||
109 | key: string, | ||
110 | value: string, | ||
111 | ): void { | ||
112 | self.i18n?.addMissingTranslation(languages, namespace, key, value); | ||
113 | }, | ||
114 | reloadTranslations: flow(function* reloadTranslations() { | ||
115 | if (self.i18n !== undefined) { | ||
116 | yield self.i18n.reloadTranslations(); | ||
117 | self.mainWindow?.reloadTranslations(); | ||
118 | } | ||
119 | }), | ||
120 | })) | ||
121 | .actions((self) => ({ | ||
82 | dispatch(action: Action): void { | 122 | dispatch(action: Action): void { |
83 | switch (action.action) { | 123 | switch (action.action) { |
84 | case 'set-browser-view-bounds': | 124 | case 'set-browser-view-bounds': |
85 | this.setBrowserViewBounds(action.browserViewBounds); | 125 | self.setBrowserViewBounds(action.browserViewBounds); |
86 | break; | 126 | break; |
87 | case 'set-selected-service-id': | 127 | case 'set-selected-service-id': |
88 | self.settings.setSelectedServiceId(action.serviceId); | 128 | self.settings.setSelectedServiceId(action.serviceId); |
@@ -98,11 +138,19 @@ const MainStore = types | |||
98 | break; | 138 | break; |
99 | case 'reload-all-translations': | 139 | case 'reload-all-translations': |
100 | if (self.i18n !== undefined) { | 140 | if (self.i18n !== undefined) { |
101 | self.i18n.reloadTranslations().catch((error) => { | 141 | self.reloadTranslations().catch((error) => { |
102 | log.error('Failed to reload translations', error); | 142 | log.error('Failed to reload translations', error); |
103 | }); | 143 | }); |
104 | } | 144 | } |
105 | break; | 145 | break; |
146 | case 'add-missing-translation': | ||
147 | self.addMissingTranslation( | ||
148 | action.languages, | ||
149 | action.namespace, | ||
150 | action.key, | ||
151 | action.value, | ||
152 | ); | ||
153 | break; | ||
106 | case 'dispatch-service-action': { | 154 | case 'dispatch-service-action': { |
107 | const { serviceId, serviceAction } = action; | 155 | const { serviceId, serviceAction } = action; |
108 | const service = self.shared.servicesById.get(serviceId); | 156 | const service = self.shared.servicesById.get(serviceId); |
@@ -123,23 +171,6 @@ const MainStore = types | |||
123 | break; | 171 | break; |
124 | } | 172 | } |
125 | }, | 173 | }, |
126 | setMainWindow(mainWindow: MainWindow | undefined): void { | ||
127 | self.mainWindow = mainWindow; | ||
128 | }, | ||
129 | openWebpageInBrowser() { | ||
130 | getEnv(self).openURLInExternalBrowser( | ||
131 | 'https://gitlab.com/say-hi-to-sophie/shophie', | ||
132 | ); | ||
133 | }, | ||
134 | openAboutDialog() { | ||
135 | getEnv(self).openAboutDialog(); | ||
136 | }, | ||
137 | beforeDestroy(): void { | ||
138 | self.mainWindow?.dispose(); | ||
139 | }, | ||
140 | setI18n(i18n: I18nStore): void { | ||
141 | self.i18n = i18n; | ||
142 | }, | ||
143 | })); | 174 | })); |
144 | 175 | ||
145 | /* | 176 | /* |