Skip to content

feat(i18n): WIP YAML support #123

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
24 changes: 24 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,3 +461,27 @@ import { useNuxtApp } from '#imports'
const { $tdr } = useNuxtApp()
</script>
```

## YAML Examples

Here are examples of how to structure your translation files using YAML:

### Example YAML File (`en.yml`)

```yaml
greeting: "Hello, {username}!"
welcome:
title: "Welcome to our site"
description: "We offer a variety of services."
nav:
home: "Home"
about: "About Us"
contact: "Contact"
```

### Example YAML File with Page-Specific Translations (`pages/index/en.yml`)
```yaml
title: "Welcome to the Home Page"
introduction: "This is the introduction text for the home page."

```
15 changes: 12 additions & 3 deletions docs/guide/folder-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,31 @@ Organizing your translation files effectively is essential for maintaining a sca

### 🔧 Basic Structure

Here’s a basic example of the folder structure you should follow:
Here’s a basic example of the folder structure you should follow. Note that `.json`, `.yaml`, and `.yml` files are supported.

```plaintext
/locales
├── /pages
│ ├── /index
│ │ ├── en.json
│ │ ├── fr.json
│ │ └── ar.json
│ │ ├── ar.json
│ │ ├── en.yml
│ │ ├── fr.yml
│ │ └── ar.yml
│ ├── /about
│ │ ├── en.json
│ │ ├── fr.json
│ │ └── ar.json
│ │ ├── ar.json
│ │ ├── en.yml
│ │ ├── fr.yml
│ │ └── ar.yml
├── en.json
├── fr.json
└── ar.json
├── en.yml
├── fr.yml
└── ar.yml
```

### 📄 Explanation of Structure
Expand Down
10 changes: 10 additions & 0 deletions docs/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ export default defineNuxtConfig({
})
```

### YAML Support

In addition to JSON, `nuxt-i18n-micro` also supports YAML files for translations. To use YAML, you need to install the `js-yaml` package as a dev dependency:

```bash
npm install -D js-yaml
```

After installing `js-yaml`, you can create YAML files (with `.yaml` or `.yml` extensions) in your `translationDir` alongside your JSON files.

### 📂 Folder Structure

Translation files are organized into global and page-specific directories:
Expand Down
4 changes: 4 additions & 0 deletions docs/guide/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,8 @@ Here are a few tips to ensure you get the best performance out of `Nuxt I18n Mic
- 💾 **Enable Caching**: Make use of the caching features to reduce server load and improve response times.
- 🏁 **Leverage Pre-rendering**: Pre-render your translations to speed up page loads and reduce runtime overhead.

### 📄 Translation Format

While `nuxt-i18n-micro` supports both JSON and YAML translation files, **JSON is recommended for optimal performance**. JSON parsing is generally faster and more efficient than YAML parsing. If performance is a priority, use JSON for your translations.

For detailed results of the performance tests, please refer to the [Performance Test Results](/guide/performance-results).
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ These results clearly demonstrate that `Nuxt I18n Micro` significantly outperfor
- ⚡ **Optimized Build and Runtime**: Reduces build times, memory usage, and server load, making it ideal for **high-traffic applications**.
- 🛠️ **Minimalist Design**: The module is structured around just **5 components** (1 module and 4 plugins), making it easy to understand, extend, and maintain.
- 📏 **Efficient Routing**: Generates only **2 routes** regardless of the number of locales, thanks to dynamic regex-based routing, unlike other i18n modules that generate separate routes for each locale.
- 🗂 **Streamlined Translation Loading**: Only **JSON files** are supported, with translations split between a global file for common texts (e.g., menus) and page-specific files, which are auto-generated in the `dev` mode if not present.
- 🗂 **Streamlined Translation Loading**: **JSON** and **YAML** files are supported, with translations split between a global file for common texts (e.g., menus) and page-specific files, which are auto-generated in the `dev` mode if not present. See the [getting started guide](/guide/getting-started) for details on YAML support and the required `js-yaml` dependency.

## ⚙️ Quick Setup

Expand Down
99 changes: 65 additions & 34 deletions src/runtime/server/routes/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { resolve, join } from 'node:path'
import { readFile } from 'node:fs/promises'
import { defineEventHandler } from 'h3'
import type { Translations, ModuleOptionsExtend, ModulePrivateOptionsExtend } from 'nuxt-i18n-micro-types'
import { loadYaml } from '../utils/load-yaml'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { useRuntimeConfig, createError, useStorage } from '#imports'
Expand All @@ -10,7 +11,13 @@ let storageInit = false

function deepMerge(target: Translations, source: Translations): Translations {
for (const key in source) {
if (key === '__proto__' || key === 'constructor') continue
if (key === '__proto__' || key === 'constructor') {
continue
}

if (source[key] === undefined) {
continue
}

if (Array.isArray(source[key])) {
target[key] = source[key]
Expand All @@ -27,6 +34,53 @@ function deepMerge(target: Translations, source: Translations): Translations {
return target
}

async function loadTranslations(rootDirs: string[], translationDir: string, locale: string, page: string, fallbackLocale?: string): Promise<Translations> {
const getTranslationPath = (locale: string, page: string) =>
page === 'general' ? `${locale}` : `pages/${page}/${locale}`

let translations: Translations = {}

const loadLocale = async (currentLocale: string) => {
const extensions = ['.json', '.yaml', '.yml']
const paths = extensions.flatMap(ext => createPaths(currentLocale, ext))

for (const { translationPath } of paths) {
try {
const content = await readFile(translationPath, 'utf-8')
let fileContent: Translations = {}

if (translationPath.endsWith('.json')) {
fileContent = JSON.parse(content) as Translations
}
else if (translationPath.endsWith('.yaml') || translationPath.endsWith('.yml')) {
fileContent = await loadYaml(translationPath) as Translations
}

translations = deepMerge(translations, fileContent)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
catch (e) {
// We ignore the error here, as it just means that the file doesn't exist which is fine
// console.error('[nuxt-i18n-micro] load locale error', e)
}
}
}

const createPaths = (currentLocale: string, ext: string) =>
rootDirs.map(dir => ({
translationPath: resolve(dir, translationDir!, `${getTranslationPath(currentLocale, page)}${ext}`),
name: `_locales/${getTranslationPath(currentLocale, page)}${ext}`,
}))

if (fallbackLocale && fallbackLocale !== locale) {
await loadLocale(fallbackLocale)
}

await loadLocale(locale)

return translations
}

export default defineEventHandler(async (event) => {
const { page, locale } = event.context.params as { page: string, locale: string }
const config = useRuntimeConfig()
Expand All @@ -37,52 +91,29 @@ export default defineEventHandler(async (event) => {
throw createError({ statusCode: 404 })
}

const getTranslationPath = (locale: string, page: string) =>
page === 'general' ? `${locale}.json` : `pages/${page}/${locale}.json`

let translations: Translations = {}
const serverStorage = useStorage('assets:server')

if (!storageInit) {
if (debug) console.log('[nuxt-i18n-micro] clear storage cache')
if (debug) {
console.log('[nuxt-i18n-micro] clear storage cache')
}
await Promise.all((await serverStorage.getKeys('_locales')).map((key: string) => serverStorage.removeItem(key)))
storageInit = true
}

const cacheName = join('_locales', getTranslationPath(locale, page))
const cacheName = join('_locales', `${locale}-${page}`)

const isThereAsset = await serverStorage.hasItem(cacheName)
let translations: Translations = {}

if (isThereAsset) {
const rawContent = await serverStorage.getItem<Translations | string>(cacheName) ?? {}
return typeof rawContent === 'string' ? JSON.parse(rawContent) : rawContent
translations = typeof rawContent === 'string' ? JSON.parse(rawContent) : rawContent
}

const createPaths = (locale: string) =>
rootDirs.map(dir => ({
translationPath: resolve(dir, translationDir!, getTranslationPath(locale, page)),
name: `_locales/${getTranslationPath(locale, page)}`,
}))

const paths = [
...(fallbackLocale && fallbackLocale !== locale ? createPaths(fallbackLocale) : []),
...createPaths(locale),
]

for (const { translationPath, name } of paths) {
try {
if (debug) console.log('[nuxt-i18n-micro] load locale', translationPath, name)

const content = await readFile(translationPath, 'utf-8')
const fileContent = JSON.parse(content!) as Translations

translations = deepMerge(translations, fileContent)
}
catch (e) {
if (debug) console.error('[nuxt-i18n-micro] load locale error', e)
}
else {
translations = await loadTranslations(rootDirs, translationDir, locale, page, fallbackLocale)
await serverStorage.setItem(cacheName, translations)
}

await serverStorage.setItem(cacheName, translations)

return translations
})
1 change: 1 addition & 0 deletions src/runtime/server/types/js-yaml.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module 'js-yaml'; // Declare to allow optional dependency import
16 changes: 16 additions & 0 deletions src/runtime/server/utils/load-yaml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { readFile } from 'node:fs/promises'

export async function loadYaml(filePath: string) {
try {
const yaml = await import('js-yaml')
const content = await readFile(filePath, 'utf-8')
return yaml.load(content)
}
catch (e: unknown) {
console.error(e)
console.warn(
'js-yaml is not installed, please install it if you want to use YAML files for translations.',
)
return null
}
}
Loading