diff --git a/src/apps/main/config/handlers.ts b/src/apps/main/config/handlers.ts index 5e9ef089d3..e51c88e103 100644 --- a/src/apps/main/config/handlers.ts +++ b/src/apps/main/config/handlers.ts @@ -1,4 +1,5 @@ import { ipcMain } from 'electron'; +import { isLanguage } from '../../shared/Locale/Language'; import { getConfigKey, setConfigKey } from './service'; @@ -7,3 +8,9 @@ ipcMain.handle('get-config-key', (_, key) => getConfigKey(key)); ipcMain.on('set-config-key', (_, { key, value }) => { setConfigKey({ key, value }); }); + +ipcMain.on('set-prefered-language', (_, language) => { + if (!isLanguage(language)) return; + + setConfigKey({ key: 'preferedLanguage', value: language }); +}); diff --git a/src/apps/main/config/service.ts b/src/apps/main/config/service.ts index 90e0bfb7c5..a09db366f4 100644 --- a/src/apps/main/config/service.ts +++ b/src/apps/main/config/service.ts @@ -4,6 +4,7 @@ import { electronStore } from '../config'; import { broadcastTheme } from '../../../core/theme'; import { broadcastLanguage } from './language'; import type { SetConfigKeyProps, StoredValues } from './service.types'; +import { setMainI18nLanguage } from '../localize/i18n.service'; export const getConfigKey = (key: T): AppStore[T] => { return electronStore.get(key); @@ -14,6 +15,10 @@ export const setConfigKey = ({ key, value }: SetConfigKeyProps): void => { electronStore.set(key, value); - if (key === 'preferedLanguage') broadcastLanguage(); - else if (key === 'preferedTheme') broadcastTheme(); + if (key === 'preferedLanguage') { + broadcastLanguage(); + void setMainI18nLanguage({ language: value }); + } else if (key === 'preferedTheme') { + broadcastTheme(); + } }; diff --git a/src/apps/main/interface.d.ts b/src/apps/main/interface.d.ts index ed82c30205..4c0a8a1093 100644 --- a/src/apps/main/interface.d.ts +++ b/src/apps/main/interface.d.ts @@ -26,6 +26,7 @@ import { UserNotification } from '../../infra/drive-server/out/dto'; export interface IElectronAPI { getConfigKey(key: T): Promise; setConfigKey(key: T, value: AppStore[T]): void; + setPreferedLanguage(language: AppStore['preferedLanguage']): void; listenToConfigKeyChange(key: StoredValues, fn: (value: T) => void): () => void; toggleDarkMode(mode: ConfigTheme): Promise; getPreferredAppLanguage(): Promise>; diff --git a/src/apps/main/localize/i18n.service.ts b/src/apps/main/localize/i18n.service.ts new file mode 100644 index 0000000000..fdad05f08d --- /dev/null +++ b/src/apps/main/localize/i18n.service.ts @@ -0,0 +1,105 @@ +import i18next from 'i18next'; +import { logger } from '@internxt/drive-desktop-core/build/backend'; +import enTranslations from '../../renderer/localize/locales/en.json'; +import esTranslations from '../../renderer/localize/locales/es.json'; +import frTranslations from '../../renderer/localize/locales/fr.json'; +import { DEFAULT_LANGUAGE, Language, isLanguage } from '../../shared/Locale/Language'; + +type TranslationOptions = { + [key: string]: unknown; +}; + +type TranslateMainPops = { + key: string; + defaultValue: string; + options?: TranslationOptions; +}; + +type SetMainI18nLanguagePops = { + language: Language; +}; + +type SetupMainI18nPops = { + language?: string; +}; + +const languageChangeListeners = new Set<() => void>(); +let isMainI18nInitialized = false; + +function notifyMainLanguageChanged() { + for (const listener of languageChangeListeners) { + listener(); + } +} + +export async function setupMainI18n({ language }: SetupMainI18nPops = {}) { + if (isMainI18nInitialized) return; + + const initialLanguage = language && isLanguage(language) ? language : DEFAULT_LANGUAGE; + + try { + await i18next.init({ + resources: { + en: { + translation: enTranslations, + }, + es: { + translation: esTranslations, + }, + fr: { + translation: frTranslations, + }, + }, + lng: initialLanguage, + fallbackLng: DEFAULT_LANGUAGE, + defaultNS: 'translation', + ns: ['translation'], + debug: false, + interpolation: { + escapeValue: false, + }, + }); + + i18next.on('languageChanged', notifyMainLanguageChanged); + isMainI18nInitialized = true; + } catch (exc) { + logger.error({ + msg: 'Unable to initialize i18next for Main process', + exc, + language: initialLanguage, + }); + } +} + +export async function setMainI18nLanguage({ language }: SetMainI18nLanguagePops) { + await setupMainI18n(); + + if (!isMainI18nInitialized || i18next.language === language) return; + + try { + await i18next.changeLanguage(language); + } catch (exc) { + logger.error({ + msg: 'Unable to change i18next language for Main process', + exc, + language, + }); + } +} + +export function translateMain({ key, defaultValue, options }: TranslateMainPops) { + if (!isMainI18nInitialized) return defaultValue; + + return i18next.t(key, { + defaultValue, + ...(options || {}), + }); +} + +export function onMainI18nLanguageChange(listener: () => void) { + languageChangeListeners.add(listener); + + return () => { + languageChangeListeners.delete(listener); + }; +} diff --git a/src/apps/main/preload.d.ts b/src/apps/main/preload.d.ts index 1a440efea5..e17fd8e68c 100644 --- a/src/apps/main/preload.d.ts +++ b/src/apps/main/preload.d.ts @@ -15,6 +15,8 @@ declare interface Window { setConfigKey: typeof import('./config/service').setConfigKey; + setPreferedLanguage: (language: import('../shared/Locale/Language').Language) => void; + pathChanged(path: string): void; getGeneralIssues: () => Promise; diff --git a/src/apps/main/preload.js b/src/apps/main/preload.js index 58b7ba486f..8eea65cd76 100644 --- a/src/apps/main/preload.js +++ b/src/apps/main/preload.js @@ -9,6 +9,9 @@ contextBridge.exposeInMainWorld('electron', { setConfigKey(key, value) { return ipcRenderer.send('set-config-key', { key, value }); }, + setPreferedLanguage(language) { + return ipcRenderer.send('set-prefered-language', language); + }, listenToConfigKeyChange(key, fn) { const eventName = `${key}-updated`; const callback = (_, v) => fn(v); diff --git a/src/apps/main/tray/tray-menu.ts b/src/apps/main/tray/tray-menu.ts index 26d164f77b..988452aaa3 100644 --- a/src/apps/main/tray/tray-menu.ts +++ b/src/apps/main/tray/tray-menu.ts @@ -2,6 +2,7 @@ import { Menu, nativeImage, Tray } from 'electron'; import path from 'node:path'; import PackageJson from '../../../../package.json'; import { TrayMenuState } from './types'; +import { translateMain } from '../localize/i18n.service'; export class TrayMenu { private readonly tray: Tray; @@ -21,22 +22,7 @@ export class TrayMenu { this.tray = new Tray(trayIcon); this.setState('LOADING'); - - const contextMenu = Menu.buildFromTemplate([ - { - label: `Internxt ${PackageJson.version}`, - click: () => { - this.onClick(); - }, - }, - { - label: 'Quit', - click: () => { - this.onQuit(); - }, - }, - ]); - this.tray.setContextMenu(contextMenu); + this.setContextMenu(); } getIconPath(state: TrayMenuState) { @@ -53,17 +39,64 @@ export class TrayMenu { this.setTooltip(state); } + refreshTranslations() { + this.setContextMenu(); + + if (this.currentState) { + this.setTooltip(this.currentState); + } + } + setImage(imagePath: string) { const image = nativeImage.createFromPath(imagePath); this.tray.setImage(image); } + setContextMenu() { + const contextMenu = Menu.buildFromTemplate([ + { + label: translateMain({ + key: 'main.tray.menu.open-app', + defaultValue: `Internxt ${PackageJson.version}`, + options: { version: PackageJson.version }, + }), + click: () => { + void this.onClick(); + }, + }, + { + label: translateMain({ + key: 'main.tray.menu.quit', + defaultValue: 'Quit', + }), + click: () => { + this.onQuit(); + }, + }, + ]); + + this.tray.setContextMenu(contextMenu); + } + setTooltip(state: TrayMenuState) { - const messages: Record = { - SYNCING: 'Sync in process', - IDLE: `Internxt ${PackageJson.version}`, - ALERT: 'There are some issues with your sync', - LOADING: 'Loading Internxt...', + const messages = { + SYNCING: translateMain({ + key: 'main.tray.tooltip.syncing', + defaultValue: 'Sync in process', + }), + IDLE: translateMain({ + key: 'main.tray.tooltip.idle', + defaultValue: `Internxt ${PackageJson.version}`, + options: { version: PackageJson.version }, + }), + ALERT: translateMain({ + key: 'main.tray.tooltip.alert', + defaultValue: 'There are some issues with your sync', + }), + LOADING: translateMain({ + key: 'main.tray.tooltip.loading', + defaultValue: 'Loading Internxt...', + }), }; const message = messages[state]; diff --git a/src/apps/main/tray/tray-setup.ts b/src/apps/main/tray/tray-setup.ts index 6d80693f88..525c0623f9 100644 --- a/src/apps/main/tray/tray-setup.ts +++ b/src/apps/main/tray/tray-setup.ts @@ -6,8 +6,10 @@ import { getIsLoggedIn } from '../auth/handlers'; import { getAuthWindow } from '../windows/auth'; import { TrayMenuState } from './types'; import { PATHS } from '../../../core/electron/paths'; +import { onMainI18nLanguageChange } from '../localize/i18n.service'; let tray: TrayMenu | null = null; +let removeMainI18nListener: (() => void) | null = null; // v.2.6.0 // Esteban Galvis Triana @@ -64,5 +66,11 @@ export function setupTrayIcon() { tray = new TrayMenu(iconsPath, onTrayClick, onQuitClick); + if (!removeMainI18nListener) { + removeMainI18nListener = onMainI18nLanguageChange(() => { + tray?.refreshTranslations(); + }); + } + return tray; } diff --git a/src/apps/renderer/localize/locales/en.json b/src/apps/renderer/localize/locales/en.json index 72b99c2299..8c6954c8e9 100644 --- a/src/apps/renderer/localize/locales/en.json +++ b/src/apps/renderer/localize/locales/en.json @@ -433,5 +433,19 @@ }, "ctaUpgrade": "Upgrade plan", "plan": "{{planName}} -> up to {{planMaxFileSize}}" + }, + "main": { + "tray": { + "menu": { + "open-app": "Internxt {{version}}", + "quit": "Quit" + }, + "tooltip": { + "syncing": "Sync in process", + "idle": "Internxt {{version}}", + "alert": "There are some issues with your sync", + "loading": "Loading Internxt..." + } + } } } diff --git a/src/apps/renderer/localize/locales/es.json b/src/apps/renderer/localize/locales/es.json index b5300ac717..80976c0a29 100644 --- a/src/apps/renderer/localize/locales/es.json +++ b/src/apps/renderer/localize/locales/es.json @@ -433,5 +433,19 @@ }, "ctaUpgrade": "Mejorar plan", "plan": "{{planName}} -> hasta {{planMaxFileSize}}" + }, + "main": { + "tray": { + "menu": { + "open-app": "Internxt {{version}}", + "quit": "Salir" + }, + "tooltip": { + "syncing": "Sincronizacion en curso", + "idle": "Internxt {{version}}", + "alert": "Hay algunos problemas con tu sincronizacion", + "loading": "Cargando Internxt..." + } + } } } diff --git a/src/apps/renderer/localize/locales/fr.json b/src/apps/renderer/localize/locales/fr.json index 7cccf062aa..684e72f6ec 100644 --- a/src/apps/renderer/localize/locales/fr.json +++ b/src/apps/renderer/localize/locales/fr.json @@ -433,5 +433,19 @@ }, "ctaUpgrade": "Mettre à jour le plan", "plan": "{{planName}} -> jusqu'à {{planMaxFileSize}}" + }, + "main": { + "tray": { + "menu": { + "open-app": "Internxt {{version}}", + "quit": "Fermer" + }, + "tooltip": { + "syncing": "Synchronisation en cours", + "idle": "Internxt {{version}}", + "alert": "Il y a des problemes avec votre synchronisation", + "loading": "Chargement d'Internxt..." + } + } } } diff --git a/src/apps/renderer/pages/Settings/General/LanguagePicker.tsx b/src/apps/renderer/pages/Settings/General/LanguagePicker.tsx index aaee1ba278..4a1e0033c0 100644 --- a/src/apps/renderer/pages/Settings/General/LanguagePicker.tsx +++ b/src/apps/renderer/pages/Settings/General/LanguagePicker.tsx @@ -39,7 +39,7 @@ export default function LanguagePicker(): JSX.Element { const lang = value; i18next.changeLanguage(lang); dayjs.locale(DayJsLocales[lang]); - window.electron.setConfigKey('preferedLanguage', lang); + window.electron.setPreferedLanguage(lang); refreshPreferedLanguage(); }; diff --git a/src/core/bootstrap/register-app-ready-flow.ts b/src/core/bootstrap/register-app-ready-flow.ts index a00f59d7d6..ed6c82ac74 100644 --- a/src/core/bootstrap/register-app-ready-flow.ts +++ b/src/core/bootstrap/register-app-ready-flow.ts @@ -11,6 +11,8 @@ import { setupAppImageDeeplink } from '../../apps/main/auth/deeplink/setup-appim import { INTERNXT_VERSION } from '../utils/utils'; import { checkForUpdates } from '../../apps/main/auto-update/check-for-updates'; import { setPendingUpdateInfo } from './bootstrap-runtime-state'; +import { setupMainI18n } from '../../apps/main/localize/i18n.service'; +import { getLanguage } from '../../apps/main/config/language'; export function registerAppReadyFlow() { app @@ -32,6 +34,7 @@ export function registerAppReadyFlow() { * while the exact behavior of the context menu options is being determined. */ // await installNautilusExtension(); + await setupMainI18n({ language: getLanguage() }); setupThemeListener(); setupTrayIcon();