Feature: Allow custom language pack for webapp
This commit is contained in:
parent
c3d3d947d7
commit
d9a8461a2e
@ -1,31 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<select class="form-select" @change="updateLanguage()" v-model="$i18n.locale">
|
<select class="form-select" @change="setLocale(($event.target as HTMLSelectElement).value)" :value="$i18n.locale">
|
||||||
<option v-for="locale in $i18n.availableLocales" :key="`locale-${locale}`" :value="locale">
|
<option v-for="locale in allLocales" :key="`locale-${locale.code}`" :value="locale.code">
|
||||||
{{ getLocaleName(locale) }}
|
{{ locale.name }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { LOCALES } from '@/locales';
|
import { allLocales, setLocale } from '@/i18n';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'LocaleSwitcher',
|
name: 'LocaleSwitcher',
|
||||||
methods: {
|
data() {
|
||||||
updateLanguage() {
|
return {
|
||||||
localStorage.setItem('locale', this.$i18n.locale);
|
allLocales,
|
||||||
},
|
};
|
||||||
getLocaleName(locale: string): string {
|
|
||||||
return LOCALES.find((i) => i.value === locale)?.caption || '';
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
mounted() {
|
methods: {
|
||||||
if (localStorage.getItem('locale')) {
|
setLocale,
|
||||||
this.$i18n.locale = localStorage.getItem('locale') || 'en';
|
|
||||||
} else {
|
|
||||||
localStorage.setItem('locale', this.$i18n.locale);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
145
webapp/src/i18n.ts
Normal file
145
webapp/src/i18n.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,78 +0,0 @@
|
|||||||
import type { I18nOptions } from 'vue-i18n';
|
|
||||||
|
|
||||||
export enum Locales {
|
|
||||||
EN = 'en',
|
|
||||||
DE = 'de',
|
|
||||||
FR = 'fr',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LOCALES = [
|
|
||||||
{ value: Locales.EN, caption: 'English' },
|
|
||||||
{ value: Locales.DE, caption: 'Deutsch' },
|
|
||||||
{ value: Locales.FR, caption: 'Français' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const dateTimeFormats: I18nOptions['datetimeFormats'] = {};
|
|
||||||
export const numberFormats: I18nOptions['numberFormats'] = {};
|
|
||||||
|
|
||||||
LOCALES.forEach((locale) => {
|
|
||||||
dateTimeFormats[locale.value] = {
|
|
||||||
datetime: {
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: 'numeric',
|
|
||||||
second: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'numeric',
|
|
||||||
day: 'numeric',
|
|
||||||
hour12: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
numberFormats[locale.value] = {
|
|
||||||
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 defaultLocale = Locales.EN;
|
|
||||||
@ -1,11 +1,9 @@
|
|||||||
import messages from '@intlify/unplugin-vue-i18n/messages';
|
|
||||||
import mitt from 'mitt';
|
import mitt from 'mitt';
|
||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import { createI18n } from 'vue-i18n';
|
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import { dateTimeFormats, defaultLocale, numberFormats } from './locales';
|
|
||||||
import { tooltip } from './plugins/bootstrap';
|
import { tooltip } from './plugins/bootstrap';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
|
import { i18n } from './i18n';
|
||||||
|
|
||||||
import 'bootstrap';
|
import 'bootstrap';
|
||||||
import './scss/styles.scss';
|
import './scss/styles.scss';
|
||||||
@ -17,16 +15,6 @@ app.config.globalProperties.$emitter = emitter;
|
|||||||
|
|
||||||
app.directive('tooltip', tooltip);
|
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(router);
|
||||||
app.use(i18n);
|
app.use(i18n);
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,7 @@ export default defineConfig({
|
|||||||
VueI18nPlugin({
|
VueI18nPlugin({
|
||||||
/* options */
|
/* options */
|
||||||
include: path.resolve(path.dirname(fileURLToPath(import.meta.url)), './src/locales/**.json'),
|
include: path.resolve(path.dirname(fileURLToPath(import.meta.url)), './src/locales/**.json'),
|
||||||
|
runtimeOnly: false,
|
||||||
fullInstall: false,
|
fullInstall: false,
|
||||||
forceStringify: true,
|
forceStringify: true,
|
||||||
strictMessage: false,
|
strictMessage: false,
|
||||||
@ -55,6 +56,7 @@ export default defineConfig({
|
|||||||
assetFileNames: "assets/[name].[ext]",
|
assetFileNames: "assets/[name].[ext]",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
target: 'es2022',
|
||||||
},
|
},
|
||||||
esbuild: {
|
esbuild: {
|
||||||
drop: ['console', 'debugger'],
|
drop: ['console', 'debugger'],
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user