Skip to content

Commit

Permalink
feat: added global configuration for custom routes via globalLocaleRo…
Browse files Browse the repository at this point in the history
…utes
  • Loading branch information
s00d committed Sep 26, 2024
1 parent 3c5ad7c commit d303499
Show file tree
Hide file tree
Showing 9 changed files with 358 additions and 14 deletions.
37 changes: 37 additions & 0 deletions docs/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,43 @@ i18n: {
}
```

### 🌐 `globalLocaleRoutes`

Allows you to define custom localized routes for specific pages. You can specify a custom path for each locale for a given page, or disable localization for certain pages entirely.

**Type**: `Record<string, Record<string, string> | false>`

- **Key** (`string`): The name of the page you want to customize or disable localization for.
- **Value**:
- **`Record<string, string>`**: A set of locale codes with corresponding custom paths for the page.
- **`false`**: Disable localization for this page entirely. The page will not be localized, and it will remain accessible only through its default path.

This option gives you the flexibility to localize certain pages differently while leaving others unaffected by localization.

**Example**:

```typescript
globalLocaleRoutes: {
page2: {
en: '/custom-page2-en',
de: '/custom-page2-de',
ru: '/custom-page2-ru',
},
unlocalized: false, // Unlocalized page should not be localized
}
```

### Usage:

In the example above:
- **`page2`**: Custom localized paths are defined for the page `page2` in English (`en`), German (`de`), and Russian (`ru`). Instead of following the standard localization pattern (like `/en/page2`), each locale will have a completely custom URL, such as `/custom-page2-en` for English, `/custom-page2-de` for German, and `/custom-page2-ru` for Russian.
- **`unlocalized`**: This page will not be localized, so it remains accessible only at `/unlocalized`, without any locale prefixes or custom paths.

### Benefits:

- **Control over URL structure**: Customize the URL for each locale, which can be helpful for SEO or branding purposes.
- **Flexible localization**: Disable localization for pages where it is unnecessary, keeping the URL structure simple and consistent for those specific routes.

---

# 🔄 Caching Mechanism
Expand Down
3 changes: 2 additions & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export default defineNuxtModule<ModuleOptions>({
fallbackLocale: undefined,
localeCookie: 'user-locale',
routesLocaleLinks: {},
globalLocaleRoutes: {},
plural: (key, count, _locale, getTranslation) => {
const translation = getTranslation(key, {})
if (!translation) {
Expand All @@ -75,7 +76,7 @@ export default defineNuxtModule<ModuleOptions>({
const rootDirs = nuxt.options._layers.map(layer => layer.config.rootDir).reverse()

const localeManager = new LocaleManager(options, rootDirs)
const pageManager = new PageManager(localeManager.locales, options.defaultLocale!, options.includeDefaultLocaleRoute!)
const pageManager = new PageManager(localeManager.locales, options.defaultLocale!, options.includeDefaultLocaleRoute!, options.globalLocaleRoutes)

let plural = options.plural!
if (typeof plural !== 'string') plural = plural.toString()
Expand Down
73 changes: 62 additions & 11 deletions src/page-manager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from 'node:path'
import { readFileSync } from 'node:fs'
import type { NuxtPage } from '@nuxt/schema'
import type { Locale } from './types'
import type { GlobalLocaleRoutes, Locale } from './types'
import {
extractDefineI18nRouteConfig,
normalizePath,
Expand All @@ -21,12 +21,14 @@ export class PageManager {
includeDefaultLocaleRoute: boolean
localizedPaths: { [key: string]: { [locale: string]: string } } = {}
activeLocaleCodes: string[]
globalLocaleRoutes: Record<string, Record<string, string> | false | boolean>

constructor(locales: Locale[], defaultLocaleCode: string, includeDefaultLocaleRoute: boolean) {
constructor(locales: Locale[], defaultLocaleCode: string, includeDefaultLocaleRoute: boolean, globalLocaleRoutes: GlobalLocaleRoutes) {
this.locales = locales
this.defaultLocale = this.findLocaleByCode(defaultLocaleCode) || { code: defaultLocaleCode }
this.includeDefaultLocaleRoute = includeDefaultLocaleRoute
this.activeLocaleCodes = this.computeActiveLocaleCodes()
this.globalLocaleRoutes = globalLocaleRoutes || {}
}

private findLocaleByCode(code: string): Locale | undefined {
Expand All @@ -43,7 +45,24 @@ export class PageManager {
this.localizedPaths = this.extractLocalizedPaths(pages, rootDir)

const additionalRoutes: NuxtPage[] = []
pages.forEach(page => this.localizePage(page, additionalRoutes))
pages.forEach((page) => {
const customRoute = this.globalLocaleRoutes[page.name ?? ''] ?? null

// If globalLocaleRoutes for this page is false, skip localization
if (customRoute === false) {
return
}

// Check if the page has custom routes in globalLocaleRoutes
if (customRoute && typeof customRoute === 'object') {
// Add routes based on custom globalLocaleRoutes
this.addCustomGlobalLocalizedRoutes(page, customRoute, additionalRoutes)
}
else {
// Default behavior: localize the page as usual
this.localizePage(page, additionalRoutes)
}
})

pages.push(...additionalRoutes)
}
Expand All @@ -56,16 +75,27 @@ export class PageManager {
const localizedPaths: { [key: string]: { [locale: string]: string } } = {}

pages.forEach((page) => {
if (page.file) {
const filePath = path.resolve(rootDir, page.file)
const fileContent = readFileSync(filePath, 'utf-8')
const i18nRouteConfig = extractDefineI18nRouteConfig(fileContent, filePath)

if (i18nRouteConfig?.localeRoutes) {
const normalizedFullPath = normalizePath(path.join(parentPath, page.path))
localizedPaths[normalizedFullPath] = i18nRouteConfig.localeRoutes
const pageName = page.name ?? ''
const globalLocalePath = this.globalLocaleRoutes[pageName]

if (!globalLocalePath) {
// Fallback to extracting localized paths from the page file content (existing functionality)
if (page.file) {
const filePath = path.resolve(rootDir, page.file)
const fileContent = readFileSync(filePath, 'utf-8')
const i18nRouteConfig = extractDefineI18nRouteConfig(fileContent, filePath)

if (i18nRouteConfig?.localeRoutes) {
const normalizedFullPath = normalizePath(path.join(parentPath, page.path))
localizedPaths[normalizedFullPath] = i18nRouteConfig.localeRoutes
}
}
}
else if (typeof globalLocalePath === 'object') {
// Use globalLocaleRoutes if defined
const normalizedFullPath = normalizePath(path.join(parentPath, page.path))
localizedPaths[normalizedFullPath] = globalLocalePath
}

if (page.children?.length) {
const parentFullPath = normalizePath(path.join(parentPath, page.path))
Expand All @@ -76,6 +106,27 @@ export class PageManager {
return localizedPaths
}

private addCustomGlobalLocalizedRoutes(
page: NuxtPage,
customRoutePaths: Record<string, string>,
additionalRoutes: NuxtPage[],
) {
this.locales.forEach((locale) => {
const customPath = customRoutePaths[locale.code]
if (!customPath) return

const isDefaultLocale = isLocaleDefault(locale, this.defaultLocale, this.includeDefaultLocaleRoute)
if (isDefaultLocale) {
// Modify the page path if it's the default locale
page.path = normalizePath(customPath)
}
else {
// Create a new localized route for this locale
additionalRoutes.push(this.createLocalizedRoute(page, [locale.code], page.children ?? [], true, customPath))
}
})
}

private localizePage(
page: NuxtPage,
additionalRoutes: NuxtPage[],
Expand Down
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface DefineI18nRouteConfig {
export type Getter = (key: string, params?: Record<string, string | number | boolean>, defaultValue?: string) => unknown
export type PluralFunc = (key: string, count: number, locale: string, getter: Getter) => string | null

export type GlobalLocaleRoutes = Record<string, Record<string, string> | false | boolean> | null | undefined

export interface ModuleOptions {
locales?: Locale[]
meta?: boolean
Expand All @@ -29,6 +31,7 @@ export interface ModuleOptions {
disablePageLocales?: boolean
fallbackLocale?: string
localeCookie?: string
globalLocaleRoutes?: GlobalLocaleRoutes
}

export interface ModuleOptionsExtend extends ModuleOptions {
Expand Down
29 changes: 29 additions & 0 deletions test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,32 @@ test('test handling of missing locale data', async ({ page, goto }) => {

await expect(page.locator('#translation')).toHaveText('page.example')
})

test('Test globalLocaleRoutes for page2 and unlocalized', async ({ page, goto }) => {
// Test custom locale route for 'page2' in English
await goto('/custom-page2-en', { waitUntil: 'hydration' })

// Check that the custom route for English was applied and the content is correct
await expect(page).toHaveURL('/custom-page2-en')

// Test custom locale route for 'page2' in German
await goto('/de/custom-page2-de', { waitUntil: 'hydration' })

// Check that the custom route for German was applied and the content is correct
await expect(page).toHaveURL('/de/custom-page2-de')

// Test custom locale route for 'page2' in Russian
await goto('/ru/custom-page2-ru', { waitUntil: 'hydration' })

// Check that the custom route for Russian was applied and the content is correct
await expect(page).toHaveURL('/ru/custom-page2-ru')

// Test that the 'unlocalized' page is not affected by localization
await goto('/unlocalized', { waitUntil: 'hydration' })

// Check that the unlocalized page remains the same and isn't localized
await expect(page).toHaveURL('/unlocalized')

const response = await page.goto('/de/unlocalized', { waitUntil: 'networkidle' })
expect(response?.status()).toBe(404)
})
8 changes: 8 additions & 0 deletions test/fixtures/basic/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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',
Expand Down
81 changes: 81 additions & 0 deletions test/fixtures/basic/pages/page2.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<template>
<div>
<p id="content">
{{ $t('page.content') }}
</p>
<p id="locale">
Current Locale: {{ $getLocale() }}
</p>

<!-- Display additional info for testing -->
<p id="locales">
{{ $getLocales().map(locale => locale.code).join(', ') }}
</p>
<p id="translation">
{{ $t('page.example') }}
</p>
<p id="plural">
{{ $tc('page.apples', 2) }}
</p>
<p id="plural-component">
<i18n-t
keypath="page.apples"
:plural="5"
/>
</p>
<p id="plural-component-custom">
<i18n-t
keypath="page.apples"
:plural="5"
:custom-plural-rule="customPluralRule"
/>
</p>
<p id="plural-component-custom-zero">
<i18n-t
keypath="page.apples"
:plural="0"
:custom-plural-rule="customPluralRule"
/>
</p>
<p id="localized-route">
{{ $localeRoute({ name: 'page' }, 'de').path }}
</p>

<!-- Links for switching locales -->
<div id="locale-links">
<NuxtLink
id="link-en"
:to="$localeRoute({ name: 'page' }, 'en')"
>
Switch to English
</NuxtLink>
<NuxtLink
id="link-de"
:to="$localeRoute({ name: 'page' }, 'de')"
>
Switch to German
</NuxtLink>
</div>
</div>
</template>

<script setup>
import { useNuxtApp } from '#imports'
const { $t, $getLocale, $getLocales, $tc, $localeRoute } = useNuxtApp()
const customPluralRule = (key, count, _locale, t) => {
const translation = t(key)
if (!translation) {
return null
}
const forms = translation.toString().split('|')
if (count === 0 && forms.length > 2) {
return forms[0].trim() // Case for "no apples"
}
if (count === 1 && forms.length > 1) {
return forms[1].trim() // Case for "one apple"
}
return (forms.length > 2 ? forms[2].trim() : forms[forms.length - 1].trim()).replace('{count}', count.toString())
}
</script>
Loading

0 comments on commit d303499

Please sign in to comment.