@verbaly/compiler 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/catalog.ts","../src/check.ts","../src/codegen.ts","../src/params.ts","../src/config.ts","../src/extract.ts","../src/analyze.ts","../src/key.ts","../src/registry.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport { parseArgs } from 'node:util';\r\nimport { loadCatalogs, writeCatalog } from './catalog';\r\nimport { check, formatCheckResult } from './check';\r\nimport { writeDts } from './codegen';\r\nimport { loadConfig } from './config';\r\nimport { extractProject, pruneCatalogs, syncCatalogs } from './extract';\r\n\r\nconst HELP = `verbaly — i18n compiler\r\n\r\nUsage:\r\n verbaly extract scan sources, update catalogs and types\r\n verbaly check verify translations are complete (CI)\r\n\r\nOptions:\r\n --root <path> project root (default: cwd)\r\n --dir <path> catalogs directory (default: locales)\r\n --source <locale> source locale (default: en)\r\n --locales <csv> extra locales (default: from catalog files)\r\n --prune drop keys no longer referenced (extract)\r\n\r\nConfig file: verbaly.config.{js,mjs,json} at root (flags win).\r\n`;\r\n\r\nasync function main(): Promise<void> {\r\n const { positionals, values } = parseArgs({\r\n allowPositionals: true,\r\n options: {\r\n root: { type: 'string' },\r\n dir: { type: 'string' },\r\n source: { type: 'string' },\r\n locales: { type: 'string' },\r\n prune: { type: 'boolean' },\r\n help: { type: 'boolean', short: 'h' },\r\n },\r\n });\r\n\r\n const command = positionals[0];\r\n if (values.help || !command) {\r\n console.log(HELP);\r\n process.exitCode = command ? 0 : 1;\r\n return;\r\n }\r\n\r\n const cfg = await loadConfig(values.root ?? process.cwd(), {\r\n dir: values.dir,\r\n sourceLocale: values.source,\r\n locales: values.locales?.split(','),\r\n });\r\n\r\n if (command === 'extract') {\r\n const registry = await extractProject(cfg);\r\n const catalogs = loadCatalogs(cfg);\r\n if (values.prune) {\r\n const removed = pruneCatalogs(cfg, catalogs, registry);\r\n for (const [locale, keys] of Object.entries(removed)) {\r\n console.log(` ${locale}: -${keys.length} pruned`);\r\n }\r\n }\r\n const { added } = syncCatalogs(cfg, catalogs, registry);\r\n for (const locale of cfg.locales) {\r\n writeCatalog(cfg, locale, catalogs[locale] ?? {});\r\n }\r\n writeDts(cfg, new Map(Object.entries(catalogs[cfg.sourceLocale] ?? {})));\r\n const total = registry.messages().size;\r\n console.log(`[verbaly] ${total} messages · locales: ${cfg.locales.join(', ')}`);\r\n for (const [locale, keys] of Object.entries(added)) {\r\n console.log(` ${locale}: +${keys.length}`);\r\n }\r\n return;\r\n }\r\n\r\n if (command === 'check') {\r\n const registry = await extractProject(cfg);\r\n const result = check(cfg, loadCatalogs(cfg), registry);\r\n if (result.ok) {\r\n console.log('[verbaly] all translations complete ✓');\r\n return;\r\n }\r\n console.error(`[verbaly] check failed\\n${formatCheckResult(result)}`);\r\n process.exitCode = 1;\r\n return;\r\n }\r\n\r\n console.error(`[verbaly] unknown command \"${command}\"\\n${HELP}`);\r\n process.exitCode = 1;\r\n}\r\n\r\nmain().catch((error: unknown) => {\r\n console.error('[verbaly]', error instanceof Error ? error.message : error);\r\n process.exitCode = 1;\r\n});\r\n","import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';\r\nimport { join } from 'node:path';\r\nimport type { ResolvedConfig } from './config';\r\n\r\nexport type Catalog = Record<string, string>;\r\nexport type Catalogs = Record<string, Catalog>;\r\n\r\nexport function catalogPath(cfg: ResolvedConfig, locale: string): string {\r\n return join(cfg.dir, `${locale}.json`);\r\n}\r\n\r\nexport function readCatalog(cfg: ResolvedConfig, locale: string): Catalog {\r\n try {\r\n return JSON.parse(readFileSync(catalogPath(cfg, locale), 'utf8')) as Catalog;\r\n } catch {\r\n return {};\r\n }\r\n}\r\n\r\nexport function loadCatalogs(cfg: ResolvedConfig): Catalogs {\r\n const catalogs: Catalogs = {};\r\n for (const locale of cfg.locales) catalogs[locale] = readCatalog(cfg, locale);\r\n return catalogs;\r\n}\r\n\r\nexport function serializeCatalog(catalog: Catalog): string {\r\n const sorted: Catalog = {};\r\n for (const key of Object.keys(catalog).sort()) {\r\n const value = catalog[key];\r\n if (value !== undefined) sorted[key] = value;\r\n }\r\n return JSON.stringify(sorted, null, 2) + '\\n';\r\n}\r\n\r\nexport function writeCatalog(cfg: ResolvedConfig, locale: string, catalog: Catalog): string {\r\n const serialized = serializeCatalog(catalog);\r\n mkdirSync(cfg.dir, { recursive: true });\r\n writeFileSync(catalogPath(cfg, locale), serialized);\r\n return serialized;\r\n}\r\n","import type { Catalogs } from './catalog';\r\nimport type { ResolvedConfig } from './config';\r\nimport type { MessageRegistry } from './registry';\r\n\r\nexport interface MissingEntry {\r\n locale: string;\r\n key: string;\r\n source?: string;\r\n}\r\n\r\nexport interface UnknownEntry {\r\n key: string;\r\n files: string[];\r\n}\r\n\r\nexport interface CheckResult {\r\n ok: boolean;\r\n missing: MissingEntry[];\r\n unknown: UnknownEntry[];\r\n}\r\n\r\nexport function check(cfg: ResolvedConfig, catalogs: Catalogs, registry: MessageRegistry): CheckResult {\r\n const source = catalogs[cfg.sourceLocale] ?? {};\r\n const extracted = registry.messages();\r\n\r\n const unknown: UnknownEntry[] = [];\r\n for (const [key, files] of registry.usedKeys()) {\r\n const known =\r\n extracted.has(key) || cfg.locales.some((locale) => catalogs[locale]?.[key] !== undefined);\r\n if (!known) unknown.push({ key, files });\r\n }\r\n\r\n const needed = new Set<string>([...extracted.keys(), ...Object.keys(source)]);\r\n\r\n const missing: MissingEntry[] = [];\r\n for (const key of extracted.keys()) {\r\n if (!source[key]) {\r\n missing.push({ locale: cfg.sourceLocale, key, source: extracted.get(key)?.message });\r\n }\r\n }\r\n for (const locale of cfg.locales) {\r\n if (locale === cfg.sourceLocale) continue;\r\n for (const key of needed) {\r\n if (!catalogs[locale]?.[key]) {\r\n missing.push({ locale, key, source: source[key] ?? extracted.get(key)?.message });\r\n }\r\n }\r\n }\r\n\r\n return { ok: missing.length === 0 && unknown.length === 0, missing, unknown };\r\n}\r\n\r\nexport function formatCheckResult(result: CheckResult): string {\r\n const lines: string[] = [];\r\n if (result.missing.length > 0) {\r\n lines.push('missing translations:');\r\n for (const entry of result.missing) {\r\n const hint = entry.source ? ` — \"${truncate(entry.source, 40)}\"` : '';\r\n lines.push(` [${entry.locale}] ${entry.key}${hint}`);\r\n }\r\n }\r\n if (result.unknown.length > 0) {\r\n lines.push('unknown keys (not in any catalog):');\r\n for (const entry of result.unknown) {\r\n lines.push(` ${entry.key} — used in ${entry.files.join(', ')}`);\r\n }\r\n }\r\n return lines.join('\\n');\r\n}\r\n\r\nfunction truncate(text: string, max: number): string {\r\n return text.length > max ? text.slice(0, max - 1) + '…' : text;\r\n}\r\n","import { writeFileSync } from 'node:fs';\r\nimport { join } from 'node:path';\r\nimport type { Catalog } from './catalog';\r\nimport type { ResolvedConfig } from './config';\r\nimport { collectParams, renderParamType } from './params';\r\n\r\nexport const VIRTUAL_ID = 'virtual:verbaly';\r\n\r\nexport function generateRuntimeModule(cfg: ResolvedConfig): string {\r\n const others = cfg.locales.filter((locale) => locale !== cfg.sourceLocale);\r\n const src = JSON.stringify(cfg.sourceLocale);\r\n const loaders = others\r\n .map((locale) => ` ${JSON.stringify(locale)}: () => import('${VIRTUAL_ID}/locale/${locale}'),`)\r\n .join('\\n');\r\n\r\n return `import { createVerbaly } from 'verbaly';\r\nimport source from '${VIRTUAL_ID}/locale/${cfg.sourceLocale}';\r\n\r\nconst loaders = {\r\n${loaders}\r\n};\r\nconst loaded = new Set([${src}]);\r\n\r\nconst v = createVerbaly({\r\n locale: ${src},\r\n fallback: ${src},\r\n messages: { [${src}]: source },\r\n});\r\n\r\nexport const verbaly = v;\r\nexport const t = v.t;\r\n\r\nexport function getLocale() {\r\n return v.locale;\r\n}\r\n\r\nexport function subscribe(listener) {\r\n return v.subscribe(listener);\r\n}\r\n\r\nexport async function setLocale(locale) {\r\n const target = loaders[locale] ? locale : locale.split('-')[0];\r\n if (!loaded.has(target) && loaders[target]) {\r\n const mod = await loaders[target]();\r\n v.addMessages(target, mod.default);\r\n loaded.add(target);\r\n }\r\n v.setLocale(locale);\r\n}\r\n`;\r\n}\r\n\r\nexport function generateLocaleModule(catalog: Catalog): string {\r\n return `export default ${JSON.stringify(catalog)};\\n`;\r\n}\r\n\r\nexport function generateDts(entries: Map<string, string>): string {\r\n const lines: string[] = [];\r\n for (const [key, message] of [...entries.entries()].sort(([a], [b]) => a.localeCompare(b))) {\r\n const params = collectParams(message);\r\n if (params.size === 0) {\r\n lines.push(` ${JSON.stringify(key)}: never;`);\r\n } else {\r\n const fields = [...params.entries()]\r\n .map(([name, types]) => `${JSON.stringify(name)}: ${renderParamType(types)}`)\r\n .join('; ');\r\n lines.push(` ${JSON.stringify(key)}: { ${fields} };`);\r\n }\r\n }\r\n\r\n return `// generated by verbaly — do not edit\r\ndeclare module 'virtual:verbaly' {\r\n export interface VerbalyMessages {\r\n${lines.join('\\n')}\r\n }\r\n export type VerbalyKey = keyof VerbalyMessages & string;\r\n\r\n export const verbaly: import('verbaly').Verbaly<VerbalyKey>;\r\n\r\n export function t<K extends VerbalyKey>(\r\n key: K,\r\n ...args: [VerbalyMessages[K]] extends [never] ? [] : [VerbalyMessages[K]]\r\n ): string;\r\n export function t(strings: TemplateStringsArray, ...values: unknown[]): string;\r\n\r\n export function setLocale(locale: string): Promise<void>;\r\n export function getLocale(): string;\r\n export function subscribe(listener: () => void): () => void;\r\n}\r\n\r\ndeclare module 'virtual:verbaly/locale/*' {\r\n const messages: Record<string, string>;\r\n export default messages;\r\n}\r\n`;\r\n}\r\n\r\nexport function writeDts(cfg: ResolvedConfig, entries: Map<string, string>): void {\r\n writeFileSync(join(cfg.root, 'verbaly.d.ts'), generateDts(entries));\r\n}\r\n","import { parse, type MessageNode } from 'verbaly';\r\n\r\nexport type ParamType = 'number' | 'string' | 'date' | 'unknown';\r\n\r\nconst PLURAL_KEYS = new Set(['zero', 'one', 'two', 'few', 'many']);\r\nconst NUMBER_FORMATS = new Set(['number', 'integer', 'percent', 'currency']);\r\nconst DATE_FORMATS = new Set(['date', 'time']);\r\n\r\nexport function collectParams(message: string): Map<string, Set<ParamType>> {\r\n const out = new Map<string, Set<ParamType>>();\r\n visit(parse(message), out);\r\n return out;\r\n}\r\n\r\nfunction visit(nodes: MessageNode[], out: Map<string, Set<ParamType>>): void {\r\n for (const node of nodes) {\r\n if (node.kind !== 'param') continue;\r\n const types = out.get(node.name) ?? new Set<ParamType>();\r\n out.set(node.name, types);\r\n\r\n if (node.variants) {\r\n for (const [key, body] of node.variants) {\r\n if (PLURAL_KEYS.has(key) || /^=\\d+$/.test(key)) types.add('number');\r\n else if (key !== 'other') types.add('string');\r\n visit(body, out);\r\n }\r\n if (types.size === 0) types.add('string');\r\n } else if (node.format && NUMBER_FORMATS.has(node.format)) {\r\n types.add('number');\r\n } else if (node.format && DATE_FORMATS.has(node.format)) {\r\n types.add('date');\r\n } else {\r\n types.add('unknown');\r\n }\r\n }\r\n}\r\n\r\nexport function renderParamType(types: Set<ParamType>): string {\r\n if (types.has('unknown') || types.size === 0) return 'unknown';\r\n const parts: string[] = [];\r\n if (types.has('number')) parts.push('number');\r\n if (types.has('string')) parts.push('string');\r\n if (types.has('date')) parts.push('Date | number | string');\r\n return parts.join(' | ');\r\n}\r\n","import { existsSync, readFileSync, readdirSync } from 'node:fs';\r\nimport { join, resolve } from 'node:path';\r\nimport { pathToFileURL } from 'node:url';\r\n\r\nexport interface VerbalyConfig {\r\n root?: string;\r\n dir?: string;\r\n sourceLocale?: string;\r\n locales?: string[];\r\n include?: string[];\r\n exclude?: string[];\r\n}\r\n\r\nexport interface ResolvedConfig {\r\n root: string;\r\n dir: string;\r\n sourceLocale: string;\r\n locales: string[];\r\n include: string[];\r\n exclude: string[];\r\n}\r\n\r\nexport function resolveConfig(config: VerbalyConfig = {}): ResolvedConfig {\r\n const root = resolve(config.root ?? process.cwd());\r\n const dir = resolve(root, config.dir ?? 'locales');\r\n const sourceLocale = config.sourceLocale ?? 'en';\r\n\r\n const locales = new Set<string>([sourceLocale, ...(config.locales ?? [])]);\r\n if (existsSync(dir)) {\r\n for (const file of readdirSync(dir)) {\r\n if (file.endsWith('.json')) locales.add(file.slice(0, -5));\r\n }\r\n }\r\n\r\n return {\r\n root,\r\n dir,\r\n sourceLocale,\r\n locales: [...locales],\r\n include: config.include ?? ['src/**/*.{js,jsx,ts,tsx,mjs,mts}'],\r\n exclude: config.exclude ?? ['**/node_modules/**', '**/dist/**'],\r\n };\r\n}\r\n\r\nexport async function loadConfigFile(root: string): Promise<VerbalyConfig> {\r\n for (const name of ['verbaly.config.js', 'verbaly.config.mjs']) {\r\n const path = join(root, name);\r\n if (existsSync(path)) {\r\n const mod = (await import(pathToFileURL(path).href)) as { default?: VerbalyConfig };\r\n return mod.default ?? {};\r\n }\r\n }\r\n const jsonPath = join(root, 'verbaly.config.json');\r\n if (existsSync(jsonPath)) {\r\n return JSON.parse(readFileSync(jsonPath, 'utf8')) as VerbalyConfig;\r\n }\r\n return {};\r\n}\r\n\r\n// file config + inline overrides\r\nexport async function loadConfig(\r\n root: string,\r\n overrides: VerbalyConfig = {},\r\n): Promise<ResolvedConfig> {\r\n const fileConfig = await loadConfigFile(root);\r\n return resolveConfig({ ...fileConfig, ...definedOnly(overrides), root });\r\n}\r\n\r\nfunction definedOnly(config: VerbalyConfig): VerbalyConfig {\r\n return Object.fromEntries(\r\n Object.entries(config).filter(([, value]) => value !== undefined),\r\n ) as VerbalyConfig;\r\n}\r\n","import { readFileSync } from 'node:fs';\r\nimport { glob } from 'tinyglobby';\r\nimport { analyze } from './analyze';\r\nimport type { Catalogs } from './catalog';\r\nimport type { ResolvedConfig } from './config';\r\nimport { MessageRegistry } from './registry';\r\n\r\nexport async function extractProject(cfg: ResolvedConfig): Promise<MessageRegistry> {\r\n const files = await glob(cfg.include, {\r\n cwd: cfg.root,\r\n ignore: cfg.exclude,\r\n absolute: true,\r\n });\r\n const registry = new MessageRegistry();\r\n for (const file of files) {\r\n registry.update(file, analyze(readFileSync(file, 'utf8'), file));\r\n }\r\n return registry;\r\n}\r\n\r\nexport interface SyncResult {\r\n added: Record<string, string[]>;\r\n}\r\n\r\n// fills source texts + '' placeholders\r\nexport function syncCatalogs(\r\n cfg: ResolvedConfig,\r\n catalogs: Catalogs,\r\n registry: MessageRegistry,\r\n): SyncResult {\r\n const added: Record<string, string[]> = {};\r\n const source = (catalogs[cfg.sourceLocale] ??= {});\r\n\r\n for (const [key, msg] of registry.messages()) {\r\n if (source[key] !== msg.message) {\r\n source[key] = msg.message;\r\n (added[cfg.sourceLocale] ??= []).push(key);\r\n }\r\n }\r\n\r\n const needed = Object.keys(source);\r\n for (const locale of cfg.locales) {\r\n if (locale === cfg.sourceLocale) continue;\r\n const catalog = (catalogs[locale] ??= {});\r\n for (const key of needed) {\r\n if (catalog[key] === undefined) {\r\n catalog[key] = '';\r\n (added[locale] ??= []).push(key);\r\n }\r\n }\r\n }\r\n return { added };\r\n}\r\n\r\n// drops keys no longer referenced\r\nexport function pruneCatalogs(\r\n cfg: ResolvedConfig,\r\n catalogs: Catalogs,\r\n registry: MessageRegistry,\r\n): Record<string, string[]> {\r\n const keep = new Set([...registry.messages().keys(), ...registry.usedKeys().keys()]);\r\n const removed: Record<string, string[]> = {};\r\n for (const locale of cfg.locales) {\r\n const catalog = catalogs[locale];\r\n if (!catalog) continue;\r\n for (const key of Object.keys(catalog)) {\r\n if (!keep.has(key)) {\r\n delete catalog[key];\r\n (removed[locale] ??= []).push(key);\r\n }\r\n }\r\n }\r\n return removed;\r\n}\r\n","import { parse } from '@babel/parser';\r\nimport { stableKey } from './key';\r\n\r\nexport interface TaggedParam {\r\n name: string;\r\n start: number;\r\n end: number;\r\n}\r\n\r\nexport interface TaggedMessage {\r\n key: string;\r\n message: string;\r\n params: TaggedParam[];\r\n start: number;\r\n end: number;\r\n tagStart: number;\r\n tagEnd: number;\r\n file: string;\r\n}\r\n\r\nexport interface UsedKey {\r\n key: string;\r\n file: string;\r\n}\r\n\r\nexport interface Analysis {\r\n tagged: TaggedMessage[];\r\n usedKeys: UsedKey[];\r\n}\r\n\r\ninterface AstNode {\r\n type: string;\r\n start: number;\r\n end: number;\r\n [key: string]: unknown;\r\n}\r\n\r\nconst SKIP_KEYS = new Set(['loc', 'leadingComments', 'trailingComments', 'innerComments', 'extra']);\r\n\r\nexport function analyze(code: string, file: string): Analysis {\r\n const jsx = /\\.[jt]sx$/.test(file);\r\n const ast = parse(code, {\r\n sourceType: 'module',\r\n errorRecovery: true,\r\n plugins: jsx ? ['typescript', 'jsx'] : ['typescript'],\r\n });\r\n\r\n const tagged: TaggedMessage[] = [];\r\n const usedKeys: UsedKey[] = [];\r\n\r\n walk(ast.program as unknown as AstNode, (node) => {\r\n if (node.type === 'TaggedTemplateExpression') {\r\n const tag = node.tag as AstNode;\r\n if (!isTReference(tag)) return;\r\n const quasi = node.quasi as AstNode;\r\n const message = buildMessage(code, quasi);\r\n if (!message) return;\r\n tagged.push({\r\n key: stableKey(message.text),\r\n message: message.text,\r\n params: message.params,\r\n start: node.start,\r\n end: node.end,\r\n tagStart: tag.start,\r\n tagEnd: tag.end,\r\n file,\r\n });\r\n } else if (node.type === 'CallExpression') {\r\n const callee = node.callee as AstNode;\r\n if (!isTReference(callee)) return;\r\n const args = node.arguments as AstNode[];\r\n const first = args[0];\r\n if (first?.type === 'StringLiteral') {\r\n usedKeys.push({ key: first.value as string, file });\r\n }\r\n }\r\n });\r\n\r\n return { tagged, usedKeys };\r\n}\r\n\r\nfunction isTReference(node: AstNode): boolean {\r\n if (node.type === 'Identifier') return node.name === 't';\r\n if (node.type === 'MemberExpression' && !node.computed) {\r\n const prop = node.property as AstNode;\r\n return prop.type === 'Identifier' && prop.name === 't';\r\n }\r\n return false;\r\n}\r\n\r\ninterface BuiltMessage {\r\n text: string;\r\n params: TaggedParam[];\r\n}\r\n\r\nfunction buildMessage(code: string, quasi: AstNode): BuiltMessage | undefined {\r\n const quasis = quasi.quasis as AstNode[];\r\n const expressions = quasi.expressions as AstNode[];\r\n const params: TaggedParam[] = [];\r\n const taken = new Map<string, string>();\r\n\r\n let text = escapeText(cookedValue(quasis[0]));\r\n\r\n for (let i = 0; i < expressions.length; i++) {\r\n const expr = expressions[i];\r\n if (!expr) return undefined;\r\n const source = code.slice(expr.start, expr.end);\r\n const name = uniqueName(deriveName(expr, i), source, taken);\r\n params.push({ name, start: expr.start, end: expr.end });\r\n text += `{${name}}` + escapeText(cookedValue(quasis[i + 1]));\r\n }\r\n return { text, params };\r\n}\r\n\r\nfunction cookedValue(element: AstNode | undefined): string {\r\n if (!element) return '';\r\n const value = element.value as { cooked?: string; raw: string };\r\n return value.cooked ?? value.raw;\r\n}\r\n\r\n// literal braces → escaped\r\nfunction escapeText(text: string): string {\r\n return text.replace(/[{}]/g, (m) => m + m);\r\n}\r\n\r\nfunction deriveName(expr: AstNode, index: number): string {\r\n if (expr.type === 'Identifier') return expr.name as string;\r\n if (expr.type === 'MemberExpression' && !expr.computed) {\r\n const prop = expr.property as AstNode;\r\n if (prop.type === 'Identifier') return prop.name as string;\r\n }\r\n return `_${index}`;\r\n}\r\n\r\nfunction uniqueName(base: string, source: string, taken: Map<string, string>): string {\r\n let name = base;\r\n let n = 2;\r\n while (taken.has(name) && taken.get(name) !== source) {\r\n name = `${base}${n}`;\r\n n += 1;\r\n }\r\n taken.set(name, source);\r\n return name;\r\n}\r\n\r\nfunction walk(node: AstNode, visit: (node: AstNode) => void): void {\r\n visit(node);\r\n for (const [key, value] of Object.entries(node)) {\r\n if (SKIP_KEYS.has(key)) continue;\r\n if (Array.isArray(value)) {\r\n for (const item of value) {\r\n if (isNode(item)) walk(item, visit);\r\n }\r\n } else if (isNode(value)) {\r\n walk(value, visit);\r\n }\r\n }\r\n}\r\n\r\nfunction isNode(value: unknown): value is AstNode {\r\n return (\r\n typeof value === 'object' &&\r\n value !== null &&\r\n typeof (value as { type?: unknown }).type === 'string'\r\n );\r\n}\r\n","import { createHash } from 'node:crypto';\r\n\r\nexport function stableKey(message: string): string {\r\n return createHash('sha256').update(message).digest('base64url').slice(0, 8);\r\n}\r\n","import type { Analysis, TaggedMessage } from './analyze';\r\n\r\nexport class MessageRegistry {\r\n private files = new Map<string, Analysis>();\r\n\r\n update(file: string, analysis: Analysis): void {\r\n this.files.set(file, analysis);\r\n }\r\n\r\n remove(file: string): void {\r\n this.files.delete(file);\r\n }\r\n\r\n messages(): Map<string, TaggedMessage> {\r\n const out = new Map<string, TaggedMessage>();\r\n for (const analysis of this.files.values()) {\r\n for (const msg of analysis.tagged) {\r\n if (!out.has(msg.key)) out.set(msg.key, msg);\r\n }\r\n }\r\n return out;\r\n }\r\n\r\n usedKeys(): Map<string, string[]> {\r\n const out = new Map<string, string[]>();\r\n for (const analysis of this.files.values()) {\r\n for (const used of analysis.usedKeys) {\r\n const files = out.get(used.key) ?? [];\r\n if (!files.includes(used.file)) files.push(used.file);\r\n out.set(used.key, files);\r\n }\r\n }\r\n return out;\r\n }\r\n}\r\n"],"mappings":"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","names":["writeFileSync","join","writeFileSync","join","readFileSync","join","readFileSync","parse","parse","visit","readFileSync"]}
@@ -0,0 +1,106 @@
1
+ import MagicString from 'magic-string';
2
+
3
+ interface TaggedParam {
4
+ name: string;
5
+ start: number;
6
+ end: number;
7
+ }
8
+ interface TaggedMessage {
9
+ key: string;
10
+ message: string;
11
+ params: TaggedParam[];
12
+ start: number;
13
+ end: number;
14
+ tagStart: number;
15
+ tagEnd: number;
16
+ file: string;
17
+ }
18
+ interface UsedKey {
19
+ key: string;
20
+ file: string;
21
+ }
22
+ interface Analysis {
23
+ tagged: TaggedMessage[];
24
+ usedKeys: UsedKey[];
25
+ }
26
+ declare function analyze(code: string, file: string): Analysis;
27
+
28
+ interface VerbalyConfig {
29
+ root?: string;
30
+ dir?: string;
31
+ sourceLocale?: string;
32
+ locales?: string[];
33
+ include?: string[];
34
+ exclude?: string[];
35
+ }
36
+ interface ResolvedConfig {
37
+ root: string;
38
+ dir: string;
39
+ sourceLocale: string;
40
+ locales: string[];
41
+ include: string[];
42
+ exclude: string[];
43
+ }
44
+ declare function resolveConfig(config?: VerbalyConfig): ResolvedConfig;
45
+ declare function loadConfigFile(root: string): Promise<VerbalyConfig>;
46
+ declare function loadConfig(root: string, overrides?: VerbalyConfig): Promise<ResolvedConfig>;
47
+
48
+ type Catalog = Record<string, string>;
49
+ type Catalogs = Record<string, Catalog>;
50
+ declare function catalogPath(cfg: ResolvedConfig, locale: string): string;
51
+ declare function readCatalog(cfg: ResolvedConfig, locale: string): Catalog;
52
+ declare function loadCatalogs(cfg: ResolvedConfig): Catalogs;
53
+ declare function serializeCatalog(catalog: Catalog): string;
54
+ declare function writeCatalog(cfg: ResolvedConfig, locale: string, catalog: Catalog): string;
55
+
56
+ declare class MessageRegistry {
57
+ private files;
58
+ update(file: string, analysis: Analysis): void;
59
+ remove(file: string): void;
60
+ messages(): Map<string, TaggedMessage>;
61
+ usedKeys(): Map<string, string[]>;
62
+ }
63
+
64
+ interface MissingEntry {
65
+ locale: string;
66
+ key: string;
67
+ source?: string;
68
+ }
69
+ interface UnknownEntry {
70
+ key: string;
71
+ files: string[];
72
+ }
73
+ interface CheckResult {
74
+ ok: boolean;
75
+ missing: MissingEntry[];
76
+ unknown: UnknownEntry[];
77
+ }
78
+ declare function check(cfg: ResolvedConfig, catalogs: Catalogs, registry: MessageRegistry): CheckResult;
79
+ declare function formatCheckResult(result: CheckResult): string;
80
+
81
+ declare const VIRTUAL_ID = "virtual:verbaly";
82
+ declare function generateRuntimeModule(cfg: ResolvedConfig): string;
83
+ declare function generateLocaleModule(catalog: Catalog): string;
84
+ declare function generateDts(entries: Map<string, string>): string;
85
+ declare function writeDts(cfg: ResolvedConfig, entries: Map<string, string>): void;
86
+
87
+ declare function extractProject(cfg: ResolvedConfig): Promise<MessageRegistry>;
88
+ interface SyncResult {
89
+ added: Record<string, string[]>;
90
+ }
91
+ declare function syncCatalogs(cfg: ResolvedConfig, catalogs: Catalogs, registry: MessageRegistry): SyncResult;
92
+ declare function pruneCatalogs(cfg: ResolvedConfig, catalogs: Catalogs, registry: MessageRegistry): Record<string, string[]>;
93
+
94
+ declare function stableKey(message: string): string;
95
+
96
+ type ParamType = 'number' | 'string' | 'date' | 'unknown';
97
+ declare function collectParams(message: string): Map<string, Set<ParamType>>;
98
+ declare function renderParamType(types: Set<ParamType>): string;
99
+
100
+ interface TransformResult {
101
+ code: string;
102
+ map: ReturnType<MagicString['generateMap']>;
103
+ }
104
+ declare function transformCode(code: string, file: string, analysis?: Analysis): TransformResult | null;
105
+
106
+ export { type Analysis, type Catalog, type Catalogs, type CheckResult, MessageRegistry, type MissingEntry, type ParamType, type ResolvedConfig, type SyncResult, type TaggedMessage, type TaggedParam, type TransformResult, type UnknownEntry, type UsedKey, VIRTUAL_ID, type VerbalyConfig, analyze, catalogPath, check, collectParams, extractProject, formatCheckResult, generateDts, generateLocaleModule, generateRuntimeModule, loadCatalogs, loadConfig, loadConfigFile, pruneCatalogs, readCatalog, renderParamType, resolveConfig, serializeCatalog, stableKey, syncCatalogs, transformCode, writeCatalog, writeDts };
package/dist/index.js ADDED
@@ -0,0 +1,502 @@
1
+ // src/analyze.ts
2
+ import { parse } from "@babel/parser";
3
+
4
+ // src/key.ts
5
+ import { createHash } from "crypto";
6
+ function stableKey(message) {
7
+ return createHash("sha256").update(message).digest("base64url").slice(0, 8);
8
+ }
9
+
10
+ // src/analyze.ts
11
+ var SKIP_KEYS = /* @__PURE__ */ new Set(["loc", "leadingComments", "trailingComments", "innerComments", "extra"]);
12
+ function analyze(code, file) {
13
+ const jsx = /\.[jt]sx$/.test(file);
14
+ const ast = parse(code, {
15
+ sourceType: "module",
16
+ errorRecovery: true,
17
+ plugins: jsx ? ["typescript", "jsx"] : ["typescript"]
18
+ });
19
+ const tagged = [];
20
+ const usedKeys = [];
21
+ walk(ast.program, (node) => {
22
+ if (node.type === "TaggedTemplateExpression") {
23
+ const tag = node.tag;
24
+ if (!isTReference(tag)) return;
25
+ const quasi = node.quasi;
26
+ const message = buildMessage(code, quasi);
27
+ if (!message) return;
28
+ tagged.push({
29
+ key: stableKey(message.text),
30
+ message: message.text,
31
+ params: message.params,
32
+ start: node.start,
33
+ end: node.end,
34
+ tagStart: tag.start,
35
+ tagEnd: tag.end,
36
+ file
37
+ });
38
+ } else if (node.type === "CallExpression") {
39
+ const callee = node.callee;
40
+ if (!isTReference(callee)) return;
41
+ const args = node.arguments;
42
+ const first = args[0];
43
+ if (first?.type === "StringLiteral") {
44
+ usedKeys.push({ key: first.value, file });
45
+ }
46
+ }
47
+ });
48
+ return { tagged, usedKeys };
49
+ }
50
+ function isTReference(node) {
51
+ if (node.type === "Identifier") return node.name === "t";
52
+ if (node.type === "MemberExpression" && !node.computed) {
53
+ const prop = node.property;
54
+ return prop.type === "Identifier" && prop.name === "t";
55
+ }
56
+ return false;
57
+ }
58
+ function buildMessage(code, quasi) {
59
+ const quasis = quasi.quasis;
60
+ const expressions = quasi.expressions;
61
+ const params = [];
62
+ const taken = /* @__PURE__ */ new Map();
63
+ let text = escapeText(cookedValue(quasis[0]));
64
+ for (let i = 0; i < expressions.length; i++) {
65
+ const expr = expressions[i];
66
+ if (!expr) return void 0;
67
+ const source = code.slice(expr.start, expr.end);
68
+ const name = uniqueName(deriveName(expr, i), source, taken);
69
+ params.push({ name, start: expr.start, end: expr.end });
70
+ text += `{${name}}` + escapeText(cookedValue(quasis[i + 1]));
71
+ }
72
+ return { text, params };
73
+ }
74
+ function cookedValue(element) {
75
+ if (!element) return "";
76
+ const value = element.value;
77
+ return value.cooked ?? value.raw;
78
+ }
79
+ function escapeText(text) {
80
+ return text.replace(/[{}]/g, (m) => m + m);
81
+ }
82
+ function deriveName(expr, index) {
83
+ if (expr.type === "Identifier") return expr.name;
84
+ if (expr.type === "MemberExpression" && !expr.computed) {
85
+ const prop = expr.property;
86
+ if (prop.type === "Identifier") return prop.name;
87
+ }
88
+ return `_${index}`;
89
+ }
90
+ function uniqueName(base, source, taken) {
91
+ let name = base;
92
+ let n = 2;
93
+ while (taken.has(name) && taken.get(name) !== source) {
94
+ name = `${base}${n}`;
95
+ n += 1;
96
+ }
97
+ taken.set(name, source);
98
+ return name;
99
+ }
100
+ function walk(node, visit2) {
101
+ visit2(node);
102
+ for (const [key, value] of Object.entries(node)) {
103
+ if (SKIP_KEYS.has(key)) continue;
104
+ if (Array.isArray(value)) {
105
+ for (const item of value) {
106
+ if (isNode(item)) walk(item, visit2);
107
+ }
108
+ } else if (isNode(value)) {
109
+ walk(value, visit2);
110
+ }
111
+ }
112
+ }
113
+ function isNode(value) {
114
+ return typeof value === "object" && value !== null && typeof value.type === "string";
115
+ }
116
+
117
+ // src/catalog.ts
118
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
119
+ import { join } from "path";
120
+ function catalogPath(cfg, locale) {
121
+ return join(cfg.dir, `${locale}.json`);
122
+ }
123
+ function readCatalog(cfg, locale) {
124
+ try {
125
+ return JSON.parse(readFileSync(catalogPath(cfg, locale), "utf8"));
126
+ } catch {
127
+ return {};
128
+ }
129
+ }
130
+ function loadCatalogs(cfg) {
131
+ const catalogs = {};
132
+ for (const locale of cfg.locales) catalogs[locale] = readCatalog(cfg, locale);
133
+ return catalogs;
134
+ }
135
+ function serializeCatalog(catalog) {
136
+ const sorted = {};
137
+ for (const key of Object.keys(catalog).sort()) {
138
+ const value = catalog[key];
139
+ if (value !== void 0) sorted[key] = value;
140
+ }
141
+ return JSON.stringify(sorted, null, 2) + "\n";
142
+ }
143
+ function writeCatalog(cfg, locale, catalog) {
144
+ const serialized = serializeCatalog(catalog);
145
+ mkdirSync(cfg.dir, { recursive: true });
146
+ writeFileSync(catalogPath(cfg, locale), serialized);
147
+ return serialized;
148
+ }
149
+
150
+ // src/check.ts
151
+ function check(cfg, catalogs, registry) {
152
+ const source = catalogs[cfg.sourceLocale] ?? {};
153
+ const extracted = registry.messages();
154
+ const unknown = [];
155
+ for (const [key, files] of registry.usedKeys()) {
156
+ const known = extracted.has(key) || cfg.locales.some((locale) => catalogs[locale]?.[key] !== void 0);
157
+ if (!known) unknown.push({ key, files });
158
+ }
159
+ const needed = /* @__PURE__ */ new Set([...extracted.keys(), ...Object.keys(source)]);
160
+ const missing = [];
161
+ for (const key of extracted.keys()) {
162
+ if (!source[key]) {
163
+ missing.push({ locale: cfg.sourceLocale, key, source: extracted.get(key)?.message });
164
+ }
165
+ }
166
+ for (const locale of cfg.locales) {
167
+ if (locale === cfg.sourceLocale) continue;
168
+ for (const key of needed) {
169
+ if (!catalogs[locale]?.[key]) {
170
+ missing.push({ locale, key, source: source[key] ?? extracted.get(key)?.message });
171
+ }
172
+ }
173
+ }
174
+ return { ok: missing.length === 0 && unknown.length === 0, missing, unknown };
175
+ }
176
+ function formatCheckResult(result) {
177
+ const lines = [];
178
+ if (result.missing.length > 0) {
179
+ lines.push("missing translations:");
180
+ for (const entry of result.missing) {
181
+ const hint = entry.source ? ` \u2014 "${truncate(entry.source, 40)}"` : "";
182
+ lines.push(` [${entry.locale}] ${entry.key}${hint}`);
183
+ }
184
+ }
185
+ if (result.unknown.length > 0) {
186
+ lines.push("unknown keys (not in any catalog):");
187
+ for (const entry of result.unknown) {
188
+ lines.push(` ${entry.key} \u2014 used in ${entry.files.join(", ")}`);
189
+ }
190
+ }
191
+ return lines.join("\n");
192
+ }
193
+ function truncate(text, max) {
194
+ return text.length > max ? text.slice(0, max - 1) + "\u2026" : text;
195
+ }
196
+
197
+ // src/codegen.ts
198
+ import { writeFileSync as writeFileSync2 } from "fs";
199
+ import { join as join2 } from "path";
200
+
201
+ // src/params.ts
202
+ import { parse as parse2 } from "verbaly";
203
+ var PLURAL_KEYS = /* @__PURE__ */ new Set(["zero", "one", "two", "few", "many"]);
204
+ var NUMBER_FORMATS = /* @__PURE__ */ new Set(["number", "integer", "percent", "currency"]);
205
+ var DATE_FORMATS = /* @__PURE__ */ new Set(["date", "time"]);
206
+ function collectParams(message) {
207
+ const out = /* @__PURE__ */ new Map();
208
+ visit(parse2(message), out);
209
+ return out;
210
+ }
211
+ function visit(nodes, out) {
212
+ for (const node of nodes) {
213
+ if (node.kind !== "param") continue;
214
+ const types = out.get(node.name) ?? /* @__PURE__ */ new Set();
215
+ out.set(node.name, types);
216
+ if (node.variants) {
217
+ for (const [key, body] of node.variants) {
218
+ if (PLURAL_KEYS.has(key) || /^=\d+$/.test(key)) types.add("number");
219
+ else if (key !== "other") types.add("string");
220
+ visit(body, out);
221
+ }
222
+ if (types.size === 0) types.add("string");
223
+ } else if (node.format && NUMBER_FORMATS.has(node.format)) {
224
+ types.add("number");
225
+ } else if (node.format && DATE_FORMATS.has(node.format)) {
226
+ types.add("date");
227
+ } else {
228
+ types.add("unknown");
229
+ }
230
+ }
231
+ }
232
+ function renderParamType(types) {
233
+ if (types.has("unknown") || types.size === 0) return "unknown";
234
+ const parts = [];
235
+ if (types.has("number")) parts.push("number");
236
+ if (types.has("string")) parts.push("string");
237
+ if (types.has("date")) parts.push("Date | number | string");
238
+ return parts.join(" | ");
239
+ }
240
+
241
+ // src/codegen.ts
242
+ var VIRTUAL_ID = "virtual:verbaly";
243
+ function generateRuntimeModule(cfg) {
244
+ const others = cfg.locales.filter((locale) => locale !== cfg.sourceLocale);
245
+ const src = JSON.stringify(cfg.sourceLocale);
246
+ const loaders = others.map((locale) => ` ${JSON.stringify(locale)}: () => import('${VIRTUAL_ID}/locale/${locale}'),`).join("\n");
247
+ return `import { createVerbaly } from 'verbaly';
248
+ import source from '${VIRTUAL_ID}/locale/${cfg.sourceLocale}';
249
+
250
+ const loaders = {
251
+ ${loaders}
252
+ };
253
+ const loaded = new Set([${src}]);
254
+
255
+ const v = createVerbaly({
256
+ locale: ${src},
257
+ fallback: ${src},
258
+ messages: { [${src}]: source },
259
+ });
260
+
261
+ export const verbaly = v;
262
+ export const t = v.t;
263
+
264
+ export function getLocale() {
265
+ return v.locale;
266
+ }
267
+
268
+ export function subscribe(listener) {
269
+ return v.subscribe(listener);
270
+ }
271
+
272
+ export async function setLocale(locale) {
273
+ const target = loaders[locale] ? locale : locale.split('-')[0];
274
+ if (!loaded.has(target) && loaders[target]) {
275
+ const mod = await loaders[target]();
276
+ v.addMessages(target, mod.default);
277
+ loaded.add(target);
278
+ }
279
+ v.setLocale(locale);
280
+ }
281
+ `;
282
+ }
283
+ function generateLocaleModule(catalog) {
284
+ return `export default ${JSON.stringify(catalog)};
285
+ `;
286
+ }
287
+ function generateDts(entries) {
288
+ const lines = [];
289
+ for (const [key, message] of [...entries.entries()].sort(([a], [b]) => a.localeCompare(b))) {
290
+ const params = collectParams(message);
291
+ if (params.size === 0) {
292
+ lines.push(` ${JSON.stringify(key)}: never;`);
293
+ } else {
294
+ const fields = [...params.entries()].map(([name, types]) => `${JSON.stringify(name)}: ${renderParamType(types)}`).join("; ");
295
+ lines.push(` ${JSON.stringify(key)}: { ${fields} };`);
296
+ }
297
+ }
298
+ return `// generated by verbaly \u2014 do not edit
299
+ declare module 'virtual:verbaly' {
300
+ export interface VerbalyMessages {
301
+ ${lines.join("\n")}
302
+ }
303
+ export type VerbalyKey = keyof VerbalyMessages & string;
304
+
305
+ export const verbaly: import('verbaly').Verbaly<VerbalyKey>;
306
+
307
+ export function t<K extends VerbalyKey>(
308
+ key: K,
309
+ ...args: [VerbalyMessages[K]] extends [never] ? [] : [VerbalyMessages[K]]
310
+ ): string;
311
+ export function t(strings: TemplateStringsArray, ...values: unknown[]): string;
312
+
313
+ export function setLocale(locale: string): Promise<void>;
314
+ export function getLocale(): string;
315
+ export function subscribe(listener: () => void): () => void;
316
+ }
317
+
318
+ declare module 'virtual:verbaly/locale/*' {
319
+ const messages: Record<string, string>;
320
+ export default messages;
321
+ }
322
+ `;
323
+ }
324
+ function writeDts(cfg, entries) {
325
+ writeFileSync2(join2(cfg.root, "verbaly.d.ts"), generateDts(entries));
326
+ }
327
+
328
+ // src/config.ts
329
+ import { existsSync, readFileSync as readFileSync2, readdirSync } from "fs";
330
+ import { join as join3, resolve } from "path";
331
+ import { pathToFileURL } from "url";
332
+ function resolveConfig(config = {}) {
333
+ const root = resolve(config.root ?? process.cwd());
334
+ const dir = resolve(root, config.dir ?? "locales");
335
+ const sourceLocale = config.sourceLocale ?? "en";
336
+ const locales = /* @__PURE__ */ new Set([sourceLocale, ...config.locales ?? []]);
337
+ if (existsSync(dir)) {
338
+ for (const file of readdirSync(dir)) {
339
+ if (file.endsWith(".json")) locales.add(file.slice(0, -5));
340
+ }
341
+ }
342
+ return {
343
+ root,
344
+ dir,
345
+ sourceLocale,
346
+ locales: [...locales],
347
+ include: config.include ?? ["src/**/*.{js,jsx,ts,tsx,mjs,mts}"],
348
+ exclude: config.exclude ?? ["**/node_modules/**", "**/dist/**"]
349
+ };
350
+ }
351
+ async function loadConfigFile(root) {
352
+ for (const name of ["verbaly.config.js", "verbaly.config.mjs"]) {
353
+ const path = join3(root, name);
354
+ if (existsSync(path)) {
355
+ const mod = await import(pathToFileURL(path).href);
356
+ return mod.default ?? {};
357
+ }
358
+ }
359
+ const jsonPath = join3(root, "verbaly.config.json");
360
+ if (existsSync(jsonPath)) {
361
+ return JSON.parse(readFileSync2(jsonPath, "utf8"));
362
+ }
363
+ return {};
364
+ }
365
+ async function loadConfig(root, overrides = {}) {
366
+ const fileConfig = await loadConfigFile(root);
367
+ return resolveConfig({ ...fileConfig, ...definedOnly(overrides), root });
368
+ }
369
+ function definedOnly(config) {
370
+ return Object.fromEntries(
371
+ Object.entries(config).filter(([, value]) => value !== void 0)
372
+ );
373
+ }
374
+
375
+ // src/extract.ts
376
+ import { readFileSync as readFileSync3 } from "fs";
377
+ import { glob } from "tinyglobby";
378
+
379
+ // src/registry.ts
380
+ var MessageRegistry = class {
381
+ files = /* @__PURE__ */ new Map();
382
+ update(file, analysis) {
383
+ this.files.set(file, analysis);
384
+ }
385
+ remove(file) {
386
+ this.files.delete(file);
387
+ }
388
+ messages() {
389
+ const out = /* @__PURE__ */ new Map();
390
+ for (const analysis of this.files.values()) {
391
+ for (const msg of analysis.tagged) {
392
+ if (!out.has(msg.key)) out.set(msg.key, msg);
393
+ }
394
+ }
395
+ return out;
396
+ }
397
+ usedKeys() {
398
+ const out = /* @__PURE__ */ new Map();
399
+ for (const analysis of this.files.values()) {
400
+ for (const used of analysis.usedKeys) {
401
+ const files = out.get(used.key) ?? [];
402
+ if (!files.includes(used.file)) files.push(used.file);
403
+ out.set(used.key, files);
404
+ }
405
+ }
406
+ return out;
407
+ }
408
+ };
409
+
410
+ // src/extract.ts
411
+ async function extractProject(cfg) {
412
+ const files = await glob(cfg.include, {
413
+ cwd: cfg.root,
414
+ ignore: cfg.exclude,
415
+ absolute: true
416
+ });
417
+ const registry = new MessageRegistry();
418
+ for (const file of files) {
419
+ registry.update(file, analyze(readFileSync3(file, "utf8"), file));
420
+ }
421
+ return registry;
422
+ }
423
+ function syncCatalogs(cfg, catalogs, registry) {
424
+ const added = {};
425
+ const source = catalogs[cfg.sourceLocale] ??= {};
426
+ for (const [key, msg] of registry.messages()) {
427
+ if (source[key] !== msg.message) {
428
+ source[key] = msg.message;
429
+ (added[cfg.sourceLocale] ??= []).push(key);
430
+ }
431
+ }
432
+ const needed = Object.keys(source);
433
+ for (const locale of cfg.locales) {
434
+ if (locale === cfg.sourceLocale) continue;
435
+ const catalog = catalogs[locale] ??= {};
436
+ for (const key of needed) {
437
+ if (catalog[key] === void 0) {
438
+ catalog[key] = "";
439
+ (added[locale] ??= []).push(key);
440
+ }
441
+ }
442
+ }
443
+ return { added };
444
+ }
445
+ function pruneCatalogs(cfg, catalogs, registry) {
446
+ const keep = /* @__PURE__ */ new Set([...registry.messages().keys(), ...registry.usedKeys().keys()]);
447
+ const removed = {};
448
+ for (const locale of cfg.locales) {
449
+ const catalog = catalogs[locale];
450
+ if (!catalog) continue;
451
+ for (const key of Object.keys(catalog)) {
452
+ if (!keep.has(key)) {
453
+ delete catalog[key];
454
+ (removed[locale] ??= []).push(key);
455
+ }
456
+ }
457
+ }
458
+ return removed;
459
+ }
460
+
461
+ // src/transform.ts
462
+ import MagicString from "magic-string";
463
+ function transformCode(code, file, analysis) {
464
+ const { tagged } = analysis ?? analyze(code, file);
465
+ if (tagged.length === 0) return null;
466
+ const s = new MagicString(code);
467
+ for (const msg of tagged) {
468
+ const tagSource = code.slice(msg.tagStart, msg.tagEnd);
469
+ const seen = /* @__PURE__ */ new Set();
470
+ const entries = msg.params.filter((p) => !seen.has(p.name) && seen.add(p.name));
471
+ const args = entries.length ? `, { ${entries.map((p) => `${JSON.stringify(p.name)}: ${code.slice(p.start, p.end)}`).join(", ")} }` : "";
472
+ s.overwrite(msg.start, msg.end, `${tagSource}(${JSON.stringify(msg.key)}${args})`);
473
+ }
474
+ return { code: s.toString(), map: s.generateMap({ hires: true }) };
475
+ }
476
+ export {
477
+ MessageRegistry,
478
+ VIRTUAL_ID,
479
+ analyze,
480
+ catalogPath,
481
+ check,
482
+ collectParams,
483
+ extractProject,
484
+ formatCheckResult,
485
+ generateDts,
486
+ generateLocaleModule,
487
+ generateRuntimeModule,
488
+ loadCatalogs,
489
+ loadConfig,
490
+ loadConfigFile,
491
+ pruneCatalogs,
492
+ readCatalog,
493
+ renderParamType,
494
+ resolveConfig,
495
+ serializeCatalog,
496
+ stableKey,
497
+ syncCatalogs,
498
+ transformCode,
499
+ writeCatalog,
500
+ writeDts
501
+ };
502
+ //# sourceMappingURL=index.js.map