diff --git a/webapp/src/components/LocaleSwitcher.vue b/webapp/src/components/LocaleSwitcher.vue
index 4dc7545..be65b76 100644
--- a/webapp/src/components/LocaleSwitcher.vue
+++ b/webapp/src/components/LocaleSwitcher.vue
@@ -1,31 +1,24 @@
-
diff --git a/webapp/src/i18n.ts b/webapp/src/i18n.ts
new file mode 100644
index 0000000..454e891
--- /dev/null
+++ b/webapp/src/i18n.ts
@@ -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);
+ });
+}
diff --git a/webapp/src/locales/index.ts b/webapp/src/locales/index.ts
deleted file mode 100644
index 596a419..0000000
--- a/webapp/src/locales/index.ts
+++ /dev/null
@@ -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;
diff --git a/webapp/src/main.ts b/webapp/src/main.ts
index 129da4c..ef2243e 100644
--- a/webapp/src/main.ts
+++ b/webapp/src/main.ts
@@ -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);
diff --git a/webapp/vite.config.ts b/webapp/vite.config.ts
index 8977d6f..ed6b672 100644
--- a/webapp/vite.config.ts
+++ b/webapp/vite.config.ts
@@ -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'],