diff --git a/docs/examples.md b/docs/examples.md index b05b5177..db1c68a7 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -461,3 +461,27 @@ import { useNuxtApp } from '#imports' const { $tdr } = useNuxtApp() ``` + +## 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." + +``` diff --git a/docs/guide/folder-structure.md b/docs/guide/folder-structure.md index e83b623e..ffdbe1ae 100644 --- a/docs/guide/folder-structure.md +++ b/docs/guide/folder-structure.md @@ -14,7 +14,7 @@ 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 @@ -22,14 +22,23 @@ Here’s a basic example of the folder structure you should follow: β”‚ β”œβ”€β”€ /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 diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index b99152fb..a835f116 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -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: diff --git a/docs/guide/performance.md b/docs/guide/performance.md index b3907b7a..1a1ed02b 100644 --- a/docs/guide/performance.md +++ b/docs/guide/performance.md @@ -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). diff --git a/docs/index.md b/docs/index.md index e5f04b5f..8dfd2c5d 100755 --- a/docs/index.md +++ b/docs/index.md @@ -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 diff --git a/src/runtime/server/routes/get.ts b/src/runtime/server/routes/get.ts index 61e84752..33c9e000 100644 --- a/src/runtime/server/routes/get.ts +++ b/src/runtime/server/routes/get.ts @@ -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' @@ -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] @@ -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 { + 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() @@ -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(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 }) diff --git a/src/runtime/server/types/js-yaml.d.ts b/src/runtime/server/types/js-yaml.d.ts new file mode 100644 index 00000000..326410bf --- /dev/null +++ b/src/runtime/server/types/js-yaml.d.ts @@ -0,0 +1 @@ +declare module 'js-yaml'; // Declare to allow optional dependency import diff --git a/src/runtime/server/utils/load-yaml.ts b/src/runtime/server/utils/load-yaml.ts new file mode 100644 index 00000000..dfa65051 --- /dev/null +++ b/src/runtime/server/utils/load-yaml.ts @@ -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 + } +}