Feature: Allow custom language pack for webapp
This commit is contained in:
parent
c3d3d947d7
commit
d9a8461a2e
@ -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
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 { 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';
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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,
|
||||
@ -55,6 +56,7 @@ export default defineConfig({
|
||||
assetFileNames: "assets/[name].[ext]",
|
||||
},
|
||||
},
|
||||
target: 'es2022',
|
||||
},
|
||||
esbuild: {
|
||||
drop: ['console', 'debugger'],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user