Skip to content

Commit

Permalink
feat(i18n): add routesLocaleLinks to link locale files across differe…
Browse files Browse the repository at this point in the history
…nt routes

feat(i18n): add baseURL handling to support CDN usage
feat(i18n): add dateBuild to locale fetching for translation updates after rebuilds
test(i18n): add tests for routesLocaleLinks functionality
docs(i18n): update README with routesLocaleLinks documentation
  • Loading branch information
s00d committed Aug 20, 2024
1 parent d2c7e53 commit 0f8e415
Show file tree
Hide file tree
Showing 28 changed files with 238,600 additions and 41 deletions.
80 changes: 68 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,17 +276,31 @@ const i18n = useI18n()
</script>
```
Вот обновленный раздел `README.md` с описанием работы параметра `routesLocaleLinks` и улучшенным оформлением параметров модуля.

## Module Options

The module accepts the following options in the Nuxt configuration:

**locales**: An array of locale objects. Each locale should have the following properties:
### **locales**: `Locale[]`

- **code**: *(string, required)* A unique identifier for the locale, such as `'en'` for English or `'fr'` for French.
- **iso**: *(string, optional)* The ISO code for the locale, which is typically used for setting the `lang` attribute in HTML or for other internationalization purposes (e.g., `'en-US'`, `'fr-FR'`).
- **dir**: *(string, optional)* The text direction for the locale. It can be either `'rtl'` for right-to-left languages (like Arabic or Hebrew) or `'ltr'` for left-to-right languages (like English or French). This is useful for ensuring that text is displayed correctly depending on the language's writing system.
- **disabled**: *(boolean, optional)* A flag indicating whether this locale should be disabled or excluded from certain layers or operations. When `disabled` is set to `true`, this locale will be ignored in situations where only active or enabled locales are considered. This can be particularly useful when working with different layers of content or features in a multi-locale application, allowing developers to temporarily or permanently exclude specific locales from being processed, displayed, or made available to users without removing them entirely from the configuration.
```typescript
interface Locale {
code: string
disabled?: boolean
iso?: string
dir?: 'rtl' | 'ltr'
}
```

An array of locale objects. Each locale should have the following properties:

- **`code`**: *(string, required)* A unique identifier for the locale, such as `'en'` for English or `'fr'` for French.
- **`iso`**: *(string, optional)* The ISO code for the locale, which is typically used for setting the `lang` attribute in HTML or for other internationalization purposes (e.g., `'en-US'`, `'fr-FR'`).
- **`dir`**: *(string, optional)* The text direction for the locale. It can be either `'rtl'` for right-to-left languages (like Arabic or Hebrew) or `'ltr'` for left-to-right languages (like English or French).
- **`disabled`**: *(boolean, optional)* A flag indicating whether this locale should be disabled or excluded from certain layers or operations.

Example:

```typescript
locales: [
Expand All @@ -296,19 +310,61 @@ locales: [
]
```

**meta**: A boolean indicating whether to automatically generate SEO-related meta tags (like `alternate` links).
### **meta**: `boolean`

Indicates whether to automatically generate SEO-related meta tags (like `alternate` links).

### **defaultLocale**: `string`

The default locale code. For example, `'en'`.

### **translationDir**: `string`

The directory where translation files are stored. Default value is `'locales'`.

### **autoDetectLanguage**: `boolean`

If `true`, the module automatically detects the user's preferred language and redirects accordingly.

### **plural**: `function`

A custom function for handling pluralization logic.

### **includeDefaultLocaleRoute**: `boolean`

**defaultLocale**: The default locale code (e.g., `'en'`).
If `true`, all routes without a locale prefix will redirect to the default locale route.

**translationDir**: The directory where translation files are stored (e.g., `'locales'`).
### **routesLocaleLinks**: `Record<string, string>`

**autoDetectLanguage**: If `true`, automatically detects the user's preferred language and redirects accordingly.
This parameter allows you to create links between different pages' locale files. It is particularly useful in cases where you have similar pages (e.g., a main page and a page with a slug) and want them to share the same translation file.

For example, if you have a page with a slug (`dir1-slug`) and a main page (`index`), you can set up `routesLocaleLinks` so that `dir1-slug` will use the locale file of `index`, avoiding the need to maintain duplicate translation files.

Example:

```typescript
import MyModule from '../../../src/module'

export default defineNuxtConfig({
modules: [
MyModule,
],

i18n: {
routesLocaleLinks: {
'dir1-slug': 'index',
},
},

compatibilityDate: '2024-08-16',
})
```

**plural**: A custom function for handling pluralization.
In this example, the page `dir1-slug` will load its translations from the `index` page's locale file.

**includeDefaultLocaleRoute**: A boolean. If enabled, all routes without a locale prefix will redirect to the default locale route.
### **cache**: `boolean`

**cache**: (In development) A boolean option designed to optimize performance when working with large JSON translation files. When enabled, it caches translations specific to the current page, reducing search times and minimizing client-side load. This cached data is then sent to the client, resulting in faster page loads and improved user experience.
(In development) This option is designed to optimize performance when working with large JSON translation files. When enabled, it caches translations specific to the current page, reducing search times and minimizing client-side load.

## Locale Loading in Nuxt I18n Micro

Expand Down
5 changes: 5 additions & 0 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export default defineNuxtConfig({
defaultLocale: 'en',
translationDir: 'locales',
autoDetectLanguage: true,
routesLocaleLinks: {
'dir1-slug': 'dir1',
'dir1-subdir-hash-subhash': 'dir1-subdir',
'dir1-subdir-slug-id-key': 'dir1-subdir',
},
},
devtools: { enabled: true },
compatibilityDate: '2024-08-14',
Expand Down
2 changes: 1 addition & 1 deletion playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
},
"dependencies": {
"nuxt": "^3.12.4",
"nuxt-i18n-micro": "^1.2.0"
"nuxt-i18n-micro": "^1.3.0"
}
}
9 changes: 9 additions & 0 deletions playground/pages/dir1/[slug].vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template>
<div>
<p>{{ $t('key2') }}</p>
</div>
</template>

<script setup lang="ts">
</script>
5 changes: 5 additions & 0 deletions playground/pages/dir1/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<div>
<p>{{ $t('key2') }}</p>
</div>
</template>
8 changes: 8 additions & 0 deletions playground/pages/dir1/subdir/[hash]/[subhash].vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<template>
<div>
index subdir
</div>
</template>

<script setup lang="ts">
</script>
9 changes: 9 additions & 0 deletions playground/pages/dir1/subdir/[slug]-[id]-[key].vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template>
<div>
index [slug]-[id]-[key]
</div>
</template>

<script setup lang="ts">
</script>
5 changes: 5 additions & 0 deletions playground/pages/dir1/subdir/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<div>
index subdir
</div>
</template>
9 changes: 9 additions & 0 deletions playground/pages/dir2/[slug].vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template>
<div>
index slug dir2
</div>
</template>

<script setup lang="ts">
</script>
37 changes: 22 additions & 15 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import path from 'node:path'
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
import {
addComponentsDir,
addImportsDir,
addPlugin,
addPrerenderRoutes,
addServerHandler,
createResolver,
defineNuxtModule,
extendPages,
addComponentsDir,
} from '@nuxt/kit'
import type { HookResult } from '@nuxt/schema'
import { watch } from 'chokidar'
Expand All @@ -29,6 +29,7 @@ export interface ModuleOptions {
translationDir?: string
autoDetectLanguage?: boolean
includeDefaultLocaleRoute?: boolean
routesLocaleLinks?: Record<string, string>
cache?: boolean
plural?: string
}
Expand All @@ -37,6 +38,8 @@ export interface ModuleOptionsExtend extends ModuleOptions {
rootDir: string
pluralString: string
rootDirs: string[]
dateBuild: number
baseURL: string
}

declare module '@nuxt/schema' {
Expand Down Expand Up @@ -66,6 +69,7 @@ export default defineNuxtModule<ModuleOptions>({
translationDir: 'locales',
autoDetectLanguage: true,
includeDefaultLocaleRoute: false,
routesLocaleLinks: {},
cache: false,
plural: `function (translation, count, _locale) {
const forms = translation.toString().split('|')
Expand Down Expand Up @@ -107,7 +111,10 @@ export default defineNuxtModule<ModuleOptions>({
translationDir: options.translationDir ?? 'locales',
autoDetectLanguage: options.autoDetectLanguage ?? true,
includeDefaultLocaleRoute: options.includeDefaultLocaleRoute ?? false,
routesLocaleLinks: options.routesLocaleLinks ?? {},
cache: options.cache ?? false,
dateBuild: Date.now(),
baseURL: nuxt.options.app.baseURL,
}

addPlugin({
Expand Down Expand Up @@ -163,7 +170,9 @@ export default defineNuxtModule<ModuleOptions>({
const pagesDir = path.resolve(nuxt.options.rootDir, options.translationDir!, 'pages')

extendPages((pages) => {
const pagesNames = pages.map(page => page.name)
const pagesNames = pages
.map(page => page.name)
.filter(name => name && (!options.routesLocaleLinks || !options.routesLocaleLinks[name]))

function ensureFileExists(filePath: string) {
const fileDir = path.dirname(filePath) // Get the directory of the file
Expand All @@ -185,23 +194,21 @@ export default defineNuxtModule<ModuleOptions>({
ensureFileExists(globalFilePath)

// Process page-specific translation files
pages.forEach((page) => {
const pageFilePath = path.join(pagesDir, `${page.name}/${locale.code}.json`)
pagesNames.forEach((name) => {
const pageFilePath = path.join(pagesDir, `${name}/${locale.code}.json`)
ensureFileExists(pageFilePath)
})
})
const newRoutes = pages.map((page) => {
return {
...page,
path: `/:locale(${localeRegex})${page.path}`,
name: `localized-${page.name}`,
meta: {
...page.meta,
},
}
})

// Добавляем новые маршруты
const newRoutes = pages.map(page => ({
...page,
path: `/:locale(${localeRegex})${page.path}`,
name: `localized-${page.name}`,
meta: {
...page.meta,
},
}))

pages.push(...newRoutes)

nuxt.options.generate.routes = Array.isArray(nuxt.options.generate.routes) ? nuxt.options.generate.routes : []
Expand Down
28 changes: 18 additions & 10 deletions src/runtime/plugins/01.plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,30 +111,38 @@ export default defineNuxtPlugin(async (nuxtApp) => {
const config = useRuntimeConfig()
const i18nConfig: ModuleOptionsExtend = config.public.i18nConfig as ModuleOptionsExtend

const initialLocale = (route.params?.locale ?? i18nConfig.defaultLocale).toString()
const initialRouteName = (route.name as string).replace(`localized-`, '')

const plural = new Function('return ' + i18nConfig.plural)()

router.beforeEach(async (to, from, next) => {
if (import.meta.client) {
const locale = (to.params?.locale ?? i18nConfig.defaultLocale).toString()
const routeName = (to.name as string).replace(`localized-`, '')
if (!i18nHelper.hasPageTranslation(locale, routeName)) {
const data: Translations = await $fetch(`/_locales/${routeName}/${locale}/data.json`)
await i18nHelper!.loadPageTranslations(locale, routeName, data ?? {})
const initialRouteName = (to.name as string).replace(`localized-`, '')
let routeName = initialRouteName
if (i18nConfig.routesLocaleLinks && i18nConfig.routesLocaleLinks[routeName]) {
routeName = i18nConfig.routesLocaleLinks[routeName]
}

if (!i18nHelper.hasPageTranslation(locale, initialRouteName)) {
const data: Translations = await $fetch(`/_locales/${routeName}/${locale}/data.json?v=${i18nConfig.dateBuild}`, { baseURL: i18nConfig.baseURL })
await i18nHelper!.loadPageTranslations(locale, initialRouteName, data ?? {})
}
}
next()
})

const data: Translations = await $fetch(`/_locales/general/${initialLocale}/data.json`)
const initialLocale = (route.params?.locale ?? i18nConfig.defaultLocale).toString()
const initialRouteName = (route.name as string).replace(`localized-`, '')

const data: Translations = await $fetch(`/_locales/general/${initialLocale}/data.json?v=${i18nConfig.dateBuild}`, { baseURL: i18nConfig.baseURL })
await i18nHelper!.loadTranslations(initialLocale, initialRouteName, data ?? {})

if (import.meta.server) {
const locale = (route.params?.locale ?? i18nConfig.defaultLocale).toString()
const routeName = (route.name as string).replace(`localized-`, '')
const data: Translations = await $fetch(`/_locales/${routeName}/${locale}/data.json`)
let routeName = initialRouteName
if (i18nConfig.routesLocaleLinks && i18nConfig.routesLocaleLinks[routeName]) {
routeName = i18nConfig.routesLocaleLinks[routeName]
}
const data: Translations = await $fetch(`/_locales/${routeName}/${locale}/data.json?v=${i18nConfig.dateBuild}`, { baseURL: i18nConfig.baseURL })
await i18nHelper!.loadPageTranslations(initialLocale, initialRouteName, data ?? {})
}

Expand Down
1 change: 1 addition & 0 deletions src/runtime/plugins/03.define.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export default defineNuxtPlugin((_nuxtApp) => {
if (i18nConfig.includeDefaultLocaleRoute) {
defaultRouteName = `localized-${defaultRouteName}`
newParams.locale = i18nConfig.defaultLocale!
newParams.name = defaultRouteName
}

return router.push(resolvedRoute)
Expand Down
8 changes: 8 additions & 0 deletions test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ test('test index', async ({ page, goto }) => {
await expect(page.locator('#locale')).toHaveText('de')
})

test('test links', async ({ page, goto }) => {
await goto('/dir1/test', { waitUntil: 'hydration' })
await expect(page.locator('#test_link')).toHaveText('link in en')

await goto('/de/dir1/test', { waitUntil: 'hydration' })
await expect(page.locator('#test_link')).toHaveText('link in de')
})

test('test plugin methods output on page', async ({ page, goto }) => {
// Navigate to the /page route
await goto('/page', { waitUntil: 'hydration' })
Expand Down
3 changes: 2 additions & 1 deletion test/fixtures/basic/locales/pages/index/de.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"key1": "text in de"
"key1": "text in de",
"test_link": "link in de"
}
5 changes: 3 additions & 2 deletions test/fixtures/basic/locales/pages/index/en.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"key1": "text in en"
}
"key1": "text in en",
"test_link": "link in en"
}
3 changes: 3 additions & 0 deletions test/fixtures/basic/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export default defineNuxtConfig({
defaultLocale: 'en',
translationDir: 'locales',
autoDetectLanguage: true,
routesLocaleLinks: {
'dir1-slug': 'index',
},
},

compatibilityDate: '2024-08-16',
Expand Down
11 changes: 11 additions & 0 deletions test/fixtures/basic/pages/dir1/[slug].vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<template>
<div>
<p id="test_link">
{{ $t('test_link') }}
</p>
</div>
</template>

<script setup lang="ts">
</script>
Loading

0 comments on commit 0f8e415

Please sign in to comment.