@dialexjs/cli 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +86 -3
- package/dist/api.d.mts +229 -0
- package/dist/api.d.mts.map +1 -0
- package/dist/api.mjs +2 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1088 -0
- package/dist/index.mjs.map +1 -0
- package/dist/project-D4PmfqxM.mjs +883 -0
- package/dist/project-D4PmfqxM.mjs.map +1 -0
- package/dist/translate/index.d.mts +92 -0
- package/dist/translate/index.d.mts.map +1 -0
- package/dist/translate/index.mjs +289 -0
- package/dist/translate/index.mjs.map +1 -0
- package/package.json +50 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-D4PmfqxM.mjs","names":["unwrapNode","propertyKeyName","isObjectNode","findProperty"],"sources":["../src/utils/dictionary-edit.ts","../src/utils/icu-check.ts","../src/analysis.ts","../src/utils/static-config.ts","../src/utils/project.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport { loadFile, writeFile, generateCode, parseExpression, parseModule } from \"magicast\";\n\n/**\n * AST-level helpers for reading and editing `.content.ts` dictionary files.\n *\n * Magicast's proxy layer cannot represent every value (for example template-literal\n * functions), so these helpers work on the underlying AST nodes and use Magicast only\n * to parse and print, which preserves the rest of the file's formatting.\n *\n * Supported shapes:\n * - `export default defineDictionary(\"name\", { en: {...}, tr: {...} })`\n * - `export default defineDictionary({ name: \"name\", dictionary: { ... } })`\n * - `export default { name: \"name\", dictionary: { ... } }`\n */\n\ntype Node = { type: string; [key: string]: any };\n\nexport const TODO_PREFIX = \"[TODO] \";\n\nconst IDENTIFIER = /^[A-Za-z_$][\\w$]*$/;\n\nexport interface DictionaryFile {\n file: string;\n /** Dictionary name from `defineDictionary(\"name\", ...)` or the `name` property, if literal. */\n name?: string;\n mod: Awaited<ReturnType<typeof loadFile>>;\n /** The object whose keys are locales. */\n localesObj: Node;\n /** Whether the file ended with a newline when loaded; restored on save. */\n endsWithNewline: boolean;\n /**\n * `false` when part of the dictionary cannot be read statically (spreads, computed keys, or a\n * locale whose content is not an object literal). Leaf lists and parity checks are unreliable\n * for such files.\n */\n complete: boolean;\n}\n\n/** A zero-based line/column range, as used by editors. */\nexport interface SourceRange {\n start: { line: number; column: number };\n end: { line: number; column: number };\n}\n\nexport type LeafKind = \"string\" | \"function\" | \"other\";\n\nexport interface Leaf {\n path: string[];\n kind: LeafKind;\n /** Present when `kind` is `\"string\"`. */\n value?: string;\n}\n\nfunction unwrap(node: Node): Node {\n while (\n node &&\n (node.type === \"TSAsExpression\" ||\n node.type === \"TSSatisfiesExpression\" ||\n node.type === \"ParenthesizedExpression\")\n ) {\n node = node.expression;\n }\n return node;\n}\n\nconst isObject = (node: Node) => node?.type === \"ObjectExpression\";\n\nfunction keyName(prop: Node): string | undefined {\n const key = prop?.key;\n if (!key || prop.computed) return undefined;\n if (key.type === \"Identifier\") return key.name;\n if (key.type === \"StringLiteral\" || (key.type === \"Literal\" && typeof key.value === \"string\")) {\n return key.value;\n }\n if (key.type === \"NumericLiteral\") return String(key.value);\n return undefined;\n}\n\nconst isPropLike = (p: Node) =>\n p?.type === \"ObjectProperty\" || p?.type === \"Property\" || p?.type === \"ObjectMethod\";\n\nfunction findProp(obj: Node, key: string): Node | undefined {\n return obj.properties.find((p: Node) => isPropLike(p) && keyName(p) === key);\n}\n\nfunction stringValue(node: Node): string | undefined {\n node = unwrap(node);\n if (node?.type === \"StringLiteral\") return node.value;\n if (node?.type === \"Literal\" && typeof node.value === \"string\") return node.value;\n if (node?.type === \"TemplateLiteral\" && node.expressions.length === 0) {\n return node.quasis[0]?.value?.cooked ?? undefined;\n }\n return undefined;\n}\n\nfunction leafKind(node: Node): LeafKind {\n node = unwrap(node);\n if (stringValue(node) !== undefined) return \"string\";\n if (\n node?.type === \"ArrowFunctionExpression\" ||\n node?.type === \"FunctionExpression\" ||\n node?.type === \"ObjectMethod\"\n ) {\n return \"function\";\n }\n return \"other\";\n}\n\nfunction dictionaryProp(obj: Node): Node | undefined {\n const prop = findProp(obj, \"dictionary\");\n const value = unwrap(prop?.value);\n return isObject(value) ? value : undefined;\n}\n\nfunction locateName(declaration: Node): string | undefined {\n const decl = unwrap(declaration);\n if (decl?.type === \"CallExpression\") {\n const first = unwrap(decl.arguments[0]);\n if (isObject(first)) return stringValue(findProp(first, \"name\")?.value);\n return stringValue(first);\n }\n if (isObject(decl)) return stringValue(findProp(decl, \"name\")?.value);\n return undefined;\n}\n\nfunction locateLocalesObject(declaration: Node): Node | undefined {\n const decl = unwrap(declaration);\n if (decl?.type === \"CallExpression\") {\n const first = unwrap(decl.arguments[0]);\n const second = unwrap(decl.arguments[1]);\n if (isObject(second)) return second;\n if (isObject(first)) return dictionaryProp(first);\n return undefined;\n }\n if (isObject(decl)) return dictionaryProp(decl);\n return undefined;\n}\n\n/** Quotes a property key only when it is not a valid identifier. */\nfunction keyCode(key: string): string {\n return IDENTIFIER.test(key) ? key : JSON.stringify(key);\n}\n\n/** Builds a detached `key: <value>` property node from source text. */\nfunction makeProp(key: string, valueCode: string): Node {\n const expr: Node = parseExpression(`({ ${keyCode(key)}: ${valueCode} })`);\n return expr.$ast.properties[0];\n}\n\nfunction makeStringNode(value: string): Node {\n return makeProp(\"_\", JSON.stringify(value)).value;\n}\n\n/**\n * A dictionary is statically readable when every locale is an object literal and no object on the\n * way down uses spreads or computed keys.\n */\nfunction isStaticallyComplete(localesObj: Node): boolean {\n const walk = (obj: Node): boolean =>\n obj.properties.every((prop: Node) => {\n if (!isPropLike(prop) || keyName(prop) === undefined) return false;\n if (prop.type === \"ObjectMethod\") return true;\n const value = unwrap(prop.value);\n return isObject(value) ? walk(value) : true;\n });\n\n return localesObj.properties.every((prop: Node) => {\n if (!isPropLike(prop) || keyName(prop) === undefined || prop.type === \"ObjectMethod\")\n return false;\n const value = unwrap(prop.value);\n return isObject(value) && walk(value);\n });\n}\n\n/**\n * Parses dictionary source text (for example an unsaved editor buffer). Returns `undefined` when\n * the file does not have an editable shape; throws on syntax errors.\n */\nexport function parseDictionaryText(file: string, text: string): DictionaryFile | undefined {\n const mod = parseModule(text);\n const exported: Node = (mod.exports as any).default;\n const ast: Node = exported?.$ast;\n if (!ast) return undefined;\n const declaration = ast.declaration ?? ast;\n const localesObj = locateLocalesObject(declaration);\n return localesObj\n ? {\n file,\n mod,\n localesObj,\n name: locateName(declaration),\n endsWithNewline: text.endsWith(\"\\n\"),\n complete: isStaticallyComplete(localesObj),\n }\n : undefined;\n}\n\n/** Loads a dictionary file; returns `undefined` when its shape is not editable. */\nexport async function loadDictionaryFile(file: string): Promise<DictionaryFile | undefined> {\n return parseDictionaryText(file, fs.readFileSync(file, \"utf-8\"));\n}\n\n/** The source text with all edits applied, without writing anything. */\nexport function renderDictionaryFile(df: DictionaryFile): string {\n const code = generateCode(df.mod as any).code;\n return df.endsWithNewline && !code.endsWith(\"\\n\") ? `${code}\\n` : code;\n}\n\nexport async function saveDictionaryFile(df: DictionaryFile): Promise<void> {\n await writeFile(df.mod, df.file);\n if (df.endsWithNewline) {\n const text = fs.readFileSync(df.file, \"utf-8\");\n if (!text.endsWith(\"\\n\")) fs.writeFileSync(df.file, text + \"\\n\", \"utf-8\");\n }\n}\n\nexport function listLocales(df: DictionaryFile): string[] {\n return df.localesObj.properties\n .filter(isPropLike)\n .map(keyName)\n .filter((k: string | undefined): k is string => k !== undefined);\n}\n\nfunction localeObject(df: DictionaryFile, locale: string): Node | undefined {\n const value = unwrap(findProp(df.localesObj, locale)?.value);\n return isObject(value) ? value : undefined;\n}\n\n/** Lists every leaf (string, function, or other value) of a locale, in source order. */\nexport function listLeaves(df: DictionaryFile, locale: string): Leaf[] {\n const root = localeObject(df, locale);\n if (!root) return [];\n\n const leaves: Leaf[] = [];\n const walk = (obj: Node, prefix: string[]) => {\n for (const prop of obj.properties) {\n if (!isPropLike(prop)) continue;\n const name = keyName(prop);\n if (name === undefined) continue;\n const path = [...prefix, name];\n const value = prop.type === \"ObjectMethod\" ? prop : unwrap(prop.value);\n if (isObject(value)) {\n walk(value, path);\n } else {\n const kind = leafKind(value);\n leaves.push({ path, kind, ...(kind === \"string\" ? { value: stringValue(value) } : {}) });\n }\n }\n };\n walk(root, []);\n return leaves;\n}\n\nfunction getProp(df: DictionaryFile, locale: string, path: string[]): Node | undefined {\n let obj = localeObject(df, locale);\n for (let i = 0; obj && i < path.length; i++) {\n const prop = findProp(obj, path[i]);\n if (!prop) return undefined;\n if (i === path.length - 1) return prop;\n const next = unwrap(prop.value);\n obj = isObject(next) ? next : undefined;\n }\n return undefined;\n}\n\nexport function hasPath(df: DictionaryFile, locale: string, path: string[]): boolean {\n return getProp(df, locale, path) !== undefined;\n}\n\nexport function getString(df: DictionaryFile, locale: string, path: string[]): string | undefined {\n const prop = getProp(df, locale, path);\n return prop && prop.type !== \"ObjectMethod\" ? stringValue(prop.value) : undefined;\n}\n\nfunction ensureLocale(df: DictionaryFile, locale: string): Node {\n let obj = localeObject(df, locale);\n if (!obj) {\n const prop = makeProp(locale, \"{}\");\n df.localesObj.properties.push(prop);\n obj = prop.value as Node;\n }\n return obj;\n}\n\n/** Returns the object that should contain the last path segment, creating objects as needed. */\nfunction ensureParent(df: DictionaryFile, locale: string, path: string[]): Node | undefined {\n let obj = ensureLocale(df, locale);\n for (const segment of path.slice(0, -1)) {\n let prop = findProp(obj, segment);\n if (!prop) {\n prop = makeProp(segment, \"{}\");\n obj.properties.push(prop);\n }\n const next = unwrap(prop.value);\n if (!isObject(next)) return undefined;\n obj = next;\n }\n return obj;\n}\n\nexport type SetResult = \"created\" | \"updated\" | \"skipped\";\n\n/**\n * Sets a string value at `path`. Existing function or non-string values are left alone\n * (`\"skipped\"`) because they cannot be translated as plain text.\n */\nexport function setString(\n df: DictionaryFile,\n locale: string,\n path: string[],\n value: string,\n): SetResult {\n const parent = ensureParent(df, locale, path);\n if (!parent) return \"skipped\";\n\n const last = path[path.length - 1];\n const existing = findProp(parent, last);\n if (existing) {\n if (existing.type === \"ObjectMethod\" || leafKind(existing.value) !== \"string\") {\n return \"skipped\";\n }\n existing.value = makeStringNode(value);\n return \"updated\";\n }\n parent.properties.push(makeProp(last, JSON.stringify(value)));\n return \"created\";\n}\n\n/**\n * Copies the leaf at `path` from one locale to another. String values can be rewritten with\n * `mapString` (for example to add a `[TODO]` marker); other values are copied verbatim.\n * Returns `false` when the source has no such leaf or the target already has one.\n */\nexport function copyLeaf(\n df: DictionaryFile,\n fromLocale: string,\n toLocale: string,\n path: string[],\n mapString?: (value: string) => string,\n): boolean {\n const source = getProp(df, fromLocale, path);\n if (!source || hasPath(df, toLocale, path)) return false;\n\n const parent = ensureParent(df, toLocale, path);\n if (!parent) return false;\n\n const last = path[path.length - 1];\n const text = source.type === \"ObjectMethod\" ? undefined : stringValue(source.value);\n if (text !== undefined && mapString) {\n parent.properties.push(makeProp(last, JSON.stringify(mapString(text))));\n } else {\n const code = generateCode(source as any).code;\n const expr: Node = parseExpression(`({ ${code} })`);\n parent.properties.push(expr.$ast.properties[0]);\n }\n return true;\n}\n\n/** Leaf paths of a runtime dictionary value (plain objects recurse; everything else is a leaf). */\nexport function flattenPaths(value: unknown, prefix: string[] = []): string[][] {\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n return Object.entries(value).flatMap(([k, v]) => flattenPaths(v, [...prefix, k]));\n }\n return prefix.length > 0 ? [prefix] : [];\n}\n\nconst toRange = (loc: Node | undefined): SourceRange | undefined =>\n loc\n ? {\n start: { line: loc.start.line - 1, column: loc.start.column },\n end: { line: loc.end.line - 1, column: loc.end.column },\n }\n : undefined;\n\n/**\n * Where a locale (no `path`) or a key within it is written. The range covers the property key, so\n * it is a good place to attach a diagnostic or jump to.\n */\nexport function locationOf(\n df: DictionaryFile,\n locale: string,\n path: string[] = [],\n): SourceRange | undefined {\n const prop = path.length === 0 ? findProp(df.localesObj, locale) : getProp(df, locale, path);\n return toRange((prop?.key ?? prop)?.loc);\n}\n\n/** The source text of the dictionary object itself (the object whose keys are locales). */\nexport function dictionaryLocation(df: DictionaryFile): SourceRange | undefined {\n return toRange(df.localesObj.loc);\n}\n\n/** The source code of a leaf's value (a function's source, or a string with its quotes). */\nexport function getLeafSource(\n df: DictionaryFile,\n locale: string,\n path: string[],\n): string | undefined {\n const prop = getProp(df, locale, path);\n if (!prop) return undefined;\n return generateCode((prop.type === \"ObjectMethod\" ? prop : prop.value) as any).code;\n}\n\n// AST helpers shared with the static config reader.\nexport {\n unwrap as unwrapNode,\n isObject as isObjectNode,\n keyName as propertyKeyName,\n findProp as findProperty,\n};\nexport type { Node as AstNode };\n","import {\n argumentSignature,\n findPlurals,\n getArguments,\n isIcuStructured,\n parseMessage,\n type IcuNode,\n} from \"dialexjs/icu\";\n\nexport interface IcuFinding {\n level: \"error\" | \"warning\";\n code: \"invalid-icu\" | \"icu-args-mismatch\" | \"icu-plural-categories\";\n locale: string;\n /** Dotted key including the dictionary name, e.g. `cart.items`. */\n key: string;\n message: string;\n}\n\n/** `locale -> dotted path within the dictionary -> string value` */\nexport type LocaleStrings = Map<string, Map<string, string>>;\n\n/** CLDR order. `resolvedOptions().pluralCategories` order differs between Node versions. */\nconst CATEGORY_ORDER = [\"zero\", \"one\", \"two\", \"few\", \"many\"];\n\nfunction requiredCategories(locale: string, ordinal: boolean): string[] {\n try {\n return new Intl.PluralRules(locale, { type: ordinal ? \"ordinal\" : \"cardinal\" })\n .resolvedOptions()\n .pluralCategories.filter((c) => c !== \"other\")\n .sort((a, b) => CATEGORY_ORDER.indexOf(a) - CATEGORY_ORDER.indexOf(b));\n } catch {\n return []; // not a valid BCP 47 tag; nothing to compare against\n }\n}\n\n/**\n * Validates ICU messages across the locales of one dictionary:\n *\n * - invalid syntax: an error for messages that clearly use ICU (plural/select/number/date/time),\n * a warning for plain `{placeholders}` that do not parse (they may not be meant as ICU);\n * - every locale must use the same arguments as the reference locale;\n * - plural options should cover the categories the locale's plural rules need.\n */\nexport function checkIcu(\n dictionary: string,\n strings: LocaleStrings,\n defaultLocale: string,\n): IcuFinding[] {\n const findings: IcuFinding[] = [];\n const parsed = new Map<string, Map<string, IcuNode[]>>();\n\n for (const [locale, entries] of strings) {\n const ok = new Map<string, IcuNode[]>();\n for (const [path, value] of entries) {\n if (!value.includes(\"{\")) continue;\n const key = `${dictionary}.${path}`;\n try {\n ok.set(path, parseMessage(value));\n } catch (error) {\n const structured = isIcuStructured(value);\n findings.push({\n level: structured ? \"error\" : \"warning\",\n code: \"invalid-icu\",\n locale,\n key,\n message: structured\n ? `Invalid ICU message \"${key}\" (${locale}): ${(error as Error).message}`\n : `\"${key}\" (${locale}) contains braces but is not valid ICU, so t() values cannot be applied: ${(error as Error).message}`,\n });\n }\n }\n parsed.set(locale, ok);\n }\n\n // Compare every locale against the reference (default locale first)\n const order = [defaultLocale, ...[...parsed.keys()].filter((l) => l !== defaultLocale)];\n const paths = new Set([...parsed.values()].flatMap((m) => [...m.keys()]));\n\n for (const path of paths) {\n const key = `${dictionary}.${path}`;\n const reference = order.find((l) => parsed.get(l)?.has(path));\n if (!reference) continue;\n const expected = argumentSignature(parsed.get(reference)!.get(path)!);\n\n for (const locale of order) {\n const nodes = parsed.get(locale)?.get(path);\n if (nodes && locale !== reference) {\n const found = argumentSignature(nodes);\n if (found !== expected) {\n findings.push({\n level: \"error\",\n code: \"icu-args-mismatch\",\n locale,\n key,\n message: `Locale \"${locale}\" uses different arguments than \"${reference}\" for \"${key}\" (expected ${expected || \"none\"}, found ${found || \"none\"})`,\n });\n }\n }\n if (!nodes) continue;\n\n for (const plural of findPlurals(nodes)) {\n const missing = requiredCategories(locale, plural.ordinal).filter(\n (c) => !(c in plural.options),\n );\n if (missing.length > 0) {\n findings.push({\n level: \"warning\",\n code: \"icu-plural-categories\",\n locale,\n key,\n message: `Locale \"${locale}\" plural \"${plural.name}\" in \"${key}\" has no ${missing.map((c) => `\"${c}\"`).join(\", \")} option (used by this language; \"other\" will be shown instead)`,\n });\n }\n }\n }\n }\n\n return findings;\n}\n\n/** Arguments of a message, or `undefined` when it does not parse. */\nexport function tryGetArguments(message: string) {\n try {\n return getArguments(parseMessage(message));\n } catch {\n return undefined;\n }\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport fg from \"fast-glob\";\nimport {\n TODO_PREFIX,\n dictionaryLocation,\n flattenPaths,\n listLeaves,\n listLocales,\n loadDictionaryFile,\n locationOf,\n type DictionaryFile,\n type SourceRange,\n} from \"./utils/dictionary-edit.js\";\nimport { checkIcu, type LocaleStrings } from \"./utils/icu-check.js\";\n\n/**\n * Project analysis shared by `dialex check` and the editor extension.\n *\n * It reads dictionaries from the syntax tree, so it never runs project code. Dictionaries that\n * cannot be read that way (spreads, computed keys, imported values) are skipped with an `info`\n * issue, unless `runtime: true` is passed, in which case they are imported and read from the\n * evaluated module (the CLI does this; editors must not).\n */\n\nexport type IssueLevel = \"error\" | \"warning\" | \"info\";\n\nexport type IssueCode =\n | \"invalid-dictionary\"\n | \"unanalyzable\"\n | \"missing-locale\"\n | \"missing-key\"\n | \"invalid-locale\"\n | \"todo-placeholder\"\n | \"invalid-icu\"\n | \"icu-args-mismatch\"\n | \"icu-plural-categories\"\n | \"unknown-dictionary\"\n | \"unknown-path\"\n | \"unused-key\"\n | \"unused-dictionary\";\n\nexport interface AnalysisIssue {\n /** Absolute path of the file the issue is in. */\n file: string;\n level: IssueLevel;\n code: IssueCode;\n message: string;\n /** Where to point, when known. Lines and columns are zero-based. */\n range?: SourceRange;\n /** The dictionary the issue is about, when it is about one. */\n dictionary?: string;\n locale?: string;\n /** Key path within the dictionary. */\n path?: string[];\n /** For `missing-key`: a locale that has the key, to copy it from. */\n sourceLocale?: string;\n}\n\nexport interface AnalysisConfig {\n defaultLocale?: string;\n locales?: string[];\n include?: string | string[];\n}\n\nexport interface AnalysisOptions {\n root: string;\n config: AnalysisConfig;\n /** Source globs to scan for dictionary references. Pass `[]` to skip scanning. */\n src?: string[];\n /** Import dictionaries the syntax tree cannot fully describe. Runs project code. */\n runtime?: boolean;\n /** Extra ignore globs, relative to `root`. */\n ignore?: string[];\n /** Report possibly unused keys and dictionaries. @default true */\n unused?: boolean;\n}\n\nexport interface LeafInfo {\n kind: \"string\" | \"function\" | \"other\";\n value?: string;\n}\n\nexport interface AnalyzedDictionary {\n name: string;\n /** Absolute path. */\n file: string;\n /** Locales in source order. */\n locales: string[];\n /** `locale -> dotted path -> leaf` */\n leaves: Map<string, Map<string, LeafInfo>>;\n /** Union of dotted leaf paths across locales, in order of first appearance. */\n paths: string[];\n /** The parsed file, present when it was read statically. */\n df?: DictionaryFile;\n}\n\nexport interface AnalysisResult {\n issues: AnalysisIssue[];\n dictionaries: AnalyzedDictionary[];\n /** Absolute paths of every dictionary file that matched. */\n files: string[];\n sourceFilesScanned: number;\n}\n\nconst DICTIONARY_IGNORE = [\"**/node_modules/**\", \"**/dist/**\", \"**/.next/**\"];\n\nexport const DEFAULT_SOURCE_GLOB = \"**/*.{ts,tsx,js,jsx,mjs,cjs,vue,svelte,astro,mdx}\";\nconst SOURCE_IGNORE = [\n ...DICTIONARY_IGNORE,\n \"**/.nuxt/**\",\n \"**/.output/**\",\n \"**/.svelte-kit/**\",\n \"**/.astro/**\",\n \"**/coverage/**\",\n \"**/graphify-out/**\",\n \"**/*.d.ts\",\n \"**/i18n.generated.*\",\n \"**/dialex.config.*\",\n \"**/i18n.config.*\",\n];\n\n/** `getDictionary(\"x\")`, `useDictionary(\"x\")`, `@I18nDictionary(\"x\")` */\nconst DICTIONARY_CALL =\n /\\b(?:getDictionary|useDictionary|I18nDictionary)\\(\\s*([\"'`])([^\"'`$\\n]+)\\1/dg;\n/** `t(\"dictionary.path.to.key\")` */\nconst T_CALL = /(?<![\\w$.])t\\(\\s*([\"'`])([\\w-]+(?:\\.[\\w-]+)+)\\1/dg;\n\nexport interface Reference {\n /** A dictionary name, or a full `dictionary.key.path`. */\n kind: \"dictionary\" | \"path\";\n value: string;\n /** Offsets of the string's contents (without the quotes) in the scanned text. */\n start: number;\n end: number;\n}\n\n/** Finds `getDictionary(\"x\")`-style and `t(\"x.y.z\")` references in source text. */\nexport function scanReferences(text: string): Reference[] {\n const references: Reference[] = [];\n const collect = (pattern: RegExp, kind: Reference[\"kind\"]) => {\n for (const match of text.matchAll(pattern)) {\n const [start, end] = match.indices![2] as [number, number];\n references.push({ kind, value: match[2], start, end });\n }\n };\n collect(DICTIONARY_CALL, \"dictionary\");\n collect(T_CALL, \"path\");\n return references.sort((a, b) => a.start - b.start);\n}\n\nfunction lineStarts(text: string): number[] {\n const starts = [0];\n for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) starts.push(i + 1);\n return starts;\n}\n\nfunction positionAt(starts: number[], offset: number) {\n let low = 0;\n let high = starts.length - 1;\n while (low < high) {\n const mid = Math.ceil((low + high) / 2);\n if (starts[mid] <= offset) low = mid;\n else high = mid - 1;\n }\n return { line: low, column: offset - starts[low] };\n}\n\ninterface DictionaryData {\n name: string;\n locales: string[];\n leaves: Map<string, Map<string, LeafInfo>>;\n df?: DictionaryFile;\n}\n\ntype Loaded =\n | { ok: true; data: DictionaryData }\n | { ok: false; level: IssueLevel; code: IssueCode; message: string };\n\nfunction fromSyntaxTree(df: DictionaryFile): DictionaryData {\n const locales = listLocales(df);\n const leaves = new Map<string, Map<string, LeafInfo>>();\n for (const locale of locales) {\n const entries = new Map<string, LeafInfo>();\n for (const leaf of listLeaves(df, locale)) {\n entries.set(leaf.path.join(\".\"), { kind: leaf.kind, value: leaf.value });\n }\n leaves.set(locale, entries);\n }\n return { name: df.name!, locales, leaves, df };\n}\n\nasync function fromRuntime(file: string): Promise<Loaded> {\n try {\n const mod = await import(`${pathToFileURL(file).href}?t=${Date.now()}_${Math.random()}`);\n const definition = mod.default || mod;\n if (!definition || typeof definition !== \"object\" || !definition.dictionary) {\n return {\n ok: false,\n level: \"error\",\n code: \"invalid-dictionary\",\n message: \"Default export does not appear to be a valid defineDictionary output.\",\n };\n }\n\n const dictionary = definition.dictionary as Record<string, Record<string, any>>;\n const leaves = new Map<string, Map<string, LeafInfo>>();\n const invalid: string[] = [];\n\n for (const [locale, content] of Object.entries(dictionary)) {\n if (!content || typeof content !== \"object\") {\n invalid.push(locale);\n continue;\n }\n const entries = new Map<string, LeafInfo>();\n for (const leafPath of flattenPaths(content)) {\n let value: any = content;\n for (const segment of leafPath) value = value?.[segment];\n entries.set(\n leafPath.join(\".\"),\n typeof value === \"string\"\n ? { kind: \"string\", value }\n : { kind: typeof value === \"function\" ? \"function\" : \"other\" },\n );\n }\n leaves.set(locale, entries);\n }\n\n if (invalid.length > 0) {\n return {\n ok: false,\n level: \"error\",\n code: \"invalid-locale\",\n message: invalid.map((l) => `Locale \"${l}\" content is empty or invalid.`).join(\"\\n\"),\n };\n }\n\n return {\n ok: true,\n data: { name: String(definition.name ?? \"\"), locales: [...leaves.keys()], leaves },\n };\n } catch (error: any) {\n return {\n ok: false,\n level: \"error\",\n code: \"invalid-dictionary\",\n message: `Failed to import dictionary: ${error.message || String(error)}`,\n };\n }\n}\n\nasync function loadDictionaryData(file: string, runtime: boolean): Promise<Loaded> {\n let astError: Error | undefined;\n try {\n const df = await loadDictionaryFile(file);\n if (df?.complete && df.name) return { ok: true, data: fromSyntaxTree(df) };\n } catch (error) {\n astError = error as Error;\n }\n\n if (runtime) return fromRuntime(file);\n\n return astError\n ? {\n ok: false,\n level: \"error\",\n code: \"invalid-dictionary\",\n message: `Failed to parse dictionary: ${astError.message}`,\n }\n : {\n ok: false,\n level: \"info\",\n code: \"unanalyzable\",\n message:\n \"This dictionary uses spreads, computed keys, imported values, or a computed name, so it cannot be analyzed statically.\",\n };\n}\n\nexport async function analyzeProject(options: AnalysisOptions): Promise<AnalysisResult> {\n const { root, config } = options;\n const include = config.include || \"**/*.content.ts\";\n const configuredLocales = config.locales || [];\n const defaultLocale = config.defaultLocale || \"en\";\n const extraIgnore = options.ignore ?? [];\n\n const files = fg.sync(include, {\n cwd: root,\n absolute: true,\n ignore: [...DICTIONARY_IGNORE, ...extraIgnore],\n });\n\n const issues: AnalysisIssue[] = [];\n const dictionaries: AnalyzedDictionary[] = [];\n\n // 1. Dictionaries: parity, placeholders, ICU\n for (const file of files) {\n const loaded = await loadDictionaryData(file, options.runtime === true);\n if (!loaded.ok) {\n for (const message of loaded.message.split(\"\\n\")) {\n issues.push({ file, level: loaded.level, code: loaded.code, message });\n }\n continue;\n }\n\n const { data } = loaded;\n const { df } = data;\n const at = (locale?: string, keyPath?: string[]) =>\n df && locale ? locationOf(df, locale, keyPath) : undefined;\n const add = (issue: Omit<AnalysisIssue, \"file\" | \"dictionary\">) =>\n issues.push({ ...issue, file, dictionary: data.name });\n\n for (const required of configuredLocales) {\n if (!data.locales.includes(required)) {\n add({\n level: \"error\",\n code: \"missing-locale\",\n message: `Missing configured locale \"${required}\"`,\n range: df ? dictionaryLocation(df) : undefined,\n locale: required,\n });\n }\n }\n\n const paths: string[] = [];\n const seen = new Set<string>();\n for (const locale of data.locales) {\n for (const leafPath of data.leaves.get(locale)!.keys()) {\n if (!seen.has(leafPath)) {\n seen.add(leafPath);\n paths.push(leafPath);\n }\n }\n }\n\n for (const locale of data.locales) {\n const present = data.leaves.get(locale)!;\n for (const leafPath of paths) {\n if (present.has(leafPath)) continue;\n const sourceLocale = [defaultLocale, ...data.locales].find(\n (l) => l !== locale && data.leaves.get(l)?.has(leafPath),\n );\n add({\n level: \"error\",\n code: \"missing-key\",\n message: `Locale \"${locale}\" is missing key \"${leafPath}\"`,\n range: at(locale),\n locale,\n path: leafPath.split(\".\"),\n sourceLocale,\n });\n }\n }\n\n const strings: LocaleStrings = new Map();\n for (const locale of data.locales) {\n const entries = new Map<string, string>();\n strings.set(locale, entries);\n for (const [leafPath, leaf] of data.leaves.get(locale)!) {\n if (leaf.kind !== \"string\" || leaf.value === undefined) continue;\n entries.set(leafPath, leaf.value);\n if (leaf.value.startsWith(TODO_PREFIX)) {\n add({\n level: \"warning\",\n code: \"todo-placeholder\",\n message: `Locale \"${locale}\" key \"${leafPath}\" still has a ${TODO_PREFIX.trim()} placeholder`,\n range: at(locale, leafPath.split(\".\")),\n locale,\n path: leafPath.split(\".\"),\n });\n }\n }\n }\n\n for (const finding of checkIcu(data.name, strings, defaultLocale)) {\n const keyPath = finding.key.slice(data.name.length + 1).split(\".\");\n add({\n level: finding.level,\n code: finding.code,\n message: finding.message,\n range: at(finding.locale, keyPath),\n locale: finding.locale,\n path: keyPath,\n });\n }\n\n dictionaries.push({\n name: data.name,\n file,\n locales: data.locales,\n leaves: data.leaves,\n paths,\n df,\n });\n }\n\n // 2. Source code: references to dictionaries and keys\n const known = new Map(dictionaries.map((d) => [d.name, d]));\n const dictionaryFiles = new Set(files);\n const sourceGlobs = options.src ?? [DEFAULT_SOURCE_GLOB];\n const sourceFiles =\n sourceGlobs.length === 0\n ? []\n : fg\n .sync(sourceGlobs, {\n cwd: root,\n absolute: true,\n ignore: [...SOURCE_IGNORE, ...extraIgnore],\n })\n .filter((f) => !dictionaryFiles.has(f));\n\n if (sourceFiles.length > 0) {\n const tokens = new Set<string>();\n\n for (const file of sourceFiles) {\n let text: string;\n try {\n text = fs.readFileSync(file, \"utf-8\");\n } catch {\n continue;\n }\n for (const token of text.match(/[\\w$-]+/g) ?? []) tokens.add(token);\n\n const starts = lineStarts(text);\n const rangeOf = (start: number, end: number): SourceRange => ({\n start: positionAt(starts, start),\n end: positionAt(starts, end),\n });\n\n for (const ref of scanReferences(text)) {\n if (ref.kind === \"dictionary\") {\n if (!known.has(ref.value)) {\n issues.push({\n file,\n level: \"error\",\n code: \"unknown-dictionary\",\n message: `Unknown dictionary \"${ref.value}\"`,\n range: rangeOf(ref.start, ref.end),\n dictionary: ref.value,\n });\n }\n continue;\n }\n\n const [name, ...rest] = ref.value.split(\".\");\n const dictionary = known.get(name);\n if (!dictionary) continue; // `t(...)` may belong to another library\n const keyPath = rest.join(\".\");\n const exists = dictionary.paths.some((p) => p === keyPath || p.startsWith(`${keyPath}.`));\n if (!exists) {\n issues.push({\n file,\n level: \"error\",\n code: \"unknown-path\",\n message: `Unknown translation path \"${ref.value}\"`,\n range: rangeOf(ref.start, ref.end),\n dictionary: name,\n path: rest,\n });\n }\n }\n }\n\n // 3. Possibly unused dictionaries and keys (heuristic: warnings only)\n if (options.unused !== false) {\n for (const dictionary of dictionaries) {\n if (!tokens.has(dictionary.name)) {\n issues.push({\n file: dictionary.file,\n level: \"warning\",\n code: \"unused-dictionary\",\n message: `Dictionary \"${dictionary.name}\" is never referenced`,\n range: dictionary.df ? dictionaryLocation(dictionary.df) : undefined,\n dictionary: dictionary.name,\n });\n continue;\n }\n for (const keyPath of dictionary.paths) {\n const last = keyPath.split(\".\").pop()!;\n if (tokens.has(last)) continue;\n const holder = dictionary.locales.find((l) => dictionary.leaves.get(l)?.has(keyPath));\n issues.push({\n file: dictionary.file,\n level: \"warning\",\n code: \"unused-key\",\n message: `Key \"${keyPath}\" is possibly unused`,\n range:\n dictionary.df && holder\n ? locationOf(dictionary.df, holder, keyPath.split(\".\"))\n : undefined,\n dictionary: dictionary.name,\n path: keyPath.split(\".\"),\n });\n }\n }\n }\n }\n\n return { issues, dictionaries, files, sourceFilesScanned: sourceFiles.length };\n}\n\n/** Project root-relative path with forward slashes, for display. */\nexport function relativeFile(root: string, file: string): string {\n return path.relative(root, file);\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { parseModule } from \"magicast\";\nimport {\n findProperty,\n isObjectNode,\n propertyKeyName,\n unwrapNode,\n type AstNode,\n} from \"./dictionary-edit.js\";\n\n/**\n * The parts of `dialex.config` that tooling needs, read **without running the file**. Editors must\n * not execute workspace code (it may be untrusted), so values are read from the syntax tree and\n * only literals are understood.\n */\nexport interface StaticConfig {\n defaultLocale: string;\n locales: string[];\n include: string | string[];\n fallbacks: Record<string, string[]>;\n prefixDefault: boolean;\n lazy: boolean;\n}\n\nexport interface StaticConfigResult {\n config: StaticConfig;\n /** The file the values came from, if any. */\n file?: string;\n /** Things that could not be read statically, for example `locales: getLocales()`. */\n notes: string[];\n}\n\nconst CONFIG_NAMES = [\"dialex.config\", \"i18n.config\"];\nconst EXTENSIONS = [\"ts\", \"mts\", \"cts\", \"js\", \"mjs\", \"cjs\", \"json\"];\n\nexport const DEFAULT_STATIC_CONFIG: StaticConfig = {\n defaultLocale: \"en\",\n locales: [],\n include: \"**/*.content.ts\",\n fallbacks: {},\n prefixDefault: true,\n lazy: false,\n};\n\nconst NOT_LITERAL = Symbol(\"not literal\");\n\nfunction literal(node: AstNode | undefined): unknown {\n node = unwrapNode(node as AstNode);\n switch (node?.type) {\n case \"StringLiteral\":\n case \"NumericLiteral\":\n case \"BooleanLiteral\":\n return node.value;\n case \"Literal\":\n return node.value;\n case \"TemplateLiteral\":\n return node.expressions.length === 0 ? (node.quasis[0]?.value?.cooked ?? \"\") : NOT_LITERAL;\n case \"ArrayExpression\": {\n const items = node.elements.map((e: AstNode) => literal(e));\n return items.includes(NOT_LITERAL) ? NOT_LITERAL : items;\n }\n case \"ObjectExpression\": {\n const result: Record<string, unknown> = {};\n for (const prop of node.properties) {\n const key = propertyKeyName(prop);\n const value = key === undefined ? NOT_LITERAL : literal(prop.value);\n if (value === NOT_LITERAL) return NOT_LITERAL;\n result[key as string] = value;\n }\n return result;\n }\n default:\n return NOT_LITERAL;\n }\n}\n\n/** The object literal passed to `defineConfig({...})`, or exported directly. */\nfunction findConfigObject(declaration: AstNode): AstNode | undefined {\n const node = unwrapNode(declaration);\n if (node?.type === \"CallExpression\") return findConfigObject(node.arguments[0]);\n return isObjectNode(node) ? node : undefined;\n}\n\nconst isStringArray = (value: unknown): value is string[] =>\n Array.isArray(value) && value.every((v) => typeof v === \"string\");\n\nfunction apply(object: Record<string, unknown>, config: StaticConfig, notes: string[]) {\n const accept = <K extends keyof StaticConfig>(\n key: K,\n valid: (value: unknown) => boolean,\n expected: string,\n ) => {\n if (!(key in object)) return;\n const value = object[key];\n if (valid(value)) (config as any)[key] = value;\n else notes.push(`\"${key}\" could not be read statically (expected ${expected}).`);\n };\n\n accept(\"defaultLocale\", (v) => typeof v === \"string\" && v !== \"\", \"a string literal\");\n accept(\"locales\", isStringArray, \"an array of string literals\");\n accept(\n \"include\",\n (v) => typeof v === \"string\" || isStringArray(v),\n \"a string or array of strings\",\n );\n accept(\n \"fallbacks\",\n (v) =>\n typeof v === \"object\" && v !== null && Object.values(v).every((list) => isStringArray(list)),\n \"an object of string arrays\",\n );\n accept(\"prefixDefault\", (v) => typeof v === \"boolean\", \"true or false\");\n accept(\"lazy\", (v) => typeof v === \"boolean\", \"true or false\");\n}\n\n/** Reads a config file's source text. Exposed for tests and for unsaved buffers. */\nexport function parseStaticConfig(text: string, file = \"dialex.config.ts\"): StaticConfigResult {\n const notes: string[] = [];\n const config: StaticConfig = { ...DEFAULT_STATIC_CONFIG };\n\n if (file.endsWith(\".json\")) {\n try {\n const data = JSON.parse(text);\n if (data && typeof data === \"object\") apply(data, config, notes);\n } catch (error) {\n notes.push(`Could not parse ${path.basename(file)}: ${(error as Error).message}`);\n }\n return { config, file, notes };\n }\n\n try {\n const exported = (parseModule(text).exports as any).default;\n const object = findConfigObject(exported?.$ast?.declaration ?? exported?.$ast);\n if (!object) {\n notes.push(`${path.basename(file)} does not export a config object literal.`);\n } else {\n const values: Record<string, unknown> = {};\n for (const key of [\n \"defaultLocale\",\n \"locales\",\n \"include\",\n \"fallbacks\",\n \"prefixDefault\",\n \"lazy\",\n ]) {\n const prop = findProperty(object, key);\n if (!prop) continue;\n const value = literal(prop.value);\n if (value === NOT_LITERAL)\n notes.push(`\"${key}\" could not be read statically (not a literal).`);\n else values[key] = value;\n }\n apply(values, config, notes);\n }\n } catch (error) {\n notes.push(`Could not parse ${path.basename(file)}: ${(error as Error).message}`);\n }\n\n return { config, file, notes };\n}\n\n/** Reads `dialex.config.*` (or `i18n.config.*`) from `root`, or from `configFile` if given. */\nexport function readStaticConfig(root: string, configFile?: string): StaticConfigResult {\n const candidates = configFile\n ? [path.resolve(root, configFile)]\n : CONFIG_NAMES.flatMap((name) => EXTENSIONS.map((ext) => path.join(root, `${name}.${ext}`)));\n\n for (const file of candidates) {\n if (fs.existsSync(file)) return parseStaticConfig(fs.readFileSync(file, \"utf-8\"), file);\n }\n return { config: { ...DEFAULT_STATIC_CONFIG }, notes: [] };\n}\n","import path from \"node:path\";\nimport fg from \"fast-glob\";\nimport { resolveI18nConfig } from \"dialexjs/scanner\";\nimport type { I18nConfig } from \"dialexjs\";\nimport { loadDictionaryFile, type DictionaryFile } from \"./dictionary-edit.js\";\nimport { readStaticConfig } from \"./static-config.js\";\n\nexport interface ProjectDictionary {\n /** Path relative to the project root. */\n rel: string;\n name: string;\n df: DictionaryFile;\n}\n\nexport interface Project {\n root: string;\n config: I18nConfig;\n dictionaries: ProjectDictionary[];\n /** Dictionary files that could not be parsed into an editable shape. */\n unsupported: string[];\n /** Config values that could not be read statically (static loading only). */\n notes: string[];\n}\n\nasync function loadDictionaries(root: string, config: I18nConfig) {\n const include = config.include || \"**/*.content.ts\";\n const files = fg.sync(include, {\n cwd: root,\n absolute: true,\n ignore: [\"**/node_modules/**\", \"**/dist/**\", \"**/.next/**\"],\n });\n\n const dictionaries: ProjectDictionary[] = [];\n const unsupported: string[] = [];\n\n for (const file of files) {\n const rel = path.relative(root, file);\n try {\n const df = await loadDictionaryFile(file);\n if (!df || !df.name) unsupported.push(rel);\n else dictionaries.push({ rel, name: df.name, df });\n } catch {\n unsupported.push(rel); // syntax error: reported by the analysis, nothing to edit here\n }\n }\n\n return { dictionaries, unsupported };\n}\n\n/**\n * Resolves the config (running `dialex.config.*` through the loader, which executes it) and loads\n * every dictionary file for AST-level reading and editing.\n */\nexport async function loadProject(root: string, configFile?: string): Promise<Project> {\n const config = resolveI18nConfig(root, configFile ? { configFile } : {});\n return { root, config, notes: [], ...(await loadDictionaries(root, config)) };\n}\n\n/**\n * Like `loadProject`, but reads the config from the syntax tree without executing it. Use this in\n * editors and anywhere project code must not run.\n */\nexport async function loadStaticProject(root: string, configFile?: string): Promise<Project> {\n const { config, notes } = readStaticConfig(root, configFile);\n return { root, config, notes, ...(await loadDictionaries(root, config)) };\n}\n\n/** `home` + `[\"nav\", \"about\"]` -> `home.nav.about` */\nexport const toKey = (dictionary: string, keyPath: string[]) => [dictionary, ...keyPath].join(\".\");\n"],"mappings":";;;;;;;;AAkBA,MAAa,cAAc;AAE3B,MAAM,aAAa;AAkCnB,SAAS,OAAO,MAAkB;CAChC,OACE,SACC,KAAK,SAAS,oBACb,KAAK,SAAS,2BACd,KAAK,SAAS,4BAEhB,OAAO,KAAK;CAEd,OAAO;AACT;AAEA,MAAM,YAAY,SAAe,MAAM,SAAS;AAEhD,SAAS,QAAQ,MAAgC;CAC/C,MAAM,MAAM,MAAM;CAClB,IAAI,CAAC,OAAO,KAAK,UAAU,OAAO,KAAA;CAClC,IAAI,IAAI,SAAS,cAAc,OAAO,IAAI;CAC1C,IAAI,IAAI,SAAS,mBAAoB,IAAI,SAAS,aAAa,OAAO,IAAI,UAAU,UAClF,OAAO,IAAI;CAEb,IAAI,IAAI,SAAS,kBAAkB,OAAO,OAAO,IAAI,KAAK;AAE5D;AAEA,MAAM,cAAc,MAClB,GAAG,SAAS,oBAAoB,GAAG,SAAS,cAAc,GAAG,SAAS;AAExE,SAAS,SAAS,KAAW,KAA+B;CAC1D,OAAO,IAAI,WAAW,MAAM,MAAY,WAAW,CAAC,KAAK,QAAQ,CAAC,MAAM,GAAG;AAC7E;AAEA,SAAS,YAAY,MAAgC;CACnD,OAAO,OAAO,IAAI;CAClB,IAAI,MAAM,SAAS,iBAAiB,OAAO,KAAK;CAChD,IAAI,MAAM,SAAS,aAAa,OAAO,KAAK,UAAU,UAAU,OAAO,KAAK;CAC5E,IAAI,MAAM,SAAS,qBAAqB,KAAK,YAAY,WAAW,GAClE,OAAO,KAAK,OAAO,EAAE,EAAE,OAAO,UAAU,KAAA;AAG5C;AAEA,SAAS,SAAS,MAAsB;CACtC,OAAO,OAAO,IAAI;CAClB,IAAI,YAAY,IAAI,MAAM,KAAA,GAAW,OAAO;CAC5C,IACE,MAAM,SAAS,6BACf,MAAM,SAAS,wBACf,MAAM,SAAS,gBAEf,OAAO;CAET,OAAO;AACT;AAEA,SAAS,eAAe,KAA6B;CAEnD,MAAM,QAAQ,OADD,SAAS,KAAK,YACH,CAAC,EAAE,KAAK;CAChC,OAAO,SAAS,KAAK,IAAI,QAAQ,KAAA;AACnC;AAEA,SAAS,WAAW,aAAuC;CACzD,MAAM,OAAO,OAAO,WAAW;CAC/B,IAAI,MAAM,SAAS,kBAAkB;EACnC,MAAM,QAAQ,OAAO,KAAK,UAAU,EAAE;EACtC,IAAI,SAAS,KAAK,GAAG,OAAO,YAAY,SAAS,OAAO,MAAM,CAAC,EAAE,KAAK;EACtE,OAAO,YAAY,KAAK;CAC1B;CACA,IAAI,SAAS,IAAI,GAAG,OAAO,YAAY,SAAS,MAAM,MAAM,CAAC,EAAE,KAAK;AAEtE;AAEA,SAAS,oBAAoB,aAAqC;CAChE,MAAM,OAAO,OAAO,WAAW;CAC/B,IAAI,MAAM,SAAS,kBAAkB;EACnC,MAAM,QAAQ,OAAO,KAAK,UAAU,EAAE;EACtC,MAAM,SAAS,OAAO,KAAK,UAAU,EAAE;EACvC,IAAI,SAAS,MAAM,GAAG,OAAO;EAC7B,IAAI,SAAS,KAAK,GAAG,OAAO,eAAe,KAAK;EAChD;CACF;CACA,IAAI,SAAS,IAAI,GAAG,OAAO,eAAe,IAAI;AAEhD;;AAGA,SAAS,QAAQ,KAAqB;CACpC,OAAO,WAAW,KAAK,GAAG,IAAI,MAAM,KAAK,UAAU,GAAG;AACxD;;AAGA,SAAS,SAAS,KAAa,WAAyB;CAEtD,OADmB,gBAAgB,MAAM,QAAQ,GAAG,EAAE,IAAI,UAAU,IAC1D,CAAC,CAAC,KAAK,WAAW;AAC9B;AAEA,SAAS,eAAe,OAAqB;CAC3C,OAAO,SAAS,KAAK,KAAK,UAAU,KAAK,CAAC,CAAC,CAAC;AAC9C;;;;;AAMA,SAAS,qBAAqB,YAA2B;CACvD,MAAM,QAAQ,QACZ,IAAI,WAAW,OAAO,SAAe;EACnC,IAAI,CAAC,WAAW,IAAI,KAAK,QAAQ,IAAI,MAAM,KAAA,GAAW,OAAO;EAC7D,IAAI,KAAK,SAAS,gBAAgB,OAAO;EACzC,MAAM,QAAQ,OAAO,KAAK,KAAK;EAC/B,OAAO,SAAS,KAAK,IAAI,KAAK,KAAK,IAAI;CACzC,CAAC;CAEH,OAAO,WAAW,WAAW,OAAO,SAAe;EACjD,IAAI,CAAC,WAAW,IAAI,KAAK,QAAQ,IAAI,MAAM,KAAA,KAAa,KAAK,SAAS,gBACpE,OAAO;EACT,MAAM,QAAQ,OAAO,KAAK,KAAK;EAC/B,OAAO,SAAS,KAAK,KAAK,KAAK,KAAK;CACtC,CAAC;AACH;;;;;AAMA,SAAgB,oBAAoB,MAAc,MAA0C;CAC1F,MAAM,MAAM,YAAY,IAAI;CAE5B,MAAM,MADkB,IAAI,QAAgB,SAChB;CAC5B,IAAI,CAAC,KAAK,OAAO,KAAA;CACjB,MAAM,cAAc,IAAI,eAAe;CACvC,MAAM,aAAa,oBAAoB,WAAW;CAClD,OAAO,aACH;EACE;EACA;EACA;EACA,MAAM,WAAW,WAAW;EAC5B,iBAAiB,KAAK,SAAS,IAAI;EACnC,UAAU,qBAAqB,UAAU;CAC3C,IACA,KAAA;AACN;;AAGA,eAAsB,mBAAmB,MAAmD;CAC1F,OAAO,oBAAoB,MAAM,GAAG,aAAa,MAAM,OAAO,CAAC;AACjE;;AAGA,SAAgB,qBAAqB,IAA4B;CAC/D,MAAM,OAAO,aAAa,GAAG,GAAU,CAAC,CAAC;CACzC,OAAO,GAAG,mBAAmB,CAAC,KAAK,SAAS,IAAI,IAAI,GAAG,KAAK,MAAM;AACpE;AAEA,eAAsB,mBAAmB,IAAmC;CAC1E,MAAM,UAAU,GAAG,KAAK,GAAG,IAAI;CAC/B,IAAI,GAAG,iBAAiB;EACtB,MAAM,OAAO,GAAG,aAAa,GAAG,MAAM,OAAO;EAC7C,IAAI,CAAC,KAAK,SAAS,IAAI,GAAG,GAAG,cAAc,GAAG,MAAM,OAAO,MAAM,OAAO;CAC1E;AACF;AAEA,SAAgB,YAAY,IAA8B;CACxD,OAAO,GAAG,WAAW,WAClB,OAAO,UAAU,CAAC,CAClB,IAAI,OAAO,CAAC,CACZ,QAAQ,MAAuC,MAAM,KAAA,CAAS;AACnE;AAEA,SAAS,aAAa,IAAoB,QAAkC;CAC1E,MAAM,QAAQ,OAAO,SAAS,GAAG,YAAY,MAAM,CAAC,EAAE,KAAK;CAC3D,OAAO,SAAS,KAAK,IAAI,QAAQ,KAAA;AACnC;;AAGA,SAAgB,WAAW,IAAoB,QAAwB;CACrE,MAAM,OAAO,aAAa,IAAI,MAAM;CACpC,IAAI,CAAC,MAAM,OAAO,CAAC;CAEnB,MAAM,SAAiB,CAAC;CACxB,MAAM,QAAQ,KAAW,WAAqB;EAC5C,KAAK,MAAM,QAAQ,IAAI,YAAY;GACjC,IAAI,CAAC,WAAW,IAAI,GAAG;GACvB,MAAM,OAAO,QAAQ,IAAI;GACzB,IAAI,SAAS,KAAA,GAAW;GACxB,MAAM,OAAO,CAAC,GAAG,QAAQ,IAAI;GAC7B,MAAM,QAAQ,KAAK,SAAS,iBAAiB,OAAO,OAAO,KAAK,KAAK;GACrE,IAAI,SAAS,KAAK,GAChB,KAAK,OAAO,IAAI;QACX;IACL,MAAM,OAAO,SAAS,KAAK;IAC3B,OAAO,KAAK;KAAE;KAAM;KAAM,GAAI,SAAS,WAAW,EAAE,OAAO,YAAY,KAAK,EAAE,IAAI,CAAC;IAAG,CAAC;GACzF;EACF;CACF;CACA,KAAK,MAAM,CAAC,CAAC;CACb,OAAO;AACT;AAEA,SAAS,QAAQ,IAAoB,QAAgB,MAAkC;CACrF,IAAI,MAAM,aAAa,IAAI,MAAM;CACjC,KAAK,IAAI,IAAI,GAAG,OAAO,IAAI,KAAK,QAAQ,KAAK;EAC3C,MAAM,OAAO,SAAS,KAAK,KAAK,EAAE;EAClC,IAAI,CAAC,MAAM,OAAO,KAAA;EAClB,IAAI,MAAM,KAAK,SAAS,GAAG,OAAO;EAClC,MAAM,OAAO,OAAO,KAAK,KAAK;EAC9B,MAAM,SAAS,IAAI,IAAI,OAAO,KAAA;CAChC;AAEF;AAEA,SAAgB,QAAQ,IAAoB,QAAgB,MAAyB;CACnF,OAAO,QAAQ,IAAI,QAAQ,IAAI,MAAM,KAAA;AACvC;AAEA,SAAgB,UAAU,IAAoB,QAAgB,MAAoC;CAChG,MAAM,OAAO,QAAQ,IAAI,QAAQ,IAAI;CACrC,OAAO,QAAQ,KAAK,SAAS,iBAAiB,YAAY,KAAK,KAAK,IAAI,KAAA;AAC1E;AAEA,SAAS,aAAa,IAAoB,QAAsB;CAC9D,IAAI,MAAM,aAAa,IAAI,MAAM;CACjC,IAAI,CAAC,KAAK;EACR,MAAM,OAAO,SAAS,QAAQ,IAAI;EAClC,GAAG,WAAW,WAAW,KAAK,IAAI;EAClC,MAAM,KAAK;CACb;CACA,OAAO;AACT;;AAGA,SAAS,aAAa,IAAoB,QAAgB,MAAkC;CAC1F,IAAI,MAAM,aAAa,IAAI,MAAM;CACjC,KAAK,MAAM,WAAW,KAAK,MAAM,GAAG,EAAE,GAAG;EACvC,IAAI,OAAO,SAAS,KAAK,OAAO;EAChC,IAAI,CAAC,MAAM;GACT,OAAO,SAAS,SAAS,IAAI;GAC7B,IAAI,WAAW,KAAK,IAAI;EAC1B;EACA,MAAM,OAAO,OAAO,KAAK,KAAK;EAC9B,IAAI,CAAC,SAAS,IAAI,GAAG,OAAO,KAAA;EAC5B,MAAM;CACR;CACA,OAAO;AACT;;;;;AAQA,SAAgB,UACd,IACA,QACA,MACA,OACW;CACX,MAAM,SAAS,aAAa,IAAI,QAAQ,IAAI;CAC5C,IAAI,CAAC,QAAQ,OAAO;CAEpB,MAAM,OAAO,KAAK,KAAK,SAAS;CAChC,MAAM,WAAW,SAAS,QAAQ,IAAI;CACtC,IAAI,UAAU;EACZ,IAAI,SAAS,SAAS,kBAAkB,SAAS,SAAS,KAAK,MAAM,UACnE,OAAO;EAET,SAAS,QAAQ,eAAe,KAAK;EACrC,OAAO;CACT;CACA,OAAO,WAAW,KAAK,SAAS,MAAM,KAAK,UAAU,KAAK,CAAC,CAAC;CAC5D,OAAO;AACT;;;;;;AAOA,SAAgB,SACd,IACA,YACA,UACA,MACA,WACS;CACT,MAAM,SAAS,QAAQ,IAAI,YAAY,IAAI;CAC3C,IAAI,CAAC,UAAU,QAAQ,IAAI,UAAU,IAAI,GAAG,OAAO;CAEnD,MAAM,SAAS,aAAa,IAAI,UAAU,IAAI;CAC9C,IAAI,CAAC,QAAQ,OAAO;CAEpB,MAAM,OAAO,KAAK,KAAK,SAAS;CAChC,MAAM,OAAO,OAAO,SAAS,iBAAiB,KAAA,IAAY,YAAY,OAAO,KAAK;CAClF,IAAI,SAAS,KAAA,KAAa,WACxB,OAAO,WAAW,KAAK,SAAS,MAAM,KAAK,UAAU,UAAU,IAAI,CAAC,CAAC,CAAC;MACjE;EACL,MAAM,OAAO,aAAa,MAAa,CAAC,CAAC;EACzC,MAAM,OAAa,gBAAgB,MAAM,KAAK,IAAI;EAClD,OAAO,WAAW,KAAK,KAAK,KAAK,WAAW,EAAE;CAChD;CACA,OAAO;AACT;;AAGA,SAAgB,aAAa,OAAgB,SAAmB,CAAC,GAAe;CAC9E,IAAI,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,GAC5D,OAAO,OAAO,QAAQ,KAAK,CAAC,CAAC,SAAS,CAAC,GAAG,OAAO,aAAa,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC;CAElF,OAAO,OAAO,SAAS,IAAI,CAAC,MAAM,IAAI,CAAC;AACzC;AAEA,MAAM,WAAW,QACf,MACI;CACE,OAAO;EAAE,MAAM,IAAI,MAAM,OAAO;EAAG,QAAQ,IAAI,MAAM;CAAO;CAC5D,KAAK;EAAE,MAAM,IAAI,IAAI,OAAO;EAAG,QAAQ,IAAI,IAAI;CAAO;AACxD,IACA,KAAA;;;;;AAMN,SAAgB,WACd,IACA,QACA,OAAiB,CAAC,GACO;CACzB,MAAM,OAAO,KAAK,WAAW,IAAI,SAAS,GAAG,YAAY,MAAM,IAAI,QAAQ,IAAI,QAAQ,IAAI;CAC3F,OAAO,SAAS,MAAM,OAAO,KAAA,EAAO,GAAG;AACzC;;AAGA,SAAgB,mBAAmB,IAA6C;CAC9E,OAAO,QAAQ,GAAG,WAAW,GAAG;AAClC;;AAGA,SAAgB,cACd,IACA,QACA,MACoB;CACpB,MAAM,OAAO,QAAQ,IAAI,QAAQ,IAAI;CACrC,IAAI,CAAC,MAAM,OAAO,KAAA;CAClB,OAAO,aAAc,KAAK,SAAS,iBAAiB,OAAO,KAAK,KAAa,CAAC,CAAC;AACjF;;;;AC5XA,MAAM,iBAAiB;CAAC;CAAQ;CAAO;CAAO;CAAO;AAAM;AAE3D,SAAS,mBAAmB,QAAgB,SAA4B;CACtE,IAAI;EACF,OAAO,IAAI,KAAK,YAAY,QAAQ,EAAE,MAAM,UAAU,YAAY,WAAW,CAAC,CAAC,CAC5E,gBAAgB,CAAC,CACjB,iBAAiB,QAAQ,MAAM,MAAM,OAAO,CAAC,CAC7C,MAAM,GAAG,MAAM,eAAe,QAAQ,CAAC,IAAI,eAAe,QAAQ,CAAC,CAAC;CACzE,QAAQ;EACN,OAAO,CAAC;CACV;AACF;;;;;;;;;AAUA,SAAgB,SACd,YACA,SACA,eACc;CACd,MAAM,WAAyB,CAAC;CAChC,MAAM,yBAAS,IAAI,IAAoC;CAEvD,KAAK,MAAM,CAAC,QAAQ,YAAY,SAAS;EACvC,MAAM,qBAAK,IAAI,IAAuB;EACtC,KAAK,MAAM,CAAC,MAAM,UAAU,SAAS;GACnC,IAAI,CAAC,MAAM,SAAS,GAAG,GAAG;GAC1B,MAAM,MAAM,GAAG,WAAW,GAAG;GAC7B,IAAI;IACF,GAAG,IAAI,MAAM,aAAa,KAAK,CAAC;GAClC,SAAS,OAAO;IACd,MAAM,aAAa,gBAAgB,KAAK;IACxC,SAAS,KAAK;KACZ,OAAO,aAAa,UAAU;KAC9B,MAAM;KACN;KACA;KACA,SAAS,aACL,wBAAwB,IAAI,KAAK,OAAO,KAAM,MAAgB,YAC9D,IAAI,IAAI,KAAK,OAAO,2EAA4E,MAAgB;IACtH,CAAC;GACH;EACF;EACA,OAAO,IAAI,QAAQ,EAAE;CACvB;CAGA,MAAM,QAAQ,CAAC,eAAe,GAAG,CAAC,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,QAAQ,MAAM,MAAM,aAAa,CAAC;CACtF,MAAM,QAAQ,IAAI,IAAI,CAAC,GAAG,OAAO,OAAO,CAAC,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;CAExE,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,MAAM,GAAG,WAAW,GAAG;EAC7B,MAAM,YAAY,MAAM,MAAM,MAAM,OAAO,IAAI,CAAC,CAAC,EAAE,IAAI,IAAI,CAAC;EAC5D,IAAI,CAAC,WAAW;EAChB,MAAM,WAAW,kBAAkB,OAAO,IAAI,SAAS,CAAC,CAAE,IAAI,IAAI,CAAE;EAEpE,KAAK,MAAM,UAAU,OAAO;GAC1B,MAAM,QAAQ,OAAO,IAAI,MAAM,CAAC,EAAE,IAAI,IAAI;GAC1C,IAAI,SAAS,WAAW,WAAW;IACjC,MAAM,QAAQ,kBAAkB,KAAK;IACrC,IAAI,UAAU,UACZ,SAAS,KAAK;KACZ,OAAO;KACP,MAAM;KACN;KACA;KACA,SAAS,WAAW,OAAO,mCAAmC,UAAU,SAAS,IAAI,cAAc,YAAY,OAAO,UAAU,SAAS,OAAO;IAClJ,CAAC;GAEL;GACA,IAAI,CAAC,OAAO;GAEZ,KAAK,MAAM,UAAU,YAAY,KAAK,GAAG;IACvC,MAAM,UAAU,mBAAmB,QAAQ,OAAO,OAAO,CAAC,CAAC,QACxD,MAAM,EAAE,KAAK,OAAO,QACvB;IACA,IAAI,QAAQ,SAAS,GACnB,SAAS,KAAK;KACZ,OAAO;KACP,MAAM;KACN;KACA;KACA,SAAS,WAAW,OAAO,YAAY,OAAO,KAAK,QAAQ,IAAI,WAAW,QAAQ,KAAK,MAAM,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;IACpH,CAAC;GAEL;EACF;CACF;CAEA,OAAO;AACT;;;ACZA,MAAM,oBAAoB;CAAC;CAAsB;CAAc;AAAa;AAE5E,MAAa,sBAAsB;AACnC,MAAM,gBAAgB;CACpB,GAAG;CACH;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAGA,MAAM,kBACJ;;AAEF,MAAM,SAAS;;AAYf,SAAgB,eAAe,MAA2B;CACxD,MAAM,aAA0B,CAAC;CACjC,MAAM,WAAW,SAAiB,SAA4B;EAC5D,KAAK,MAAM,SAAS,KAAK,SAAS,OAAO,GAAG;GAC1C,MAAM,CAAC,OAAO,OAAO,MAAM,QAAS;GACpC,WAAW,KAAK;IAAE;IAAM,OAAO,MAAM;IAAI;IAAO;GAAI,CAAC;EACvD;CACF;CACA,QAAQ,iBAAiB,YAAY;CACrC,QAAQ,QAAQ,MAAM;CACtB,OAAO,WAAW,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AACpD;AAEA,SAAS,WAAW,MAAwB;CAC1C,MAAM,SAAS,CAAC,CAAC;CACjB,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,IAAI,KAAK,WAAW,CAAC,MAAM,IAAI,OAAO,KAAK,IAAI,CAAC;CACtF,OAAO;AACT;AAEA,SAAS,WAAW,QAAkB,QAAgB;CACpD,IAAI,MAAM;CACV,IAAI,OAAO,OAAO,SAAS;CAC3B,OAAO,MAAM,MAAM;EACjB,MAAM,MAAM,KAAK,MAAM,MAAM,QAAQ,CAAC;EACtC,IAAI,OAAO,QAAQ,QAAQ,MAAM;OAC5B,OAAO,MAAM;CACpB;CACA,OAAO;EAAE,MAAM;EAAK,QAAQ,SAAS,OAAO;CAAK;AACnD;AAaA,SAAS,eAAe,IAAoC;CAC1D,MAAM,UAAU,YAAY,EAAE;CAC9B,MAAM,yBAAS,IAAI,IAAmC;CACtD,KAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,0BAAU,IAAI,IAAsB;EAC1C,KAAK,MAAM,QAAQ,WAAW,IAAI,MAAM,GACtC,QAAQ,IAAI,KAAK,KAAK,KAAK,GAAG,GAAG;GAAE,MAAM,KAAK;GAAM,OAAO,KAAK;EAAM,CAAC;EAEzE,OAAO,IAAI,QAAQ,OAAO;CAC5B;CACA,OAAO;EAAE,MAAM,GAAG;EAAO;EAAS;EAAQ;CAAG;AAC/C;AAEA,eAAe,YAAY,MAA+B;CACxD,IAAI;EACF,MAAM,MAAM,MAAM,OAAO,GAAG,cAAc,IAAI,CAAC,CAAC,KAAK,KAAK,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO;EACpF,MAAM,aAAa,IAAI,WAAW;EAClC,IAAI,CAAC,cAAc,OAAO,eAAe,YAAY,CAAC,WAAW,YAC/D,OAAO;GACL,IAAI;GACJ,OAAO;GACP,MAAM;GACN,SAAS;EACX;EAGF,MAAM,aAAa,WAAW;EAC9B,MAAM,yBAAS,IAAI,IAAmC;EACtD,MAAM,UAAoB,CAAC;EAE3B,KAAK,MAAM,CAAC,QAAQ,YAAY,OAAO,QAAQ,UAAU,GAAG;GAC1D,IAAI,CAAC,WAAW,OAAO,YAAY,UAAU;IAC3C,QAAQ,KAAK,MAAM;IACnB;GACF;GACA,MAAM,0BAAU,IAAI,IAAsB;GAC1C,KAAK,MAAM,YAAY,aAAa,OAAO,GAAG;IAC5C,IAAI,QAAa;IACjB,KAAK,MAAM,WAAW,UAAU,QAAQ,QAAQ;IAChD,QAAQ,IACN,SAAS,KAAK,GAAG,GACjB,OAAO,UAAU,WACb;KAAE,MAAM;KAAU;IAAM,IACxB,EAAE,MAAM,OAAO,UAAU,aAAa,aAAa,QAAQ,CACjE;GACF;GACA,OAAO,IAAI,QAAQ,OAAO;EAC5B;EAEA,IAAI,QAAQ,SAAS,GACnB,OAAO;GACL,IAAI;GACJ,OAAO;GACP,MAAM;GACN,SAAS,QAAQ,KAAK,MAAM,WAAW,EAAE,+BAA+B,CAAC,CAAC,KAAK,IAAI;EACrF;EAGF,OAAO;GACL,IAAI;GACJ,MAAM;IAAE,MAAM,OAAO,WAAW,QAAQ,EAAE;IAAG,SAAS,CAAC,GAAG,OAAO,KAAK,CAAC;IAAG;GAAO;EACnF;CACF,SAAS,OAAY;EACnB,OAAO;GACL,IAAI;GACJ,OAAO;GACP,MAAM;GACN,SAAS,gCAAgC,MAAM,WAAW,OAAO,KAAK;EACxE;CACF;AACF;AAEA,eAAe,mBAAmB,MAAc,SAAmC;CACjF,IAAI;CACJ,IAAI;EACF,MAAM,KAAK,MAAM,mBAAmB,IAAI;EACxC,IAAI,IAAI,YAAY,GAAG,MAAM,OAAO;GAAE,IAAI;GAAM,MAAM,eAAe,EAAE;EAAE;CAC3E,SAAS,OAAO;EACd,WAAW;CACb;CAEA,IAAI,SAAS,OAAO,YAAY,IAAI;CAEpC,OAAO,WACH;EACE,IAAI;EACJ,OAAO;EACP,MAAM;EACN,SAAS,+BAA+B,SAAS;CACnD,IACA;EACE,IAAI;EACJ,OAAO;EACP,MAAM;EACN,SACE;CACJ;AACN;AAEA,eAAsB,eAAe,SAAmD;CACtF,MAAM,EAAE,MAAM,WAAW;CACzB,MAAM,UAAU,OAAO,WAAW;CAClC,MAAM,oBAAoB,OAAO,WAAW,CAAC;CAC7C,MAAM,gBAAgB,OAAO,iBAAiB;CAC9C,MAAM,cAAc,QAAQ,UAAU,CAAC;CAEvC,MAAM,QAAQ,GAAG,KAAK,SAAS;EAC7B,KAAK;EACL,UAAU;EACV,QAAQ,CAAC,GAAG,mBAAmB,GAAG,WAAW;CAC/C,CAAC;CAED,MAAM,SAA0B,CAAC;CACjC,MAAM,eAAqC,CAAC;CAG5C,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,SAAS,MAAM,mBAAmB,MAAM,QAAQ,YAAY,IAAI;EACtE,IAAI,CAAC,OAAO,IAAI;GACd,KAAK,MAAM,WAAW,OAAO,QAAQ,MAAM,IAAI,GAC7C,OAAO,KAAK;IAAE;IAAM,OAAO,OAAO;IAAO,MAAM,OAAO;IAAM;GAAQ,CAAC;GAEvE;EACF;EAEA,MAAM,EAAE,SAAS;EACjB,MAAM,EAAE,OAAO;EACf,MAAM,MAAM,QAAiB,YAC3B,MAAM,SAAS,WAAW,IAAI,QAAQ,OAAO,IAAI,KAAA;EACnD,MAAM,OAAO,UACX,OAAO,KAAK;GAAE,GAAG;GAAO;GAAM,YAAY,KAAK;EAAK,CAAC;EAEvD,KAAK,MAAM,YAAY,mBACrB,IAAI,CAAC,KAAK,QAAQ,SAAS,QAAQ,GACjC,IAAI;GACF,OAAO;GACP,MAAM;GACN,SAAS,8BAA8B,SAAS;GAChD,OAAO,KAAK,mBAAmB,EAAE,IAAI,KAAA;GACrC,QAAQ;EACV,CAAC;EAIL,MAAM,QAAkB,CAAC;EACzB,MAAM,uBAAO,IAAI,IAAY;EAC7B,KAAK,MAAM,UAAU,KAAK,SACxB,KAAK,MAAM,YAAY,KAAK,OAAO,IAAI,MAAM,CAAC,CAAE,KAAK,GACnD,IAAI,CAAC,KAAK,IAAI,QAAQ,GAAG;GACvB,KAAK,IAAI,QAAQ;GACjB,MAAM,KAAK,QAAQ;EACrB;EAIJ,KAAK,MAAM,UAAU,KAAK,SAAS;GACjC,MAAM,UAAU,KAAK,OAAO,IAAI,MAAM;GACtC,KAAK,MAAM,YAAY,OAAO;IAC5B,IAAI,QAAQ,IAAI,QAAQ,GAAG;IAC3B,MAAM,eAAe,CAAC,eAAe,GAAG,KAAK,OAAO,CAAC,CAAC,MACnD,MAAM,MAAM,UAAU,KAAK,OAAO,IAAI,CAAC,CAAC,EAAE,IAAI,QAAQ,CACzD;IACA,IAAI;KACF,OAAO;KACP,MAAM;KACN,SAAS,WAAW,OAAO,oBAAoB,SAAS;KACxD,OAAO,GAAG,MAAM;KAChB;KACA,MAAM,SAAS,MAAM,GAAG;KACxB;IACF,CAAC;GACH;EACF;EAEA,MAAM,0BAAyB,IAAI,IAAI;EACvC,KAAK,MAAM,UAAU,KAAK,SAAS;GACjC,MAAM,0BAAU,IAAI,IAAoB;GACxC,QAAQ,IAAI,QAAQ,OAAO;GAC3B,KAAK,MAAM,CAAC,UAAU,SAAS,KAAK,OAAO,IAAI,MAAM,GAAI;IACvD,IAAI,KAAK,SAAS,YAAY,KAAK,UAAU,KAAA,GAAW;IACxD,QAAQ,IAAI,UAAU,KAAK,KAAK;IAChC,IAAI,KAAK,MAAM,WAAA,SAAsB,GACnC,IAAI;KACF,OAAO;KACP,MAAM;KACN,SAAS,WAAW,OAAO,SAAS,SAAS,gBAAgB,YAAY,KAAK,EAAE;KAChF,OAAO,GAAG,QAAQ,SAAS,MAAM,GAAG,CAAC;KACrC;KACA,MAAM,SAAS,MAAM,GAAG;IAC1B,CAAC;GAEL;EACF;EAEA,KAAK,MAAM,WAAW,SAAS,KAAK,MAAM,SAAS,aAAa,GAAG;GACjE,MAAM,UAAU,QAAQ,IAAI,MAAM,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,GAAG;GACjE,IAAI;IACF,OAAO,QAAQ;IACf,MAAM,QAAQ;IACd,SAAS,QAAQ;IACjB,OAAO,GAAG,QAAQ,QAAQ,OAAO;IACjC,QAAQ,QAAQ;IAChB,MAAM;GACR,CAAC;EACH;EAEA,aAAa,KAAK;GAChB,MAAM,KAAK;GACX;GACA,SAAS,KAAK;GACd,QAAQ,KAAK;GACb;GACA;EACF,CAAC;CACH;CAGA,MAAM,QAAQ,IAAI,IAAI,aAAa,KAAK,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;CAC1D,MAAM,kBAAkB,IAAI,IAAI,KAAK;CACrC,MAAM,cAAc,QAAQ,OAAO,CAAA,mDAAoB;CACvD,MAAM,cACJ,YAAY,WAAW,IACnB,CAAC,IACD,GACG,KAAK,aAAa;EACjB,KAAK;EACL,UAAU;EACV,QAAQ,CAAC,GAAG,eAAe,GAAG,WAAW;CAC3C,CAAC,CAAC,CACD,QAAQ,MAAM,CAAC,gBAAgB,IAAI,CAAC,CAAC;CAE9C,IAAI,YAAY,SAAS,GAAG;EAC1B,MAAM,yBAAS,IAAI,IAAY;EAE/B,KAAK,MAAM,QAAQ,aAAa;GAC9B,IAAI;GACJ,IAAI;IACF,OAAO,GAAG,aAAa,MAAM,OAAO;GACtC,QAAQ;IACN;GACF;GACA,KAAK,MAAM,SAAS,KAAK,MAAM,UAAU,KAAK,CAAC,GAAG,OAAO,IAAI,KAAK;GAElE,MAAM,SAAS,WAAW,IAAI;GAC9B,MAAM,WAAW,OAAe,SAA8B;IAC5D,OAAO,WAAW,QAAQ,KAAK;IAC/B,KAAK,WAAW,QAAQ,GAAG;GAC7B;GAEA,KAAK,MAAM,OAAO,eAAe,IAAI,GAAG;IACtC,IAAI,IAAI,SAAS,cAAc;KAC7B,IAAI,CAAC,MAAM,IAAI,IAAI,KAAK,GACtB,OAAO,KAAK;MACV;MACA,OAAO;MACP,MAAM;MACN,SAAS,uBAAuB,IAAI,MAAM;MAC1C,OAAO,QAAQ,IAAI,OAAO,IAAI,GAAG;MACjC,YAAY,IAAI;KAClB,CAAC;KAEH;IACF;IAEA,MAAM,CAAC,MAAM,GAAG,QAAQ,IAAI,MAAM,MAAM,GAAG;IAC3C,MAAM,aAAa,MAAM,IAAI,IAAI;IACjC,IAAI,CAAC,YAAY;IACjB,MAAM,UAAU,KAAK,KAAK,GAAG;IAE7B,IAAI,CADW,WAAW,MAAM,MAAM,MAAM,MAAM,WAAW,EAAE,WAAW,GAAG,QAAQ,EAAE,CAC7E,GACR,OAAO,KAAK;KACV;KACA,OAAO;KACP,MAAM;KACN,SAAS,6BAA6B,IAAI,MAAM;KAChD,OAAO,QAAQ,IAAI,OAAO,IAAI,GAAG;KACjC,YAAY;KACZ,MAAM;IACR,CAAC;GAEL;EACF;EAGA,IAAI,QAAQ,WAAW,OACrB,KAAK,MAAM,cAAc,cAAc;GACrC,IAAI,CAAC,OAAO,IAAI,WAAW,IAAI,GAAG;IAChC,OAAO,KAAK;KACV,MAAM,WAAW;KACjB,OAAO;KACP,MAAM;KACN,SAAS,eAAe,WAAW,KAAK;KACxC,OAAO,WAAW,KAAK,mBAAmB,WAAW,EAAE,IAAI,KAAA;KAC3D,YAAY,WAAW;IACzB,CAAC;IACD;GACF;GACA,KAAK,MAAM,WAAW,WAAW,OAAO;IACtC,MAAM,OAAO,QAAQ,MAAM,GAAG,CAAC,CAAC,IAAI;IACpC,IAAI,OAAO,IAAI,IAAI,GAAG;IACtB,MAAM,SAAS,WAAW,QAAQ,MAAM,MAAM,WAAW,OAAO,IAAI,CAAC,CAAC,EAAE,IAAI,OAAO,CAAC;IACpF,OAAO,KAAK;KACV,MAAM,WAAW;KACjB,OAAO;KACP,MAAM;KACN,SAAS,QAAQ,QAAQ;KACzB,OACE,WAAW,MAAM,SACb,WAAW,WAAW,IAAI,QAAQ,QAAQ,MAAM,GAAG,CAAC,IACpD,KAAA;KACN,YAAY,WAAW;KACvB,MAAM,QAAQ,MAAM,GAAG;IACzB,CAAC;GACH;EACF;CAEJ;CAEA,OAAO;EAAE;EAAQ;EAAc;EAAO,oBAAoB,YAAY;CAAO;AAC/E;;AAGA,SAAgB,aAAa,MAAc,MAAsB;CAC/D,OAAO,KAAK,SAAS,MAAM,IAAI;AACjC;;;ACvdA,MAAM,eAAe,CAAC,iBAAiB,aAAa;AACpD,MAAM,aAAa;CAAC;CAAM;CAAO;CAAO;CAAM;CAAO;CAAO;AAAM;AAElE,MAAa,wBAAsC;CACjD,eAAe;CACf,SAAS,CAAC;CACV,SAAS;CACT,WAAW,CAAC;CACZ,eAAe;CACf,MAAM;AACR;AAEA,MAAM,cAAc,OAAO,aAAa;AAExC,SAAS,QAAQ,MAAoC;CACnD,OAAOA,OAAW,IAAe;CACjC,QAAQ,MAAM,MAAd;EACE,KAAK;EACL,KAAK;EACL,KAAK,kBACH,OAAO,KAAK;EACd,KAAK,WACH,OAAO,KAAK;EACd,KAAK,mBACH,OAAO,KAAK,YAAY,WAAW,IAAK,KAAK,OAAO,EAAE,EAAE,OAAO,UAAU,KAAM;EACjF,KAAK,mBAAmB;GACtB,MAAM,QAAQ,KAAK,SAAS,KAAK,MAAe,QAAQ,CAAC,CAAC;GAC1D,OAAO,MAAM,SAAS,WAAW,IAAI,cAAc;EACrD;EACA,KAAK,oBAAoB;GACvB,MAAM,SAAkC,CAAC;GACzC,KAAK,MAAM,QAAQ,KAAK,YAAY;IAClC,MAAM,MAAMC,QAAgB,IAAI;IAChC,MAAM,QAAQ,QAAQ,KAAA,IAAY,cAAc,QAAQ,KAAK,KAAK;IAClE,IAAI,UAAU,aAAa,OAAO;IAClC,OAAO,OAAiB;GAC1B;GACA,OAAO;EACT;EACA,SACE,OAAO;CACX;AACF;;AAGA,SAAS,iBAAiB,aAA2C;CACnE,MAAM,OAAOD,OAAW,WAAW;CACnC,IAAI,MAAM,SAAS,kBAAkB,OAAO,iBAAiB,KAAK,UAAU,EAAE;CAC9E,OAAOE,SAAa,IAAI,IAAI,OAAO,KAAA;AACrC;AAEA,MAAM,iBAAiB,UACrB,MAAM,QAAQ,KAAK,KAAK,MAAM,OAAO,MAAM,OAAO,MAAM,QAAQ;AAElE,SAAS,MAAM,QAAiC,QAAsB,OAAiB;CACrF,MAAM,UACJ,KACA,OACA,aACG;EACH,IAAI,EAAE,OAAO,SAAS;EACtB,MAAM,QAAQ,OAAO;EACrB,IAAI,MAAM,KAAK,GAAG,OAAgB,OAAO;OACpC,MAAM,KAAK,IAAI,IAAI,2CAA2C,SAAS,GAAG;CACjF;CAEA,OAAO,kBAAkB,MAAM,OAAO,MAAM,YAAY,MAAM,IAAI,kBAAkB;CACpF,OAAO,WAAW,eAAe,6BAA6B;CAC9D,OACE,YACC,MAAM,OAAO,MAAM,YAAY,cAAc,CAAC,GAC/C,8BACF;CACA,OACE,cACC,MACC,OAAO,MAAM,YAAY,MAAM,QAAQ,OAAO,OAAO,CAAC,CAAC,CAAC,OAAO,SAAS,cAAc,IAAI,CAAC,GAC7F,4BACF;CACA,OAAO,kBAAkB,MAAM,OAAO,MAAM,WAAW,eAAe;CACtE,OAAO,SAAS,MAAM,OAAO,MAAM,WAAW,eAAe;AAC/D;;AAGA,SAAgB,kBAAkB,MAAc,OAAO,oBAAwC;CAC7F,MAAM,QAAkB,CAAC;CACzB,MAAM,SAAuB,EAAE,GAAG,sBAAsB;CAExD,IAAI,KAAK,SAAS,OAAO,GAAG;EAC1B,IAAI;GACF,MAAM,OAAO,KAAK,MAAM,IAAI;GAC5B,IAAI,QAAQ,OAAO,SAAS,UAAU,MAAM,MAAM,QAAQ,KAAK;EACjE,SAAS,OAAO;GACd,MAAM,KAAK,mBAAmB,KAAK,SAAS,IAAI,EAAE,IAAK,MAAgB,SAAS;EAClF;EACA,OAAO;GAAE;GAAQ;GAAM;EAAM;CAC/B;CAEA,IAAI;EACF,MAAM,WAAY,YAAY,IAAI,CAAC,CAAC,QAAgB;EACpD,MAAM,SAAS,iBAAiB,UAAU,MAAM,eAAe,UAAU,IAAI;EAC7E,IAAI,CAAC,QACH,MAAM,KAAK,GAAG,KAAK,SAAS,IAAI,EAAE,0CAA0C;OACvE;GACL,MAAM,SAAkC,CAAC;GACzC,KAAK,MAAM,OAAO;IAChB;IACA;IACA;IACA;IACA;IACA;GACF,GAAG;IACD,MAAM,OAAOC,SAAa,QAAQ,GAAG;IACrC,IAAI,CAAC,MAAM;IACX,MAAM,QAAQ,QAAQ,KAAK,KAAK;IAChC,IAAI,UAAU,aACZ,MAAM,KAAK,IAAI,IAAI,gDAAgD;SAChE,OAAO,OAAO;GACrB;GACA,MAAM,QAAQ,QAAQ,KAAK;EAC7B;CACF,SAAS,OAAO;EACd,MAAM,KAAK,mBAAmB,KAAK,SAAS,IAAI,EAAE,IAAK,MAAgB,SAAS;CAClF;CAEA,OAAO;EAAE;EAAQ;EAAM;CAAM;AAC/B;;AAGA,SAAgB,iBAAiB,MAAc,YAAyC;CACtF,MAAM,aAAa,aACf,CAAC,KAAK,QAAQ,MAAM,UAAU,CAAC,IAC/B,aAAa,SAAS,SAAS,WAAW,KAAK,QAAQ,KAAK,KAAK,MAAM,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC;CAE7F,KAAK,MAAM,QAAQ,YACjB,IAAI,GAAG,WAAW,IAAI,GAAG,OAAO,kBAAkB,GAAG,aAAa,MAAM,OAAO,GAAG,IAAI;CAExF,OAAO;EAAE,QAAQ,EAAE,GAAG,sBAAsB;EAAG,OAAO,CAAC;CAAE;AAC3D;;;ACpJA,eAAe,iBAAiB,MAAc,QAAoB;CAChE,MAAM,UAAU,OAAO,WAAW;CAClC,MAAM,QAAQ,GAAG,KAAK,SAAS;EAC7B,KAAK;EACL,UAAU;EACV,QAAQ;GAAC;GAAsB;GAAc;EAAa;CAC5D,CAAC;CAED,MAAM,eAAoC,CAAC;CAC3C,MAAM,cAAwB,CAAC;CAE/B,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,MAAM,KAAK,SAAS,MAAM,IAAI;EACpC,IAAI;GACF,MAAM,KAAK,MAAM,mBAAmB,IAAI;GACxC,IAAI,CAAC,MAAM,CAAC,GAAG,MAAM,YAAY,KAAK,GAAG;QACpC,aAAa,KAAK;IAAE;IAAK,MAAM,GAAG;IAAM;GAAG,CAAC;EACnD,QAAQ;GACN,YAAY,KAAK,GAAG;EACtB;CACF;CAEA,OAAO;EAAE;EAAc;CAAY;AACrC;;;;;AAMA,eAAsB,YAAY,MAAc,YAAuC;CACrF,MAAM,SAAS,kBAAkB,MAAM,aAAa,EAAE,WAAW,IAAI,CAAC,CAAC;CACvE,OAAO;EAAE;EAAM;EAAQ,OAAO,CAAC;EAAG,GAAI,MAAM,iBAAiB,MAAM,MAAM;CAAG;AAC9E;;;;;AAMA,eAAsB,kBAAkB,MAAc,YAAuC;CAC3F,MAAM,EAAE,QAAQ,UAAU,iBAAiB,MAAM,UAAU;CAC3D,OAAO;EAAE;EAAM;EAAQ;EAAO,GAAI,MAAM,iBAAiB,MAAM,MAAM;CAAG;AAC1E;;AAGA,MAAa,SAAS,YAAoB,YAAsB,CAAC,YAAY,GAAG,OAAO,CAAC,CAAC,KAAK,GAAG"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { TranslateProvider, TranslateProvider as TranslateProvider$1 } from "dialexjs";
|
|
2
|
+
//#region src/translate/claude.d.ts
|
|
3
|
+
interface ClaudeProviderOptions {
|
|
4
|
+
/** Defaults to the `ANTHROPIC_API_KEY` environment variable. */
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
/** Anthropic model ID. */
|
|
7
|
+
model?: string;
|
|
8
|
+
/** Strings per request. */
|
|
9
|
+
batchSize?: number;
|
|
10
|
+
/** Extra guidance appended to the prompt, e.g. "This is a banking app; use formal register." */
|
|
11
|
+
context?: string;
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
/** Injected for tests. */
|
|
14
|
+
fetch?: typeof fetch;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Translates with the Anthropic Messages API.
|
|
18
|
+
* Reads `ANTHROPIC_API_KEY` unless `apiKey` is given. Nothing is sent until `translate` is called.
|
|
19
|
+
*/
|
|
20
|
+
export declare function claudeProvider(options?: ClaudeProviderOptions): TranslateProvider$1;
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/translate/deepl.d.ts
|
|
23
|
+
interface DeepLProviderOptions {
|
|
24
|
+
/** Defaults to the `DEEPL_API_KEY` environment variable. Keys ending in `:fx` use the free API. */
|
|
25
|
+
apiKey?: string;
|
|
26
|
+
formality?: "default" | "more" | "less" | "prefer_more" | "prefer_less";
|
|
27
|
+
baseUrl?: string;
|
|
28
|
+
/** Injected for tests. */
|
|
29
|
+
fetch?: typeof fetch;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Translates with the DeepL API.
|
|
33
|
+
* Reads `DEEPL_API_KEY` unless `apiKey` is given. Nothing is sent until `translate` is called.
|
|
34
|
+
*/
|
|
35
|
+
export declare function deeplProvider(options?: DeepLProviderOptions): TranslateProvider$1;
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/translate/gemini.d.ts
|
|
38
|
+
interface GeminiProviderOptions {
|
|
39
|
+
/** Defaults to the `GEMINI_API_KEY` environment variable, then `GOOGLE_API_KEY`. */
|
|
40
|
+
apiKey?: string;
|
|
41
|
+
/** Model ID. Defaults to the `GEMINI_MODEL` environment variable, then a built-in default. */
|
|
42
|
+
model?: string;
|
|
43
|
+
/** Strings per request. */
|
|
44
|
+
batchSize?: number;
|
|
45
|
+
/** Extra guidance appended to the prompt, e.g. "This is a banking app; use formal register." */
|
|
46
|
+
context?: string;
|
|
47
|
+
/** @default "https://generativelanguage.googleapis.com/v1beta" */
|
|
48
|
+
baseUrl?: string;
|
|
49
|
+
/** Sampling temperature. @default 0 */
|
|
50
|
+
temperature?: number;
|
|
51
|
+
/** Injected for tests. */
|
|
52
|
+
fetch?: typeof fetch;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Translates with the Google Gemini API.
|
|
56
|
+
* Reads `GEMINI_API_KEY` (or `GOOGLE_API_KEY`) unless `apiKey` is given. The key is sent in a
|
|
57
|
+
* header, never in the URL. Nothing is sent until `translate` is called.
|
|
58
|
+
*/
|
|
59
|
+
export declare function geminiProvider(options?: GeminiProviderOptions): TranslateProvider$1;
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/translate/openai.d.ts
|
|
62
|
+
interface OpenAIProviderOptions {
|
|
63
|
+
/** Defaults to the `OPENAI_API_KEY` environment variable. */
|
|
64
|
+
apiKey?: string;
|
|
65
|
+
/** Model ID. Defaults to the `OPENAI_MODEL` environment variable, then a built-in default. */
|
|
66
|
+
model?: string;
|
|
67
|
+
/** Strings per request. */
|
|
68
|
+
batchSize?: number;
|
|
69
|
+
/** Extra guidance appended to the prompt, e.g. "This is a banking app; use formal register." */
|
|
70
|
+
context?: string;
|
|
71
|
+
/**
|
|
72
|
+
* API root, for OpenAI-compatible servers (Azure OpenAI, Ollama, OpenRouter, ...).
|
|
73
|
+
* With a custom `baseUrl` an API key is optional.
|
|
74
|
+
* @default "https://api.openai.com/v1"
|
|
75
|
+
*/
|
|
76
|
+
baseUrl?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Sampling temperature. Left out by default because some models only accept their default.
|
|
79
|
+
*/
|
|
80
|
+
temperature?: number;
|
|
81
|
+
/** Injected for tests. */
|
|
82
|
+
fetch?: typeof fetch;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Translates with the OpenAI Chat Completions API (the API behind ChatGPT), or any server that
|
|
86
|
+
* speaks it. Reads `OPENAI_API_KEY` unless `apiKey` is given. Nothing is sent until `translate`
|
|
87
|
+
* is called.
|
|
88
|
+
*/
|
|
89
|
+
export declare function openaiProvider(options?: OpenAIProviderOptions): TranslateProvider$1;
|
|
90
|
+
//#endregion
|
|
91
|
+
export type { ClaudeProviderOptions, DeepLProviderOptions, GeminiProviderOptions, OpenAIProviderOptions, TranslateProvider };
|
|
92
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":["TranslateProvider","TranslateProvider","TranslateProvider","TranslateProvider"],"sources":["../../src/translate/claude.ts","../../src/translate/deepl.ts","../../src/translate/gemini.ts","../../src/translate/openai.ts"],"mappings":";;UASiB;;EAEf;;EAEA;;EAEA;;EAEA;EACA;;EAEA,eAAe;;;;;;wBASD,eAAe,UAAS,wBAA6BA;;;UC3BpD;;EAEf;EACA;EACA;;EAEA,eAAe;;;;;;wBAyCD,cAAc,UAAS,uBAA4BC;;;UCxClD;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA,eAAe;;;;;;;wBAWD,eAAe,UAAS,wBAA6BC;;;UCzBpD;;EAEf;;EAEA;;EAEA;;EAEA;;;;;;EAMA;;;;EAIA;;EAEA,eAAe;;;;;;;wBAWD,eAAe,UAAS,wBAA6BC"}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
//#region src/translate/llm.ts
|
|
2
|
+
/** The instruction every chat provider gives the model. Keep providers on this one prompt. */
|
|
3
|
+
function buildSystemPrompt({ from, to, context, format = "array" }) {
|
|
4
|
+
return [
|
|
5
|
+
`You translate user-interface strings from ${from} to ${to}.`,
|
|
6
|
+
"The user message is a JSON object with a `texts` array.",
|
|
7
|
+
format === "array" ? "Reply with ONLY a JSON array of translated strings: same length, same order, no commentary." : "Reply with ONLY a JSON object {\"translations\": [...]} whose array holds the translated strings: same length, same order, no commentary.",
|
|
8
|
+
"Keep placeholders such as {name}, %s, %d, HTML tags, URLs, and emoji exactly as written.",
|
|
9
|
+
"Some strings are ICU MessageFormat, e.g. {count, plural, one {# item} other {# items}}.",
|
|
10
|
+
"For those, keep the structure, argument names, types, and select keys unchanged, keep # as is,",
|
|
11
|
+
"and translate only the text inside the branches.",
|
|
12
|
+
`Give plural messages the plural categories that ${to} needs (CLDR), keeping any =0 / =1 branches and "other".`,
|
|
13
|
+
context
|
|
14
|
+
].filter(Boolean).join("\n");
|
|
15
|
+
}
|
|
16
|
+
function stripFences(text) {
|
|
17
|
+
return text.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "").trim();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Reads a model's reply: a JSON array of strings, or `{ "translations": [...] }`, optionally in a
|
|
21
|
+
* Markdown code fence. Throws if it is not valid JSON or does not hold `expected` strings.
|
|
22
|
+
*/
|
|
23
|
+
function parseTranslations(text, expected, vendor) {
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(stripFences(text));
|
|
27
|
+
} catch {
|
|
28
|
+
throw new Error(`${vendor} returned a response that is not valid JSON.`);
|
|
29
|
+
}
|
|
30
|
+
const list = Array.isArray(parsed) || parsed === null || typeof parsed !== "object" ? parsed : parsed.translations;
|
|
31
|
+
if (!Array.isArray(list) || list.length !== expected || !list.every((s) => typeof s === "string")) throw new Error(`${vendor} returned an unexpected shape (expected ${expected} strings).`);
|
|
32
|
+
return list;
|
|
33
|
+
}
|
|
34
|
+
/** Sends `texts` in chunks of `batchSize`, keeping the order of the results. */
|
|
35
|
+
async function translateInBatches(texts, batchSize, requestChunk) {
|
|
36
|
+
const results = [];
|
|
37
|
+
for (let i = 0; i < texts.length; i += batchSize) results.push(...await requestChunk(texts.slice(i, i + batchSize)));
|
|
38
|
+
return results;
|
|
39
|
+
}
|
|
40
|
+
/** `"<vendor> API error <status>: <start of the body>"`, with any secrets blanked out. */
|
|
41
|
+
async function readApiError(response, vendor, secrets = []) {
|
|
42
|
+
let detail = (await response.text()).slice(0, 300);
|
|
43
|
+
for (const secret of secrets) if (secret) detail = detail.split(secret).join("***");
|
|
44
|
+
return /* @__PURE__ */ new Error(`${vendor} API error ${response.status}: ${detail}`);
|
|
45
|
+
}
|
|
46
|
+
const TRUNCATED = (vendor) => `${vendor}'s response was cut off before it finished. Lower batchSize and try again.`;
|
|
47
|
+
const truncatedError = (vendor) => new Error(TRUNCATED(vendor));
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/translate/claude.ts
|
|
50
|
+
const DEFAULT_MODEL$2 = "claude-sonnet-5-5";
|
|
51
|
+
/**
|
|
52
|
+
* Translates with the Anthropic Messages API.
|
|
53
|
+
* Reads `ANTHROPIC_API_KEY` unless `apiKey` is given. Nothing is sent until `translate` is called.
|
|
54
|
+
*/
|
|
55
|
+
function claudeProvider(options = {}) {
|
|
56
|
+
const batchSize = options.batchSize ?? 40;
|
|
57
|
+
return {
|
|
58
|
+
name: "claude",
|
|
59
|
+
supportsIcu: true,
|
|
60
|
+
async translate(texts, from, to) {
|
|
61
|
+
const apiKey = options.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
62
|
+
if (!apiKey) throw new Error("ANTHROPIC_API_KEY is not set (or pass apiKey to claudeProvider).");
|
|
63
|
+
const doFetch = options.fetch ?? fetch;
|
|
64
|
+
const url = `${options.baseUrl ?? "https://api.anthropic.com"}/v1/messages`;
|
|
65
|
+
const system = buildSystemPrompt({
|
|
66
|
+
from,
|
|
67
|
+
to,
|
|
68
|
+
context: options.context
|
|
69
|
+
});
|
|
70
|
+
return translateInBatches(texts, batchSize, async (chunk) => {
|
|
71
|
+
const response = await doFetch(url, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: {
|
|
74
|
+
"content-type": "application/json",
|
|
75
|
+
"x-api-key": apiKey,
|
|
76
|
+
"anthropic-version": "2023-06-01"
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
model: options.model ?? DEFAULT_MODEL$2,
|
|
80
|
+
max_tokens: 8192,
|
|
81
|
+
system,
|
|
82
|
+
messages: [{
|
|
83
|
+
role: "user",
|
|
84
|
+
content: JSON.stringify({ texts: chunk })
|
|
85
|
+
}]
|
|
86
|
+
})
|
|
87
|
+
});
|
|
88
|
+
if (!response.ok) throw await readApiError(response, "Anthropic", [apiKey]);
|
|
89
|
+
const data = await response.json();
|
|
90
|
+
if (data.stop_reason === "max_tokens") throw truncatedError("Claude");
|
|
91
|
+
return parseTranslations((data.content ?? []).filter((block) => block.type === "text").map((block) => block.text ?? "").join(""), chunk.length, "Claude");
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/translate/deepl.ts
|
|
98
|
+
const BATCH_SIZE = 50;
|
|
99
|
+
const PLACEHOLDER = /\{[^{}]+\}|%\d*\$?[sdif]/g;
|
|
100
|
+
/** Maps a BCP 47 tag to DeepL's language codes. */
|
|
101
|
+
function toDeepLLanguage(code, target) {
|
|
102
|
+
const [primary, ...rest] = code.split("-");
|
|
103
|
+
const upper = [primary.toUpperCase(), ...rest.map((r) => r.toUpperCase())].join("-");
|
|
104
|
+
if (!target) return primary.toUpperCase();
|
|
105
|
+
if (rest.length === 0) {
|
|
106
|
+
if (primary === "en") return "EN-US";
|
|
107
|
+
if (primary === "pt") return "PT-PT";
|
|
108
|
+
}
|
|
109
|
+
return upper;
|
|
110
|
+
}
|
|
111
|
+
/** Replaces placeholders with `<x id="n"/>` tags that DeepL is told to leave alone. */
|
|
112
|
+
function protect(text) {
|
|
113
|
+
const placeholders = [];
|
|
114
|
+
return {
|
|
115
|
+
text: text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(PLACEHOLDER, (match) => {
|
|
116
|
+
placeholders.push(match.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"));
|
|
117
|
+
return `<x id="${placeholders.length - 1}"/>`;
|
|
118
|
+
}),
|
|
119
|
+
placeholders
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function restore(text, placeholders) {
|
|
123
|
+
return text.replace(/<x id="(\d+)"\s*\/>/g, (_, i) => placeholders[Number(i)] ?? "").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Translates with the DeepL API.
|
|
127
|
+
* Reads `DEEPL_API_KEY` unless `apiKey` is given. Nothing is sent until `translate` is called.
|
|
128
|
+
*/
|
|
129
|
+
function deeplProvider(options = {}) {
|
|
130
|
+
return {
|
|
131
|
+
name: "deepl",
|
|
132
|
+
async translate(texts, from, to) {
|
|
133
|
+
const apiKey = options.apiKey ?? process.env.DEEPL_API_KEY;
|
|
134
|
+
if (!apiKey) throw new Error("DEEPL_API_KEY is not set (or pass apiKey to deeplProvider).");
|
|
135
|
+
const doFetch = options.fetch ?? fetch;
|
|
136
|
+
const baseUrl = options.baseUrl ?? (apiKey.endsWith(":fx") ? "https://api-free.deepl.com" : "https://api.deepl.com");
|
|
137
|
+
const results = [];
|
|
138
|
+
for (let i = 0; i < texts.length; i += BATCH_SIZE) {
|
|
139
|
+
const chunk = texts.slice(i, i + BATCH_SIZE).map(protect);
|
|
140
|
+
const response = await doFetch(`${baseUrl}/v2/translate`, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: {
|
|
143
|
+
"content-type": "application/json",
|
|
144
|
+
authorization: `DeepL-Auth-Key ${apiKey}`
|
|
145
|
+
},
|
|
146
|
+
body: JSON.stringify({
|
|
147
|
+
text: chunk.map((c) => c.text),
|
|
148
|
+
source_lang: toDeepLLanguage(from, false),
|
|
149
|
+
target_lang: toDeepLLanguage(to, true),
|
|
150
|
+
tag_handling: "xml",
|
|
151
|
+
ignore_tags: ["x"],
|
|
152
|
+
...options.formality ? { formality: options.formality } : {}
|
|
153
|
+
})
|
|
154
|
+
});
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
const detail = (await response.text()).slice(0, 300);
|
|
157
|
+
throw new Error(`DeepL API error ${response.status}: ${detail}`);
|
|
158
|
+
}
|
|
159
|
+
const translations = (await response.json()).translations ?? [];
|
|
160
|
+
if (translations.length !== chunk.length) throw new Error(`DeepL returned ${translations.length} translations for ${chunk.length} strings.`);
|
|
161
|
+
translations.forEach((t, index) => results.push(restore(t.text, chunk[index].placeholders)));
|
|
162
|
+
}
|
|
163
|
+
return results;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/translate/gemini.ts
|
|
169
|
+
const DEFAULT_MODEL$1 = "gemini-2.5-flash";
|
|
170
|
+
const DEFAULT_BASE_URL$1 = "https://generativelanguage.googleapis.com/v1beta";
|
|
171
|
+
/**
|
|
172
|
+
* Translates with the Google Gemini API.
|
|
173
|
+
* Reads `GEMINI_API_KEY` (or `GOOGLE_API_KEY`) unless `apiKey` is given. The key is sent in a
|
|
174
|
+
* header, never in the URL. Nothing is sent until `translate` is called.
|
|
175
|
+
*/
|
|
176
|
+
function geminiProvider(options = {}) {
|
|
177
|
+
const batchSize = options.batchSize ?? 40;
|
|
178
|
+
return {
|
|
179
|
+
name: "gemini",
|
|
180
|
+
supportsIcu: true,
|
|
181
|
+
async translate(texts, from, to) {
|
|
182
|
+
const apiKey = options.apiKey ?? process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
|
|
183
|
+
if (!apiKey) throw new Error("GEMINI_API_KEY (or GOOGLE_API_KEY) is not set (or pass apiKey to geminiProvider).");
|
|
184
|
+
const doFetch = options.fetch ?? fetch;
|
|
185
|
+
const model = (options.model ?? process.env.GEMINI_MODEL ?? DEFAULT_MODEL$1).replace(/^models\//, "");
|
|
186
|
+
const url = `${(options.baseUrl ?? DEFAULT_BASE_URL$1).replace(/\/+$/, "")}/models/${encodeURIComponent(model)}:generateContent`;
|
|
187
|
+
const system = buildSystemPrompt({
|
|
188
|
+
from,
|
|
189
|
+
to,
|
|
190
|
+
context: options.context
|
|
191
|
+
});
|
|
192
|
+
return translateInBatches(texts, batchSize, async (chunk) => {
|
|
193
|
+
const response = await doFetch(url, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: {
|
|
196
|
+
"content-type": "application/json",
|
|
197
|
+
"x-goog-api-key": apiKey
|
|
198
|
+
},
|
|
199
|
+
body: JSON.stringify({
|
|
200
|
+
systemInstruction: { parts: [{ text: system }] },
|
|
201
|
+
contents: [{
|
|
202
|
+
role: "user",
|
|
203
|
+
parts: [{ text: JSON.stringify({ texts: chunk }) }]
|
|
204
|
+
}],
|
|
205
|
+
generationConfig: {
|
|
206
|
+
temperature: options.temperature ?? 0,
|
|
207
|
+
responseMimeType: "application/json",
|
|
208
|
+
responseSchema: {
|
|
209
|
+
type: "ARRAY",
|
|
210
|
+
items: { type: "STRING" }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
});
|
|
215
|
+
if (!response.ok) throw await readApiError(response, "Gemini", [apiKey]);
|
|
216
|
+
const data = await response.json();
|
|
217
|
+
if (data.promptFeedback?.blockReason) throw new Error(`Gemini blocked the request (${data.promptFeedback.blockReason}).`);
|
|
218
|
+
const candidate = data.candidates?.[0];
|
|
219
|
+
if (!candidate) throw new Error("Gemini returned no candidates.");
|
|
220
|
+
if (candidate.finishReason === "MAX_TOKENS") throw truncatedError("Gemini");
|
|
221
|
+
const text = (candidate.content?.parts ?? []).map((part) => part.text ?? "").join("");
|
|
222
|
+
if (!text) throw new Error(`Gemini returned no text${candidate.finishReason ? ` (finishReason ${candidate.finishReason})` : ""}.`);
|
|
223
|
+
return parseTranslations(text, chunk.length, "Gemini");
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/translate/openai.ts
|
|
230
|
+
const DEFAULT_MODEL = "gpt-4o-mini";
|
|
231
|
+
const DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
232
|
+
/**
|
|
233
|
+
* Translates with the OpenAI Chat Completions API (the API behind ChatGPT), or any server that
|
|
234
|
+
* speaks it. Reads `OPENAI_API_KEY` unless `apiKey` is given. Nothing is sent until `translate`
|
|
235
|
+
* is called.
|
|
236
|
+
*/
|
|
237
|
+
function openaiProvider(options = {}) {
|
|
238
|
+
const batchSize = options.batchSize ?? 40;
|
|
239
|
+
return {
|
|
240
|
+
name: "openai",
|
|
241
|
+
supportsIcu: true,
|
|
242
|
+
async translate(texts, from, to) {
|
|
243
|
+
const apiKey = options.apiKey ?? process.env.OPENAI_API_KEY;
|
|
244
|
+
const customServer = options.baseUrl !== void 0;
|
|
245
|
+
if (!apiKey && !customServer) throw new Error("OPENAI_API_KEY is not set (or pass apiKey to openaiProvider).");
|
|
246
|
+
const doFetch = options.fetch ?? fetch;
|
|
247
|
+
const url = `${(options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "")}/chat/completions`;
|
|
248
|
+
const model = options.model ?? process.env.OPENAI_MODEL ?? DEFAULT_MODEL;
|
|
249
|
+
const system = buildSystemPrompt({
|
|
250
|
+
from,
|
|
251
|
+
to,
|
|
252
|
+
context: options.context,
|
|
253
|
+
format: "object"
|
|
254
|
+
});
|
|
255
|
+
return translateInBatches(texts, batchSize, async (chunk) => {
|
|
256
|
+
const response = await doFetch(url, {
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers: {
|
|
259
|
+
"content-type": "application/json",
|
|
260
|
+
...apiKey ? { authorization: `Bearer ${apiKey}` } : {}
|
|
261
|
+
},
|
|
262
|
+
body: JSON.stringify({
|
|
263
|
+
model,
|
|
264
|
+
messages: [{
|
|
265
|
+
role: "system",
|
|
266
|
+
content: system
|
|
267
|
+
}, {
|
|
268
|
+
role: "user",
|
|
269
|
+
content: JSON.stringify({ texts: chunk })
|
|
270
|
+
}],
|
|
271
|
+
response_format: { type: "json_object" },
|
|
272
|
+
...options.temperature !== void 0 ? { temperature: options.temperature } : {}
|
|
273
|
+
})
|
|
274
|
+
});
|
|
275
|
+
if (!response.ok) throw await readApiError(response, "OpenAI", [apiKey]);
|
|
276
|
+
const choice = (await response.json()).choices?.[0];
|
|
277
|
+
if (choice?.finish_reason === "length") throw truncatedError("OpenAI");
|
|
278
|
+
if (choice?.message?.refusal) throw new Error(`OpenAI declined the request: ${choice.message.refusal}`);
|
|
279
|
+
const content = choice?.message?.content;
|
|
280
|
+
if (!content) throw new Error("OpenAI returned an empty response.");
|
|
281
|
+
return parseTranslations(content, chunk.length, "OpenAI");
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
//#endregion
|
|
287
|
+
export { claudeProvider, deeplProvider, geminiProvider, openaiProvider };
|
|
288
|
+
|
|
289
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["DEFAULT_MODEL","DEFAULT_MODEL","DEFAULT_BASE_URL"],"sources":["../../src/translate/llm.ts","../../src/translate/claude.ts","../../src/translate/deepl.ts","../../src/translate/gemini.ts","../../src/translate/openai.ts"],"sourcesContent":["/**\n * Plumbing shared by the chat-model providers (Claude, OpenAI, Gemini): one system prompt, one\n * way to read the model's JSON reply, batching, and API error reporting.\n */\n\nexport interface PromptOptions {\n from: string;\n to: string;\n /** Extra guidance, e.g. \"This is a banking app; use formal register.\" */\n context?: string;\n /**\n * How the model should shape its reply. APIs with a JSON mode that only allows objects\n * (OpenAI) need `\"object\"`; the others can return a bare array.\n */\n format?: \"array\" | \"object\";\n}\n\n/** The instruction every chat provider gives the model. Keep providers on this one prompt. */\nexport function buildSystemPrompt({ from, to, context, format = \"array\" }: PromptOptions): string {\n return [\n `You translate user-interface strings from ${from} to ${to}.`,\n \"The user message is a JSON object with a `texts` array.\",\n format === \"array\"\n ? \"Reply with ONLY a JSON array of translated strings: same length, same order, no commentary.\"\n : 'Reply with ONLY a JSON object {\"translations\": [...]} whose array holds the translated strings: same length, same order, no commentary.',\n \"Keep placeholders such as {name}, %s, %d, HTML tags, URLs, and emoji exactly as written.\",\n \"Some strings are ICU MessageFormat, e.g. {count, plural, one {# item} other {# items}}.\",\n \"For those, keep the structure, argument names, types, and select keys unchanged, keep # as is,\",\n \"and translate only the text inside the branches.\",\n `Give plural messages the plural categories that ${to} needs (CLDR), keeping any =0 / =1 branches and \"other\".`,\n context,\n ]\n .filter(Boolean)\n .join(\"\\n\");\n}\n\nfunction stripFences(text: string): string {\n return text\n .trim()\n .replace(/^```(?:json)?\\s*/i, \"\")\n .replace(/\\s*```$/, \"\")\n .trim();\n}\n\n/**\n * Reads a model's reply: a JSON array of strings, or `{ \"translations\": [...] }`, optionally in a\n * Markdown code fence. Throws if it is not valid JSON or does not hold `expected` strings.\n */\nexport function parseTranslations(text: string, expected: number, vendor: string): string[] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(stripFences(text));\n } catch {\n throw new Error(`${vendor} returned a response that is not valid JSON.`);\n }\n\n const list =\n Array.isArray(parsed) || parsed === null || typeof parsed !== \"object\"\n ? parsed\n : (parsed as { translations?: unknown }).translations;\n\n if (\n !Array.isArray(list) ||\n list.length !== expected ||\n !list.every((s) => typeof s === \"string\")\n ) {\n throw new Error(`${vendor} returned an unexpected shape (expected ${expected} strings).`);\n }\n return list as string[];\n}\n\n/** Sends `texts` in chunks of `batchSize`, keeping the order of the results. */\nexport async function translateInBatches(\n texts: string[],\n batchSize: number,\n requestChunk: (chunk: string[]) => Promise<string[]>,\n): Promise<string[]> {\n const results: string[] = [];\n for (let i = 0; i < texts.length; i += batchSize) {\n results.push(...(await requestChunk(texts.slice(i, i + batchSize))));\n }\n return results;\n}\n\n/** `\"<vendor> API error <status>: <start of the body>\"`, with any secrets blanked out. */\nexport async function readApiError(\n response: Response,\n vendor: string,\n secrets: (string | undefined)[] = [],\n): Promise<Error> {\n let detail = (await response.text()).slice(0, 300);\n for (const secret of secrets) {\n if (secret) detail = detail.split(secret).join(\"***\");\n }\n return new Error(`${vendor} API error ${response.status}: ${detail}`);\n}\n\nconst TRUNCATED = (vendor: string) =>\n `${vendor}'s response was cut off before it finished. Lower batchSize and try again.`;\n\nexport const truncatedError = (vendor: string) => new Error(TRUNCATED(vendor));\n","import type { TranslateProvider } from \"dialexjs\";\nimport {\n buildSystemPrompt,\n parseTranslations,\n readApiError,\n translateInBatches,\n truncatedError,\n} from \"./llm.js\";\n\nexport interface ClaudeProviderOptions {\n /** Defaults to the `ANTHROPIC_API_KEY` environment variable. */\n apiKey?: string;\n /** Anthropic model ID. */\n model?: string;\n /** Strings per request. */\n batchSize?: number;\n /** Extra guidance appended to the prompt, e.g. \"This is a banking app; use formal register.\" */\n context?: string;\n baseUrl?: string;\n /** Injected for tests. */\n fetch?: typeof fetch;\n}\n\nconst DEFAULT_MODEL = \"claude-sonnet-5-5\";\n\n/**\n * Translates with the Anthropic Messages API.\n * Reads `ANTHROPIC_API_KEY` unless `apiKey` is given. Nothing is sent until `translate` is called.\n */\nexport function claudeProvider(options: ClaudeProviderOptions = {}): TranslateProvider {\n const batchSize = options.batchSize ?? 40;\n\n return {\n name: \"claude\",\n supportsIcu: true,\n async translate(texts, from, to) {\n const apiKey = options.apiKey ?? process.env.ANTHROPIC_API_KEY;\n if (!apiKey)\n throw new Error(\"ANTHROPIC_API_KEY is not set (or pass apiKey to claudeProvider).\");\n\n const doFetch = options.fetch ?? fetch;\n const url = `${options.baseUrl ?? \"https://api.anthropic.com\"}/v1/messages`;\n const system = buildSystemPrompt({ from, to, context: options.context });\n\n return translateInBatches(texts, batchSize, async (chunk) => {\n const response = await doFetch(url, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n \"x-api-key\": apiKey,\n \"anthropic-version\": \"2023-06-01\",\n },\n body: JSON.stringify({\n model: options.model ?? DEFAULT_MODEL,\n max_tokens: 8192,\n system,\n messages: [{ role: \"user\", content: JSON.stringify({ texts: chunk }) }],\n }),\n });\n\n if (!response.ok) throw await readApiError(response, \"Anthropic\", [apiKey]);\n\n const data = (await response.json()) as {\n content?: { type: string; text?: string }[];\n stop_reason?: string;\n };\n if (data.stop_reason === \"max_tokens\") throw truncatedError(\"Claude\");\n\n const text = (data.content ?? [])\n .filter((block) => block.type === \"text\")\n .map((block) => block.text ?? \"\")\n .join(\"\");\n return parseTranslations(text, chunk.length, \"Claude\");\n });\n },\n };\n}\n","import type { TranslateProvider } from \"dialexjs\";\n\nexport interface DeepLProviderOptions {\n /** Defaults to the `DEEPL_API_KEY` environment variable. Keys ending in `:fx` use the free API. */\n apiKey?: string;\n formality?: \"default\" | \"more\" | \"less\" | \"prefer_more\" | \"prefer_less\";\n baseUrl?: string;\n /** Injected for tests. */\n fetch?: typeof fetch;\n}\n\nconst BATCH_SIZE = 50;\nconst PLACEHOLDER = /\\{[^{}]+\\}|%\\d*\\$?[sdif]/g;\n\n/** Maps a BCP 47 tag to DeepL's language codes. */\nexport function toDeepLLanguage(code: string, target: boolean): string {\n const [primary, ...rest] = code.split(\"-\");\n const upper = [primary.toUpperCase(), ...rest.map((r) => r.toUpperCase())].join(\"-\");\n if (!target) return primary.toUpperCase();\n if (rest.length === 0) {\n if (primary === \"en\") return \"EN-US\";\n if (primary === \"pt\") return \"PT-PT\";\n }\n return upper;\n}\n\n/** Replaces placeholders with `<x id=\"n\"/>` tags that DeepL is told to leave alone. */\nfunction protect(text: string): { text: string; placeholders: string[] } {\n const placeholders: string[] = [];\n const escaped = text.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n const protectedText = escaped.replace(PLACEHOLDER, (match) => {\n placeholders.push(match.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\"));\n return `<x id=\"${placeholders.length - 1}\"/>`;\n });\n return { text: protectedText, placeholders };\n}\n\nfunction restore(text: string, placeholders: string[]): string {\n return text\n .replace(/<x id=\"(\\d+)\"\\s*\\/>/g, (_, i) => placeholders[Number(i)] ?? \"\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/&/g, \"&\");\n}\n\n/**\n * Translates with the DeepL API.\n * Reads `DEEPL_API_KEY` unless `apiKey` is given. Nothing is sent until `translate` is called.\n */\nexport function deeplProvider(options: DeepLProviderOptions = {}): TranslateProvider {\n return {\n name: \"deepl\",\n async translate(texts, from, to) {\n const apiKey = options.apiKey ?? process.env.DEEPL_API_KEY;\n if (!apiKey) throw new Error(\"DEEPL_API_KEY is not set (or pass apiKey to deeplProvider).\");\n\n const doFetch = options.fetch ?? fetch;\n const baseUrl =\n options.baseUrl ??\n (apiKey.endsWith(\":fx\") ? \"https://api-free.deepl.com\" : \"https://api.deepl.com\");\n\n const results: string[] = [];\n for (let i = 0; i < texts.length; i += BATCH_SIZE) {\n const chunk = texts.slice(i, i + BATCH_SIZE).map(protect);\n const response = await doFetch(`${baseUrl}/v2/translate`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `DeepL-Auth-Key ${apiKey}`,\n },\n body: JSON.stringify({\n text: chunk.map((c) => c.text),\n source_lang: toDeepLLanguage(from, false),\n target_lang: toDeepLLanguage(to, true),\n tag_handling: \"xml\",\n ignore_tags: [\"x\"],\n ...(options.formality ? { formality: options.formality } : {}),\n }),\n });\n\n if (!response.ok) {\n const detail = (await response.text()).slice(0, 300);\n throw new Error(`DeepL API error ${response.status}: ${detail}`);\n }\n\n const data = (await response.json()) as { translations?: { text: string }[] };\n const translations = data.translations ?? [];\n if (translations.length !== chunk.length) {\n throw new Error(\n `DeepL returned ${translations.length} translations for ${chunk.length} strings.`,\n );\n }\n translations.forEach((t, index) =>\n results.push(restore(t.text, chunk[index].placeholders)),\n );\n }\n return results;\n },\n };\n}\n","import type { TranslateProvider } from \"dialexjs\";\nimport {\n buildSystemPrompt,\n parseTranslations,\n readApiError,\n translateInBatches,\n truncatedError,\n} from \"./llm.js\";\n\nexport interface GeminiProviderOptions {\n /** Defaults to the `GEMINI_API_KEY` environment variable, then `GOOGLE_API_KEY`. */\n apiKey?: string;\n /** Model ID. Defaults to the `GEMINI_MODEL` environment variable, then a built-in default. */\n model?: string;\n /** Strings per request. */\n batchSize?: number;\n /** Extra guidance appended to the prompt, e.g. \"This is a banking app; use formal register.\" */\n context?: string;\n /** @default \"https://generativelanguage.googleapis.com/v1beta\" */\n baseUrl?: string;\n /** Sampling temperature. @default 0 */\n temperature?: number;\n /** Injected for tests. */\n fetch?: typeof fetch;\n}\n\nconst DEFAULT_MODEL = \"gemini-2.5-flash\";\nconst DEFAULT_BASE_URL = \"https://generativelanguage.googleapis.com/v1beta\";\n\n/**\n * Translates with the Google Gemini API.\n * Reads `GEMINI_API_KEY` (or `GOOGLE_API_KEY`) unless `apiKey` is given. The key is sent in a\n * header, never in the URL. Nothing is sent until `translate` is called.\n */\nexport function geminiProvider(options: GeminiProviderOptions = {}): TranslateProvider {\n const batchSize = options.batchSize ?? 40;\n\n return {\n name: \"gemini\",\n supportsIcu: true,\n async translate(texts, from, to) {\n const apiKey = options.apiKey ?? process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;\n if (!apiKey) {\n throw new Error(\n \"GEMINI_API_KEY (or GOOGLE_API_KEY) is not set (or pass apiKey to geminiProvider).\",\n );\n }\n\n const doFetch = options.fetch ?? fetch;\n const model = (options.model ?? process.env.GEMINI_MODEL ?? DEFAULT_MODEL).replace(\n /^models\\//,\n \"\",\n );\n const url = `${(options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\")}/models/${encodeURIComponent(model)}:generateContent`;\n const system = buildSystemPrompt({ from, to, context: options.context });\n\n return translateInBatches(texts, batchSize, async (chunk) => {\n const response = await doFetch(url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", \"x-goog-api-key\": apiKey },\n body: JSON.stringify({\n systemInstruction: { parts: [{ text: system }] },\n contents: [{ role: \"user\", parts: [{ text: JSON.stringify({ texts: chunk }) }] }],\n generationConfig: {\n temperature: options.temperature ?? 0,\n responseMimeType: \"application/json\",\n responseSchema: { type: \"ARRAY\", items: { type: \"STRING\" } },\n },\n }),\n });\n\n if (!response.ok) throw await readApiError(response, \"Gemini\", [apiKey]);\n\n const data = (await response.json()) as {\n promptFeedback?: { blockReason?: string };\n candidates?: {\n finishReason?: string;\n content?: { parts?: { text?: string }[] };\n }[];\n };\n\n if (data.promptFeedback?.blockReason) {\n throw new Error(`Gemini blocked the request (${data.promptFeedback.blockReason}).`);\n }\n const candidate = data.candidates?.[0];\n if (!candidate) throw new Error(\"Gemini returned no candidates.\");\n if (candidate.finishReason === \"MAX_TOKENS\") throw truncatedError(\"Gemini\");\n\n const text = (candidate.content?.parts ?? []).map((part) => part.text ?? \"\").join(\"\");\n if (!text) {\n throw new Error(\n `Gemini returned no text${candidate.finishReason ? ` (finishReason ${candidate.finishReason})` : \"\"}.`,\n );\n }\n return parseTranslations(text, chunk.length, \"Gemini\");\n });\n },\n };\n}\n","import type { TranslateProvider } from \"dialexjs\";\nimport {\n buildSystemPrompt,\n parseTranslations,\n readApiError,\n translateInBatches,\n truncatedError,\n} from \"./llm.js\";\n\nexport interface OpenAIProviderOptions {\n /** Defaults to the `OPENAI_API_KEY` environment variable. */\n apiKey?: string;\n /** Model ID. Defaults to the `OPENAI_MODEL` environment variable, then a built-in default. */\n model?: string;\n /** Strings per request. */\n batchSize?: number;\n /** Extra guidance appended to the prompt, e.g. \"This is a banking app; use formal register.\" */\n context?: string;\n /**\n * API root, for OpenAI-compatible servers (Azure OpenAI, Ollama, OpenRouter, ...).\n * With a custom `baseUrl` an API key is optional.\n * @default \"https://api.openai.com/v1\"\n */\n baseUrl?: string;\n /**\n * Sampling temperature. Left out by default because some models only accept their default.\n */\n temperature?: number;\n /** Injected for tests. */\n fetch?: typeof fetch;\n}\n\nconst DEFAULT_MODEL = \"gpt-4o-mini\";\nconst DEFAULT_BASE_URL = \"https://api.openai.com/v1\";\n\n/**\n * Translates with the OpenAI Chat Completions API (the API behind ChatGPT), or any server that\n * speaks it. Reads `OPENAI_API_KEY` unless `apiKey` is given. Nothing is sent until `translate`\n * is called.\n */\nexport function openaiProvider(options: OpenAIProviderOptions = {}): TranslateProvider {\n const batchSize = options.batchSize ?? 40;\n\n return {\n name: \"openai\",\n supportsIcu: true,\n async translate(texts, from, to) {\n const apiKey = options.apiKey ?? process.env.OPENAI_API_KEY;\n const customServer = options.baseUrl !== undefined;\n if (!apiKey && !customServer) {\n throw new Error(\"OPENAI_API_KEY is not set (or pass apiKey to openaiProvider).\");\n }\n\n const doFetch = options.fetch ?? fetch;\n const url = `${(options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\")}/chat/completions`;\n const model = options.model ?? process.env.OPENAI_MODEL ?? DEFAULT_MODEL;\n const system = buildSystemPrompt({ from, to, context: options.context, format: \"object\" });\n\n return translateInBatches(texts, batchSize, async (chunk) => {\n const response = await doFetch(url, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n ...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),\n },\n body: JSON.stringify({\n model,\n messages: [\n { role: \"system\", content: system },\n { role: \"user\", content: JSON.stringify({ texts: chunk }) },\n ],\n // `json_object` is understood by OpenAI-compatible servers; `json_schema` is not.\n response_format: { type: \"json_object\" },\n ...(options.temperature !== undefined ? { temperature: options.temperature } : {}),\n }),\n });\n\n if (!response.ok) throw await readApiError(response, \"OpenAI\", [apiKey]);\n\n const data = (await response.json()) as {\n choices?: {\n finish_reason?: string;\n message?: { content?: string | null; refusal?: string | null };\n }[];\n };\n const choice = data.choices?.[0];\n if (choice?.finish_reason === \"length\") throw truncatedError(\"OpenAI\");\n if (choice?.message?.refusal) {\n throw new Error(`OpenAI declined the request: ${choice.message.refusal}`);\n }\n\n const content = choice?.message?.content;\n if (!content) throw new Error(\"OpenAI returned an empty response.\");\n return parseTranslations(content, chunk.length, \"OpenAI\");\n });\n },\n };\n}\n"],"mappings":"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"}
|