diff --git a/src/module.ts b/src/module.ts index c48bd706..e0e2927a 100644 --- a/src/module.ts +++ b/src/module.ts @@ -99,6 +99,7 @@ export default defineNuxtModule({ dateBuild: Date.now(), baseURL: nuxt.options.app.baseURL, hashMode: nuxt.options?.router?.options?.hashMode ?? false, + globalLocaleRoutes: options.globalLocaleRoutes ?? {}, } nuxt.options.runtimeConfig.i18nConfig = { rootDir: nuxt.options.rootDir, @@ -134,12 +135,12 @@ export default defineNuxtModule({ addImportsDir(resolver.resolve('./runtime/composables')) - if (options.includeDefaultLocaleRoute) { - addServerHandler({ - middleware: true, - handler: resolver.resolve('./runtime/server/middleware/i18n-redirect'), - }) - } + // if (options.includeDefaultLocaleRoute) { + // addServerHandler({ + // middleware: true, + // handler: resolver.resolve('./runtime/server/middleware/i18n-redirect'), + // }) + // } addServerHandler({ route: '/_locales/:page/:locale/data.json', diff --git a/src/runtime/plugins/01.plugin.ts b/src/runtime/plugins/01.plugin.ts index 0e11c436..6fbb51aa 100644 --- a/src/runtime/plugins/01.plugin.ts +++ b/src/runtime/plugins/01.plugin.ts @@ -144,6 +144,15 @@ function getLocalizedRoute( }) } + if (i18nConfig.includeDefaultLocaleRoute) { + const globalLocaleRoutes = i18nConfig.globalLocaleRoutes ?? {} + if (globalLocaleRoutes[routeName] == false) { + const newParams = resolveParams(to) + delete newParams.locale + return router.resolve({ name: routeName, params: newParams }) + } + } + // Determine the new route name based on locale and configuration const newRouteName = currentLocale !== i18nConfig.defaultLocale || i18nConfig.includeDefaultLocaleRoute diff --git a/src/runtime/plugins/03.define.ts b/src/runtime/plugins/03.define.ts index 80c2025c..6e193c2d 100644 --- a/src/runtime/plugins/03.define.ts +++ b/src/runtime/plugins/03.define.ts @@ -1,3 +1,4 @@ +import type { RouteLocationNormalizedLoaded } from 'vue-router' import type { ModuleOptionsExtend } from '../../types' import type { Translations } from '../plugins/01.plugin' import { defineNuxtPlugin, navigateTo, useNuxtApp, useRuntimeConfig } from '#app' @@ -6,12 +7,13 @@ import { useRoute, useRouter } from '#imports' // Тип для локалей type LocalesObject = Record -export default defineNuxtPlugin((_nuxtApp) => { +export default defineNuxtPlugin(async (_nuxtApp) => { const config = useRuntimeConfig() const route = useRoute() const router = useRouter() const i18nConfig: ModuleOptionsExtend = config.public.i18nConfig as ModuleOptionsExtend + const globalLocaleRoutes = i18nConfig.globalLocaleRoutes ?? {} // Функция нормализации, которая объединяет массивы и объекты в единый массив строк const normalizeLocales = (locales?: string[] | LocalesObject): LocalesObject => { @@ -29,29 +31,44 @@ export default defineNuxtPlugin((_nuxtApp) => { return {} } - useRouter().beforeEach(async (to, from, next) => { - if (i18nConfig.includeDefaultLocaleRoute) { - const currentLocale = (to.params.locale || i18nConfig.defaultLocale!).toString() - const { name } = to + // Логика для редиректа по умолчанию, используем как на сервере, так и на клиенте + const handleRedirect = async (to: RouteLocationNormalizedLoaded) => { + const currentLocale = (to.params.locale || i18nConfig.defaultLocale!).toString() + const { name } = to - let defaultRouteName = name?.toString() - .replace('localized-', '') - .replace(new RegExp(`-${currentLocale}$`), '') + let defaultRouteName = name?.toString() + .replace('localized-', '') + .replace(new RegExp(`-${currentLocale}$`), '') - if (!to.params.locale) { - if (router.hasRoute(`localized-${to.name?.toString()}-${currentLocale}`)) { - defaultRouteName = `localized-${to.name?.toString()}-${currentLocale}` - } - else { - defaultRouteName = `localized-${to.name?.toString()}` - } - - const newParams = { ...to.params } - newParams.locale = i18nConfig.defaultLocale! - newParams.name = defaultRouteName + if (!to.params.locale) { + const name = to.name?.toString() ?? '' + if (globalLocaleRoutes[name] === false) { + return + } - await navigateTo({ name: defaultRouteName, params: newParams }, { redirectCode: 301, external: true }) + if (router.hasRoute(`localized-${to.name?.toString()}-${currentLocale}`)) { + defaultRouteName = `localized-${to.name?.toString()}-${currentLocale}` } + else { + defaultRouteName = `localized-${to.name?.toString()}` + } + + const newParams = { ...to.params } + newParams.locale = i18nConfig.defaultLocale! + + return navigateTo({ name: defaultRouteName, params: newParams }, { redirectCode: 301, external: true }) + } + } + + if (import.meta.server) { + if (i18nConfig.includeDefaultLocaleRoute) { + await handleRedirect(route) + } + } + + router.beforeEach(async (to, from, next) => { + if (i18nConfig.includeDefaultLocaleRoute) { + await handleRedirect(to) } next() }) @@ -72,7 +89,7 @@ export default defineNuxtPlugin((_nuxtApp) => { nuxtApp.$mergeTranslations(translation) } - // Если текущей локали есть в объекте locales + // Если текущей локали нет в объекте locales if (!normalizedLocales[currentLocale]) { let defaultRouteName = route.name?.toString() .replace('localized-', '') @@ -82,6 +99,10 @@ export default defineNuxtPlugin((_nuxtApp) => { delete newParams.locale if (i18nConfig.includeDefaultLocaleRoute) { + const name = route.name?.toString() ?? '' + if (globalLocaleRoutes[name] === false) { + return + } if (router.hasRoute(`localized-${defaultRouteName}-${currentLocale}`)) { defaultRouteName = `localized-${defaultRouteName}-${currentLocale}` } diff --git a/src/runtime/server/middleware/i18n-redirect.ts b/src/runtime/server/middleware/i18n-redirect.ts deleted file mode 100644 index dc6b7d38..00000000 --- a/src/runtime/server/middleware/i18n-redirect.ts +++ /dev/null @@ -1,40 +0,0 @@ -// src/runtime/server/middleware/i18n-redirect.ts - -import { defineEventHandler, sendRedirect } from 'h3' -import type { H3Event } from 'h3' -import type { Locale, ModuleOptionsExtend } from '../../../types' -import { useRuntimeConfig } from '#imports' - -export default defineEventHandler(async (event: H3Event) => { - const config = useRuntimeConfig() - const i18nConfig = config.public.i18nConfig as ModuleOptionsExtend - - // 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] - - // Check if the last part of the path has a file extension (e.g., data.json?v=123456) - const lastPart = pathParts[pathParts.length - 1] - const hasFileExtension = /\.[^/?]+(?:\?.*)?$/.test(lastPart) - - // If the last part contains a file extension, do not perform redirection - if (hasFileExtension) { - return - } - - // 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/fixtures/dynamic/locales/pages/index/de.json b/test/fixtures/dynamic/locales/pages/index/de.json deleted file mode 100644 index 9e26dfee..00000000 --- a/test/fixtures/dynamic/locales/pages/index/de.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/test/fixtures/dynamic/locales/pages/index/en.json b/test/fixtures/dynamic/locales/pages/index/en.json deleted file mode 100644 index 9e26dfee..00000000 --- a/test/fixtures/dynamic/locales/pages/index/en.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/test/fixtures/dynamic/locales/pages/index/ru.json b/test/fixtures/dynamic/locales/pages/index/ru.json deleted file mode 100644 index 9e26dfee..00000000 --- a/test/fixtures/dynamic/locales/pages/index/ru.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/test/fixtures/dynamic/locales/pages/page/de.json b/test/fixtures/dynamic/locales/pages/page/de.json deleted file mode 100644 index 9e26dfee..00000000 --- a/test/fixtures/dynamic/locales/pages/page/de.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/test/fixtures/dynamic/locales/pages/page/en.json b/test/fixtures/dynamic/locales/pages/page/en.json deleted file mode 100644 index 9e26dfee..00000000 --- a/test/fixtures/dynamic/locales/pages/page/en.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/test/fixtures/dynamic/locales/pages/page/ru.json b/test/fixtures/dynamic/locales/pages/page/ru.json deleted file mode 100644 index 9e26dfee..00000000 --- a/test/fixtures/dynamic/locales/pages/page/ru.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/test/fixtures/undefault/nuxt.config.ts b/test/fixtures/undefault/nuxt.config.ts index 94b19b76..09b4ffde 100644 --- a/test/fixtures/undefault/nuxt.config.ts +++ b/test/fixtures/undefault/nuxt.config.ts @@ -19,6 +19,14 @@ export default defineNuxtConfig({ routesLocaleLinks: { 'dir1-slug': 'index', }, + globalLocaleRoutes: { + page2: { + en: '/custom-page2-en', + de: '/custom-page2-de', + ru: '/custom-page2-ru', + }, + unlocalized: false, // Unlocalized page should not be localized + }, }, compatibilityDate: '2024-08-16', diff --git a/test/fixtures/undefault/pages/page2.vue b/test/fixtures/undefault/pages/page2.vue new file mode 100644 index 00000000..72712320 --- /dev/null +++ b/test/fixtures/undefault/pages/page2.vue @@ -0,0 +1,20 @@ + + diff --git a/test/fixtures/undefault/pages/unlocalized.vue b/test/fixtures/undefault/pages/unlocalized.vue new file mode 100644 index 00000000..7204bc29 --- /dev/null +++ b/test/fixtures/undefault/pages/unlocalized.vue @@ -0,0 +1,52 @@ + + + diff --git a/test/redirect.test.ts b/test/redirect.test.ts index 76c3e7e9..416be6a2 100644 --- a/test/redirect.test.ts +++ b/test/redirect.test.ts @@ -5,6 +5,10 @@ test.use({ nuxt: { rootDir: fileURLToPath(new URL('./fixtures/redirect', import.meta.url)), }, + // launchOptions: { + // headless: false, // Показывать браузер + // slowMo: 500, // Замедлить выполнение шагов (в миллисекундах) для лучшей видимости + // }, }) test('test language detection and redirect based on navigator.languages', async ({ page, goto }) => { diff --git a/test/undefault.test.ts b/test/undefault.test.ts new file mode 100644 index 00000000..2c95ad84 --- /dev/null +++ b/test/undefault.test.ts @@ -0,0 +1,45 @@ +import { fileURLToPath } from 'node:url' +import { expect, test } from '@nuxt/test-utils/playwright' + +test.use({ + nuxt: { + rootDir: fileURLToPath(new URL('./fixtures/undefault', import.meta.url)), + }, + // launchOptions: { + // headless: false, // Показывать браузер + // slowMo: 500, // Замедлить выполнение шагов (в миллисекундах) для лучшей видимости + // }, +}) + +// Тест для английского языка +test('test redirection and link clicks in English', async ({ page, goto }) => { + await goto('/', { waitUntil: 'hydration' }) + // Переход на страницу /page2, должно произойти редирект на /en/custom-page2-en + await goto('/page2', { waitUntil: 'hydration' }) + + await expect(page).toHaveURL('/en/custom-page2-en') + + // Клик по ссылке 'unlocalized', должно редиректнуть на /unlocalized + await page.click('#unlocalized') + await expect(page).toHaveURL('/unlocalized') + + // Клик по ссылке 'page2', должно вернуться на /en/custom-page2-en + await page.click('#link-en') + await expect(page).toHaveURL('/en/custom-page2-en') +}) + +// Тест для немецкого языка +test('test redirection and link clicks in German', async ({ page, goto }) => { + await goto('/', { waitUntil: 'hydration' }) + // Переход на страницу /page2, должно произойти редирект на /de/custom-page2-de + await goto('/de/custom-page2-de', { waitUntil: 'hydration' }) + await expect(page).toHaveURL('/de/custom-page2-de') + + // Клик по ссылке 'unlocalized', должно редиректнуть на /unlocalized + await page.click('#unlocalized') + await expect(page).toHaveURL('/unlocalized') + + // Клик по ссылке 'page2', должно вернуться на /en/custom-page2-en + await page.click('#link-de') + await expect(page).toHaveURL('/de/custom-page2-de') +})