refactor: upgrade to i18n9

This commit is contained in:
axolotle 2024-03-04 03:31:24 +01:00
parent dd9ae21472
commit 81707bb11a
8 changed files with 97 additions and 57 deletions

View file

@ -21,7 +21,7 @@
"fork-awesome": "^1.2.0", "fork-awesome": "^1.2.0",
"simple-evaluate": "^1.4.6", "simple-evaluate": "^1.4.6",
"vue": "3.3.4", "vue": "3.3.4",
"vue-i18n": "^8.28.2", "vue-i18n": "^9.10.1",
"vue-router": "^4.3.0", "vue-router": "^4.3.0",
"vue-showdown": "^2.4.1", "vue-showdown": "^2.4.1",
"vuex": "^4.1.0" "vuex": "^4.1.0"

View file

@ -10,7 +10,7 @@ class APIError extends Error {
super( super(
error error
? error.replaceAll('\n', '<br>') ? error.replaceAll('\n', '<br>')
: i18n.t('error_server_unexpected'), : i18n.global.t('error_server_unexpected'),
) )
const urlObj = new URL(url) const urlObj = new URL(url)
this.name = 'APIError' this.name = 'APIError'
@ -39,7 +39,9 @@ class APIErrorLog extends APIError {
// 0 — (means "the connexion has been closed" apparently) // 0 — (means "the connexion has been closed" apparently)
class APIConnexionError extends APIError { class APIConnexionError extends APIError {
constructor(method, response) { 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' this.name = 'APIConnexionError'
} }
} }
@ -57,7 +59,7 @@ class APIBadRequestError extends APIError {
// 401 — Unauthorized // 401 — Unauthorized
class APIUnauthorizedError extends APIError { class APIUnauthorizedError extends APIError {
constructor(method, response, errorData) { constructor(method, response, errorData) {
super(method, response, { error: i18n.t('unauthorized') }) super(method, response, { error: i18n.global.t('unauthorized') })
this.name = 'APIUnauthorizedError' this.name = 'APIUnauthorizedError'
} }
} }
@ -65,7 +67,7 @@ class APIUnauthorizedError extends APIError {
// 404 — Not Found // 404 — Not Found
class APINotFoundError extends APIError { class APINotFoundError extends APIError {
constructor(method, response, errorData) { constructor(method, response, errorData) {
errorData.error = i18n.t('api_not_found') errorData.error = i18n.global.t('api_not_found')
super(method, response, errorData) super(method, response, errorData)
this.name = 'APINotFoundError' this.name = 'APINotFoundError'
} }
@ -83,7 +85,7 @@ class APIInternalError extends APIError {
// 502 — Bad gateway (means API is down) // 502 — Bad gateway (means API is down)
class APINotRespondingError extends APIError { class APINotRespondingError extends APIError {
constructor(method, response) { 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' this.name = 'APINotRespondingError'
} }
} }

View file

@ -152,7 +152,7 @@ export function formatYunoHostArgument(arg) {
props: defaultProps.concat(['type', 'autocomplete', 'trim']), props: defaultProps.concat(['type', 'autocomplete', 'trim']),
callback: function () { callback: function () {
if (!arg.help) { if (!arg.help) {
arg.help = i18n.t('good_practices_about_admin_password') arg.help = i18n.global.t('good_practices_about_admin_password')
} }
arg.example = '••••••••••••' arg.example = '••••••••••••'
validation.passwordLenght = validators.minLength(8) validation.passwordLenght = validators.minLength(8)
@ -180,7 +180,7 @@ export function formatYunoHostArgument(arg) {
if (arg.type !== 'select') { if (arg.type !== 'select') {
field.props.link = { field.props.link = {
name: arg.type + '-list', 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) { if (arg.helpLink) {
field.props.link = { field.props.link = {
href: arg.helpLink.href, href: arg.helpLink.href,
text: i18n.t(arg.helpLink.text), text: i18n.global.t(arg.helpLink.text),
} }
} }

View file

@ -1,9 +1,9 @@
import { nextTick } from 'vue'
import store from '@/store' import store from '@/store'
import i18n from '@/i18n' import i18n from '@/i18n'
import supportedLocales from './supportedLocales' import supportedLocales from './supportedLocales'
let dateFnsLocale export let dateFnsLocale
const loadedLanguages = []
/** /**
* Returns the first two supported locales that can be found in the `localStorage` or * Returns the first two supported locales that can be found in the `localStorage` or
@ -34,26 +34,54 @@ function getDefaultLocales() {
return defaultLocales return defaultLocales
} }
function updateDocumentLocale(locale) { export async function setI18nLocale(locale) {
document.documentElement.lang = 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. // FIXME can't currently change document direction easily since bootstrap still doesn't handle rtl.
// document.dir = locale === 'ar' ? 'rtl' : 'ltr' // 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`. * Loads a translation file and adds its content to the i18n plugin `messages`.
* *
* @return {Promise<string>} Promise that resolve the given locale string * @return {Promise<string>} Promise that resolve the given locale string
*/ */
function loadLocaleMessages(locale) { export async function loadLocaleMessages(locale) {
if (loadedLanguages.includes(locale)) { // load locale messages with dynamic import
return Promise.resolve(locale) const messages = await import(`./locales/${locale}.json`)
}
return import(`@/i18n/locales/${locale}.json`).then((messages) => { // set locale and locale message
i18n.setLocaleMessage(locale, messages.default) i18n.global.setLocaleMessage(locale, messages)
loadedLanguages.push(locale)
return locale return nextTick()
})
} }
/** /**
@ -71,19 +99,10 @@ async function loadDateFnsLocale(locale) {
/** /**
* Initialize all locales * Initialize all locales
*/ */
function initDefaultLocales() { export async function initDefaultLocales() {
// Get defined locales from `localStorage` or `navigator` // Get defined locales from `localStorage` or `navigator`
const [locale, fallbackLocale] = getDefaultLocales() const [locale, fallbackLocale] = getDefaultLocales()
store.dispatch('UPDATE_LOCALE', locale) await store.dispatch('UPDATE_LOCALE', locale)
store.dispatch('UPDATE_FALLBACKLOCALE', fallbackLocale || 'en') await store.dispatch('UPDATE_FALLBACKLOCALE', fallbackLocale || 'en')
return loadLocaleMessages('en')
}
export {
initDefaultLocales,
updateDocumentLocale,
loadLocaleMessages,
loadDateFnsLocale,
dateFnsLocale,
} }

View file

@ -3,10 +3,9 @@
* @module i18n * @module i18n
*/ */
import Vue from 'vue' import { createI18n } from 'vue-i18n'
import VueI18n from 'vue-i18n'
// Plugin Initialization export default createI18n({
Vue.use(VueI18n) // FIXME
legacy: true,
export default new VueI18n({}) })

View file

@ -199,7 +199,7 @@ export default {
? humanKey ? humanKey
: { key: humanKey } : { key: humanKey }
const humanRoute = key const humanRoute = key
? i18n.t('human_routes.' + key, args) ? i18n.global.t('human_routes.' + key, args)
: `[${method}] /${uri}` : `[${method}] /${uri}`
let request = { let request = {
@ -368,9 +368,9 @@ export default {
// if a traduction key string has been given and we also need to pass // if a traduction key string has been given and we also need to pass
// the route param as a variable. // the route param as a variable.
if (trad && param) { if (trad && param) {
text = i18n.t(trad, { [param]: to.params[param] }) text = i18n.global.t(trad, { [param]: to.params[param] })
} else if (trad) { } else if (trad) {
text = i18n.t(trad) text = i18n.global.t(trad)
} else { } else {
text = to.params[param] text = to.params[param]
} }
@ -395,7 +395,7 @@ export default {
} }
// Display a simplified breadcrumb as the document title. // 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 }) { UPDATE_TRANSITION_NAME({ state, commit }, { to, from }) {

View file

@ -3,12 +3,7 @@
* @module store/settings * @module store/settings
*/ */
import i18n from '@/i18n' import { setI18nLocale, setI18nFallbackLocale } from '@/i18n/helpers'
import {
loadLocaleMessages,
updateDocumentLocale,
loadDateFnsLocale,
} from '@/i18n/helpers'
import supportedLocales from '@/i18n/supportedLocales' import supportedLocales from '@/i18n/supportedLocales'
export default { export default {
@ -62,19 +57,14 @@ export default {
actions: { actions: {
UPDATE_LOCALE({ commit }, locale) { UPDATE_LOCALE({ commit }, locale) {
loadLocaleMessages(locale).then(() => { return setI18nLocale(locale).then(() => {
updateDocumentLocale(locale)
commit('SET_LOCALE', locale) commit('SET_LOCALE', locale)
i18n.locale = locale
}) })
// also query the date-fns locale object for filters
loadDateFnsLocale(locale)
}, },
UPDATE_FALLBACKLOCALE({ commit }, locale) { UPDATE_FALLBACKLOCALE({ commit }, locale) {
loadLocaleMessages(locale).then(() => { return setI18nFallbackLocale(locale).then(() => {
commit('SET_FALLBACKLOCALE', locale) commit('SET_FALLBACKLOCALE', locale)
i18n.fallbackLocale = [locale, 'en']
}) })
}, },

View file

@ -3,6 +3,14 @@ import { defineConfig, loadEnv } from 'vite'
import fs from 'fs' import fs from 'fs'
import createVuePlugin from '@vitejs/plugin-vue' 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 }) => { export default defineConfig(({ command, mode }) => {
// Load env file based on `mode` in the current working directory. // 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. // 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/')) { if (!id.includes('node_modules') && id.includes('api/')) {
return 'core' 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'
}
}
}, },
}, },
}, },