diff --git a/app/package.json b/app/package.json
index 9a20cdf0..17ce6d94 100644
--- a/app/package.json
+++ b/app/package.json
@@ -21,7 +21,7 @@
"fork-awesome": "^1.2.0",
"simple-evaluate": "^1.4.6",
"vue": "3.3.4",
- "vue-i18n": "^8.28.2",
+ "vue-i18n": "^9.10.1",
"vue-router": "^4.3.0",
"vue-showdown": "^2.4.1",
"vuex": "^4.1.0"
diff --git a/app/src/api/errors.js b/app/src/api/errors.js
index 066c02d8..af2f5317 100644
--- a/app/src/api/errors.js
+++ b/app/src/api/errors.js
@@ -10,7 +10,7 @@ class APIError extends Error {
super(
error
? error.replaceAll('\n', '
')
- : i18n.t('error_server_unexpected'),
+ : i18n.global.t('error_server_unexpected'),
)
const urlObj = new URL(url)
this.name = 'APIError'
@@ -39,7 +39,9 @@ class APIErrorLog extends APIError {
// 0 — (means "the connexion has been closed" apparently)
class APIConnexionError extends APIError {
constructor(method, response) {
- super(method, response, { error: i18n.t('error_connection_interrupted') })
+ super(method, response, {
+ error: i18n.global.t('error_connection_interrupted'),
+ })
this.name = 'APIConnexionError'
}
}
@@ -57,7 +59,7 @@ class APIBadRequestError extends APIError {
// 401 — Unauthorized
class APIUnauthorizedError extends APIError {
constructor(method, response, errorData) {
- super(method, response, { error: i18n.t('unauthorized') })
+ super(method, response, { error: i18n.global.t('unauthorized') })
this.name = 'APIUnauthorizedError'
}
}
@@ -65,7 +67,7 @@ class APIUnauthorizedError extends APIError {
// 404 — Not Found
class APINotFoundError extends APIError {
constructor(method, response, errorData) {
- errorData.error = i18n.t('api_not_found')
+ errorData.error = i18n.global.t('api_not_found')
super(method, response, errorData)
this.name = 'APINotFoundError'
}
@@ -83,7 +85,7 @@ class APIInternalError extends APIError {
// 502 — Bad gateway (means API is down)
class APINotRespondingError extends APIError {
constructor(method, response) {
- super(method, response, { error: i18n.t('api_not_responding') })
+ super(method, response, { error: i18n.global.t('api_not_responding') })
this.name = 'APINotRespondingError'
}
}
diff --git a/app/src/helpers/yunohostArguments.js b/app/src/helpers/yunohostArguments.js
index ece0a1fc..bba66f7f 100644
--- a/app/src/helpers/yunohostArguments.js
+++ b/app/src/helpers/yunohostArguments.js
@@ -152,7 +152,7 @@ export function formatYunoHostArgument(arg) {
props: defaultProps.concat(['type', 'autocomplete', 'trim']),
callback: function () {
if (!arg.help) {
- arg.help = i18n.t('good_practices_about_admin_password')
+ arg.help = i18n.global.t('good_practices_about_admin_password')
}
arg.example = '••••••••••••'
validation.passwordLenght = validators.minLength(8)
@@ -180,7 +180,7 @@ export function formatYunoHostArgument(arg) {
if (arg.type !== 'select') {
field.props.link = {
name: arg.type + '-list',
- text: i18n.t(`manage_${arg.type}s`),
+ text: i18n.global.t(`manage_${arg.type}s`),
}
}
},
@@ -337,7 +337,7 @@ export function formatYunoHostArgument(arg) {
if (arg.helpLink) {
field.props.link = {
href: arg.helpLink.href,
- text: i18n.t(arg.helpLink.text),
+ text: i18n.global.t(arg.helpLink.text),
}
}
diff --git a/app/src/i18n/helpers.js b/app/src/i18n/helpers.js
index cd0b7117..1899f1ca 100644
--- a/app/src/i18n/helpers.js
+++ b/app/src/i18n/helpers.js
@@ -1,9 +1,9 @@
+import { nextTick } from 'vue'
import store from '@/store'
import i18n from '@/i18n'
import supportedLocales from './supportedLocales'
-let dateFnsLocale
-const loadedLanguages = []
+export let dateFnsLocale
/**
* Returns the first two supported locales that can be found in the `localStorage` or
@@ -34,26 +34,54 @@ function getDefaultLocales() {
return defaultLocales
}
-function updateDocumentLocale(locale) {
- document.documentElement.lang = locale
+export async function setI18nLocale(locale) {
+ if (!i18n.global.availableLocales.includes(locale)) {
+ await loadLocaleMessages(locale)
+ // also query/set the date-fns locale object for time translation
+ await loadDateFnsLocale(locale)
+ }
+
+ // Preload 'en' locales as it is the hard fallback
+ if (locale !== 'en' && !i18n.global.availableLocales.includes('en')) {
+ loadLocaleMessages('en')
+ }
+
+ if (i18n.mode === 'legacy') {
+ i18n.global.locale = locale
+ } else {
+ i18n.global.locale.value = locale
+ }
+
+ document.querySelector('html').setAttribute('lang', locale)
// FIXME can't currently change document direction easily since bootstrap still doesn't handle rtl.
// document.dir = locale === 'ar' ? 'rtl' : 'ltr'
}
+export async function setI18nFallbackLocale(locale) {
+ if (!i18n.global.availableLocales.includes(locale)) {
+ await loadLocaleMessages(locale)
+ }
+
+ if (i18n.mode === 'legacy') {
+ i18n.global.fallbackLocale = [locale, 'en']
+ } else {
+ i18n.global.fallbackLocale.value = [locale, 'en']
+ }
+}
+
/**
* Loads a translation file and adds its content to the i18n plugin `messages`.
*
* @return {Promise} Promise that resolve the given locale string
*/
-function loadLocaleMessages(locale) {
- if (loadedLanguages.includes(locale)) {
- return Promise.resolve(locale)
- }
- return import(`@/i18n/locales/${locale}.json`).then((messages) => {
- i18n.setLocaleMessage(locale, messages.default)
- loadedLanguages.push(locale)
- return locale
- })
+export async function loadLocaleMessages(locale) {
+ // load locale messages with dynamic import
+ const messages = await import(`./locales/${locale}.json`)
+
+ // set locale and locale message
+ i18n.global.setLocaleMessage(locale, messages)
+
+ return nextTick()
}
/**
@@ -71,19 +99,10 @@ async function loadDateFnsLocale(locale) {
/**
* Initialize all locales
*/
-function initDefaultLocales() {
+export async function initDefaultLocales() {
// Get defined locales from `localStorage` or `navigator`
const [locale, fallbackLocale] = getDefaultLocales()
- store.dispatch('UPDATE_LOCALE', locale)
- store.dispatch('UPDATE_FALLBACKLOCALE', fallbackLocale || 'en')
- return loadLocaleMessages('en')
-}
-
-export {
- initDefaultLocales,
- updateDocumentLocale,
- loadLocaleMessages,
- loadDateFnsLocale,
- dateFnsLocale,
+ await store.dispatch('UPDATE_LOCALE', locale)
+ await store.dispatch('UPDATE_FALLBACKLOCALE', fallbackLocale || 'en')
}
diff --git a/app/src/i18n/index.js b/app/src/i18n/index.js
index f575be80..df48cfc5 100644
--- a/app/src/i18n/index.js
+++ b/app/src/i18n/index.js
@@ -3,10 +3,9 @@
* @module i18n
*/
-import Vue from 'vue'
-import VueI18n from 'vue-i18n'
+import { createI18n } from 'vue-i18n'
-// Plugin Initialization
-Vue.use(VueI18n)
-
-export default new VueI18n({})
+export default createI18n({
+ // FIXME
+ legacy: true,
+})
diff --git a/app/src/store/info.js b/app/src/store/info.js
index 5bc5ce3f..fc6698bc 100644
--- a/app/src/store/info.js
+++ b/app/src/store/info.js
@@ -199,7 +199,7 @@ export default {
? humanKey
: { key: humanKey }
const humanRoute = key
- ? i18n.t('human_routes.' + key, args)
+ ? i18n.global.t('human_routes.' + key, args)
: `[${method}] /${uri}`
let request = {
@@ -368,9 +368,9 @@ export default {
// if a traduction key string has been given and we also need to pass
// the route param as a variable.
if (trad && param) {
- text = i18n.t(trad, { [param]: to.params[param] })
+ text = i18n.global.t(trad, { [param]: to.params[param] })
} else if (trad) {
- text = i18n.t(trad)
+ text = i18n.global.t(trad)
} else {
text = to.params[param]
}
@@ -395,7 +395,7 @@ export default {
}
// Display a simplified breadcrumb as the document title.
- document.title = `${getTitle(breadcrumb)} | ${i18n.t('yunohost_admin')}`
+ document.title = `${getTitle(breadcrumb)} | ${i18n.global.t('yunohost_admin')}`
},
UPDATE_TRANSITION_NAME({ state, commit }, { to, from }) {
diff --git a/app/src/store/settings.js b/app/src/store/settings.js
index a67e98d0..68ba324c 100644
--- a/app/src/store/settings.js
+++ b/app/src/store/settings.js
@@ -3,12 +3,7 @@
* @module store/settings
*/
-import i18n from '@/i18n'
-import {
- loadLocaleMessages,
- updateDocumentLocale,
- loadDateFnsLocale,
-} from '@/i18n/helpers'
+import { setI18nLocale, setI18nFallbackLocale } from '@/i18n/helpers'
import supportedLocales from '@/i18n/supportedLocales'
export default {
@@ -62,19 +57,14 @@ export default {
actions: {
UPDATE_LOCALE({ commit }, locale) {
- loadLocaleMessages(locale).then(() => {
- updateDocumentLocale(locale)
+ return setI18nLocale(locale).then(() => {
commit('SET_LOCALE', locale)
- i18n.locale = locale
})
- // also query the date-fns locale object for filters
- loadDateFnsLocale(locale)
},
UPDATE_FALLBACKLOCALE({ commit }, locale) {
- loadLocaleMessages(locale).then(() => {
+ return setI18nFallbackLocale(locale).then(() => {
commit('SET_FALLBACKLOCALE', locale)
- i18n.fallbackLocale = [locale, 'en']
})
},
diff --git a/app/vite.config.js b/app/vite.config.js
index ae773995..01fb6825 100644
--- a/app/vite.config.js
+++ b/app/vite.config.js
@@ -3,6 +3,14 @@ import { defineConfig, loadEnv } from 'vite'
import fs from 'fs'
import createVuePlugin from '@vitejs/plugin-vue'
+import supportedLocales from './src/i18n/supportedLocales'
+
+const supportedDatefnsLocales = Object.entries(supportedLocales).map(
+ ([locale, { dateFnsLocale }]) => {
+ return dateFnsLocale || locale
+ },
+)
+
export default defineConfig(({ command, mode }) => {
// Load env file based on `mode` in the current working directory.
// Set the third parameter to '' to load all env regardless of the `VITE_` prefix.
@@ -53,6 +61,28 @@ export default defineConfig(({ command, mode }) => {
if (!id.includes('node_modules') && id.includes('api/')) {
return 'core'
}
+ // Translations
+ if (id.includes('locales')) {
+ const match = /.*\/i18n\/locales\/([\w-]+)\.json/.exec(id)
+ return `locales/${match[1]}/translations`
+ }
+ // Split date-fns locales
+ if (id.includes('date-fns')) {
+ const match = /.*\/date-fns\/esm\/locale\/([\w-]+)\/.*\.js/.exec(
+ id,
+ )
+ if (match) {
+ if (supportedDatefnsLocales.includes(match[1])) {
+ return `locales/${match[1]}/date-fns`
+ } else {
+ // FIXME: currently difficult to cherry pick only needed locales,
+ // hopefully this chunk should not be fetched.
+ return 'locales/not-used'
+ }
+ } else {
+ return 'date-fns'
+ }
+ }
},
},
},