Skip to content

Commit

Permalink
feat: basic messages type generation
Browse files Browse the repository at this point in the history
  • Loading branch information
BobbieGoede committed Oct 3, 2024
1 parent 989d775 commit 282df90
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 12 deletions.
14 changes: 13 additions & 1 deletion build.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { defineBuildConfig } from 'unbuild'

export default defineBuildConfig({
externals: ['node:fs', 'node:url', 'webpack', '@babel/parser', 'unplugin-vue-router', 'unplugin-vue-router/options']
externals: [
'node:fs',
'node:url',
'webpack',
'@babel/parser',
'unplugin-vue-router',
'unplugin-vue-router/options',
'jiti',
'confbox',
'ofetch',
'destr'
],
failOnWarn: false
})
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
"@unhead/vue": "^1.8.8",
"bumpp": "^9.4.1",
"changelogithub": "^0.13.7",
"confbox": "^0.1.7",
"consola": "^3",
"eslint": "^9.5.0",
"eslint-config-prettier": "^9.1.0",
Expand All @@ -128,7 +129,7 @@
"globals": "^15.6.0",
"globby": "^14.0.0",
"h3": "^1.12.0",
"jiti": "^1.20.0",
"jiti": "2.0.0",
"jsdom": "^24.1.0",
"lint-staged": "^15.2.7",
"nitropack": "^2.9.7",
Expand Down
21 changes: 12 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { setupNitro } from './nitro'
import { extendBundler } from './bundler'
import { NUXT_I18N_MODULE_ID, DEFAULT_OPTIONS } from './constants'
import type { HookResult } from '@nuxt/schema'
import type { I18nPublicRuntimeConfig, LocaleObject, NuxtI18nOptions } from './types'
import type { I18nPublicRuntimeConfig, LocaleInfo, LocaleObject, NuxtI18nOptions } from './types'
import type { Locale } from 'vue-i18n'
import { createContext } from './context'
import { prepareOptions } from './prepare/options'
Expand All @@ -18,6 +18,7 @@ import { prepareStrategy } from './prepare/strategy'
import { prepareLayers } from './prepare/layers'
import { prepareTranspile } from './prepare/transpile'
import { prepareVite } from './prepare/vite'
import { enableVueI18nTypeGeneration } from './type-generation'

export * from './types'

Expand Down Expand Up @@ -75,6 +76,8 @@ export default defineNuxtModule<NuxtI18nOptions>({
*/
prepareRuntime(ctx, nuxt)

enableVueI18nTypeGeneration(ctx, nuxt)

/**
* disable preloading/prefetching lazy loaded locales
*/
Expand Down Expand Up @@ -148,6 +151,12 @@ declare module '@nuxt/schema' {
}
interface NuxtOptions {
['i18n']: UserNuxtI18nOptions
/**
* @internal
*/
_i18n: {
locales: LocaleInfo[]
}
}
interface NuxtHooks extends ModuleHooks {}
interface PublicRuntimeConfig extends ModulePublicRuntimeConfig {}
Expand Down
213 changes: 213 additions & 0 deletions src/type-generation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import type { Nuxt } from '@nuxt/schema'
import { createJiti } from 'jiti'
import { addTypeTemplate, updateTemplates } from '@nuxt/kit'
import { deepCopy } from '@intlify/shared'
import { readFile } from './utils'
import { extname, resolve } from 'pathe'

import type { I18nOptions } from 'vue-i18n'
import type { NumberFormatOptions } from '@intlify/core'
import type { I18nNuxtContext } from './context'

// https://github.com/unjs/c12/blob/main/src/loader.ts#L26
const PARSERS = {
'.yaml': () => import('confbox/yaml').then(r => r.parseYAML),
'.yml': () => import('confbox/yaml').then(r => r.parseYAML),
'.jsonc': () => import('confbox/jsonc').then(r => r.parseJSONC),
'.json5': () => import('confbox/json5').then(r => r.parseJSON5),
'.toml': () => import('confbox/toml').then(r => r.parseTOML),
'.json': () => JSON.parse
} as const

const SUPPORTED_EXTENSIONS = [
// with jiti
'.js',
'.ts',
'.mjs',
'.cjs',
'.mts',
'.cts',
'.json',
// with confbox
'.jsonc',
'.json5',
'.yaml',
'.yml',
'.toml'
] as const

export function enableVueI18nTypeGeneration(
{ options: _options, localeInfo, vueI18nConfigPaths }: I18nNuxtContext,
nuxt: Nuxt
) {
const jiti = createJiti(nuxt.options.rootDir, {
interopDefault: true,
moduleCache: false,
fsCache: false,
requireCache: false,
extensions: [...SUPPORTED_EXTENSIONS]
})

const keyTranslationMap = new Map<string, string>()

function generateInterface(obj: Record<string, unknown>, indentLevel = 1) {
const indent = ' '.repeat(indentLevel)
let str = ''

for (const key in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue

keyTranslationMap.set(key, String(obj[key]))

if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
str += `${indent}${key}: {\n`
str += generateInterface(obj[key] as Record<string, unknown>, indentLevel + 1)
str += `${indent}};\n`
} else {
str += `${indent}/**\n`
str += `${indent} * ${JSON.stringify(obj[key])}\n`
str += `${indent} */\n`
let propertyType = Array.isArray(obj[key]) ? 'unknown[]' : typeof obj[key]
if (propertyType === 'function') {
propertyType = '() => string'
}
str += `${indent}${key}: ${propertyType};\n`
}
}
return str
}

nuxt.options._i18n = { locales: localeInfo }

addTypeTemplate({
filename: 'types/i18n-messages.d.ts',
getContents: async ({ nuxt }) => {
const messages = {}
const dateFormats = {}
const numberFormats = {}

// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
globalThis.defineI18nLocale = val => val

// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
globalThis.defineI18nConfig = val => val

// @ts-ignore
globalThis.useRuntimeConfig = () => nuxt.options.runtimeConfig

const fetch = await import('ofetch').then(r => r.ofetch)
globalThis.$fetch = fetch

async function loadTarget(absPath: string, args: unknown[] = []) {
try {
const configFileExt = extname(absPath) || ''
let result
const contents = await readFile(absPath)
if (configFileExt in PARSERS) {
const asyncLoader = await PARSERS[configFileExt as keyof typeof PARSERS]()
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
result = asyncLoader(contents)
} else {
result = await jiti.import(absPath)
}

if (result instanceof Function) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
return (await result.call(undefined, ...args)) as unknown
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return result
} catch (err) {
console.log(err)
return undefined
}
}

for (const config of vueI18nConfigPaths) {
const res = (await loadTarget(config.absolute)) as I18nOptions | undefined

if (res == null) continue
for (const v of Object.values(res.messages ?? [])) {
deepCopy(v, messages)
}

for (const v of Object.values(res.numberFormats ?? [])) {
deepCopy(v, numberFormats)
}

for (const v of Object.values(res.datetimeFormats ?? [])) {
deepCopy(v, dateFormats)
}
}

for (const l of nuxt.options._i18n?.locales ?? []) {
for (const f of l.files) {
const resolvedPath = resolve(nuxt.options.srcDir, f.path)
// console.log(resolvedPath, f.path)
// const contents = await readFile(resolvedPath)
// console.log(resolvedPath, await loadTarget(resolvedPath, [l.code]))
try {
deepCopy((await loadTarget(resolvedPath, [l.code])) ?? {}, messages)
} catch (err) {
console.log(err)
}
}

// we could only check one locale's files (serving as master/template) for speed
// break
}

function getNumberFormatType(v: NumberFormatOptions) {
if (v.style == null) return 'NumberFormatOptions'

/**
* Generating narrowed types may not be desired as types are only updated on save
*/
// if (v.style === 'currency') return 'CurrencyNumberFormatOptions'
// if (v.style === 'decimal' || v.style === 'percent') return 'CurrencyNumberFormatOptions'
return 'NumberFormatOptions'
}

return `// generated by @nuxtjs/i18n
import type { DateTimeFormatOptions, NumberFormatOptions, SpecificNumberFormatOptions, CurrencyNumberFormatOptions } from '@intlify/core'
interface GeneratedLocaleMessage {
${generateInterface(messages).trim()}
}
interface GeneratedDateTimeFormat {
${Object.keys(dateFormats)
.map(k => `${k}: DateTimeFormatOptions;`)
.join(`\n `)}
}
interface GeneratedNumberFormat {
${Object.entries(numberFormats)
.map(([k, v]) => `${k}: ${getNumberFormatType(v as NumberFormatOptions)};`)
.join(`\n `)}
}
declare module 'vue-i18n' {
export interface DefineLocaleMessage extends GeneratedLocaleMessage {}
export interface DefineDateTimeFormat extends GeneratedDateTimeFormat {}
export interface DefineNumberFormat extends GeneratedNumberFormat {}
}
declare module '@intlify/core' {
export interface DefineCoreLocaleMessage extends GeneratedLocaleMessage {}
}
export {}`
}
})

// watch locale files for changes and update template
nuxt.hook('builder:watch', async (_, path) => {
const paths = nuxt.options._i18n.locales.flatMap(x => x.files.map(f => f.path))
if (!paths.includes(path) && !vueI18nConfigPaths.some(x => x.absolute.includes(path))) return

await updateTemplates({ filter: template => template.filename === 'types/i18n-messages.d.ts' })
})
}

0 comments on commit 282df90

Please sign in to comment.