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' + } + } }, }, },