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};