From 03618fbe7fcddd7033a5f3ebcba3b68eb936da8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ch=C5=82odnicki?= Date: Wed, 31 Mar 2021 09:39:06 +0200 Subject: [PATCH] feat: support loading messages from file without lazy-loading (#1130) "langDir" + "file" can be used without the "lazy" option now. Resolves #412 --- docs/content/en/options-reference.md | 12 ++--- docs/content/es/options-reference.md | 12 ++--- src/index.js | 13 ++--- src/templates/options.js | 36 ++++++++----- src/templates/plugin.main.js | 38 +++++++------ src/templates/utils.js | 9 ++-- test/browser.test.js | 79 ++++++++++++++++++++++++++++ test/fixture/basic/pages/index.vue | 1 + 8 files changed, 142 insertions(+), 58 deletions(-) diff --git a/docs/content/en/options-reference.md b/docs/content/en/options-reference.md index 132cd40ba..c6588dff2 100644 --- a/docs/content/en/options-reference.md +++ b/docs/content/en/options-reference.md @@ -65,7 +65,7 @@ When using an object form, the properties can be: - `iso` (required when using SEO features) - The ISO code used for SEO features and for matching browser locales when using [`detectBrowserLanguage`](#detectbrowserlanguage) functionality. Should be in one of those formats: * ISO 639-1 code (e.g. `'en'`) * ISO 639-1 and ISO 3166-1 alpha-2 codes, separated by hyphen (e.g. `'en-US'`) -- `file` (requires [`lazy`](#lazy) to be enabled) - the name of the file. Will be resolved relative to `langDir` path when loading locale messages lazily +- `file` - the name of the file. Will be resolved relative to `langDir` path when loading locale messages from file - `dir` (from `v6.19.0`) The dir property specifies the direction of the elements and content, value could be `'rtl'`, `'ltr'` or `'auto'`. - `domain` (required when using [`differentDomains`](#differentdomains)) - the domain name you'd like to use for that locale (including the port if used) - `...` - any custom property set on the object will be exposed at runtime. This can be used, for example, to define the language name for the purpose of using it in a language selector on the page. @@ -122,7 +122,7 @@ Routes generation strategy. Can be set to one of the following: Whether the translations should be lazy-loaded. If this is enabled, you MUST configure `langDir` option, and locales must be an array of objects, each containing a `file` key. -Loading locale messages lazily means that only messages for currently used locale (and potentially of the default locale, if different from current locale) will be loaded on page loading. +Loading locale messages lazily means that only messages for currently used locale (and for the fallback locale, if different from current locale) will be loaded on page loading. See also [Lazy-load translations](/lazy-load-translations). @@ -131,13 +131,7 @@ See also [Lazy-load translations](/lazy-load-translations). - type: `string` or `null` - default: `null` - - -This option only works and is required when `lazy` is enabled. - - - -Directory that contains translation files when lazy-loading messages. Use Webpack paths like `~/locales/` (with trailing slash). +Directory that contains translation files to load. Can be used with or without lazy-loading (the `lazy` option). Use Webpack paths like `~/locales/` (with trailing slash). ## `detectBrowserLanguage` diff --git a/docs/content/es/options-reference.md b/docs/content/es/options-reference.md index 1e7b8a41b..6e4bada32 100644 --- a/docs/content/es/options-reference.md +++ b/docs/content/es/options-reference.md @@ -65,7 +65,7 @@ When using an object form, the properties can be: - `iso` (required when using SEO features) - The ISO code used for SEO features and for matching browser locales when using [`detectBrowserLanguage`](#detectbrowserlanguage) functionality. Should be in one of those formats: * ISO 639-1 code (e.g. `'en'`) * ISO 639-1 and ISO 3166-1 alpha-2 codes, separated by hyphen (e.g. `'en-US'`) -- `file` (requires [`lazy`](#lazy) to be enabled) - the name of the file. Will be resolved relative to `langDir` path when loading locale messages lazily +- `file` - the name of the file. Will be resolved relative to `langDir` path when loading locale messages from file - `dir` (from `v6.19.0`) The dir property specifies the direction of the elements and content, value could be `'rtl'`, `'ltr'` or `'auto'`. - `domain` (required when using [`differentDomains`](#differentdomains)) - the domain name you'd like to use for that locale (including the port if used) - `...` - any custom property set on the object will be exposed at runtime. This can be used, for example, to define the language name for the purpose of using it in a language selector on the page. @@ -122,7 +122,7 @@ Routes generation strategy. Can be set to one of the following: Whether the translations should be lazy-loaded. If this is enabled, you MUST configure `langDir` option, and locales must be an array of objects, each containing a `file` key. -Loading locale messages lazily means that only messages for currently used locale (and potentially of the default locale, if different from current locale) will be loaded on page loading. +Loading locale messages lazily means that only messages for currently used locale (and for the fallback locale, if different from current locale) will be loaded on page loading. See also [Lazy-load translations](/lazy-load-translations). @@ -131,13 +131,7 @@ See also [Lazy-load translations](/lazy-load-translations). - type: `string` or `null` - default: `null` - - -This option only works and is required when `lazy` is enabled. - - - -Directory that contains translation files when lazy-loading messages. Use Webpack paths like `~/locales/` (with trailing slash). +Directory that contains translation files to load. Can be used with or without lazy-loading (the `lazy` option). Use Webpack paths like `~/locales/` (with trailing slash). ## `detectBrowserLanguage` diff --git a/src/index.js b/src/index.js index 80e00b027..6d97eb6a7 100644 --- a/src/index.js +++ b/src/index.js @@ -18,16 +18,17 @@ export default function (moduleOptions) { return } - if (options.lazy) { - if (!options.langDir) { - throw new Error(formatMessage('When using the "lazy" option you must also set the "langDir" option.')) - } + if (options.lazy && !options.langDir) { + throw new Error(formatMessage('When using the "lazy" option you must also set the "langDir" option.')) + } + + if (options.langDir) { if (!options.locales.length || typeof options.locales[0] === 'string') { - throw new Error(formatMessage('When using the "langDir" option the "locales" option must be a list of objects.')) + throw new Error(formatMessage('When using the "langDir" option the "locales" must be a list of objects.')) } for (const locale of options.locales) { if (typeof (locale) === 'string' || !locale.file) { - throw new Error(formatMessage(`All locales must be objects and have the "file" property set when using "lazy".\nFound none in:\n${JSON.stringify(locale, null, 2)}.`)) + throw new Error(formatMessage(`All locales must be objects and have the "file" property set when using "langDir".\nFound none in:\n${JSON.stringify(locale, null, 2)}.`)) } } options.langDir = this.nuxt.resolver.resolveAlias(options.langDir) diff --git a/src/templates/options.js b/src/templates/options.js index 53ad74ac5..9158b5d8e 100644 --- a/src/templates/options.js +++ b/src/templates/options.js @@ -1,17 +1,29 @@ <% const { lazy, locales, langDir, vueI18n } = options.options const { fallbackLocale } = vueI18n || {} -let fallbackLocaleFile = '' -if (lazy && langDir && vueI18n && fallbackLocale && typeof (fallbackLocale) === 'string') { - const l = locales.find(l => l.code === fallbackLocale) - if (l) { - fallbackLocaleFile = l.file -%>import fallbackMessages from '<%= `../${relativeToBuild(langDir, l.file)}` %>' +const syncLocaleFiles = new Set() +const asyncLocaleFiles = new Set() +if (langDir) { + if (fallbackLocale && typeof (fallbackLocale) === 'string') { + const localeObject = locales.find(l => l.code === fallbackLocale) + if (localeObject) { + syncLocaleFiles.add(localeObject.file) + } + } + for (const locale of locales) { + if (!syncLocaleFiles.has(locale.file) && !asyncLocaleFiles.has(locale.file)) { + (lazy ? asyncLocaleFiles : syncLocaleFiles).add(locale.file) + } + } + for (const file of syncLocaleFiles) { +%>import locale<%= hash(file) %> from '<%= `../${relativeToBuild(langDir, file)}` %>' <% } } +%> +<% function stringifyValue(value) { if (value === undefined || typeof value === 'function') { return String(value); @@ -41,18 +53,16 @@ for (const [rootKey, rootValue] of Object.entries(options)) { } } -if (lazy && langDir) { %> +if (langDir) { %> export const localeMessages = { <% - const files = new Set(locales.map(l => l.file)) // The messages for the fallback locale are imported synchronously and available from the main bundle as then // it doesn't need to be included in every server-side response and can take better advantage of browser caching. - for (const file of files) { - if (file === fallbackLocaleFile) {%> - <%= `'${file}': () => Promise.resolve(fallbackMessages),` %><% - } else {%> + for (const file of syncLocaleFiles) {%> + <%= `'${file}': () => Promise.resolve(locale${hash(file)}),` %><% + } + for (const file of asyncLocaleFiles) {%> <%= `'${file}': () => import('../${relativeToBuild(langDir, file)}' /* webpackChunkName: "lang-${file}" */),` %><% - } } %> } diff --git a/src/templates/plugin.main.js b/src/templates/plugin.main.js index e77b93a08..2bd3557be 100644 --- a/src/templates/plugin.main.js +++ b/src/templates/plugin.main.js @@ -89,29 +89,33 @@ export default async (context) => { app.i18n.setLocaleCookie(newLocale) } - if (options.lazy) { + if (options.langDir) { const i18nFallbackLocale = app.i18n.fallbackLocale - // Load fallback locale(s). - if (i18nFallbackLocale) { + if (options.lazy) { + // Load fallback locale(s). + if (i18nFallbackLocale) { /** @type {Promise[]} */ - let localesToLoadPromises = [] - if (Array.isArray(i18nFallbackLocale)) { - localesToLoadPromises = i18nFallbackLocale.map(fbLocale => loadLanguageAsync(context, fbLocale)) - } else if (typeof i18nFallbackLocale === 'object') { - if (i18nFallbackLocale[newLocale]) { - localesToLoadPromises = localesToLoadPromises.concat(i18nFallbackLocale[newLocale].map(fbLocale => loadLanguageAsync(context, fbLocale))) + let localesToLoadPromises = [] + if (Array.isArray(i18nFallbackLocale)) { + localesToLoadPromises = i18nFallbackLocale.map(fbLocale => loadLanguageAsync(context, fbLocale)) + } else if (typeof i18nFallbackLocale === 'object') { + if (i18nFallbackLocale[newLocale]) { + localesToLoadPromises = localesToLoadPromises.concat(i18nFallbackLocale[newLocale].map(fbLocale => loadLanguageAsync(context, fbLocale))) + } + if (i18nFallbackLocale.default) { + localesToLoadPromises = localesToLoadPromises.concat(i18nFallbackLocale.default.map(fbLocale => loadLanguageAsync(context, fbLocale))) + } + } else if (newLocale !== i18nFallbackLocale) { + localesToLoadPromises.push(loadLanguageAsync(context, i18nFallbackLocale)) } - if (i18nFallbackLocale.default) { - localesToLoadPromises = localesToLoadPromises.concat(i18nFallbackLocale.default.map(fbLocale => loadLanguageAsync(context, fbLocale))) - } - } else if (newLocale !== i18nFallbackLocale) { - localesToLoadPromises.push(loadLanguageAsync(context, i18nFallbackLocale)) + await Promise.all(localesToLoadPromises) } - await Promise.all(localesToLoadPromises) + await loadLanguageAsync(context, newLocale) + } else { + // Load all locales. + await Promise.all(options.localeCodes.map(locale => loadLanguageAsync(context, locale))) } - - await loadLanguageAsync(context, newLocale) } app.i18n.locale = newLocale diff --git a/src/templates/utils.js b/src/templates/utils.js index eee547dc3..4190516ec 100755 --- a/src/templates/utils.js +++ b/src/templates/utils.js @@ -1,4 +1,4 @@ -import { localeMessages } from './options' +import { localeMessages, options } from './options' import { formatMessage } from './utils-common' /** @@ -17,11 +17,11 @@ export async function loadLanguageAsync (context, locale) { } if (!i18n.loadedLanguages.includes(locale)) { - const localeObject = /** @type {import('../../types').LocaleObject[]} */(i18n.locales).find(l => l.code === locale) + const localeObject = options.normalizedLocales.find(l => l.code === locale) if (localeObject) { const { file } = localeObject if (file) { - /* <% if (options.options.lazy && options.options.langDir) { %> */ + /* <% if (options.options.langDir) { %> */ /** @type {import('vue-i18n').LocaleMessageObject | undefined} */ let messages if (process.client) { @@ -50,9 +50,10 @@ export async function loadLanguageAsync (context, locale) { } /* <% } %> */ } else { - // eslint-disable-next-line no-console console.warn(formatMessage(`Could not find lang file for locale ${locale}`)) } + } else { + console.warn(formatMessage(`Attempted to load messages for non-existant locale code "${locale}"`)) } } } diff --git a/test/browser.test.js b/test/browser.test.js index 1c52b12bc..6b7f54277 100644 --- a/test/browser.test.js +++ b/test/browser.test.js @@ -694,6 +694,85 @@ describe(`${browserString} (with fallbackLocale, lazy)`, () => { }) }) +describe(`${browserString} (with fallbackLocale, langDir, non-lazy)`, () => { + /** @type {Nuxt} */ + let nuxt + /** @type {import('playwright-chromium').ChromiumBrowser} */ + let browser + /** @type {import('playwright-chromium').Page} */ + let page + + beforeAll(async () => { + const overrides = { + i18n: { + defaultLocale: 'pl', + lazy: false, + langDir: 'lang/', + vueI18n: { + fallbackLocale: 'pl' + } + } + } + + const localConfig = loadConfig(__dirname, 'basic', overrides, { merge: true }) + + // Override after merging options to avoid arrays being merged. + localConfig.i18n.locales = [ + { code: 'en', iso: 'en-US', file: 'en-US.js' }, + { code: 'pl', iso: 'pl-PL', file: 'pl-PL.json' }, + { code: 'no', iso: 'no-NO', file: 'no-NO.json' } + ] + + nuxt = (await setup(localConfig)).nuxt + browser = await createBrowser() + }) + + afterAll(async () => { + if (browser) { + await browser.close() + } + + await nuxt.close() + }) + + // Browser language is 'en' and so doesn't match supported ones. + // Issue https://github.com/nuxt-community/i18n-module/issues/643 + test('updates language after navigating from default to non-default locale', async () => { + page = await browser.newPage() + await page.goto(url('/')) + expect(await (await page.$('#current-locale'))?.textContent()).toBe('locale: pl') + expect(await (await page.$('#current-page'))?.textContent()).toBe('page: Strona glowna') + await navigate(page, '/no') + expect(await (await page.$('#current-locale'))?.textContent()).toBe('locale: no') + expect(await (await page.$('#current-page'))?.textContent()).toBe('page: Hjemmeside') + }) + + // Issue https://github.com/nuxt-community/i18n-module/issues/843 + test('updates language after navigating from non-default to default locale', async () => { + page = await browser.newPage() + await page.goto(url('/no')) + expect(await (await page.$('#current-locale'))?.textContent()).toBe('locale: no') + expect(await (await page.$('#current-page'))?.textContent()).toBe('page: Hjemmeside') + await navigate(page, '/') + expect(await (await page.$('#current-locale'))?.textContent()).toBe('locale: pl') + expect(await (await page.$('#current-page'))?.textContent()).toBe('page: Strona glowna') + }) + + test('messages have not been passed through Nuxt state', async () => { + page = await browser.newPage() + await page.goto(url('/')) + // @ts-ignore + const i18nState = await page.evaluate(() => window.__NUXT__.__i18n) + expect(i18nState).toBeUndefined() + }) + + test('can resolve translation for non-current locale', async () => { + page = await browser.newPage() + await page.goto(url('/')) + expect(await (await page.$('#english-translation'))?.textContent()).toBe('Homepage') + }) +}) + describe(`${browserString} (SPA)`, () => { /** @type {Nuxt} */ let nuxt diff --git a/test/fixture/basic/pages/index.vue b/test/fixture/basic/pages/index.vue index d534ca53e..9fc7a8622 100644 --- a/test/fixture/basic/pages/index.vue +++ b/test/fixture/basic/pages/index.vue @@ -5,6 +5,7 @@ {{ aboutTranslation }}
locale: {{ $i18n.locale }}
{{ $t('fn') }}
+
{{ $t('home', 'en') }}