diff --git a/assets/jsons/translations/en-EN.json b/assets/jsons/translations/en-EN.json new file mode 100644 index 00000000..89ade0a1 --- /dev/null +++ b/assets/jsons/translations/en-EN.json @@ -0,0 +1,18 @@ +{ + "pages":{ + "settings":{ + "languages":{ + "en-EN": "English, UK", + "en-US": "English, US", + "fr-FR": "Français", + "es-ES": "Español", + "translated":{ + "en-EN": "English, United Kingdom", + "en-US": "English, USA", + "fr-FR": "French", + "es-ES": "Spanish" + } + } + } + } +} \ No newline at end of file diff --git a/assets/jsons/translations/en-US.json b/assets/jsons/translations/en-US.json new file mode 100644 index 00000000..ab1f6aa0 --- /dev/null +++ b/assets/jsons/translations/en-US.json @@ -0,0 +1,18 @@ +{ + "pages":{ + "settings":{ + "languages":{ + "en-EN": "English, UK", + "en-US": "English, US", + "fr-FR": "Français", + "es-ES": "Español", + "translated":{ + "en-EN": "English, United Kingdom", + "en-US": "English, United States", + "fr-FR": "French", + "es-ES": "Spanish" + } + } + } + } +} \ No newline at end of file diff --git a/assets/jsons/translations/es-ES.json b/assets/jsons/translations/es-ES.json new file mode 100644 index 00000000..4391c47b --- /dev/null +++ b/assets/jsons/translations/es-ES.json @@ -0,0 +1,18 @@ +{ + "pages":{ + "settings":{ + "languages":{ + "en-EN": "English, UK", + "en-US": "English, US", + "fr-FR": "Français", + "es-ES": "Español", + "translated":{ + "en-EN": "Inglés, Reino Unido", + "en-US": "Inglés, Estados Unidos", + "fr-FR": "Francés", + "es-ES": "Español" + } + } + } + } +} \ No newline at end of file diff --git a/assets/jsons/translations/fr-FR.json b/assets/jsons/translations/fr-FR.json new file mode 100644 index 00000000..fc928fee --- /dev/null +++ b/assets/jsons/translations/fr-FR.json @@ -0,0 +1,18 @@ +{ + "pages":{ + "settings":{ + "languages":{ + "en-EN": "English, UK", + "en-US": "English, US", + "fr-FR": "Français", + "es-ES": "Español", + "translated":{ + "en-EN": "Anglais, Royaume-Uni", + "en-US": "Anglais, États-Unis", + "fr-FR": "Français", + "es-ES": "Espagnol" + } + } + } + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 617f00a2..0bdc8fa6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4141,6 +4141,14 @@ "semver": "^7.3.5" }, "dependencies": { + "dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "requires": { + "is-obj": "^2.0.0" + } + }, "semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -5011,11 +5019,11 @@ } }, "dot-prop": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", - "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-7.2.0.tgz", + "integrity": "sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA==", "requires": { - "is-obj": "^2.0.0" + "type-fest": "^2.11.2" } }, "dotenv": { diff --git a/package.json b/package.json index a6e4a528..7ab7aaa2 100644 --- a/package.json +++ b/package.json @@ -227,6 +227,7 @@ "@node-steam/vdf": "^2.2.0", "@reduxjs/toolkit": "^1.8.3", "dateformat": "^5.0.3", + "dot-prop": "^7.2.0", "electron-debug": "^3.2.0", "electron-log": "^4.4.7", "electron-store": "^8.0.2", diff --git a/src/renderer/components/settings/setting-radio-array.component.tsx b/src/renderer/components/settings/setting-radio-array.component.tsx index 82028594..e085cb67 100644 --- a/src/renderer/components/settings/setting-radio-array.component.tsx +++ b/src/renderer/components/settings/setting-radio-array.component.tsx @@ -14,12 +14,10 @@ export function SettingRadioArray({items, selectedItem = items[0].id, onItemSele

{i.text}

{i.icon && ( - <> -
- {i.textIcon && {i.textIcon}} -
+
+ {i.textIcon && {i.textIcon}} - +
)} )) } diff --git a/src/renderer/config/default-configuration.config.ts b/src/renderer/config/default-configuration.config.ts index 8b46b84b..8034f70c 100644 --- a/src/renderer/config/default-configuration.config.ts +++ b/src/renderer/config/default-configuration.config.ts @@ -1,9 +1,11 @@ export const defaultConfiguration: {[key in DefaultConfigKey]: any} = { "first-color": "#3b82ff", "second-color": "#ff4444", - "theme": "os" + "theme": "os", + "language": window.navigator.language.length <= 2 ? `${window.navigator.language}-${window.navigator.language.toLocaleUpperCase()}` : window.navigator.language, + "supported_languages": ["en-US", "en-EN", "fr-FR", "es-ES"], } -export type DefaultConfigKey = "first-color" | "second-color" | "theme"; +export type DefaultConfigKey = "first-color" | "second-color" | "theme" | "language" | "supported_languages"; export type ThemeConfig = "dark" | "light" | "os" \ No newline at end of file diff --git a/src/renderer/hooks/use-translation.hook.ts b/src/renderer/hooks/use-translation.hook.ts new file mode 100644 index 00000000..9484b475 --- /dev/null +++ b/src/renderer/hooks/use-translation.hook.ts @@ -0,0 +1,11 @@ +import { useMemo } from "react"; +import { I18nService } from "renderer/services/i18n.service" + +export function useTranslation(): (translationKey: string) => string{ + + const i18nService = I18nService.getInstance(); + + return (key: string) => { + return useMemo(() => i18nService.translate(key), [key, i18nService.currentLanguage]); + } +} \ No newline at end of file diff --git a/src/renderer/pages/settings-page.component.tsx b/src/renderer/pages/settings-page.component.tsx index 1665b853..b0f99012 100644 --- a/src/renderer/pages/settings-page.component.tsx +++ b/src/renderer/pages/settings-page.component.tsx @@ -5,9 +5,11 @@ import { RadioItem, SettingRadioArray } from "renderer/components/settings/setti import { BsmButton } from "renderer/components/shared/bsm-button.component"; import { DefaultConfigKey, ThemeConfig } from "renderer/config/default-configuration.config"; import { useObservable } from "renderer/hooks/use-observable.hook"; +import { useTranslation } from "renderer/hooks/use-translation.hook"; import { AuthUserService } from "renderer/services/auth-user.service"; import { BsDownloaderService } from "renderer/services/bs-downloader.service"; import { ConfigurationService } from "renderer/services/configuration.service" +import { I18nService } from "renderer/services/i18n.service"; import { IpcService } from "renderer/services/ipc.service"; import { ModalExitCode, ModalService, ModalType } from "renderer/services/modale.service"; import { NotificationService } from "renderer/services/notification.service"; @@ -24,6 +26,12 @@ export function SettingsPage() { const progressBarService: ProgressBarService = ProgressBarService.getInstance(); const authService: AuthUserService = AuthUserService.getInstance(); const notificationService: NotificationService = NotificationService.getInstance(); + const i18nService: I18nService = I18nService.getInstance(); + + const firstColor = useObservable(configService.watch("first-color")); + const secondColor = useObservable(configService.watch("second-color")); + const sessionExist = useObservable(authService.sessionExist$); + const t = useTranslation(); const themeItem: RadioItem[] = [ {id: 0, text: "Dark", value: "dark" as ThemeConfig}, @@ -31,12 +39,16 @@ export function SettingsPage() { {id: 3, text: "Operating System", value: "os" as ThemeConfig} ]; - const firstColor = useObservable(configService.watch("first-color")); - const secondColor = useObservable(configService.watch("second-color")); - const sessionExist = useObservable(authService.sessionExist$); + const languagesItems: RadioItem[] = i18nService.getSupportedLanguages().map((l, index) => { + return {id: index, text: t(`pages.settings.languages.${l}`), value: l, textIcon:t(`pages.settings.languages.translated.${l}`), icon: "trash"}; + }); + const[themeIdSelected, setThemeIdSelected]= useState(themeItem.find(e => e.value === themeService.getTheme()).id); + const[languageSelected, setLanguageSelected]= useState(languagesItems.find(e => e.value === i18nService.currentLanguage).id); const [installationFolder, setInstallationFolder] = useState(null); + console.log(t("test")); + useEffect(() => { loadInstallationFolder(); }, []); @@ -58,6 +70,12 @@ export function SettingsPage() { themeService.setTheme(themeItem.find(e => e.id === id).value); } + const handleChangeLanguage = (id: number) => { + const selectedLanguage = languagesItems.find(l => l.id === id).value; + i18nService.setLanguage(selectedLanguage); + setLanguageSelected(languagesItems.find(l => l.value === i18nService.currentLanguage).id); + } + const setDefaultInstallationFolder = () => { modalService.openModal(ModalType.INSTALLATION_FOLDER).then(async res => { if(res.exitCode !== ModalExitCode.COMPLETED){ return; } @@ -87,9 +105,9 @@ export function SettingsPage() { }; return ( -
+
-
+
@@ -115,6 +133,12 @@ export function SettingsPage() {
+ + + + +
+
) diff --git a/src/renderer/pages/version-viewer.component.tsx b/src/renderer/pages/version-viewer.component.tsx index 14e08a65..ff46f9e1 100644 --- a/src/renderer/pages/version-viewer.component.tsx +++ b/src/renderer/pages/version-viewer.component.tsx @@ -37,8 +37,6 @@ export function VersionViewer() { const bsDownloaderService = BsDownloaderService.getInstance(); const progressService = ProgressBarService.getInstance(); - const [launchRes, setLaunchRes] = useState(null); - useEffect(() => { setOculusMode(!!configService.get(LaunchMods.OCULUS_MOD)); setDesktopMode(!!configService.get(LaunchMods.DESKTOP_MOD)); diff --git a/src/renderer/services/i18n.service.ts b/src/renderer/services/i18n.service.ts new file mode 100644 index 00000000..05ffc81e --- /dev/null +++ b/src/renderer/services/i18n.service.ts @@ -0,0 +1,56 @@ +import { DefaultConfigKey } from "renderer/config/default-configuration.config"; +import { distinctUntilChanged, filter, map, Observable } from "rxjs"; +import { ConfigurationService } from "./configuration.service"; +import { getProperty } from "dot-prop"; + +export class I18nService { + + private static instance: I18nService; + + private readonly LANG_CONFIG_KEY: DefaultConfigKey = "language"; + private readonly LANG_FALLBACK = "en-EN"; + + private readonly configService: ConfigurationService; + + private dictionary: Object; + + public static getInstance(): I18nService{ + if(!I18nService.instance){ I18nService.instance = new I18nService(); } + return I18nService.instance; + } + + private constructor(){ + this.configService = ConfigurationService.getInstance(); + + this.currentLanguage$.pipe(filter(l => !!l), distinctUntilChanged()).subscribe(lang => { + this.dictionary = require(`../../../assets/jsons/translations/${lang}.json`); + }); + } + + public getSupportedLanguages(): string[]{ return this.configService.get("supported_languages" as DefaultConfigKey); } + public getFallbackLanguage(): string{ return this.LANG_FALLBACK; } + + public get currentLanguage(): string{ + return this.getSupportedLanguages().includes(this.configService.get(this.LANG_CONFIG_KEY)) ? this.configService.get(this.LANG_CONFIG_KEY) : this.LANG_FALLBACK; + } + public get currentLanguage$(): Observable{ + return this.configService.watch("language" as DefaultConfigKey).pipe(map(l => this.getSupportedLanguages().includes(l) ? l : this.LANG_FALLBACK)); + } + + public setLanguage(lang: string){ + this.configService.set("language" as DefaultConfigKey, this.getSupportedLanguages().includes(lang) ? lang : this.LANG_FALLBACK); + } + + public translate(translationKey: string): string{ + return getProperty(this.dictionary, translationKey) || translationKey; + } + + + + + + + + + +} \ No newline at end of file