diff --git a/studio/scripts/i18n.types.js b/studio/scripts/i18n.types.js index ef80660c0..094634a63 100644 --- a/studio/scripts/i18n.types.js +++ b/studio/scripts/i18n.types.js @@ -16,7 +16,7 @@ const generate = async () => { }; }); - const lang = `lang: 'en';`; + const lang = `lang: Languages;`; const main = `\n\ninterface I18n {${lang}${data.map((i) => `${i.key}: ${i.name};`).join('')}}`; const interfaces = data.map((i) => `\n\ninterface ${i.name} {${i.properties.join('')}}`).join(''); diff --git a/studio/src/app/app-root.tsx b/studio/src/app/app-root.tsx index 9a50dd66c..a658a3855 100644 --- a/studio/src/app/app-root.tsx +++ b/studio/src/app/app-root.tsx @@ -13,6 +13,7 @@ import {OfflineService} from './services/editor/offline/offline.service'; import {NavDirection, NavParams} from './stores/nav.store'; import {ColorService} from './services/color/color.service'; import {SettingsService} from './services/settings/settings.service'; +import {LangService} from './services/lang/lang.service'; @Component({ tag: 'app-root', @@ -27,6 +28,7 @@ export class AppRoot { private readonly themeService: ThemeService; private readonly colorService: ColorService; private readonly settingsService: SettingsService; + private readonly langService: LangService; @State() private loading: boolean = true; @@ -43,6 +45,7 @@ export class AppRoot { this.themeService = ThemeService.getInstance(); this.colorService = ColorService.getInstance(); this.settingsService = SettingsService.getInstance(); + this.langService = LangService.getInstance(); } async componentWillLoad() { @@ -53,6 +56,7 @@ export class AppRoot { this.themeService.initDarkModePreference(), this.colorService.init(), this.settingsService.init(), + this.langService.init(), ]; await Promise.all(promises); diff --git a/studio/src/app/definitions/i18.d.ts b/studio/src/app/definitions/i18.d.ts index 9b4de7d41..1759408e3 100644 --- a/studio/src/app/definitions/i18.d.ts +++ b/studio/src/app/definitions/i18.d.ts @@ -487,7 +487,7 @@ interface I18nPoll { } interface I18n { - lang: 'en'; + lang: Languages; core: I18nCore; nav: I18nNav; menu: I18nMenu; diff --git a/studio/src/app/definitions/languages.d.ts b/studio/src/app/definitions/languages.d.ts new file mode 100644 index 000000000..b9acd5480 --- /dev/null +++ b/studio/src/app/definitions/languages.d.ts @@ -0,0 +1 @@ +type Languages = 'en' | 'es'; diff --git a/studio/src/app/pages/core/settings/app-customization/app-customization.tsx b/studio/src/app/pages/core/settings/app-customization/app-customization.tsx index 847c4838b..1ad08a1ce 100644 --- a/studio/src/app/pages/core/settings/app-customization/app-customization.tsx +++ b/studio/src/app/pages/core/settings/app-customization/app-customization.tsx @@ -1,4 +1,4 @@ -import {Component, Fragment, h} from '@stencil/core'; +import {Component, h} from '@stencil/core'; import themeStore from '../../../../stores/theme.store'; import settingsStore from '../../../../stores/settings.store'; @@ -29,6 +29,10 @@ export class AppCustomization { settingsStore.state.editMode = settingsStore.state.editMode === 'css' ? 'properties' : 'css'; } + private toggleLang($event: CustomEvent) { + i18n.state.lang = $event.detail.value; + } + render() { return [ , @@ -39,6 +43,8 @@ export class AppCustomization { {this.renderDarkLightToggle()} + {this.renderLang()} + {this.renderEditMode()} @@ -57,25 +63,40 @@ export class AppCustomization { ); } + private renderLang() { + return ( + + {i18n.state.editor.language} + this.toggleLang($event)} + interface="popover" + mode="md" + class="ion-padding-start ion-padding-end"> + English + EspaƱol + + + ); + } + private renderEditMode() { return ( - - - {i18n.state.settings.edit_mode} - - - this.toggleEditMode()}> - - - {i18n.state.settings.properties} - - - - - CSS - - - + + {i18n.state.settings.edit_mode} + + this.toggleEditMode()} + interface="popover" + mode="md" + class="ion-padding-start ion-padding-end"> + {i18n.state.settings.properties} + CSS + + ); } } diff --git a/studio/src/app/services/lang/lang.service.ts b/studio/src/app/services/lang/lang.service.ts new file mode 100644 index 000000000..0c697520c --- /dev/null +++ b/studio/src/app/services/lang/lang.service.ts @@ -0,0 +1,67 @@ +import i18n from '../../stores/i18n.store'; + +import {get} from 'idb-keyval'; + +export class LangService { + private static instance: LangService; + + private constructor() { + // Private constructor, singleton + } + + static getInstance() { + if (!LangService.instance) { + LangService.instance = new LangService(); + } + return LangService.instance; + } + + async init() { + try { + const lang: Languages | null = await get('deckdeckgo_lang'); + + if (lang) { + i18n.state.lang = lang; + return; + } + + this.initDefaultLang(); + } catch (err) { + console.warn(`Couldn't find lang. Proceeding with default`); + } + } + + private initDefaultLang() { + const browserLang: string | undefined = this.getBrowserLang(); + i18n.state.lang = /(es|en)/gi.test(browserLang) ? (browserLang as Languages) : 'en'; + } + + /** + * From ngx-translate + * https://github.com/ngx-translate/core/blob/efcb4f43a645d9ac630aae8e50b60cc883e675fd/projects/ngx-translate/core/src/lib/translate.service.ts + * @private + */ + private getBrowserLang(): string | undefined { + if (typeof window === 'undefined' || typeof window.navigator === 'undefined') { + return undefined; + } + + let browserLang: string | null = window.navigator.languages ? window.navigator.languages[0] : null; + // @ts-ignore + browserLang = browserLang || window.navigator.language || window.navigator.browserLanguage || window.navigator.userLanguage; + + if (typeof browserLang === 'undefined') { + return undefined; + } + + if (browserLang.indexOf('-') !== -1) { + browserLang = browserLang.split('-')[0]; + } + + if (browserLang.indexOf('_') !== -1) { + browserLang = browserLang.split('_')[0]; + } + + return browserLang; + } +} diff --git a/studio/src/app/stores/i18n.store.ts b/studio/src/app/stores/i18n.store.ts index 6d76448cc..8d61d4917 100644 --- a/studio/src/app/stores/i18n.store.ts +++ b/studio/src/app/stores/i18n.store.ts @@ -1,10 +1,45 @@ import {createStore} from '@stencil/store'; import en from '../../assets/i18n/en.json'; +import {set} from 'idb-keyval'; -const {state} = createStore({ - lang: en, +const {state, onChange} = createStore({ + lang: 'en', ...(en as Partial), } as I18n); +const esI18n = async (): Promise => { + return { + lang: 'es', + ...(await import(`../../assets/i18n/es.json`)), + }; +}; + +const enI18n = (): I18n => { + return { + lang: 'en', + ...(en as Partial), + } as I18n; +}; + +onChange('lang', async (lang: Languages) => { + let bundle: I18n; + + switch (lang) { + case 'es': + bundle = await esI18n(); + break; + default: + bundle = enI18n(); + } + + Object.assign(state, bundle); +}); + +onChange('lang', (lang: Languages) => { + set('deckdeckgo_lang', lang).catch((err) => { + console.error('Failed to update IDB with new language', err); + }); +}); + export default {state};