Skip to content

Commit

Permalink
feat(i18n): support Nuxt I18n v9 (#351)
Browse files Browse the repository at this point in the history
  • Loading branch information
harlan-zw authored Aug 30, 2024
1 parent e879913 commit 92d9610
Show file tree
Hide file tree
Showing 19 changed files with 746 additions and 512 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"@nuxt/module-builder": "0.8.3",
"@nuxt/test-utils": "^3.14.1",
"@nuxt/ui": "^2.18.4",
"@nuxtjs/i18n": "8.5.1",
"@nuxtjs/i18n": "9.0.0-alpha.1",
"@nuxtjs/robots": "4.1.3",
"bumpp": "^9.5.2",
"eslint": "9.9.1",
Expand Down
981 changes: 563 additions & 418 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

27 changes: 12 additions & 15 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,22 @@ import type {
AutoI18nConfig,
ModuleRuntimeConfig,
MultiSitemapEntry,
NormalisedLocales,
SitemapDefinition,
SitemapSourceBase,
SitemapSourceInput,
SitemapSourceResolved,
ModuleOptions as _ModuleOptions, FilterInput,
NormalisedLocale,
} from './runtime/types'
import { convertNuxtPagesToSitemapEntries, generateExtraRoutesFromNuxtConfig, resolveUrls } from './util/nuxtSitemap'
import { createNitroPromise, createPagesPromise, extendTypes, getNuxtModuleOptions, resolveNitroPreset } from './util/kit'
import { includesSitemapRoot, isNuxtGenerate, setupPrerenderHandler } from './prerender'
import { mergeOnKey } from './runtime/utils-pure'
import { setupDevToolsUI } from './devtools'
import { normaliseDate } from './runtime/nitro/sitemap/urlset/normalise'
import { generatePathForI18nPages, getOnlyLocalesFromI18nConfig, splitPathForI18nLocales } from './util/i18n'
import {
generatePathForI18nPages,
normalizeLocales,
splitPathForI18nLocales,
} from './util/i18n'
import { normalizeFilters } from './util/filter'

// eslint-disable-next-line
Expand Down Expand Up @@ -155,18 +156,14 @@ export default defineNuxtModule<ModuleOptions>({
let nuxtI18nConfig = {} as NuxtI18nOptions
let resolvedAutoI18n: false | AutoI18nConfig = typeof config.autoI18n === 'boolean' ? false : config.autoI18n || false
const hasDisabledAutoI18n = typeof config.autoI18n === 'boolean' && !config.autoI18n
let normalisedLocales: NormalisedLocales = []
let normalisedLocales: AutoI18nConfig['locales'] = []
let usingI18nPages = false
if (hasNuxtModule('@nuxtjs/i18n')) {
const i18nVersion = await getNuxtModuleVersion('@nuxtjs/i18n')
if (!await hasNuxtModuleCompatibility('@nuxtjs/i18n', '>=8'))
logger.warn(`You are using @nuxtjs/i18n v${i18nVersion}. For the best compatibility, please upgrade to @nuxtjs/i18n v8.0.0 or higher.`)
nuxtI18nConfig = (await getNuxtModuleOptions('@nuxtjs/i18n') || {}) as NuxtI18nOptions
normalisedLocales = mergeOnKey((nuxtI18nConfig.locales || []).map((locale: any) => typeof locale === 'string' ? { code: locale } : locale), 'code')
const onlyLocales = getOnlyLocalesFromI18nConfig(nuxtI18nConfig)
if (onlyLocales.length) {
normalisedLocales = normalisedLocales.filter((locale: NormalisedLocale) => onlyLocales.includes(locale.code))
}
normalisedLocales = normalizeLocales(nuxtI18nConfig)
usingI18nPages = !!Object.keys(nuxtI18nConfig.pages || {}).length
if (usingI18nPages && !hasDisabledAutoI18n) {
const i18nPagesSources: SitemapSourceBase = {
Expand All @@ -189,20 +186,20 @@ export default defineNuxtModule<ModuleOptions>({
// add to sitemap
const alternatives = Object.keys(pageLocales)
.map(l => ({
hreflang: normalisedLocales.find(nl => nl.code === l)?.iso || l,
hreflang: normalisedLocales.find(nl => nl.code === l)?._hreflang || l,
href: generatePathForI18nPages({ localeCode: l, pageLocales: pageLocales[l], nuxtI18nConfig, normalisedLocales }),
}))
if (alternatives.length && nuxtI18nConfig.defaultLocale && pageLocales[nuxtI18nConfig.defaultLocale])
alternatives.push({ hreflang: 'x-default', href: generatePathForI18nPages({ normalisedLocales, localeCode: nuxtI18nConfig.defaultLocale, pageLocales: pageLocales[nuxtI18nConfig.defaultLocale], nuxtI18nConfig }) })
i18nPagesSources.urls!.push({
_sitemap: locale.iso || locale.code,
_sitemap: locale._sitemap,
loc: generatePathForI18nPages({ normalisedLocales, localeCode, pageLocales: pageLocales[localeCode], nuxtI18nConfig }),
alternatives,
})
// add extra loc with the default locale code prefix on prefix and default strategy
if (nuxtI18nConfig.strategy === 'prefix_and_default' && localeCode === nuxtI18nConfig.defaultLocale) {
i18nPagesSources.urls!.push({
_sitemap: locale.iso || locale.code,
_sitemap: locale._sitemap,
loc: generatePathForI18nPages({ normalisedLocales, localeCode, pageLocales: pageLocales[localeCode], nuxtI18nConfig, forcedStrategy: 'prefix' }),
alternatives,
})
Expand Down Expand Up @@ -240,7 +237,7 @@ export default defineNuxtModule<ModuleOptions>({
config.sitemaps = { index: [...(config.sitemaps?.index || []), ...(config.appendSitemaps || [])] }
for (const locale of resolvedAutoI18n.locales)
// @ts-expect-error untyped
config.sitemaps[locale.iso || locale.code] = { includeAppSources: true }
config.sitemaps[locale._sitemap] = { includeAppSources: true }
isI18nMapped = true
usingMultiSitemaps = true
}
Expand Down Expand Up @@ -603,7 +600,7 @@ declare module 'vue-router' {
if (!pageSource.length) {
pageSource.push(nuxt.options.app.baseURL || '/')
}
if (!resolvedConfigUrls) {
if (!resolvedConfigUrls && config.urls) {
config.urls && userGlobalSources.push({
context: {
name: 'sitemap:urls',
Expand Down
4 changes: 2 additions & 2 deletions src/prerender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ export function setupPrerenderHandler(_options: { runtimeConfig: ModuleRuntimeCo
// if it's missing a locale then we put it in the default locale sitemap
const locale = match[0] || options.autoI18n.defaultLocale
if (options.isI18nMapped) {
const { code, iso } = options.autoI18n.locales.find(l => l.code === locale) || { code: locale, iso: locale }
const { _sitemap } = options.autoI18n.locales.find(l => l.code === locale) || { _sitemap: locale }
// this will filter the results to only the sitemap that matches the locale
route._sitemap._sitemap = iso || code
route._sitemap._sitemap = _sitemap
}
}
route._sitemap = defu(extractSitemapMetaFromHtml(html, {
Expand Down
17 changes: 8 additions & 9 deletions src/runtime/nitro/sitemap/builder/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export function resolveSitemapEntries(sitemap: SitemapDefinition, sources: Sitem
}
entries.push({
href: u.loc,
hreflang: u._locale.code || autoI18n.defaultLocale,
hreflang: u._locale._hreflang || autoI18n.defaultLocale,
})
return entries
})
Expand All @@ -98,15 +98,15 @@ export function resolveSitemapEntries(sitemap: SitemapDefinition, sources: Sitem
e.alternatives = [
{
// apply default locale domain
...autoI18n.locales.find(l => [l.code, l.iso].includes(autoI18n.defaultLocale)),
...autoI18n.locales.find(l => [l.code, l.language].includes(autoI18n.defaultLocale)),
code: 'x-default',
},
...autoI18n.locales
.filter(l => !!l.domain),
]
.map((locale) => {
return {
hreflang: locale.iso || locale.code,
hreflang: locale._hreflang,
href: joinURL(withHttps(locale.domain!), e._pathWithoutPrefix),
}
})
Expand All @@ -117,15 +117,15 @@ export function resolveSitemapEntries(sitemap: SitemapDefinition, sources: Sitem
let loc = joinURL(`/${l.code}`, e._pathWithoutPrefix)
if (autoI18n.differentDomains || (['prefix_and_default', 'prefix_except_default'].includes(autoI18n.strategy) && l.code === autoI18n.defaultLocale))
loc = e._pathWithoutPrefix
const _sitemap = isI18nMapped ? (l.iso || l.code) : undefined
const _sitemap = isI18nMapped ? l._sitemap : undefined
const newEntry: NormalizedI18n = preNormalizeEntry({
_sitemap,
...e,
_index: undefined,
_key: `${_sitemap || ''}${loc}`,
_locale: l,
loc,
alternatives: [{ code: 'x-default' }, ...autoI18n.locales].map((locale) => {
alternatives: [{ code: 'x-default', _hreflang: 'x-default' }, ...autoI18n.locales].map((locale) => {
const code = locale.code === 'x-default' ? autoI18n.defaultLocale : locale.code
const isDefault = locale.code === 'x-default' || locale.code === autoI18n.defaultLocale
let href = ''
Expand All @@ -141,11 +141,10 @@ export function resolveSitemapEntries(sitemap: SitemapDefinition, sources: Sitem
href = joinURL('/', code, e._pathWithoutPrefix)
}
}
const hreflang = locale.iso || locale.code
if (!filterPath(href))
return false
return {
hreflang,
hreflang: locale._hreflang,
href,
}
}).filter(Boolean),
Expand All @@ -163,7 +162,7 @@ export function resolveSitemapEntries(sitemap: SitemapDefinition, sources: Sitem
}
}
if (isI18nMapped) {
e._sitemap = e._sitemap || e._locale.iso || e._locale.code
e._sitemap = e._sitemap || e._locale._sitemap
}
if (e._index)
_urls[e._index] = e
Expand Down Expand Up @@ -207,7 +206,7 @@ export async function buildSitemapUrls(sitemap: SitemapDefinition, resolvers: Ni
return urls
}
if (autoI18n?.differentDomains) {
const domain = autoI18n.locales.find(e => [e.iso, e.code].includes(sitemap.sitemapName))?.domain
const domain = autoI18n.locales.find(e => [e.language, e.code].includes(sitemap.sitemapName))?.domain
if (domain) {
const _tester = resolvers.canonicalUrlResolver
resolvers.canonicalUrlResolver = (path: string) => resolveSitePath(path, {
Expand Down
27 changes: 24 additions & 3 deletions src/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,12 +204,33 @@ export type AppSourceContext = 'nuxt:pages' | 'nuxt:prerender' | 'nuxt:route-rul

export type SitemapSourceInput = string | [string, FetchOptions] | SitemapSourceBase | SitemapSourceResolved

export interface NormalisedLocale { code: string, iso?: string, domain?: string }
// copied from @nuxtjs/i18n, types do not appear to be working
interface LocaleObject extends Record<string, any> {
code: string
name?: string
dir?: 'ltr' | 'rtl' | 'auto'
domain?: string
domains?: string[]
defaultForDomains?: string[]
file?: string | {
path: string
cache?: boolean
}
files?: string[] | {
path: string
cache?: boolean
}[]
isCatchallLocale?: boolean
/**
* @deprecated in v9, use `language` instead
*/
iso?: string
language?: string
}

export type NormalisedLocales = NormalisedLocale[]
export interface AutoI18nConfig {
differentDomains?: boolean
locales: NormalisedLocales
locales: (LocaleObject & { _sitemap: string, _hreflang: string })[]
defaultLocale: string
strategy: 'prefix' | 'prefix_except_default' | 'prefix_and_default' | 'no_prefix'
}
Expand Down
34 changes: 23 additions & 11 deletions src/util/i18n.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type { NuxtI18nOptions } from '@nuxtjs/i18n'
import type { NuxtI18nOptions, LocaleObject } from '@nuxtjs/i18n'
import type { Strategies } from 'vue-i18n-routing'
import { joinURL, withBase, withHttps } from 'ufo'
import type { AutoI18nConfig, FilterInput, NormalisedLocales } from '../runtime/types'
import { splitForLocales } from '../runtime/utils-pure'
import type { AutoI18nConfig, FilterInput } from '../runtime/types'
import { mergeOnKey, splitForLocales } from '../runtime/utils-pure'

export interface StrategyProps {
localeCode: string
pageLocales: string
nuxtI18nConfig: NuxtI18nOptions
forcedStrategy?: Strategies
normalisedLocales: NormalisedLocales
normalisedLocales: AutoI18nConfig['locales']
}

export function splitPathForI18nLocales(path: FilterInput, autoI18n: AutoI18nConfig) {
Expand All @@ -27,13 +27,6 @@ export function splitPathForI18nLocales(path: FilterInput, autoI18n: AutoI18nCon
]
}

export function getOnlyLocalesFromI18nConfig(nuxtI18nConfig: NuxtI18nOptions) {
const onlyLocales = nuxtI18nConfig?.bundle?.onlyLocales
if (!onlyLocales) return []
const includedLocales = typeof onlyLocales === 'string' ? [onlyLocales] : onlyLocales
return includedLocales
}

export function generatePathForI18nPages(ctx: StrategyProps): string {
const { localeCode, pageLocales, nuxtI18nConfig, forcedStrategy, normalisedLocales } = ctx
const locale = normalisedLocales.find(l => l.code === localeCode)
Expand All @@ -49,3 +42,22 @@ export function generatePathForI18nPages(ctx: StrategyProps): string {
}
return locale?.domain ? withHttps(withBase(path, locale.domain)) : path
}

export function normalizeLocales(nuxtI18nConfig: NuxtI18nOptions): AutoI18nConfig['locales'] {
let locales = nuxtI18nConfig.locales || []
let onlyLocales = nuxtI18nConfig?.bundle?.onlyLocales || []
onlyLocales = typeof onlyLocales === 'string' ? [onlyLocales] : onlyLocales
locales = mergeOnKey(locales.map((locale: any) => typeof locale === 'string' ? { code: locale } : locale), 'code')
if (onlyLocales.length) {
locales = locales.filter((locale: LocaleObject) => onlyLocales.includes(locale.code))
}
return locales.map((locale) => {
// we prefer i18n v9 config
if (locale.iso && !locale.language) {
locale.language = locale.iso
}
locale._hreflang = locale.language || locale.code
locale._sitemap = locale.language || locale.code
return locale
})
}
17 changes: 8 additions & 9 deletions src/util/nuxtSitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { extname } from 'pathe'
import { defu } from 'defu'
import type { ConsolaInstance } from 'consola'
import { withBase, withHttps } from 'ufo'
import type { NormalisedLocales, SitemapDefinition, SitemapUrl, SitemapUrlInput } from '../runtime/types'
import type { AutoI18nConfig, SitemapDefinition, SitemapUrl, SitemapUrlInput } from '../runtime/types'
import { createPathFilter } from '../runtime/utils-pure'
import type { CreateFilterOptions } from '../runtime/utils-pure'

Expand All @@ -28,7 +28,7 @@ export async function resolveUrls(urls: Required<SitemapDefinition>['urls'], ctx
}

export interface NuxtPagesToSitemapEntriesOptions {
normalisedLocales: NormalisedLocales
normalisedLocales: AutoI18nConfig['locales']
routesNameSeparator?: string
autoLastmod: boolean
defaultLocale: string
Expand Down Expand Up @@ -118,8 +118,8 @@ export function convertNuxtPagesToSitemapEntries(pages: NuxtPage[], config: Nuxt
const [name, locale] = e.page!.name.split(routesNameSeparator)
if (!acc[name])
acc[name] = []
const { iso, code } = config.normalisedLocales.find(l => l.code === locale) || { iso: locale, code: locale }
acc[name].push({ ...e, _sitemap: config.isI18nMapped ? (iso || code) : undefined, locale })
const { _sitemap } = config.normalisedLocales.find(l => l.code === locale) || { _sitemap: locale }
acc[name].push({ ...e, _sitemap: config.isI18nMapped ? _sitemap : undefined, locale })
}
else {
acc.default = acc.default || []
Expand All @@ -141,7 +141,7 @@ export function convertNuxtPagesToSitemapEntries(pages: NuxtPage[], config: Nuxt
return false
const defaultLocale = config.normalisedLocales.find(l => l.code === config.defaultLocale)
if (defaultLocale && config.isI18nMapped)
e._sitemap = defaultLocale.iso || defaultLocale.code
e._sitemap = defaultLocale._sitemap
delete e.page
delete e.locale
return { ...e }
Expand All @@ -151,12 +151,11 @@ export function convertNuxtPagesToSitemapEntries(pages: NuxtPage[], config: Nuxt
const alternatives = entries.map((entry) => {
const locale = config.normalisedLocales.find(l => l.code === entry.locale)
// check if the locale has a iso code
const hreflang = locale?.iso || entry.locale
if (!pathFilter(entry.loc))
return false
const href = locale?.domain ? withHttps(withBase(entry.loc, locale?.domain)) : entry.loc
return {
hreflang,
hreflang: locale?._hreflang,
href,
}
}).filter(Boolean)
Expand All @@ -171,8 +170,8 @@ export function convertNuxtPagesToSitemapEntries(pages: NuxtPage[], config: Nuxt
}
const e = { ...entry }
if (config.isI18nMapped) {
const { iso, code } = config.normalisedLocales.find(l => l.code === entry.locale) || { iso: locale, code: locale }
e._sitemap = iso || code
const { _sitemap } = config.normalisedLocales.find(l => l.code === entry.locale) || { _sitemap: locale }
e._sitemap = _sitemap
}
delete e.page
delete e.locale
Expand Down
6 changes: 3 additions & 3 deletions test/integration/i18n/dynamic-urls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('i18n dynamic urls', () => {
<url>
<loc>https://nuxtseo.com/english-url</loc>
<xhtml:link rel="alternate" href="https://nuxtseo.com/english-url" hreflang="x-default" />
<xhtml:link rel="alternate" href="https://nuxtseo.com/english-url" hreflang="en" />
<xhtml:link rel="alternate" href="https://nuxtseo.com/english-url" hreflang="en-US" />
</url>
<url>
<loc>https://nuxtseo.com/__sitemap/url</loc>
Expand All @@ -58,8 +58,8 @@ describe('i18n dynamic urls', () => {
<url>
<loc>https://nuxtseo.com/en/dynamic/foo</loc>
<xhtml:link rel="alternate" href="https://nuxtseo.com/en/dynamic/foo" hreflang="x-default" />
<xhtml:link rel="alternate" href="https://nuxtseo.com/en/dynamic/foo" hreflang="en" />
<xhtml:link rel="alternate" href="https://nuxtseo.com/fr/dynamic/foo" hreflang="fr" />
<xhtml:link rel="alternate" href="https://nuxtseo.com/en/dynamic/foo" hreflang="en-US" />
<xhtml:link rel="alternate" href="https://nuxtseo.com/fr/dynamic/foo" hreflang="fr-FR" />
</url>
</urlset>"
`)
Expand Down
2 changes: 1 addition & 1 deletion test/integration/i18n/filtering.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('i18n filtering', () => {
<url>
<loc>https://nuxtseo.com/no-i18n</loc>
<xhtml:link rel="alternate" href="https://nuxtseo.com/no-i18n" hreflang="x-default" />
<xhtml:link rel="alternate" href="https://nuxtseo.com/no-i18n" hreflang="en" />
<xhtml:link rel="alternate" href="https://nuxtseo.com/no-i18n" hreflang="en-US" />
</url>
<url>
<loc>https://nuxtseo.com/en/__sitemap/url</loc>
Expand Down
2 changes: 1 addition & 1 deletion test/integration/i18n/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('generate', () => {
<url>
<loc>https://nuxtseo.com/no-i18n</loc>
<xhtml:link rel="alternate" href="https://nuxtseo.com/no-i18n" hreflang="x-default" />
<xhtml:link rel="alternate" href="https://nuxtseo.com/no-i18n" hreflang="en" />
<xhtml:link rel="alternate" href="https://nuxtseo.com/no-i18n" hreflang="en-US" />
</url>
<url>
<loc>https://nuxtseo.com/en/test</loc>
Expand Down
2 changes: 1 addition & 1 deletion test/integration/i18n/prefix-and-default.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe('i18n prefix and default', () => {
<url>
<loc>https://nuxtseo.com/no-i18n</loc>
<xhtml:link rel="alternate" href="https://nuxtseo.com/no-i18n" hreflang="x-default" />
<xhtml:link rel="alternate" href="https://nuxtseo.com/no-i18n" hreflang="en" />
<xhtml:link rel="alternate" href="https://nuxtseo.com/no-i18n" hreflang="en-US" />
</url>
<url>
<loc>https://nuxtseo.com/test</loc>
Expand Down
2 changes: 1 addition & 1 deletion test/integration/i18n/prefix-except-default.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe('i18n prefix except default', () => {
<url>
<loc>https://nuxtseo.com/no-i18n</loc>
<xhtml:link rel="alternate" href="https://nuxtseo.com/no-i18n" hreflang="x-default" />
<xhtml:link rel="alternate" href="https://nuxtseo.com/no-i18n" hreflang="en" />
<xhtml:link rel="alternate" href="https://nuxtseo.com/no-i18n" hreflang="en-US" />
</url>
<url>
<loc>https://nuxtseo.com/test</loc>
Expand Down
Loading

0 comments on commit 92d9610

Please sign in to comment.