Skip to content

Commit

Permalink
feat: Support multiple languages on one or more domains while having …
Browse files Browse the repository at this point in the history
…different domains (#2705)

* support multiple languages on one or more domains

* add debug log

* fix after rebase

* update docs
  • Loading branch information
bjerggaard committed Mar 23, 2024
1 parent eafe06b commit b7a6c66
Show file tree
Hide file tree
Showing 11 changed files with 340 additions and 15 deletions.
57 changes: 57 additions & 0 deletions docs/content/docs/2.guide/9.different-domains.md
Expand Up @@ -113,3 +113,60 @@ NUXT_PUBLIC_I18N_LOCALES_FR_DOMAIN=fr.example.test
NUXT_PUBLIC_I18N_LOCALES_UK_DOMAIN=uk.staging.example.test
NUXT_PUBLIC_I18N_LOCALES_FR_DOMAIN=fr.staging.example.test
```

## Using different domains for only some of the languages

If one or more of the domains need to host multiple languages, the default language of each domain needs to have `domainDefault: true` so there is a per domain fallback locale.
The option `differentDomains` still need to be set to `true` though.

```js {}[nuxt.config.js]
export default defineNuxtConfig({
// ...
i18n: {
locales: [
{
code: 'en',
domain: 'mydomain.com',
domainDefault: true
},
{
code: 'pl',
domain: 'mydomain.com'
},
{
code: 'ua',
domain: 'mydomain.com'
},
{
code: 'es',
domain: 'es.mydomain.com',
domainDefault: true
},
{
code: 'fr',
domain: 'fr.mydomain.com',
domainDefault: true
}
],
strategy: 'prefix',
differentDomains: true
// Or enable the option in production only
// differentDomains: (process.env.NODE_ENV === 'production')
},
// ...
})
```

Given above configuration with the `prefix` strategy, following requests will be:
- https://mydomain.com -> https://mydomain.com/en (en language)
- https://mydomain.com/pl -> https://mydomain.com/pl (pl language)
- https://mydomain.com/ua -> https://mydomain.com/ua (ua language)
- https://es.mydomain.com -> https://es.mydomain.com/es (es language)
- https://fr.mydomain.com -> https://fr.mydomain.com/fr (fr language)

The same requests when using the `prefix_except_default` strategy, will be:
- https://mydomain.com -> https://mydomain.com (en language)
- https://mydomain.com/pl -> https://mydomain.com/pl (pl language)
- https://mydomain.com/ua -> https://mydomain.com/ua (ua language)
- https://es.mydomain.com -> https://es.mydomain.com (es language)
- https://fr.mydomain.com -> https://fr.mydomain.com (fr language)
1 change: 1 addition & 0 deletions docs/content/docs/3.options/2.routing.md
Expand Up @@ -44,6 +44,7 @@ When using an object form, the properties can be:
- `files` - The name of the file in which multiple locale messages are defined. Will be resolved relative to `langDir` path when loading locale messages from file.
- `dir` - The dir property specifies the direction of the elements and content, value could be `'rtl'`, `'ltr'` or `'auto'`.
- `domain` (required when using [`differentDomains`](/docs/options/domain#differentdomains)) - the domain name you'd like to use for that locale (including the port if used). This property can also be set using [`runtimeConfig`](/docs/options/runtime-config).
- `domainDefault` (required when using [`differentDomains`](/docs/options/domain#differentdomains) while one or more of the domains having multiple locales) - set `domainDefault` to `true` for each locale that should act as a default locale for the particular domain.
- `...` - 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.

You can access all the properties of the current locale through the `localeProperties` property. When using an array of codes, it will only include the `code` property.
Expand Down
1 change: 1 addition & 0 deletions specs/different_domains.spec.ts
Expand Up @@ -41,6 +41,7 @@ await setup({
}
],
differentDomains: true,
strategy: 'no_prefix',
detectBrowserLanguage: {
useCookie: true
}
Expand Down
86 changes: 86 additions & 0 deletions specs/different_domains_multi_locales_prefix.spec.ts
@@ -0,0 +1,86 @@
import { test, expect, describe } from 'vitest'
import { fileURLToPath } from 'node:url'
import { setup, $fetch, undiciRequest } from './utils'
import { getDom } from './helper'

await setup({
rootDir: fileURLToPath(new URL(`./fixtures/different_domains`, import.meta.url)),
// overrides
nuxtConfig: {
i18n: {
locales: [
{
code: 'en',
iso: 'en',
name: 'English',
domain: 'nuxt-app.localhost',
domainDefault: true
},
{
code: 'no',
iso: 'no-NO',
name: 'Norwegian',
domain: 'nuxt-app.localhost'
},
{
code: 'fr',
iso: 'fr-FR',
name: 'Français',
domain: 'fr.nuxt-app.localhost'
}
],
differentDomains: true,
strategy: 'prefix',
detectBrowserLanguage: {
useCookie: true
}
}
}
})

describe('detection locale with host on server', () => {
test.each([
['en', 'nuxt-app.localhost', 'Homepage'],
['fr', 'fr.nuxt-app.localhost', 'Accueil']
])('%s host', async (locale, host, header) => {
const res = await undiciRequest('/' + locale, {
headers: {
Host: host
}
})
const dom = getDom(await res.body.text())

expect(dom.querySelector('#lang-switcher-current-locale code').textContent).toEqual(locale)
expect(dom.querySelector('#home-header').textContent).toEqual(header)
})
})

test('detection locale with x-forwarded-host on server', async () => {
const html = await $fetch('/fr', {
headers: {
'X-Forwarded-Host': 'fr.nuxt-app.localhost'
}
})
const dom = getDom(html)

expect(dom.querySelector('#lang-switcher-current-locale code').textContent).toEqual('fr')
expect(dom.querySelector('#home-header').textContent).toEqual('Accueil')
})

test('pass `<NuxtLink> to props', async () => {
const res = await undiciRequest('/fr', {
headers: {
Host: 'fr.nuxt-app.localhost'
}
})
const dom = getDom(await res.body.text())
expect(dom.querySelector('#switch-locale-path-usages .switch-to-en a').getAttribute('href')).toEqual(
`http://nuxt-app.localhost/en`
)
expect(dom.querySelector('#switch-locale-path-usages .switch-to-no a').getAttribute('href')).toEqual(
`http://nuxt-app.localhost/no`
)
expect(dom.querySelector('#switch-locale-path-usages .switch-to-fr a').getAttribute('href')).toEqual(
`http://fr.nuxt-app.localhost/fr`
)
})
@@ -0,0 +1,95 @@
import { test, expect, describe } from 'vitest'
import { fileURLToPath } from 'node:url'
import { setup, $fetch, undiciRequest } from './utils'
import { getDom } from './helper'

await setup({
rootDir: fileURLToPath(new URL(`./fixtures/different_domains`, import.meta.url)),
// overrides
nuxtConfig: {
i18n: {
locales: [
{
code: 'en',
iso: 'en',
name: 'English',
domain: 'nuxt-app.localhost',
domainDefault: true
},
{
code: 'no',
iso: 'no-NO',
name: 'Norwegian',
domain: 'nuxt-app.localhost'
},
{
code: 'fr',
iso: 'fr-FR',
name: 'Français',
domain: 'fr.nuxt-app.localhost',
domainDefault: true
},
{
code: 'ja',
iso: 'jp-JA',
name: 'Japan',
domain: 'ja.nuxt-app.localhost',
domainDefault: true
}
],
differentDomains: true,
strategy: 'prefix_except_default',
detectBrowserLanguage: {
useCookie: true
}
}
}
})

describe('detection locale with host on server', () => {
test.each([
['/', 'en', 'nuxt-app.localhost', 'Homepage'],
['/no', 'no', 'nuxt-app.localhost', 'Hjemmeside'],
['/', 'fr', 'fr.nuxt-app.localhost', 'Accueil']
])('%s host', async (path, locale, host, header) => {
const res = await undiciRequest(path, {
headers: {
Host: host
}
})
const dom = getDom(await res.body.text())

expect(dom.querySelector('#lang-switcher-current-locale code').textContent).toEqual(locale)
expect(dom.querySelector('#home-header').textContent).toEqual(header)
})
})

test('detection locale with x-forwarded-host on server', async () => {
const html = await $fetch('/', {
headers: {
'X-Forwarded-Host': 'fr.nuxt-app.localhost'
}
})
const dom = getDom(html)

expect(dom.querySelector('#lang-switcher-current-locale code').textContent).toEqual('fr')
expect(dom.querySelector('#home-header').textContent).toEqual('Accueil')
})

test('pass `<NuxtLink> to props', async () => {
const res = await undiciRequest('/', {
headers: {
Host: 'fr.nuxt-app.localhost'
}
})
const dom = getDom(await res.body.text())
expect(dom.querySelector('#switch-locale-path-usages .switch-to-en a').getAttribute('href')).toEqual(
`http://nuxt-app.localhost`
)
expect(dom.querySelector('#switch-locale-path-usages .switch-to-no a').getAttribute('href')).toEqual(
`http://nuxt-app.localhost/no`
)
expect(dom.querySelector('#switch-locale-path-usages .switch-to-fr a').getAttribute('href')).toEqual(
`http://fr.nuxt-app.localhost`
)
})
13 changes: 13 additions & 0 deletions specs/fixtures/different_domains/i18n.config.ts
Expand Up @@ -27,6 +27,19 @@ export default {
article: 'This is blog article page'
}
}
},
no: {
welcome: 'Velkommen',
home: 'Hjemmeside',
profile: 'Profil',
about: 'Om oss',
posts: 'Artikkeler',
dynamic: 'Dynamic',
pages: {
blog: {
article: 'Dette er bloggartikkelsiden'
}
}
}
},
fallbackLocale: 'en'
Expand Down
6 changes: 6 additions & 0 deletions specs/fixtures/different_domains/nuxt.config.ts
Expand Up @@ -14,6 +14,12 @@ export default defineNuxtConfig({
name: 'English',
domain: 'en.nuxt-app.localhost'
},
{
code: 'no',
iso: 'no-NO',
name: 'Norwegian',
domain: 'no.nuxt-app.localhost'
},
{
code: 'fr',
iso: 'fr-FR',
Expand Down
7 changes: 0 additions & 7 deletions src/module.ts
Expand Up @@ -114,13 +114,6 @@ export default defineNuxtModule<NuxtI18nOptions>({
)
}

if (options.strategy === 'no_prefix' && options.differentDomains) {
logger.warn(
'`differentDomains` option and `no_prefix` strategy are not compatible. ' +
'Change strategy or disable `differentDomains` option.'
)
}

if (options.dynamicRouteParams) {
logger.warn(
'The `dynamicRouteParams` options is deprecated and will be removed in `v9`, use the `useSetI18nParams` composable instead.'
Expand Down
20 changes: 16 additions & 4 deletions src/routing.ts
@@ -1,4 +1,5 @@
import { getNormalizedLocales } from './utils'
import { isObject } from '@intlify/shared'

import type { Locale } from 'vue-i18n'
import type { NuxtPage } from '@nuxt/schema'
Expand All @@ -25,13 +26,12 @@ export function prefixLocalizedRoute(
options: LocalizeRoutesParams,
extra = false
): boolean {
const isDefaultLocale = localizeOptions.locale === (options.defaultLocale ?? '')
const isDefaultLocale = localizeOptions.locale === (localizeOptions.defaultLocale ?? '')
const isChildWithRelativePath = localizeOptions.parent != null && !localizeOptions.path.startsWith('/')

// no need to add prefix if child's path is relative
return (
!extra &&
!options.differentDomains &&
!isChildWithRelativePath &&
// skip default locale if strategy is 'prefix_except_default'
!(isDefaultLocale && options.strategy === 'prefix_except_default')
Expand Down Expand Up @@ -86,6 +86,14 @@ export function localizeRoutes(routes: NuxtPage[], options: LocalizeRoutesParams
return routes
}

let defaultLocales = [options.defaultLocale ?? '']
if (options.differentDomains) {
const domainDefaults = options.locales
.filter(locale => (isObject(locale) ? locale.domainDefault : false))
.map(locale => (isObject(locale) ? locale.code : locale))
defaultLocales = defaultLocales.concat(domainDefaults)
}

function localizeRoute(
route: NuxtPage,
{ locales = [], parent, parentLocalized, extra = false }: LocalizeRouteParams
Expand All @@ -112,7 +120,7 @@ export function localizeRoutes(routes: NuxtPage[], options: LocalizeRoutesParams
const localizedRoutes: (LocalizedRoute | NuxtPage)[] = []
for (const locale of componentOptions.locales) {
const localized: LocalizedRoute = { ...route, locale, parent }
const isDefaultLocale = locale === options.defaultLocale
const isDefaultLocale = defaultLocales.includes(locale)
const addDefaultTree = isDefaultLocale && options.strategy === 'prefix_and_default' && parent == null && !extra

// localize route again for strategy `prefix_and_default`
Expand All @@ -131,7 +139,11 @@ export function localizeRoutes(routes: NuxtPage[], options: LocalizeRoutesParams
// use custom path if found
localized.path = componentOptions.paths?.[locale] ?? localized.path

const localePrefixable = prefixLocalizedRoute(localized, options, extra)
const localePrefixable = prefixLocalizedRoute(
{ defaultLocale: isDefaultLocale ? locale : options.defaultLocale, ...localized },
options,
extra
)
if (localePrefixable) {
localized.path = join('/', locale, localized.path)

Expand Down

0 comments on commit b7a6c66

Please sign in to comment.