From b4defbf1a93aadb0b770eeabfffbdf77e7456bcc Mon Sep 17 00:00:00 2001 From: Pavel Kuzmin Date: Sat, 17 Aug 2024 14:28:21 +0500 Subject: [PATCH] fix: change default-locale-redirect to middleware fix: getLocalizedRoute with locale add: tests --- .github/workflows/ci.yml | 48 ++++++++++++ .gitignore | 2 + package-lock.json | 64 ++++++++++++++- package.json | 5 +- playground/locales/pages/subpage/de.json | 1 + playground/locales/pages/subpage/en.json | 1 + playground/locales/pages/subpage/ru.json | 1 + playground/package.json | 4 +- playground/pages/index.vue | 5 ++ src/module.ts | 25 +++++- src/runtime/01.plugin.ts | 22 ++---- src/runtime/05.default-locale-redirect.ts | 40 ---------- .../server/middleware/i18n-redirect.ts | 29 +++++++ test/basic.test.ts | 78 ++++++++++++++++--- .../basic/locales/pages/index/de.json | 1 + .../basic/locales/pages/index/en.json | 1 + .../basic/locales/pages/index/ru.json | 1 + .../fixtures/basic/locales/pages/page/de.json | 7 ++ .../fixtures/basic/locales/pages/page/en.json | 7 ++ test/fixtures/basic/pages/index.vue | 2 +- test/fixtures/basic/pages/page.vue | 46 +++++++++++ 21 files changed, 314 insertions(+), 76 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 playground/locales/pages/subpage/de.json create mode 100644 playground/locales/pages/subpage/en.json create mode 100644 playground/locales/pages/subpage/ru.json delete mode 100644 src/runtime/05.default-locale-redirect.ts create mode 100644 src/runtime/server/middleware/i18n-redirect.ts create mode 100644 test/fixtures/basic/locales/pages/index/de.json create mode 100644 test/fixtures/basic/locales/pages/index/en.json create mode 100644 test/fixtures/basic/locales/pages/index/ru.json create mode 100644 test/fixtures/basic/locales/pages/page/de.json create mode 100644 test/fixtures/basic/locales/pages/page/en.json create mode 100644 test/fixtures/basic/pages/page.vue diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..4c0f4954 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: ci + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - run: corepack enable + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npx nypm@latest i + + - name: Lint + run: npm run lint + + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - run: corepack enable + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npx nypm@latest i + + - name: Install playwright + run: npx playwright install + + - name: Playground prepare + run: npm run dev:prepare + + - name: Test + run: npm run test diff --git a/.gitignore b/.gitignore index 7a3eb22b..e052f6b6 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ coverage .nyc_output # VSCode +.vscode .vscode/* !.vscode/settings.json !.vscode/tasks.json @@ -54,3 +55,4 @@ coverage Network Trash Folder Temporary Items .apdisk +test-results diff --git a/package-lock.json b/package-lock.json index 43e08c98..39083afa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nuxt-i18n-micro", - "version": "1.0.3", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nuxt-i18n-micro", - "version": "1.0.3", + "version": "1.1.1", "license": "MIT", "dependencies": { "@nuxt/devtools-kit": "^1.3.9", @@ -21,6 +21,7 @@ "@nuxt/module-builder": "^0.8.3", "@nuxt/schema": "^3.12.4", "@nuxt/test-utils": "^3.14.1", + "@playwright/test": "^1.46.1", "@types/node": "^20.14.11", "changelogen": "^0.5.5", "eslint": "^9.7.0", @@ -2977,6 +2978,21 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@playwright/test": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.1.tgz", + "integrity": "sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==", + "dev": true, + "dependencies": { + "playwright": "1.46.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.25", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", @@ -11373,6 +11389,50 @@ "pathe": "^1.1.2" } }, + "node_modules/playwright": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz", + "integrity": "sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==", + "dev": true, + "dependencies": { + "playwright-core": "1.46.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz", + "integrity": "sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", diff --git a/package.json b/package.json index f4c3397c..c69c547c 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags", "lint": "eslint .", "lint:fix": "eslint . --fix", - "test": "vitest run", + "test": "playwright test", "test:watch": "vitest watch", "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit", "client:build": "nuxi generate client", @@ -67,11 +67,12 @@ "@nuxt/module-builder": "^0.8.3", "@nuxt/schema": "^3.12.4", "@nuxt/test-utils": "^3.14.1", + "@playwright/test": "^1.46.1", "@types/node": "^20.14.11", "changelogen": "^0.5.5", "eslint": "^9.7.0", - "nuxt": "^3.12.4", "execa": "^9.3.0", + "nuxt": "^3.12.4", "typescript": "latest", "vitest": "^2.0.3", "vue-tsc": "^2.0.26" diff --git a/playground/locales/pages/subpage/de.json b/playground/locales/pages/subpage/de.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/playground/locales/pages/subpage/de.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/playground/locales/pages/subpage/en.json b/playground/locales/pages/subpage/en.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/playground/locales/pages/subpage/en.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/playground/locales/pages/subpage/ru.json b/playground/locales/pages/subpage/ru.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/playground/locales/pages/subpage/ru.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/playground/package.json b/playground/package.json index 8498d08b..6bf7b9fa 100644 --- a/playground/package.json +++ b/playground/package.json @@ -5,9 +5,11 @@ "scripts": { "dev": "nuxi dev", "build": "nuxi build", + "prepare": "nuxt prepare", "generate": "nuxi generate" }, "dependencies": { - "nuxt": "^3.12.4" + "nuxt": "^3.12.4", + "nuxt-i18n-micro": "^1.1.1" } } diff --git a/playground/pages/index.vue b/playground/pages/index.vue index 7eaa30b2..51a7a3e2 100644 --- a/playground/pages/index.vue +++ b/playground/pages/index.vue @@ -15,11 +15,16 @@ +

+ {{ $localeRoute({ name: 'page' }, 'de').path }} +

+
Go to Page
+ test
({ } if (options.includeDefaultLocaleRoute) { - addPlugin({ - src: resolver.resolve('./runtime/05.default-locale-redirect'), - order: 4, + addServerHandler({ + middleware: true, + handler: resolver.resolve('./runtime/server/middleware/i18n-redirect.ts'), }) } @@ -216,6 +216,23 @@ export default defineNuxtModule({ } } + declare module 'vue/types/vue' { + interface Vue { + $getLocale: () => string; + $getLocales: () => string[]; + $t: >( + key: string, + params?: T, + defaultValue?: string + ) => string | number | boolean | Translations | PluralTranslations | unknown[] | unknown | null; + $tc: (key: string, count: number, defaultValue?: string) => string; + $mergeTranslations: (newTranslations: Translations) => void; + $switchLocale: (locale: string) => void; + $localeRoute: (to: RouteLocationRaw, locale?: string) => RouteLocationRaw; + $loadPageTranslations: (locale: string, routeName: string) => Promise; + } + } + export {}; ` }, diff --git a/src/runtime/01.plugin.ts b/src/runtime/01.plugin.ts index 7561a218..508987ee 100644 --- a/src/runtime/01.plugin.ts +++ b/src/runtime/01.plugin.ts @@ -144,24 +144,18 @@ function getLocalizedRoute(to: RouteLocationRaw, router: Router, route: RouteLoc const { defaultLocale } = i18nConfig const currentLocale = (locale || route.params.locale || defaultLocale)!.toString() - let resolvedRoute = router.resolve(to) + const selectRoute = router.resolve(to) - if (typeof to === 'object' && 'name' in to) { - resolvedRoute = router.resolve({ name: to.name, params: to.params, query: to.query, hash: to.hash }) - delete resolvedRoute.params.locale - - if (resolvedRoute.name) { - const routeName = (resolvedRoute.name as string).replace(`localized-`, '') - - resolvedRoute.name = currentLocale !== defaultLocale || i18nConfig.includeDefaultLocaleRoute ? `localized-${routeName}` : routeName + const routeName = (selectRoute.name as string).replace(`localized-`, '') + const newRouteName = currentLocale !== defaultLocale || i18nConfig.includeDefaultLocaleRoute ? `localized-${routeName}` : routeName + const newParams = { ...route.params } + delete newParams.locale - if (defaultLocale !== currentLocale || i18nConfig.includeDefaultLocaleRoute) { - resolvedRoute.params.locale = currentLocale - } - } + if (currentLocale !== defaultLocale || i18nConfig.includeDefaultLocaleRoute) { + newParams.locale = currentLocale } - return resolvedRoute + return router.resolve({ name: newRouteName, params: newParams }) } export default defineNuxtPlugin(async (_nuxtApp) => { diff --git a/src/runtime/05.default-locale-redirect.ts b/src/runtime/05.default-locale-redirect.ts deleted file mode 100644 index 9fa30357..00000000 --- a/src/runtime/05.default-locale-redirect.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { defineNuxtPlugin, navigateTo } from '#app' -import type { ModuleOptions } from '~/src/module' -import { useRoute, useRouter, watch, computed } from '#imports' - -interface State extends ModuleOptions { - rootDir: string -} - -export default defineNuxtPlugin(async ({ $config }) => { - const i18nConfig = $config.public.i18nConfig as State - - const router = useRouter() - const route = useRoute() - - if (!route.params?.locale) { - const routeName = route.name as string - const newRouteName = `localized-${routeName}` - const newParams = { ...route.params, locale: i18nConfig.defaultLocale } - - if (import.meta.client) { - location.href = router.resolve({ name: newRouteName, params: newParams }).href - } - else { - await navigateTo({ name: newRouteName, params: newParams }, { redirectCode: 301 }) - } - } - - if (import.meta.client) { - const routeName = computed(() => route.name) - watch(routeName, async () => { - if (!route.params?.locale) { - const routeName = route.name as string - const newRouteName = `localized-${routeName}` - const newParams = { ...route.params, locale: i18nConfig.defaultLocale } - // location.href = router.resolve({ name: newRouteName, params: newParams }).href - await navigateTo({ name: newRouteName, params: newParams }, { redirectCode: 301 }) - } - }) - } -}) diff --git a/src/runtime/server/middleware/i18n-redirect.ts b/src/runtime/server/middleware/i18n-redirect.ts new file mode 100644 index 00000000..cb5037cb --- /dev/null +++ b/src/runtime/server/middleware/i18n-redirect.ts @@ -0,0 +1,29 @@ +import { defineEventHandler, sendRedirect } from 'h3' +import type { H3Event } from 'h3' +import type { Locale } from 'nuxt-i18n-micro' +import type { ModuleOptions } from '~/src/module' +import { useRuntimeConfig } from '#imports' + +export default defineEventHandler(async (event: H3Event) => { + const config = useRuntimeConfig() + const i18nConfig = config.public.i18nConfig as ModuleOptions + + // Ensure defaultLocale is defined + if (!i18nConfig.defaultLocale) { + throw new Error('defaultLocale is not defined in the module configuration') + } + + const locales = i18nConfig.locales || [] + + const pathParts = event.path.split('/').filter(Boolean) + const localeCode = pathParts[0] + // If the locale is missing or invalid, redirect to the route with the defaultLocale + const locale = locales.find((loc: Locale) => loc.code === localeCode) + + // If the locale is missing or invalid, redirect to the route with the defaultLocale + if (!locale) { + const newPath = `/${i18nConfig.defaultLocale}/${pathParts.join('/')}` + + return sendRedirect(event, newPath, 301) + } +}) diff --git a/test/basic.test.ts b/test/basic.test.ts index 3ee5a5eb..938f1315 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -1,18 +1,72 @@ import { fileURLToPath } from 'node:url' -import { describe, it, expect } from 'vitest' -import { setup, $fetch } from '@nuxt/test-utils/e2e' +import { expect, test } from '@nuxt/test-utils/playwright' -describe('ssr', async () => { - await setup({ +test.use({ + nuxt: { rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)), - }) + }, +}) + +test('test index', async ({ page, goto }) => { + await goto('/', { waitUntil: 'hydration' }) + await expect(page.locator('#locale')).toHaveText('en') + + await goto('/de', { waitUntil: 'hydration' }) + await expect(page.locator('#locale')).toHaveText('de') +}) + +test('test plugin methods output on page', async ({ page, goto }) => { + // Navigate to the /page route + await goto('/page', { waitUntil: 'hydration' }) + + // Verify the locale + await expect(page.locator('#locale')).toHaveText('Current Locale: en') + + // Verify the list of locales + await expect(page.locator('#locales')).toHaveText('en, de') + + // Verify the translation for a key + await expect(page.locator('#translation')).toHaveText('Page example in en') // Replace with actual expected content + + // Verify the pluralization for items + await expect(page.locator('#plural')).toHaveText('2 items') // Replace with actual pluralization result + + // Verify the localized route generation + await expect(page.locator('#localized-route')).toHaveText('/de/page') +}) + +test('test locale switching on page', async ({ page, goto }) => { + // Navigate to the /page route in English + await goto('/page', { waitUntil: 'hydration' }) + + await expect(page).toHaveURL('/page') + + await expect(page.locator('#locale')).toHaveText('Current Locale: en') + + // Verify the translation for a key after switching locale + await expect(page.locator('#translation')).toHaveText('Page example in en') // Replace with actual expected content + + // Verify the pluralization for items after switching locale + await expect(page.locator('#plural')).toHaveText('2 items') // Replace with actual pluralization result in German + + // Verify the localized route generation after switching locale + await expect(page.locator('#localized-route')).toHaveText('/de/page') + + // Click the link to switch to the German locale + await page.click('#link-de') + + // Verify that the URL has changed + await expect(page).toHaveURL('/de/page') + + // Verify the locale after switching + await expect(page.locator('#locale')).toHaveText('Current Locale: de') + + // Verify the translation for a key after switching locale + await expect(page.locator('#translation')).toHaveText('Page example in de') // Replace with actual expected content - it('renders the index page', async () => { - // Get response to a server-rendered page with `$fetch`. - const html = await $fetch('/') - expect(html).toContain('
en
') + // Verify the pluralization for items after switching locale + await expect(page.locator('#plural')).toHaveText('2 items') // Replace with actual pluralization result in German - const html_de = await $fetch('/de/') - expect(html_de).toContain('
de
') - }) + // Verify the localized route generation after switching locale + await expect(page.locator('#localized-route')).toHaveText('/de/page') }) diff --git a/test/fixtures/basic/locales/pages/index/de.json b/test/fixtures/basic/locales/pages/index/de.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/test/fixtures/basic/locales/pages/index/de.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/fixtures/basic/locales/pages/index/en.json b/test/fixtures/basic/locales/pages/index/en.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/test/fixtures/basic/locales/pages/index/en.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/fixtures/basic/locales/pages/index/ru.json b/test/fixtures/basic/locales/pages/index/ru.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/test/fixtures/basic/locales/pages/index/ru.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/fixtures/basic/locales/pages/page/de.json b/test/fixtures/basic/locales/pages/page/de.json new file mode 100644 index 00000000..1701879c --- /dev/null +++ b/test/fixtures/basic/locales/pages/page/de.json @@ -0,0 +1,7 @@ +{ + "page":{ + "content": "Page content in de", + "example": "Page example in de", + "items": "{count} items" + } +} diff --git a/test/fixtures/basic/locales/pages/page/en.json b/test/fixtures/basic/locales/pages/page/en.json new file mode 100644 index 00000000..27bb76c5 --- /dev/null +++ b/test/fixtures/basic/locales/pages/page/en.json @@ -0,0 +1,7 @@ +{ + "page": { + "content": "Page content in en", + "example": "Page example in en", + "items": "{count} items" + } +} diff --git a/test/fixtures/basic/pages/index.vue b/test/fixtures/basic/pages/index.vue index e4b15959..f83fc6ff 100644 --- a/test/fixtures/basic/pages/index.vue +++ b/test/fixtures/basic/pages/index.vue @@ -1,6 +1,6 @@ diff --git a/test/fixtures/basic/pages/page.vue b/test/fixtures/basic/pages/page.vue new file mode 100644 index 00000000..6c8346f0 --- /dev/null +++ b/test/fixtures/basic/pages/page.vue @@ -0,0 +1,46 @@ + + +