From 21fcf81fcaac45b5c1ba1684f86017cb32fb3124 Mon Sep 17 00:00:00 2001 From: Bobbie Goede Date: Thu, 23 Jan 2025 16:04:32 +0100 Subject: [PATCH] fix: locale switching middleware in static build (#3323) --- pnpm-lock.yaml | 9 ++ specs/fixtures/lazy-without-server/app.vue | 11 ++ .../components/LangSwitcher.vue | 44 ++++++ .../lazy-without-server/i18n-module/index.ts | 20 +++ .../locales/lazy-locale-module-nl.ts | 5 + .../lazy-without-server/i18n.config.ts | 5 + .../lang/lazy-locale-en-GB.js | 14 ++ .../lang/lazy-locale-en-GB.ts | 3 + .../lang/lazy-locale-en.json | 9 ++ .../lang/lazy-locale-fr.json5 | 7 + .../lazy-without-server/nuxt.config.ts | 58 +++++++ .../fixtures/lazy-without-server/package.json | 15 ++ .../lazy-without-server/pages/about/index.vue | 35 +++++ .../lazy-without-server/pages/index.vue | 51 ++++++ .../lazy-without-server/pages/manual-load.vue | 18 +++ specs/ssg/basic_lazy_load.spec.ts | 147 ++++++++++++++++++ specs/utils/server.ts | 2 +- src/runtime/plugins/route-locale-detect.ts | 3 +- 18 files changed, 453 insertions(+), 3 deletions(-) create mode 100644 specs/fixtures/lazy-without-server/app.vue create mode 100644 specs/fixtures/lazy-without-server/components/LangSwitcher.vue create mode 100644 specs/fixtures/lazy-without-server/i18n-module/index.ts create mode 100644 specs/fixtures/lazy-without-server/i18n-module/locales/lazy-locale-module-nl.ts create mode 100644 specs/fixtures/lazy-without-server/i18n.config.ts create mode 100644 specs/fixtures/lazy-without-server/lang/lazy-locale-en-GB.js create mode 100644 specs/fixtures/lazy-without-server/lang/lazy-locale-en-GB.ts create mode 100644 specs/fixtures/lazy-without-server/lang/lazy-locale-en.json create mode 100644 specs/fixtures/lazy-without-server/lang/lazy-locale-fr.json5 create mode 100644 specs/fixtures/lazy-without-server/nuxt.config.ts create mode 100644 specs/fixtures/lazy-without-server/package.json create mode 100644 specs/fixtures/lazy-without-server/pages/about/index.vue create mode 100644 specs/fixtures/lazy-without-server/pages/index.vue create mode 100644 specs/fixtures/lazy-without-server/pages/manual-load.vue create mode 100644 specs/ssg/basic_lazy_load.spec.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f3ccae05..d1410f4cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -322,6 +322,15 @@ importers: specifier: latest version: 3.15.2(@parcel/watcher@2.4.1)(@types/node@20.14.9)(db0@0.2.1)(encoding@0.1.13)(eslint@9.5.0)(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.30.1)(terser@5.31.1)(typescript@5.6.2)(vite@6.0.7(@types/node@20.14.9)(jiti@1.21.0)(terser@5.31.1)(yaml@2.7.0))(vue-tsc@2.1.6(typescript@5.6.2))(yaml@2.7.0) + specs/fixtures/lazy-without-server: + devDependencies: + '@nuxtjs/i18n': + specifier: link:../../.. + version: link:../../.. + nuxt: + specifier: latest + version: 3.15.2(@parcel/watcher@2.4.1)(@types/node@20.14.9)(db0@0.2.1)(encoding@0.1.13)(eslint@9.5.0)(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.30.1)(terser@5.31.1)(typescript@5.6.2)(vite@6.0.7(@types/node@20.14.9)(jiti@1.21.0)(terser@5.31.1)(yaml@2.7.0))(vue-tsc@2.1.6(typescript@5.6.2))(yaml@2.7.0) + specs/fixtures/multi_domains_locales: devDependencies: '@nuxtjs/i18n': diff --git a/specs/fixtures/lazy-without-server/app.vue b/specs/fixtures/lazy-without-server/app.vue new file mode 100644 index 000000000..1d6984808 --- /dev/null +++ b/specs/fixtures/lazy-without-server/app.vue @@ -0,0 +1,11 @@ + + + diff --git a/specs/fixtures/lazy-without-server/components/LangSwitcher.vue b/specs/fixtures/lazy-without-server/components/LangSwitcher.vue new file mode 100644 index 000000000..ba17e8cc7 --- /dev/null +++ b/specs/fixtures/lazy-without-server/components/LangSwitcher.vue @@ -0,0 +1,44 @@ + + + diff --git a/specs/fixtures/lazy-without-server/i18n-module/index.ts b/specs/fixtures/lazy-without-server/i18n-module/index.ts new file mode 100644 index 000000000..0f25a4ad4 --- /dev/null +++ b/specs/fixtures/lazy-without-server/i18n-module/index.ts @@ -0,0 +1,20 @@ +import { createResolver, defineNuxtModule } from '@nuxt/kit' + +export default defineNuxtModule({ + async setup(options, nuxt) { + const { resolve } = createResolver(import.meta.url) + nuxt.hook('i18n:registerModule', register => { + register({ + langDir: resolve('./locales'), + locales: [ + { + code: 'nl', + language: 'nl-NL', + file: 'lazy-locale-module-nl.ts', + name: 'Nederlands' + } + ] + }) + }) + } +}) diff --git a/specs/fixtures/lazy-without-server/i18n-module/locales/lazy-locale-module-nl.ts b/specs/fixtures/lazy-without-server/i18n-module/locales/lazy-locale-module-nl.ts new file mode 100644 index 000000000..1fa03ae7d --- /dev/null +++ b/specs/fixtures/lazy-without-server/i18n-module/locales/lazy-locale-module-nl.ts @@ -0,0 +1,5 @@ +export default defineI18nLocale(locale => ({ + moduleLayerText: 'This is a merged module layer locale key in Dutch', + welcome: 'Welkom!', + dynamicTime: new Date().toISOString() +})) diff --git a/specs/fixtures/lazy-without-server/i18n.config.ts b/specs/fixtures/lazy-without-server/i18n.config.ts new file mode 100644 index 000000000..48ac97f9f --- /dev/null +++ b/specs/fixtures/lazy-without-server/i18n.config.ts @@ -0,0 +1,5 @@ +export default { + legacy: false, + messages: {}, + fallbackLocale: 'en' +} diff --git a/specs/fixtures/lazy-without-server/lang/lazy-locale-en-GB.js b/specs/fixtures/lazy-without-server/lang/lazy-locale-en-GB.js new file mode 100644 index 000000000..2bf27681a --- /dev/null +++ b/specs/fixtures/lazy-without-server/lang/lazy-locale-en-GB.js @@ -0,0 +1,14 @@ +export default defineI18nLocale(async function (locale) { + return { + html: 'This is the danger', + settings: { + nest: { + foo: { + bar: { + profile: 'Profile1' + } + } + } + } + } +}) diff --git a/specs/fixtures/lazy-without-server/lang/lazy-locale-en-GB.ts b/specs/fixtures/lazy-without-server/lang/lazy-locale-en-GB.ts new file mode 100644 index 000000000..a0a58cbda --- /dev/null +++ b/specs/fixtures/lazy-without-server/lang/lazy-locale-en-GB.ts @@ -0,0 +1,3 @@ +export default { + settings_nest_foo_bar_profile: 'Profile2' +} diff --git a/specs/fixtures/lazy-without-server/lang/lazy-locale-en.json b/specs/fixtures/lazy-without-server/lang/lazy-locale-en.json new file mode 100644 index 000000000..51b73ce28 --- /dev/null +++ b/specs/fixtures/lazy-without-server/lang/lazy-locale-en.json @@ -0,0 +1,9 @@ +{ + "home": "Homepage", + "about": "About us", + "posts": "Posts", + "dynamic": "Dynamic", + "html": "This is the danger", + "dynamicTime": "Not dynamic", + "welcome": "Welcome!" +} diff --git a/specs/fixtures/lazy-without-server/lang/lazy-locale-fr.json5 b/specs/fixtures/lazy-without-server/lang/lazy-locale-fr.json5 new file mode 100644 index 000000000..02dce68e9 --- /dev/null +++ b/specs/fixtures/lazy-without-server/lang/lazy-locale-fr.json5 @@ -0,0 +1,7 @@ +{ + home: 'Accueil', + about: 'À propos', + posts: 'Articles', + dynamic: 'Dynamique', + dynamicTime: 'Not dynamic' +} diff --git a/specs/fixtures/lazy-without-server/nuxt.config.ts b/specs/fixtures/lazy-without-server/nuxt.config.ts new file mode 100644 index 000000000..cd0442355 --- /dev/null +++ b/specs/fixtures/lazy-without-server/nuxt.config.ts @@ -0,0 +1,58 @@ +import i18nModule from './i18n-module' + +// https://nuxt.com/docs/guide/directory-structure/nuxt.config +export default defineNuxtConfig({ + vite: { + // Prevent reload by optimizing dependency before discovery + optimizeDeps: { + include: ['@unhead/vue'] + }, + // https://nuxt.com/blog/v3-11#chunk-naming + // We change the chunk file name so we can detect file requests in our tests + $client: { + build: { + rollupOptions: { + output: { + chunkFileNames: '_nuxt/[name].js', + entryFileNames: '_nuxt/[name].js' + } + } + } + } + }, + modules: [i18nModule, '@nuxtjs/i18n'], + i18n: { + debug: true, + restructureDir: false, + baseUrl: 'http://localhost:3000', + // langDir: 'lang', + // defaultLocale: 'fr', + detectBrowserLanguage: false, + compilation: { + strictMessage: false + }, + defaultLocale: 'en', + langDir: 'lang', + lazy: true, + locales: [ + { + code: 'en', + language: 'en-US', + file: 'lazy-locale-en.json', + name: 'English' + }, + { + code: 'en-GB', + language: 'en-GB', + files: ['lazy-locale-en.json', 'lazy-locale-en-GB.js', 'lazy-locale-en-GB.ts'], + name: 'English (UK)' + }, + { + code: 'fr', + language: 'fr-FR', + file: { path: 'lazy-locale-fr.json5', cache: false }, + name: 'Français' + } + ] + } +}) diff --git a/specs/fixtures/lazy-without-server/package.json b/specs/fixtures/lazy-without-server/package.json new file mode 100644 index 000000000..a95cd1de5 --- /dev/null +++ b/specs/fixtures/lazy-without-server/package.json @@ -0,0 +1,15 @@ +{ + "name": "nuxt3-test-lazy", + "private": true, + "type": "module", + "scripts": { + "dev": "nuxi dev", + "build": "nuxt build", + "generate": "nuxt generate", + "start": "node .output/server/index.mjs" + }, + "devDependencies": { + "@nuxtjs/i18n": "latest", + "nuxt": "latest" + } +} diff --git a/specs/fixtures/lazy-without-server/pages/about/index.vue b/specs/fixtures/lazy-without-server/pages/about/index.vue new file mode 100644 index 000000000..1e3fcfa77 --- /dev/null +++ b/specs/fixtures/lazy-without-server/pages/about/index.vue @@ -0,0 +1,35 @@ + + + diff --git a/specs/fixtures/lazy-without-server/pages/index.vue b/specs/fixtures/lazy-without-server/pages/index.vue new file mode 100644 index 000000000..47e46c143 --- /dev/null +++ b/specs/fixtures/lazy-without-server/pages/index.vue @@ -0,0 +1,51 @@ + + + diff --git a/specs/fixtures/lazy-without-server/pages/manual-load.vue b/specs/fixtures/lazy-without-server/pages/manual-load.vue new file mode 100644 index 000000000..28587ea98 --- /dev/null +++ b/specs/fixtures/lazy-without-server/pages/manual-load.vue @@ -0,0 +1,18 @@ + + + diff --git a/specs/ssg/basic_lazy_load.spec.ts b/specs/ssg/basic_lazy_load.spec.ts new file mode 100644 index 000000000..cca02ad27 --- /dev/null +++ b/specs/ssg/basic_lazy_load.spec.ts @@ -0,0 +1,147 @@ +import { test, expect, describe } from 'vitest' +import { fileURLToPath } from 'node:url' +import { setup, url } from '../utils' +import { getText, getData, waitForMs, renderPage, waitForURL } from '../helper' + +describe('basic lazy loading', async () => { + await setup({ + rootDir: fileURLToPath(new URL(`../fixtures/lazy-without-server`, import.meta.url)), + browser: true, + prerender: true, + nuxtConfig: { + i18n: { + debug: true + } + } + }) + + test('dynamic locale files are not cached', async () => { + const { page } = await renderPage('/nl') + + page.on('domcontentloaded', () => { + console.log('domcontentload triggered!') + }) + + // capture dynamicTime - simulates changing api response + const dynamicTime = await getText(page, '#dynamic-time') + + await page.click('#lang-switcher-with-nuxt-link-fr') + await waitForURL(page, '/fr') + expect(await getText(page, '#dynamic-time')).toEqual('Not dynamic') + + // dynamicTime depends on passage of some time + await waitForMs(100) + + // dynamicTime does not match captured dynamicTime + await page.click('#lang-switcher-with-nuxt-link-nl') + await waitForURL(page, '/nl') + expect(await getText(page, '#dynamic-time')).to.not.equal(dynamicTime) + }) + + test('locales are fetched on demand', async () => { + const home = url('/') + const { page, requests } = await renderPage(home) + + const setFromRequests = () => [...new Set(requests)].filter(x => x.includes('lazy-locale-')) + + // only default locales are fetched (en) + await page.goto(home) + console.log(setFromRequests()) + expect(setFromRequests().filter(locale => locale.includes('fr') || locale.includes('nl'))).toHaveLength(0) + + // wait for request after navigation + const localeRequestFr = page.waitForRequest(/lazy-locale-fr/) + await page.click('#lang-switcher-with-nuxt-link-fr') + await localeRequestFr + + // `fr` locale has been fetched + expect(setFromRequests().filter(locale => locale.includes('fr'))).toHaveLength(1) + + // wait for request after navigation + const localeRequestNl = page.waitForRequest(/lazy-locale-module-nl/) + await page.click('#lang-switcher-with-nuxt-link-nl') + await localeRequestNl + + // `nl` (module) locale has been fetched + expect(setFromRequests().filter(locale => locale.includes('nl'))).toHaveLength(1) + }) + + test('can access to no prefix locale (en): /', async () => { + const { page } = await renderPage('/') + + // `en` rendering + expect(await getText(page, '#home-header')).toEqual('Homepage') + expect(await getText(page, 'title')).toEqual('Homepage') + expect(await getText(page, '#link-about')).toEqual('About us') + + // lang switcher rendering + expect(await getText(page, '#set-locale-link-fr')).toEqual('Français') + + // page path + expect(await getData(page, '#home-use-async-data')).toMatchObject({ aboutPath: '/about' }) + + // current locale + expect(await getText(page, '#lang-switcher-current-locale code')).toEqual('en') + + // html tag `lang` attribute with language code + expect(await page.getAttribute('html', 'lang')).toEqual('en-US') + }) + + test('can access to prefix locale: /fr', async () => { + const { page } = await renderPage('/fr') + + console.log(page.url()) + + // `fr` rendering + expect(await getText(page, '#home-header')).toEqual('Accueil') + expect(await getText(page, 'title')).toEqual('Accueil') + expect(await getText(page, '#link-about')).toEqual('À propos') + + // lang switcher rendering + expect(await getText(page, '#set-locale-link-en')).toEqual('English') + + // page path + expect(await getData(page, '#home-use-async-data')).toMatchObject({ aboutPath: '/fr/about' }) + + // current locale + expect(await getText(page, '#lang-switcher-current-locale code')).toEqual('fr') + + // html tag `lang` attribute with language code + expect(await page.getAttribute('html', 'lang')).toEqual('fr-FR') + }) + + test('mutiple lazy loading', async () => { + const { page } = await renderPage('/en-GB') + + // `en` base rendering + expect(await getText(page, '#home-header')).toEqual('Homepage') + expect(await getText(page, 'title')).toEqual('Homepage') + expect(await getText(page, '#link-about')).toEqual('About us') + + expect(await getText(page, '#profile-js')).toEqual('Profile1') + expect(await getText(page, '#profile-ts')).toEqual('Profile2') + }) + + test('files with cache disabled bypass caching', async () => { + const { page, consoleLogs } = await renderPage('/') + + await page.click('#lang-switcher-with-nuxt-link-en-GB') + expect([...consoleLogs].filter(log => log.text.includes('lazy-locale-en-GB.js bypassing cache!'))).toHaveLength(1) + + await page.click('#lang-switcher-with-nuxt-link-fr') + expect([...consoleLogs].filter(log => log.text.includes('lazy-locale-fr.json5 bypassing cache!'))).toHaveLength(1) + + await page.click('#lang-switcher-with-nuxt-link-en-GB') + expect([...consoleLogs].filter(log => log.text.includes('lazy-locale-en-GB.js bypassing cache!'))).toHaveLength(2) + + await page.click('#lang-switcher-with-nuxt-link-fr') + expect([...consoleLogs].filter(log => log.text.includes('lazy-locale-fr.json5 bypassing cache!'))).toHaveLength(2) + }) + + test('manually loaded messages can be used in translations', async () => { + const { page } = await renderPage('/manual-load') + + expect(await getText(page, '#welcome-english')).toEqual('Welcome!') + expect(await getText(page, '#welcome-dutch')).toEqual('Welkom!') + }) +}) diff --git a/specs/utils/server.ts b/specs/utils/server.ts index 348ededdd..4e2a4502d 100644 --- a/specs/utils/server.ts +++ b/specs/utils/server.ts @@ -51,7 +51,7 @@ export async function startServer(env: Record = {}) { ctx.serverProcess.kill() throw lastError || new Error('Timeout waiting for dev server!') } else if (ctx.options.prerender) { - const command = `npx serve -s ${ctx.nuxt!.options.nitro!.output?.publicDir} -l tcp://${host}:${port} --no-port-switching` + const command = `npx serve ${ctx.nuxt!.options.nitro!.output?.publicDir} -l tcp://${host}:${port} --no-port-switching` // ; (await import('consola')).consola.restoreConsole() const [_command, ...commandArgs] = command.split(' ') diff --git a/src/runtime/plugins/route-locale-detect.ts b/src/runtime/plugins/route-locale-detect.ts index b17d41ac2..725700adc 100644 --- a/src/runtime/plugins/route-locale-detect.ts +++ b/src/runtime/plugins/route-locale-detect.ts @@ -1,5 +1,5 @@ import { unref } from 'vue' -import { hasPages, isSSG } from '#build/i18n.options.mjs' +import { hasPages } from '#build/i18n.options.mjs' import { addRouteMiddleware, defineNuxtPlugin, defineNuxtRouteMiddleware } from '#imports' import { createLogger } from 'virtual:nuxt-i18n-logger' import { detectLocale, detectRedirect, loadAndSetLocale, navigate } from '../utils' @@ -40,7 +40,6 @@ export default defineNuxtPlugin({ } const localeChangeMiddleware = defineNuxtRouteMiddleware(async (to, from) => { - if (isSSG && nuxtApp._vueI18n.__firstAccess) return __DEBUG__ && logger.log('locale-changing middleware', to, from) const locale = await nuxtApp.runWithContext(() => handleRouteDetect(to))