Skip to content

Commit

Permalink
Feature: Allow custom language pack for webapp
Browse files Browse the repository at this point in the history
  • Loading branch information
tbnobody committed Oct 21, 2024
1 parent c3d3d94 commit d9a8461
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 108 deletions.
27 changes: 10 additions & 17 deletions webapp/src/components/LocaleSwitcher.vue
Original file line number Diff line number Diff line change
@@ -1,31 +1,24 @@
<template>
<select class="form-select" @change="updateLanguage()" v-model="$i18n.locale">
<option v-for="locale in $i18n.availableLocales" :key="`locale-${locale}`" :value="locale">
{{ getLocaleName(locale) }}
<select class="form-select" @change="setLocale(($event.target as HTMLSelectElement).value)" :value="$i18n.locale">
<option v-for="locale in allLocales" :key="`locale-${locale.code}`" :value="locale.code">
{{ locale.name }}
</option>
</select>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import { LOCALES } from '@/locales';
import { allLocales, setLocale } from '@/i18n';
export default defineComponent({
name: 'LocaleSwitcher',
methods: {
updateLanguage() {
localStorage.setItem('locale', this.$i18n.locale);
},
getLocaleName(locale: string): string {
return LOCALES.find((i) => i.value === locale)?.caption || '';
},
data() {
return {
allLocales,
};
},
mounted() {
if (localStorage.getItem('locale')) {
this.$i18n.locale = localStorage.getItem('locale') || 'en';
} else {
localStorage.setItem('locale', this.$i18n.locale);
}
methods: {
setLocale,
},
});
</script>
145 changes: 145 additions & 0 deletions webapp/src/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { createI18n } from 'vue-i18n';
import messages from '@intlify/unplugin-vue-i18n/messages';
import type { I18nOptions, IntlDateTimeFormat, IntlNumberFormat } from 'vue-i18n';

export const allLocales = [
{ code: 'en', name: 'English' },
{ code: 'de', name: 'Deutsch' },
{ code: 'fr', name: 'Français' },
];

const dateTimeFormatsTemplate: IntlDateTimeFormat = {
datetime: {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour12: false,
},
};

const numberFormatTemplate: IntlNumberFormat = {
decimal: {
style: 'decimal',
},
decimalNoDigits: {
style: 'decimal',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
},
decimalOneDigit: {
style: 'decimal',
minimumFractionDigits: 1,
maximumFractionDigits: 1,
},
decimalTwoDigits: {
style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
},
percent: {
style: 'percent',
},
percentOneDigit: {
style: 'percent',
minimumFractionDigits: 1,
maximumFractionDigits: 1,
},
byte: {
style: 'unit',
unit: 'byte',
},
kilobyte: {
style: 'unit',
unit: 'kilobyte',
},
megabyte: {
style: 'unit',
unit: 'megabyte',
},
celsius: {
style: 'unit',
unit: 'celsius',
maximumFractionDigits: 1,
},
};

export const dateTimeFormats: I18nOptions['datetimeFormats'] = {};
export const numberFormats: I18nOptions['numberFormats'] = {};

allLocales.forEach((locale) => {
dateTimeFormats[locale.code] = dateTimeFormatsTemplate;
numberFormats[locale.code] = numberFormatTemplate;
});

export const i18n = createI18n({
legacy: false,
globalInjection: true,
locale: navigator.language.split('-')[0],
fallbackLocale: allLocales[0].code,
messages,
datetimeFormats: dateTimeFormats,
numberFormats: numberFormats,
});

const dynamicLocales = await loadAvailLocales();
allLocales.push(...dynamicLocales);

if (localStorage.getItem('locale')) {
setLocale(localStorage.getItem('locale') || 'en');
} else {
localStorage.setItem('locale', i18n.global.locale.value);
}

// Set new locale.
export async function setLocale(locale: string) {
// Load locale if not available yet.
if (!i18n.global.availableLocales.includes(locale)) {
const messages = await loadLocale(locale);

// fetch() error occurred.
if (messages === undefined) {
i18n.global.locale.value = allLocales[0].code;
return;
}

// Add locale.
i18n.global.setLocaleMessage(locale, messages.webapp);
i18n.global.setNumberFormat(locale, numberFormatTemplate);
i18n.global.setDateTimeFormat(locale, dateTimeFormatsTemplate);
}

// Set locale.
i18n.global.locale.value = locale;
localStorage.setItem('locale', i18n.global.locale.value);
}

// Fetch locale.
async function loadLocale(locale: string) {
return fetch(`/api/i18n/language?code=${locale}`)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error('Something went wrong!');
})
.catch((error) => {
console.error(error);
});
}

// Fetch available locales
async function loadAvailLocales() {
return fetch('/api/i18n/languages')
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error('Something went wrong!');
})
.catch((error) => {
console.error(error);
});
}
78 changes: 0 additions & 78 deletions webapp/src/locales/index.ts

This file was deleted.

14 changes: 1 addition & 13 deletions webapp/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import messages from '@intlify/unplugin-vue-i18n/messages';
import mitt from 'mitt';
import { createApp } from 'vue';
import { createI18n } from 'vue-i18n';
import App from './App.vue';
import { dateTimeFormats, defaultLocale, numberFormats } from './locales';
import { tooltip } from './plugins/bootstrap';
import router from './router';
import { i18n } from './i18n';

import 'bootstrap';
import './scss/styles.scss';
Expand All @@ -17,16 +15,6 @@ app.config.globalProperties.$emitter = emitter;

app.directive('tooltip', tooltip);

const i18n = createI18n({
legacy: false,
globalInjection: true,
locale: navigator.language.split('-')[0],
fallbackLocale: defaultLocale,
messages,
datetimeFormats: dateTimeFormats,
numberFormats: numberFormats,
});

app.use(router);
app.use(i18n);

Expand Down
2 changes: 2 additions & 0 deletions webapp/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default defineConfig({
VueI18nPlugin({
/* options */
include: path.resolve(path.dirname(fileURLToPath(import.meta.url)), './src/locales/**.json'),
runtimeOnly: false,
fullInstall: false,
forceStringify: true,
strictMessage: false,
Expand Down Expand Up @@ -55,6 +56,7 @@ export default defineConfig({
assetFileNames: "assets/[name].[ext]",
},
},
target: 'es2022',
},
esbuild: {
drop: ['console', 'debugger'],
Expand Down

0 comments on commit d9a8461

Please sign in to comment.