Skip to content

Commit

Permalink
fix: strategy: 'no_prefix' when using differentDomains (#3061)
Browse files Browse the repository at this point in the history
* fix: route localization with `differentDomains`

* fix: prevent route removal

* fix: `switchLocalePath` resolution for `differentDomains`

* docs: update notes to clarify `differentDomains` case
  • Loading branch information
BobbieGoede authored Aug 15, 2024
1 parent 6a29add commit 8536b23
Show file tree
Hide file tree
Showing 18 changed files with 110 additions and 37 deletions.
2 changes: 1 addition & 1 deletion docs/content/docs/2.guide/1.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ There are 4 supported strategies that affect how app's routes are generated:
With this strategy, your routes won't have a locale prefix added. The locale will be detected & changed without changing the URL. This implies that you have to rely on browser & cookie detection, and implement locale switches by calling the i18n API.

::callout{icon="i-heroicons-light-bulb"}
This strategy doesn't support [Custom paths](/docs/guide/custom-paths) and [Ignore routes](/docs/guide/ignoring-localized-routes) features.
This strategy doesn't support [Custom paths](/docs/guide/custom-paths) and [Ignore routes](/docs/guide/ignoring-localized-routes) features unless you're also using [`differentDomains`](/docs/guide/different-domains).
::

### `prefix_except_default`
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/2.guide/3.custom-paths.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ In some cases, you might want to translate URLs in addition to having them prefi
Which method is used is configured by setting the [`customRoutes` options](/docs/options/routing#customroutes) this is set to `'page'` by default. Using both methods at the same time is not possible.

::callout{icon="i-heroicons-exclamation-triangle" color="amber"}
Custom paths are not supported when using the `no-prefix` [strategy](/docs/guide).
Custom paths are not supported when using the `no_prefix` [strategy](/docs/guide) unless combined with [`differentDomains`](/docs/guide/different-domains).
::

### Module configuration
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/2.guide/4.ignoring-localized-routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: Customize localized route exclusions per page component.
---

::callout{icon="i-heroicons-exclamation-triangle" color="amber"}
This feature is not supported with the `no-prefix` [strategy](/docs/guide).
This feature is not supported when using the `no_prefix` [strategy](/docs/guide) unless you're also using [`differentDomains`](/docs/guide/different-domains).
::

If you'd like some pages to be available in some languages only, you can configure the list of supported languages to override the global settings. The options can be specified within either the page components themselves or globally, within the module configuration.
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/3.options/5.domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ description: Browser locale management options.
- type: `boolean`
- default: `false`

Set this to `true` when using different domains for each locale. If enabled, no prefix is added to your routes and you MUST configure locales as an array of objects, each containing a `domain` key. Refer to the [Different domains](/docs/guide/different-domains) for more information.
Set this to `true` when using different domains for each locale, with this enabled you MUST configure locales as an array of objects, each containing a `domain` key. Refer to the [Different domains](/docs/guide/different-domains) for more information.
2 changes: 1 addition & 1 deletion docs/content/docs/5.v9/2.guide/1.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ There are 4 supported strategies that affect how app's routes are generated:
With this strategy, your routes won't have a locale prefix added. The locale will be detected & changed without changing the URL. This implies that you have to rely on browser & cookie detection, and implement locale switches by calling the i18n API.

::callout{icon="i-heroicons-light-bulb"}
This strategy doesn't support [Custom paths](/docs/guide/custom-paths) and [Ignore routes](/docs/guide/ignoring-localized-routes) features.
This strategy doesn't support [Custom paths](/docs/guide/custom-paths) and [Ignore routes](/docs/guide/ignoring-localized-routes) features unless you're also using [`differentDomains`](/docs/guide/different-domains).
::

### `prefix_except_default`
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/5.v9/2.guide/3.custom-paths.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ In some cases, you might want to translate URLs in addition to having them prefi
Which method is used is configured by setting the [`customRoutes` options](/docs/options/routing#customroutes) this is set to `'page'` by default. Using both methods at the same time is not possible.

::callout{icon="i-heroicons-exclamation-triangle" color="amber"}
Custom paths are not supported when using the `no-prefix` [strategy](/docs/guide).
Custom paths are not supported when using the `no_prefix` [strategy](/docs/guide) unless combined with [`differentDomains`](/docs/guide/different-domains).
::

### Module configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: Customize localized route exclusions per page component.
---

::callout{icon="i-heroicons-exclamation-triangle" color="amber"}
This feature is not supported with the `no-prefix` [strategy](/docs/guide).
This feature is not supported when using the `no_prefix` [strategy](/docs/guide) unless you're also using [`differentDomains`](/docs/guide/different-domains).
::

If you'd like some pages to be available in some languages only, you can configure the list of supported languages to override the global settings. The options can be specified within either the page components themselves or globally, within the module configuration.
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/5.v9/3.options/5.domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ description: Browser locale management options.
- type: `boolean`
- default: `false`

Set this to `true` when using different domains for each locale. If enabled, no prefix is added to your routes and you MUST configure locales as an array of objects, each containing a `domain` key. Refer to the [Different domains](/docs/guide/different-domains) for more information.
Set this to `true` when using different domains for each locale, with this enabled you MUST configure locales as an array of objects, each containing a `domain` key. Refer to the [Different domains](/docs/guide/different-domains) for more information.
2 changes: 1 addition & 1 deletion docs/content/docs/6.v7/7.custom-paths.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: nuxt/i18n v7 custom route paths.
In some cases, you might want to translate URLs in addition to having them prefixed with the locale code. There are 2 ways of configuring custom paths for your pages: [in-component options](#in-component-options) or via the [module's configuration](#modules-configuration).

::callout{icon="i-heroicons-light-bulb"}
Custom paths are not supported when using the `no-prefix` [strategy](./strategies).
Custom paths are not supported when using the `no_prefix` [strategy](./strategies).
::

### In-component options
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/6.v7/8.ignoring-localized-routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Ignoring Localized Routes
---

::callout{icon="i-heroicons-light-bulb"}
This feature is not supported with the `no-prefix` [strategy](./strategies).
This feature is not supported with the `no_prefix` [strategy](./strategies).
::

If you'd like some pages to be available in some languages only, you can configure the list of supported languages to override the global settings. The options can be specified within either the page components themselves or globally, within then module options.
Expand Down
33 changes: 33 additions & 0 deletions specs/different_domains/different_domains.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ await setup({
strategy: 'no_prefix',
detectBrowserLanguage: {
useCookie: true
},
customRoutes: 'config',
pages: {
'localized-route': {
en: '/localized-in-english',
fr: '/localized-in-french',
ja: '/localized-in-japanese',
nl: '/localized-in-dutch'
}
}
}
}
Expand Down Expand Up @@ -152,3 +161,27 @@ test('(#2374) detect with x-forwarded-host on server', async () => {

expect(dom.querySelector('#welcome-text').textContent).toEqual('Bienvenue')
})

test("supports custom routes with `strategy: 'no_prefix'`", async () => {
const res = await undiciRequest('/localized-in-french', {
headers: {
host: 'fr.nuxt-app.localhost'
}
})
const resBody = await res.body.text()
const dom = getDom(resBody)

// `en` link uses project domain configuration, overrides layer
expect(dom.querySelector('#switch-locale-path-usages .switch-to-en a').getAttribute('href')).toEqual(
`http://en.nuxt-app.localhost/localized-in-english`
)

// `nl` link uses layer domain configuration
expect(dom.querySelector('#switch-locale-path-usages .switch-to-nl a').getAttribute('href')).toEqual(
`http://layer-nl.example.com/localized-in-dutch`
)
// `ja` link uses layer domain configuration
expect(dom.querySelector('#switch-locale-path-usages .switch-to-ja a').getAttribute('href')).toEqual(
`http://layer-ja.example.com/localized-in-japanese`
)
})
11 changes: 11 additions & 0 deletions specs/fixtures/different_domains/pages/localized-route.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script setup>
import BasicUsage from '../components/BasicUsage.vue'
import LangSwitcher from '../components/LangSwitcher.vue'
</script>

<template>
<div>
<BasicUsage />
<LangSwitcher />
</div>
</template>
2 changes: 1 addition & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export default defineNuxtModule<NuxtI18nOptions>({
* setup nuxt/pages
*/

if (options.strategy !== 'no_prefix' && localeCodes.length) {
if (localeCodes.length) {
setupPages(options, nuxt)
}

Expand Down
8 changes: 6 additions & 2 deletions src/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,12 @@ export function setupPages(options: Required<NuxtI18nOptions>, nuxt: Nuxt) {
localizedPages.unshift(indexPage)
}

pages.splice(0, pages.length)
pages.unshift(...localizedPages)
// do not mutate pages if localization is skipped
if (pages !== localizedPages) {
pages.splice(0, pages.length)
pages.unshift(...localizedPages)
}

debug('... made pages', pages)
})
}
Expand Down
30 changes: 27 additions & 3 deletions src/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function prefixLocalizedRoute(
return (
!extra &&
!isChildWithRelativePath &&
options.strategy !== 'no_prefix' &&
// skip default locale if strategy is 'prefix_except_default'
!(isDefaultLocale && options.strategy === 'prefix_except_default')
)
Expand All @@ -51,6 +52,31 @@ export type LocalizeRoutesParams = MarkRequired<
optionsResolver?: RouteOptionsResolver
}

export function shouldLocalizeRoutes(options: NuxtI18nOptions) {
if (options.strategy === 'no_prefix') {
// no_prefix is only supported when using a separate domain per locale
if (!options.differentDomains) return false

// check if domains are used multiple times
const domains = new Set<string>()
for (const locale of options.locales || []) {
if (typeof locale === 'string') continue
if (locale.domain) {
if (domains.has(locale.domain)) {
console.error(
`Cannot use \`strategy: no_prefix\` when using multiple locales on the same domain - found multiple entries with ${locale.domain}`
)
return false
}

domains.add(locale.domain)
}
}
}

return true
}

type LocalizedRoute = NuxtPage & { locale: Locale; parent: NuxtPage | undefined }
type LocalizeRouteParams = {
/**
Expand Down Expand Up @@ -82,9 +108,7 @@ type LocalizeRouteParams = {
* @public
*/
export function localizeRoutes(routes: NuxtPage[], options: LocalizeRoutesParams): NuxtPage[] {
if (options.strategy === 'no_prefix') {
return routes
}
if (!shouldLocalizeRoutes(options)) return routes

let defaultLocales = [options.defaultLocale ?? '']
if (options.differentDomains) {
Expand Down
17 changes: 3 additions & 14 deletions src/runtime/routing/compatibles/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,7 @@ export function localeLocation(
export function resolveRoute(common: CommonComposableOptions, route: RouteLocationRaw, locale: Locale | undefined) {
const { router, i18n } = common
const _locale = locale || getLocale(i18n)
const { routesNameSeparator, defaultLocale, defaultLocaleRouteNameSuffix, strategy, trailingSlash } =
common.runtimeConfig.public.i18n
const { defaultLocale, strategy, trailingSlash } = common.runtimeConfig.public.i18n
const prefixable = extendPrefixable(common.runtimeConfig)
// if route parameter is a string, check if it's a path or name of route.
let _route: RouteLocationPathRaw | RouteLocationNamedRaw
Expand Down Expand Up @@ -163,12 +162,7 @@ export function resolveRoute(common: CommonComposableOptions, route: RouteLocati
const resolvedRouteName = getRouteBaseName(common, resolvedRoute)
if (isString(resolvedRouteName)) {
localizedRoute = {
name: getLocaleRouteName(resolvedRouteName, _locale, {
defaultLocale,
strategy,
routesNameSeparator,
defaultLocaleRouteNameSuffix
}),
name: getLocaleRouteName(resolvedRouteName, _locale, common.runtimeConfig.public.i18n),
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- FIXME
params: resolvedRoute.params,
Expand All @@ -194,12 +188,7 @@ export function resolveRoute(common: CommonComposableOptions, route: RouteLocati
localizedRoute.name = getRouteBaseName(common, router.currentRoute.value)
}

localizedRoute.name = getLocaleRouteName(localizedRoute.name, _locale, {
defaultLocale,
strategy,
routesNameSeparator,
defaultLocaleRouteNameSuffix
})
localizedRoute.name = getLocaleRouteName(localizedRoute.name, _locale, common.runtimeConfig.public.i18n)
}

try {
Expand Down
14 changes: 11 additions & 3 deletions src/runtime/routing/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,18 @@ export function getLocaleRouteName(
defaultLocale,
strategy,
routesNameSeparator,
defaultLocaleRouteNameSuffix
}: { defaultLocale: string; strategy: Strategies; routesNameSeparator: string; defaultLocaleRouteNameSuffix: string }
defaultLocaleRouteNameSuffix,
differentDomains
}: {
defaultLocale: string
strategy: Strategies
routesNameSeparator: string
defaultLocaleRouteNameSuffix: string
differentDomains: boolean
}
) {
let name = getRouteName(routeName) + (strategy === 'no_prefix' ? '' : routesNameSeparator + locale)
const localizedRoutes = strategy !== 'no_prefix' || differentDomains
let name = getRouteName(routeName) + (localizedRoutes ? routesNameSeparator + locale : '')
if (locale === defaultLocale && strategy === 'prefix_and_default') {
name += routesNameSeparator + defaultLocaleRouteNameSuffix
}
Expand Down
12 changes: 8 additions & 4 deletions test/routing-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ describe('getLocaleRouteName', () => {
defaultLocale: 'en',
strategy: 'prefix_and_default',
routesNameSeparator: '___',
defaultLocaleRouteNameSuffix: 'default'
defaultLocaleRouteNameSuffix: 'default',
differentDomains: false
}),
'route1___en___default'
)
Expand All @@ -67,7 +68,8 @@ describe('getLocaleRouteName', () => {
defaultLocale: 'en',
strategy: 'prefix_except_default',
routesNameSeparator: '___',
defaultLocaleRouteNameSuffix: 'default'
defaultLocaleRouteNameSuffix: 'default',
differentDomains: false
}),
'route1___en'
)
Expand All @@ -81,7 +83,8 @@ describe('getLocaleRouteName', () => {
defaultLocale: 'en',
strategy: 'no_prefix',
routesNameSeparator: '___',
defaultLocaleRouteNameSuffix: 'default'
defaultLocaleRouteNameSuffix: 'default',
differentDomains: false
}),
'route1'
)
Expand All @@ -96,7 +99,8 @@ describe('getLocaleRouteName', () => {
defaultLocale: 'en',
strategy: 'prefix_and_default',
routesNameSeparator: '___',
defaultLocaleRouteNameSuffix: 'default'
defaultLocaleRouteNameSuffix: 'default',
differentDomains: false
}),
'(null)___en___default'
)
Expand Down

0 comments on commit 8536b23

Please sign in to comment.