Skip to content
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

feat(i18n): WIP YAML support using js-yaml with optional dependency. #118

Open
wants to merge 1 commit 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
85 changes: 84 additions & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import path from 'node:path'
import fs from 'node:fs'
import { readFile } from 'node:fs/promises'
import { globby } from 'globby'
import {
addComponentsDir,
addImportsDir,
Expand All @@ -15,7 +17,7 @@ import {
} from '@nuxt/kit'
import type { HookResult, NuxtPage } from '@nuxt/schema'
import { watch } from 'chokidar'
import type { ModuleOptions, ModuleOptionsExtend, ModulePrivateOptionsExtend, Locale, PluralFunc, GlobalLocaleRoutes, Getter, LocaleCode, Strategies } from 'nuxt-i18n-micro-types'
import type { ModuleOptions, ModuleOptionsExtend, ModulePrivateOptionsExtend, Locale, PluralFunc, GlobalLocaleRoutes, Getter, LocaleCode, Strategies, Translations, Translation } from 'nuxt-i18n-micro-types'
import {
isNoPrefixStrategy,
isPrefixStrategy,
Expand All @@ -25,6 +27,7 @@ import { setupDevToolsUI } from './devtools'
import { PageManager } from './page-manager'
import type { PluginsInjections } from './runtime/plugins/01.plugin'
import { LocaleManager } from './locale-manager'
import { deepMerge, loadYaml } from './utils'

function generateI18nTypes() {
return `
Expand Down Expand Up @@ -362,6 +365,86 @@ export default defineNuxtModule<ModuleOptions>({
}
})

nuxt.hook('nitro:init', async (nitro) => {
logger.debug('[nuxt-i18n-micro] clear storage cache')
await nitro.storage.clear('i18n-locales')
if (!await nitro.storage.hasItem(`i18n-locales:.gitignore`)) {
// await nitro.storage.setItem(`${output}:.gitignore`, '*')
const dir = path.join(nuxt.options.rootDir, 'server/assets/i18n-locales')
fs.mkdirSync(dir, { recursive: true })
fs.writeFileSync(`${dir}/.gitignore`, '*')
}

const translationDir = options.translationDir ?? ''
const fallbackLocale = options.fallbackLocale ?? null
const translationsByLocale: Record<string, Translations> = {}

try {
for (const rootDir of rootDirs) {
const baseDir = path.resolve(rootDir, translationDir)
const jsonFiles = await globby('**/*.json', { cwd: baseDir })
const yamlFiles = await globby('**/*.{yaml,yml}', { cwd: baseDir })

const promises = [...jsonFiles, ...yamlFiles].map(async (file) => {
const filePath = path.join(baseDir, file)
let data: Translations | null = null
if (file.endsWith('.json')) {
const content = await readFile(filePath, 'utf-8')
data = JSON.parse(content) as Translations
}
else if (file.endsWith('.yaml') || file.endsWith('.yml')) {
data = await loadYaml(filePath) as Translations
}

if (!data) return

const parts = file.split('/')
const locale = parts.pop()?.replace(/\.(json|yaml|yml)$/, '') || ''
const pageKey = parts.pop() || 'general'

if (!translationsByLocale[locale]) {
translationsByLocale[locale] = {}
}

translationsByLocale[locale] = deepMerge<Translations>({
[pageKey]: data,
}, translationsByLocale[locale])
})

await Promise.all(promises)
}

const savePromises: Promise<void>[] = []

for (const [locale, translations] of Object.entries(translationsByLocale)) {
for (const [key, value] of Object.entries(translations)) {
const storageKey = `i18n-locales:${locale}:${key}`
const promise = (async () => {
let translation: Translation = value
if (fallbackLocale) {
translation = deepMerge<Translation>(
translation,
translationsByLocale[fallbackLocale][key] ?? {},
)
}
if (typeof translation === 'object' && translation !== null) {
await nitro.storage.setItem(storageKey, translation)
if (options.debug) {
logger.log(`[nuxt-i18n-micro] Translation saved to Nitro storage with key: ${storageKey}`)
}
}
})()

savePromises.push(promise)
}
}
await Promise.all(savePromises)
}
catch (err) {
logger.error('[nuxt-i18n-micro] Error processing translations:', err)
}
})

if (!options.disableUpdater) {
nuxt.hook('nitro:build:before', async (_nitro) => {
const isProd = nuxt.options.dev === false
Expand Down
1 change: 1 addition & 0 deletions src/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
61 changes: 61 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import path from 'node:path'
import { readFile } from 'node:fs/promises'
import type { NuxtPage } from '@nuxt/schema'
import type { Locale, LocaleCode } from 'nuxt-i18n-micro-types'

Expand Down Expand Up @@ -79,3 +80,63 @@ const normalizeRegex = (toNorm?: string): string | undefined => {
if (typeof toNorm === 'undefined') return undefined
return toNorm.startsWith('/') && toNorm.endsWith('/') ? toNorm?.slice(1, -1) : toNorm
}

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 {
console.warn(
'js-yaml is not installed, please install it if you want to use YAML files for translations.',
)
return null
}
}

export function deepMerge<T extends object | unknown>(target: T, source: T): T {
if (typeof source !== 'object' || source === null) {
// If source is not an object, return target if it already exists, otherwise overwrite with source
return target === undefined ? source : target
}

if (Array.isArray(target)) {
// If source is an array, overwrite target with source
return target as T
}

if (source instanceof Object) {
// Ensure target is an object to merge into
if (!(target instanceof Object) || Array.isArray(target)) {
target = {} as T
}

for (const key in source) {
if (key === '__proto__' || key === 'constructor') continue

// Type guard to ensure that key exists on target and is of type object
if (
target !== null
&& typeof (target as Record<string, unknown>)[key] === 'object'
&& (target as Record<string, unknown>)[key] !== null
) {
// If target has a key that is an object, merge recursively
(target as Record<string, unknown>)[key] = deepMerge(
(target as Record<string, unknown>)[key],
(source as Record<string, unknown>)[key],
)
}
else {
// If the key doesn't exist in target, or it's not an object, overwrite with source value
if (target instanceof Object && !(key in target)) {
(target as Record<string, unknown>)[key] = (
source as Record<string, unknown>
)[key]
}
}
}
}

return target
}