Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions internals.d.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
declare module '#build/i18n-options.mjs' {
import type { LocaleObject, VueI18nConfig } from '@nuxtjs/i18n'
import type { LocaleObject, NormalizedLocaleObject, VueI18nConfig } from '@nuxtjs/i18n'

export type { LocaleObject }

type LocaleLoader = { key: string, cache: boolean, load: () => Promise<never> }
export const localeLoaders: Record<string, LocaleLoader[]>
export const vueI18nConfigs: VueI18nConfig[]
export const localeCodes: string[]
export const normalizedLocales: LocaleObject[]
export const normalizedLocales: NormalizedLocaleObject[]
}

declare module '#internal/i18n-options.mjs' {
import type { LocaleObject, VueI18nConfig } from '@nuxtjs/i18n'
import type { NormalizedLocaleObject, VueI18nConfig } from '@nuxtjs/i18n'

type LocaleLoader = { key: string, cache: boolean, load: () => Promise<never> }
export const localeLoaders: Record<string, LocaleLoader[]>
export const vueI18nConfigs: VueI18nConfig[]
export const localeCodes: string[]
export const normalizedLocales: LocaleObject[]
export const normalizedLocales: NormalizedLocaleObject[]
}

declare module '#internal/i18n-locale-detector.mjs' {
Expand Down
4 changes: 2 additions & 2 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { resolveRawResourcePaths } from './resources'
import { generateLoaderOptions } from './gen'

import type { Resolver } from '@nuxt/kit'
import type { FileMeta, LocaleInfo, LocaleObject, NuxtI18nOptions } from './types'
import type { FileMeta, LocaleInfo, NormalizedLocaleObject, NuxtI18nOptions } from './types'
import type { Nuxt, NuxtConfigLayer } from '@nuxt/schema'

export interface I18nNuxtContext {
Expand All @@ -25,7 +25,7 @@ export interface I18nNuxtContext {
* Locale-derived context, resolved in `modules:done` once all modules have registered locales.
*/
export interface ResolvedI18nContext extends I18nNuxtContext {
normalizedLocales: LocaleObject<string>[]
normalizedLocales: NormalizedLocaleObject<string>[]
localeCodes: string[]
localeInfo: LocaleInfo[]
/** flattened `localeInfo` file metas */
Expand Down
2 changes: 1 addition & 1 deletion src/gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { LocaleObject } from './types'
import type { ResolvedI18nContext } from './context'

// copy - generators run repeatedly (template re-emits) and must not alter context locales
function stripLocaleFiles(locale: LocaleObject) {
function stripLocaleFiles<T extends LocaleObject>(locale: T): T {
const stripped = assign({}, locale)
delete stripped.files
delete stripped.file
Expand Down
7 changes: 5 additions & 2 deletions src/prepare/options.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { I18nNuxtContext } from '../context'
import type { Nuxt } from '@nuxt/schema'
import { logger } from '../utils'
import { logger, normalizeDomainLocale } from '../utils'
import { checkLayerOptions } from '../layers'
import { isString } from '@intlify/shared'

Expand Down Expand Up @@ -33,7 +33,10 @@ export function prepareOptions(ctx: I18nNuxtContext, nuxt: Nuxt) {
}

if (multiDomainLocales) {
const hasDomainLocales = (options.locales || []).some(locale => !isString(locale) && locale.domains?.length)
// `domain` normalizes into `domains` and configures the feature just as well
const hasDomainLocales = (options.locales || []).some(
locale => !isString(locale) && normalizeDomainLocale(locale).domains.length,
)

if (!hasDomainLocales) {
logger.warn(
Expand Down
18 changes: 6 additions & 12 deletions src/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import {
joinPath,
localizeSingleRoute,
} from './kit/gen'
import { normalizeDomainLocale } from './utils'
import type { LocaleObject, Strategies } from './types'
import type { NormalizedLocaleObject, Strategies } from './types'

export type RouteResources = {
/** plain paths mounted as-is for at least one locale */
Expand Down Expand Up @@ -97,8 +96,7 @@ export function shouldLocalizeRoutes(options: SetupLocalizeRoutesOptions) {
// check if domains are used multiple times, compared by host as domains may include a protocol
const domains = new Set<string>()
for (const locale of options.locales) {
for (const domain of locale.domains?.length ? locale.domains : [locale.domain]) {
if (!domain) { continue }
for (const domain of locale.domains) {
const host = domain.replace(/^https?:\/\//, '')
if (domains.has(host)) {
console.error(
Expand All @@ -114,13 +112,9 @@ export function shouldLocalizeRoutes(options: SetupLocalizeRoutesOptions) {
return true
}

/**
* Locales acting as the default (unprefixed) locale for at least one domain. Normalizing here
* rather than reading `domainDefault` directly keeps this in step with the runtime, which
* resolves the domain default from `defaultForDomains` alone.
*/
function getDomainDefaultLocales(locales: LocaleObject[]): string[] {
return locales.filter(locale => normalizeDomainLocale(locale).defaultForDomains?.length).map(locale => locale.code)
/** Locales acting as the default (unprefixed) locale for at least one domain */
function getDomainDefaultLocales(locales: NormalizedLocaleObject[]): string[] {
return locales.filter(locale => locale.defaultForDomains.length).map(locale => locale.code)
}

const usesDefaultVariants = (strategy: Strategies | undefined) =>
Expand Down Expand Up @@ -157,7 +151,7 @@ type SetupLocalizeRoutesOptions = {
trailingSlash?: boolean
differentDomains?: boolean
multiDomainLocales?: boolean
locales: LocaleObject[]
locales: NormalizedLocaleObject[]
routesNameSeparator: string
defaultLocaleRouteNameSuffix: string
defaultLocale?: string
Expand Down
10 changes: 7 additions & 3 deletions src/runtime/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type {
BaseUrlResolveHandler,
DetectBrowserLanguageOptions,
I18nPublicRuntimeConfig,
LocaleObject,
NormalizedLocaleObject,
} from '#internal-i18n-types'

export const useLocaleConfigs = () =>
Expand Down Expand Up @@ -52,7 +52,7 @@ export interface NuxtI18nContext {
/** Set locale - suspend if `skipSettingLocaleOnNavigate` is enabled */
setLocaleSuspend: (locale: string) => Promise<void>
/** Get normalized runtime locales */
getLocales: () => LocaleObject[]
getLocales: () => NormalizedLocaleObject[]
/** Set locale to locale cookie */
setCookieLocale: (locale: string) => void
/** Get current base URL */
Expand Down Expand Up @@ -235,7 +235,11 @@ export function createNuxtI18nContext(nuxt: NuxtApp, vueI18n: I18n, defaultLocal
await ctx.vueI18n.__resolvePendingLocalePromise?.()
}
},
getLocales: () => unref(i18n.locales).map(x => (isString(x) ? { code: x } : (x as LocaleObject<string>))),
// string locales carry no domain configuration, they normalize to the empty form
getLocales: () =>
unref(i18n.locales).map(x =>
isString(x) ? { code: x, domains: [], defaultForDomains: [] } : (x as NormalizedLocaleObject<string>),
),
setCookieLocale: (locale: string) => {
if (detectConfig.useCookie && isSupportedLocale(locale)) {
localeCookie.value = locale
Expand Down
3 changes: 2 additions & 1 deletion src/runtime/plugins/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ export default defineNuxtPlugin({
composer.strategy = __I18N_STRATEGY__
composer.localeProperties = computed(() =>
withRuntimeDomain(
normalizedLocales.find(l => l.code === composer.locale.value) || { code: composer.locale.value },
normalizedLocales.find(l => l.code === composer.locale.value)
|| { code: composer.locale.value, domains: [], defaultForDomains: [] },
runtimeI18n.domainLocales,
),
)
Expand Down
8 changes: 4 additions & 4 deletions src/runtime/routing/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { isLocaleOnHost } from '../shared/domain'

import type { RouteLocationPathRaw, RouteRecordNameGeneric, Router } from 'vue-router'
import type { PrefixableOptions } from '#i18n-kit/routing'
import type { LocaleObject, Strategies } from '#internal-i18n-types'
import type { NormalizedLocaleObject, Strategies } from '#internal-i18n-types'
import type { RouteLike, RouteLikeWithName, RouteLikeWithPath } from './routing'
import type { I18nRouteMeta, RouteLocationGenericPath } from '../types'

Expand All @@ -19,7 +19,7 @@ import type { I18nRouteMeta, RouteLocationGenericPath } from '../types'
export type RoutingContext = {
router: Router
getLocale: () => string
getLocales: () => LocaleObject[]
getLocales: () => NormalizedLocaleObject[]
getBaseUrl: (locale?: string) => string
/** Extracts the route base name (without locale suffix) */
getRouteBaseName: (route: RouteRecordNameGeneric | RouteLocationGenericPath | null) => string | undefined
Expand All @@ -43,7 +43,7 @@ export interface RoutingContextOptions {
router: Router
defaultLocale: string
getLocale: () => string
getLocales: () => LocaleObject[]
getLocales: () => NormalizedLocaleObject[]
getBaseUrl: (locale?: string) => string
/** Host of the current request/page, used for domain-based behavior */
getHost: () => string | undefined
Expand Down Expand Up @@ -206,7 +206,7 @@ export function createRoutingContext(options: RoutingContextOptions): RoutingCon
return path
}

if (stripsDefaultPrefix && target?.defaultForDomains?.length && getLocaleFromRoutePath(path) === locale) {
if (stripsDefaultPrefix && target?.defaultForDomains.length && getLocaleFromRoutePath(path) === locale) {
path = path.slice(locale.length + 1) || '/'
}
return joinURL(options.getBaseUrl(locale), path)
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/routing/head.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { assign } from '@intlify/shared'
import { type HeadContext, localeHead as _localeHead } from '#i18n-kit/head'

import type { I18nHeadMetaInfo, I18nHeadOptions, SeoAttributesOptions } from '#internal-i18n-types'
import type { I18nHeadMetaInfo, I18nHeadOptions, LocaleObject, SeoAttributesOptions } from '#internal-i18n-types'
import type { ComposableContext } from '../composable-context'
import type { I18nRouteMeta } from '../types'
import { localeRoute, switchLocalePath } from './routing'
Expand Down Expand Up @@ -33,7 +33,7 @@
// current locale's domain so canonical and og:url self-reference the current host (#2595)
baseUrl = ctx.routingOptions.domains ? ctx.getBaseUrl(locale) : ctx.getBaseUrl(),
): HeadContext {
const currentLocale = locales.find(l => l.code === locale) || { code: locale }
const currentLocale: LocaleObject = locales.find(l => l.code === locale) || { code: locale }
// deduplicate, layered configs merge `canonicalQueries` arrays with duplicate entries
const canonicalQueries = [...new Set((typeof config.seo === 'object' && config.seo?.canonicalQueries) || [])]

Expand Down Expand Up @@ -85,7 +85,7 @@
/**
* Returns localized head properties for locale-related aspects.
*
* @param ctx - Context used internally by composable functions.

Check warning on line 88 in src/runtime/routing/head.ts

View workflow job for this annotation

GitHub Actions / lint (lts/*, ubuntu-latest)

Missing @param "options.seo"

Check warning on line 88 in src/runtime/routing/head.ts

View workflow job for this annotation

GitHub Actions / lint (lts/*, ubuntu-latest)

Missing @param "options.lang"

Check warning on line 88 in src/runtime/routing/head.ts

View workflow job for this annotation

GitHub Actions / lint (lts/*, ubuntu-latest)

Missing @param "options.dir"
* @param options - An options, see about details {@link I18nHeadOptions}.
*
* @returns The localized {@link I18nHeadMetaInfo | head properties}.
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/shared/detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { isLocaleServedOnHost, matchDomainLocale, withRuntimeDomain } from './do
import { isString } from '@intlify/shared'
import { isSupportedLocale } from './locales'
import { type useI18nDetection, useRuntimeI18n } from '../shared/utils'
import type { I18nPublicRuntimeConfig, LocaleObject } from '../../types'
import type { I18nPublicRuntimeConfig, NormalizedLocaleObject } from '../../types'

import type { H3Event } from 'h3'
import type { CompatRoute } from '../types'
Expand Down Expand Up @@ -42,7 +42,7 @@ export const useDetectors = (event: H3Event | undefined, config: { cookieKey: st
const runtimeI18n = useRuntimeI18n(nuxtApp)
// constant for the lifetime of these detectors, a fresh set is created per request
let host: string | undefined
let locales: LocaleObject[] | undefined
let locales: NormalizedLocaleObject[] | undefined
const getHost = () => (host ??= getRequestHost(event))
const getLocales = () => (locales ??= getDomainLocales(runtimeI18n.domainLocales))

Expand Down
37 changes: 20 additions & 17 deletions src/runtime/shared/domain.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { hasProtocol } from 'ufo'
import { normalizedLocales } from '#build/i18n-options.mjs'
import { toArray } from './utils'

import type { I18nPublicRuntimeConfig, LocaleObject } from '#internal-i18n-types'
import type { I18nPublicRuntimeConfig, NormalizedLocaleObject } from '#internal-i18n-types'

/**
* Configured domains may include a protocol (used when generating URLs), comparisons
Expand All @@ -14,20 +13,18 @@ export const normalizeDomain = (domain: string = '') => domain.replace(/^https?:
/**
* Whether the locale is served on the given host
*/
export function isLocaleOnHost(locale: LocaleObject | undefined, host: string): boolean {
return (
!!locale && (normalizeDomain(locale.domain) === host || toArray(locale.domains).some(x => normalizeDomain(x) === host))
)
export function isLocaleOnHost(locale: NormalizedLocaleObject | undefined, host: string): boolean {
return !!locale?.domains.some(x => normalizeDomain(x) === host)
}

/**
* Whether the locale can be served on the given host. Unlike {@link isLocaleOnHost} a locale
* configured without domains qualifies, as does any locale on a host configured for none of
* them (a staging domain, a health check by IP) so the site keeps working there.
*/
export function isLocaleServedOnHost(locales: LocaleObject[], host: string, locale: string): boolean {
export function isLocaleServedOnHost(locales: NormalizedLocaleObject[], host: string, locale: string): boolean {
const target = locales.find(l => l.code === locale)
if (!target?.domain && !target?.domains?.length) { return true }
if (!target?.domains.length) { return true }
return isLocaleOnHost(target, host) || !locales.some(l => isLocaleOnHost(l, host))
}

Expand All @@ -36,14 +33,18 @@ export function isLocaleServedOnHost(locales: LocaleObject[], host: string, loca
* default for. Undefined when the current host can serve it, which keeps hosts that match no
* configured domain on relative URLs instead of sending them to a configured domain.
*/
function relocateHostForLocale(host: string, locale: string, locales: LocaleObject[]): string | undefined {
function relocateHostForLocale(host: string, locale: string, locales: NormalizedLocaleObject[]): string | undefined {
if (isLocaleServedOnHost(locales, host, locale)) { return }

const target = locales.find(l => l.code === locale)
return target?.defaultForDomains?.[0] || target?.domain || target?.domains?.[0]
return target?.defaultForDomains[0] || target?.domain || target?.domains[0]
}

export function matchDomainLocale(locales: LocaleObject[], host: string, pathLocale: string): string | undefined {
export function matchDomainLocale(
locales: NormalizedLocaleObject[],
host: string,
pathLocale: string,
): string | undefined {
const matches = locales.filter(locale => isLocaleOnHost(locale, host))

if (matches.length <= 1) {
Expand All @@ -54,22 +55,22 @@ export function matchDomainLocale(locales: LocaleObject[], host: string, pathLoc
// match by current path locale
matches.find(l => l.code === pathLocale)?.code
// fallback to default locale for the domain
|| matches.find(l => l.defaultForDomains?.some(domain => normalizeDomain(domain) === host) ?? l.domainDefault)?.code
|| matches.find(l => l.defaultForDomains.some(domain => normalizeDomain(domain) === host))?.code
)
}

export function domainFromLocale(
domainLocales: Record<string, { domain: string | undefined }>,
url: { host: string, protocol: string },
locale: string,
locales: LocaleObject[] = normalizedLocales,
locales: NormalizedLocaleObject[] = normalizedLocales,
): string | undefined {
const lang = locales.find(x => x.code === locale)
// lookup the `differentDomain` origin associated with given locale
const domain
= domainLocales?.[locale]?.domain
|| lang?.domain
|| lang?.domains?.find(v => normalizeDomain(v) === url.host)
|| lang?.domains.find(v => normalizeDomain(v) === url.host)
|| relocateHostForLocale(url.host, locale, locales)

if (!domain) {
Expand All @@ -88,15 +89,17 @@ export function domainFromLocale(
* Returns the locale object with the domain overridden by `domainLocales` runtime config (see also `getHostLocale`),
* a no-op outside domain setups since the override entries are empty.
*/
export function withRuntimeDomain<T extends string | LocaleObject>(
export function withRuntimeDomain<T extends string | NormalizedLocaleObject>(
locale: T,
domainLocales: I18nPublicRuntimeConfig['domainLocales'],
): T {
if (typeof locale === 'string') {
return locale
}
const properties = locale as LocaleObject
const properties = locale as NormalizedLocaleObject
const domain = domainLocales[properties.code]?.domain
// `domainLocales` is seeded from the configured scalar `domain`, an entry matching it is not an
// override - rebuilding on it would drop the locale's other domains
if (!domain || domain === properties.domain) {
return locale
}
Expand All @@ -107,6 +110,6 @@ export function withRuntimeDomain<T extends string | LocaleObject>(
...properties,
domain,
domains: [domain],
...(properties.defaultForDomains?.length ? { defaultForDomains: [domain] } : {}),
defaultForDomains: properties.defaultForDomains.length ? [domain] : [],
} as T
}
8 changes: 4 additions & 4 deletions src/runtime/shared/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { localeCodes, localeLoaders, normalizedLocales } from '#build/i18n-optio
import { isArray, isString } from '@intlify/shared'
import { normalizeDomain } from './domain'
import type { FallbackLocale } from 'vue-i18n'
import type { LocaleObject } from '#internal-i18n-types'
import type { NormalizedLocaleObject } from '#internal-i18n-types'

type LocaleConfig = { cacheable: boolean, fallbacks: string[] }
export function createLocaleConfigs(fallbackLocale: FallbackLocale): Record<string, LocaleConfig> {
Expand Down Expand Up @@ -55,8 +55,8 @@ export function isLocaleWithFallbacksCacheable(locale: string, fallbackLocales:
/**
* The locale configured as the default for `host`, undefined when no locale claims it
*/
export function getDefaultLocaleForDomain(host: string, locales: LocaleObject[] = normalizedLocales): string | undefined {
return locales.find(l => l.defaultForDomains?.some(domain => normalizeDomain(domain) === host))?.code
export function getDefaultLocaleForDomain(host: string, locales: NormalizedLocaleObject[] = normalizedLocales): string | undefined {
return locales.find(l => l.defaultForDomains.some(domain => normalizeDomain(domain) === host))?.code
}

/**
Expand All @@ -67,7 +67,7 @@ export function getDefaultLocaleForDomain(host: string, locales: LocaleObject[]
export function resolveDefaultLocale(
host: string,
defaultLocale: string | undefined,
locales: LocaleObject[] = normalizedLocales,
locales: NormalizedLocaleObject[] = normalizedLocales,
): string {
return getDefaultLocaleForDomain(host, locales) || defaultLocale || ''
}
Expand Down
Loading
Loading