@krizaka/i18n 0.2.0 → 0.2.2
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/dist/bin.js +2 -1
- package/dist/bin.js.map +1 -1
- package/dist/check.js +2 -1
- package/dist/check.js.map +1 -1
- package/package.json +4 -4
package/dist/bin.js
CHANGED
|
@@ -111,7 +111,8 @@ var wholeBranch = (branch) => new RegExp(`[.'"\`]${branch.replace(/[.*+?^${}()|[
|
|
|
111
111
|
function findUnused(keys, sources) {
|
|
112
112
|
const text = sources.flatMap((entry) => readSources(entry)).join("\n");
|
|
113
113
|
return keys.filter((full) => {
|
|
114
|
-
const
|
|
114
|
+
const arrayAt = full.search(/\[\d+\]/);
|
|
115
|
+
const key = arrayAt < 0 ? full : full.slice(0, arrayAt);
|
|
115
116
|
if (text.includes(key)) return false;
|
|
116
117
|
const parts = key.split(".");
|
|
117
118
|
for (let i = parts.length - 1; i > 0; i--) {
|
package/dist/bin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/check.ts","../src/format.ts","../src/scan.ts","../src/bin.ts"],"sourcesContent":["/**\n * `krizaka-i18n` — the i18n gates of every Krizaka app, one implementation:\n *\n * krizaka-i18n check <dir> [--reference en] [--unused <src>]… [--json]\n * krizaka-i18n scan <path>… [--allow <word,…>]… [--skip <regex>]… [--summary] [--json]\n *\n * Exit 0 when clean, 1 when problems are found, 2 on a usage error.\n */\nimport path from \"node:path\";\nimport { parseArgs } from \"node:util\";\n\nimport { checkMessages, formatReport } from \"./check\";\nimport { type Offender, scanHardcoded } from \"./scan\";\n\nexport const USAGE = `Usage:\n krizaka-i18n check <dir> [--reference en] [--unused <src>]… [--json]\n The catalogues of <dir> (<locale>.json) against the reference: same keys, no empty string, same {placeholders},\n same markup (<b>, <a> only). --unused <src> also fails on keys no source under <src> reads.\n krizaka-i18n scan <path>… [--allow <word,…>]… [--skip <regex>]… [--summary] [--json]\n Hard-coded user-facing strings in .tsx sources (needs the typescript package). --allow lists brand names,\n --skip leaves files out (regular expression on the path), --summary counts per file.\n --json prints a machine-readable report on stdout (for CI).`;\n\nexport interface CliIO {\n stdout: (line: string) => void;\n stderr: (line: string) => void;\n cwd: string;\n /** Loads the TypeScript compiler (an optional peer dependency) for `scan`. */\n loadTypeScript: () => Promise<typeof import(\"typescript\")>;\n}\n\nconst defaultIO: CliIO = {\n stdout: (line) => console.log(line),\n stderr: (line) => console.error(line),\n cwd: process.cwd(),\n loadTypeScript: async () => (await import(\"typescript\")).default,\n};\n\n/** Runs the CLI and returns its exit code. */\nexport async function main(argv: readonly string[], io: CliIO = defaultIO): Promise<number> {\n const [command, ...rest] = argv;\n if (command === undefined || command === \"--help\" || command === \"-h\" || command === \"help\") {\n io.stdout(USAGE);\n return command === undefined ? 2 : 0;\n }\n\n let parsed;\n try {\n parsed = parseArgs({\n args: [...rest],\n allowPositionals: true,\n options: {\n json: { type: \"boolean\" },\n reference: { type: \"string\" },\n unused: { type: \"string\", multiple: true },\n allow: { type: \"string\", multiple: true },\n skip: { type: \"string\", multiple: true },\n summary: { type: \"boolean\" },\n },\n });\n } catch (error) {\n io.stderr(`${(error as Error).message}\\n\\n${USAGE}`);\n return 2;\n }\n const { values, positionals } = parsed;\n const resolve = (p: string) => path.resolve(io.cwd, p);\n\n if (command === \"check\") {\n if (positionals.length !== 1) {\n io.stderr(`check takes one folder of catalogues.\\n\\n${USAGE}`);\n return 2;\n }\n const result = checkMessages({\n dir: resolve(positionals[0] as string),\n reference: values.reference,\n unused: values.unused?.map(resolve),\n });\n if (values.json) io.stdout(JSON.stringify(result, null, 2));\n else (result.ok ? io.stdout : io.stderr)(formatReport(result));\n return result.ok ? 0 : 1;\n }\n\n if (command === \"scan\") {\n if (!positionals.length) {\n io.stderr(`scan takes at least one folder.\\n\\n${USAGE}`);\n return 2;\n }\n let ts;\n try {\n ts = await io.loadTypeScript();\n } catch {\n io.stderr(\"scan needs the typescript package: npm install -D typescript\");\n return 2;\n }\n const offenders = scanHardcoded(ts, {\n paths: positionals.map(resolve),\n allow: values.allow?.flatMap((list) => list.split(\",\")).map((w) => w.trim()).filter(Boolean),\n skip: values.skip,\n root: io.cwd,\n });\n if (values.json) io.stdout(JSON.stringify({ ok: offenders.length === 0, offenders }, null, 2));\n else printScan(offenders, Boolean(values.summary), io);\n return offenders.length ? 1 : 0;\n }\n\n io.stderr(`Unknown command \"${command}\".\\n\\n${USAGE}`);\n return 2;\n}\n\nfunction printScan(offenders: Offender[], summary: boolean, io: CliIO): void {\n if (!offenders.length) {\n io.stdout(\"✓ i18n: no hard-coded user-facing string.\");\n return;\n }\n if (summary) {\n const byFile = new Map<string, number>();\n for (const o of offenders) byFile.set(o.file, (byFile.get(o.file) ?? 0) + 1);\n for (const [file, n] of [...byFile].sort((a, b) => b[1] - a[1])) io.stdout(`${String(n).padStart(4)} ${file}`);\n } else {\n for (const o of offenders) io.stdout(`${o.file}:${o.line} [${o.kind}] ${o.text}`);\n }\n io.stderr(`✗ i18n: ${offenders.length} hard-coded user-facing string(s): move them to the message catalogue.`);\n}\n","/**\n * The catalogue gate: `<dir>/en.json` (the reference) and every other `<dir>/<locale>.json` have exactly the same keys,\n * no empty string, the same `{placeholders}` and the same markup (`<b>`, `<a>`, the only tags a message may carry).\n * Optionally, every key is read somewhere in the sources (`unused`). Run by `krizaka-i18n check <dir>` in each app's\n * lint and CI; it replaces the scripts each app used to carry.\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { isPluralMessage, placeholdersOf } from \"./format\";\nimport type { PluralMessage } from \"./types\";\n\nexport type ProblemKind = \"invalid\" | \"missing\" | \"unknown\" | \"empty\" | \"placeholders\" | \"markup\" | \"plural\" | \"unused\";\n\nexport interface Problem {\n /** The catalogue the problem is in (`\"fr\"`), or the reference for an unused key. */\n locale: string;\n /** The dotted key (`site.nav.title`, `steps[2].title`), or the file for an invalid catalogue. */\n key: string;\n kind: ProblemKind;\n message: string;\n}\n\nexport interface CheckOptions {\n /** The folder of the catalogues (`messages`): one `<locale>.json` per locale. */\n dir: string;\n /** The reference locale (default `en`): the others must match its keys. */\n reference?: string;\n /** Folders (or files) of sources: every key must appear in them (heuristic, see `findUnused`). */\n unused?: readonly string[];\n}\n\nexport interface CheckResult {\n ok: boolean;\n reference: string;\n locales: string[];\n /** The number of messages of the reference (a plural set counts once). */\n messages: number;\n problems: Problem[];\n}\n\ntype Leaf = string | PluralMessage;\n\n/** The leaves of a catalogue by dotted key: strings, plural sets (one leaf) and other scalars; lists as `key[i]`. */\nexport function flatten(value: unknown, prefix = \"\", out = new Map<string, unknown>()): Map<string, unknown> {\n if (Array.isArray(value)) value.forEach((item, i) => flatten(item, `${prefix}[${i}]`, out));\n else if (value && typeof value === \"object\" && !isPluralMessage(value)) {\n for (const [key, item] of Object.entries(value)) flatten(item, prefix ? `${prefix}.${key}` : key, out);\n } else out.set(prefix, value);\n return out;\n}\n\nconst forms = (leaf: Leaf): string[] => (typeof leaf === \"string\" ? [leaf] : (Object.values(leaf) as string[]));\nconst signature = (list: string[]): string => list.sort().join(\",\");\nconst placeholders = (leaf: Leaf): string => signature([...new Set(forms(leaf).flatMap(placeholdersOf))]);\nconst TAG = /<\\/?([A-Za-z][\\w-]*)[^>]*>/g;\nconst tags = (text: string): string[] => [...text.matchAll(TAG)].map((m) => m[0]);\nconst markup = (leaf: Leaf): string => signature([...new Set(forms(leaf).flatMap(tags))]);\nconst ALLOWED_TAGS = new Set([\"<b>\", \"</b>\", \"<a>\", \"</a>\"]);\n\nfunction checkLeaf(locale: string, key: string, value: unknown, problems: Problem[]): value is Leaf {\n if (typeof value !== \"string\" && !isPluralMessage(value)) {\n problems.push({ locale, key, kind: \"invalid\", message: `${locale}: ${key} is not a message (${JSON.stringify(value)})` });\n return false;\n }\n for (const text of forms(value)) {\n if (!text.trim()) problems.push({ locale, key, kind: \"empty\", message: `${locale}: empty ${key}` });\n const bad = tags(text).filter((tag) => !ALLOWED_TAGS.has(tag));\n if (bad.length) problems.push({ locale, key, kind: \"markup\", message: `${locale}: unsupported markup ${bad.join(\" \")} in ${key} (only <b> and <a>)` });\n }\n return true;\n}\n\nfunction load(file: string, problems: Problem[], locale: string): Map<string, unknown> | undefined {\n try {\n return flatten(JSON.parse(fs.readFileSync(file, \"utf8\")));\n } catch (error) {\n problems.push({ locale, key: file, kind: \"invalid\", message: `${locale}: cannot read ${file} (${(error as Error).message})` });\n return undefined;\n }\n}\n\n/** Runs the gate on a folder of catalogues. Never throws for a content problem: it reports. */\nexport function checkMessages(options: CheckOptions): CheckResult {\n const reference = options.reference ?? \"en\";\n const problems: Problem[] = [];\n const files = fs.existsSync(options.dir) ? fs.readdirSync(options.dir).filter((f) => f.endsWith(\".json\")).sort() : [];\n const locales = files.map((f) => f.slice(0, -\".json\".length));\n const result = (messages: number): CheckResult => ({ ok: problems.length === 0, reference, locales, messages, problems });\n\n if (!locales.includes(reference)) {\n problems.push({ locale: reference, key: options.dir, kind: \"invalid\", message: `no ${reference}.json in ${options.dir}` });\n return result(0);\n }\n const ref = load(path.join(options.dir, `${reference}.json`), problems, reference);\n if (!ref) return result(0);\n for (const [key, value] of ref) checkLeaf(reference, key, value, problems);\n\n for (const locale of locales.filter((l) => l !== reference)) {\n const other = load(path.join(options.dir, `${locale}.json`), problems, locale);\n if (!other) continue;\n for (const key of ref.keys()) if (!other.has(key)) problems.push({ locale, key, kind: \"missing\", message: `${locale}: missing ${key}` });\n for (const [key, value] of other) {\n if (!ref.has(key)) {\n problems.push({ locale, key, kind: \"unknown\", message: `${locale}: unknown ${key} (not in ${reference}.json)` });\n continue;\n }\n if (!checkLeaf(locale, key, value, problems)) continue;\n const expected = ref.get(key);\n if (typeof expected !== \"string\" && !isPluralMessage(expected)) continue;\n if (typeof expected !== typeof value) {\n problems.push({ locale, key, kind: \"plural\", message: `${locale}: ${key} must be ${typeof expected === \"string\" ? \"a string\" : \"a plural set\"} as in ${reference}.json` });\n continue;\n }\n if (placeholders(expected) !== placeholders(value)) problems.push({ locale, key, kind: \"placeholders\", message: `${locale}: placeholders differ in ${key}` });\n if (markup(expected) !== markup(value)) problems.push({ locale, key, kind: \"markup\", message: `${locale}: markup differs in ${key}` });\n }\n }\n\n if (options.unused?.length) {\n for (const key of findUnused([...ref.keys()], options.unused)) {\n problems.push({ locale: reference, key, kind: \"unused\", message: `unused ${key} (read nowhere in ${options.unused.join(\", \")})` });\n }\n }\n return result(ref.size);\n}\n\nconst SOURCE = /\\.(?:[cm]?[jt]sx?)$/;\nconst SKIP_DIRS = new Set([\"node_modules\", \".next\", \"dist\", \"build\", \"coverage\", \".turbo\", \".git\"]);\n\nfunction readSources(entry: string, out: string[] = []): string[] {\n const stat = fs.statSync(entry, { throwIfNoEntry: false });\n if (!stat) return out;\n if (stat.isFile()) {\n if (SOURCE.test(entry)) out.push(fs.readFileSync(entry, \"utf8\"));\n return out;\n }\n for (const name of fs.readdirSync(entry)) if (!SKIP_DIRS.has(name)) readSources(path.join(entry, name), out);\n return out;\n}\n\n/** A branch taken whole: `const h = t.home;`, `messages(\"legal\")` — `.home` or `\"home\"` not followed by `.` or a name. */\nconst wholeBranch = (branch: string): RegExp => new RegExp(`[.'\"\\`]${branch.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}(?![\\\\w.$-])`);\n\n/**\n * The keys no source reads. A key counts as read when its dotted path appears in a source (`t(\"home.title\")`,\n * `t.home.title`), when one of its branches is read dynamically (`` `home.${id}` ``, `home[id]`, `\"home.\" + id`), or\n * when a branch is taken whole (`const h = t.home;`, `messages(\"legal\")`).\n * A heuristic: it finds dead keys, it does not prove a key is shown.\n */\nexport function findUnused(keys: readonly string[], sources: readonly string[]): string[] {\n const text = sources.flatMap((entry) => readSources(entry)).join(\"\\n\");\n return keys.filter((full) => {\n const key = full.replace(/\\[\\d+\\].*$/, \"\");\n if (text.includes(key)) return false;\n const parts = key.split(\".\");\n for (let i = parts.length - 1; i > 0; i--) {\n const branch = parts.slice(0, i).join(\".\");\n if (text.includes(`${branch}.\\${`) || text.includes(`${branch}[`) || text.includes(`${branch}.\" +`) || text.includes(`${branch}.' +`)) return false;\n if (wholeBranch(branch).test(text)) return false;\n }\n return true;\n });\n}\n\n/** The human report of a check, as printed by the CLI. */\nexport function formatReport(result: CheckResult, limit = 50): string {\n if (result.ok) {\n return `✓ i18n: ${result.messages} messages, ${result.locales.length > 1 ? \"every locale complete\" : `${result.reference} only`}`;\n }\n const shown = result.problems.slice(0, limit).map((p) => p.message);\n const more = result.problems.length > limit ? `\\n … and ${result.problems.length - limit} more` : \"\";\n return `✗ i18n: ${result.problems.length} problem(s)\\n ${shown.join(\"\\n \")}${more}`;\n}\n","import type { PluralMessage, Values } from \"./types\";\n\n/** A `{name}` placeholder: letters, digits and `_`. */\nexport const PLACEHOLDER = /\\{(\\w+)\\}/g;\n\n/** The plural categories, in CLDR order (`zero` first: the explicit \"exactly 0\" form). */\nexport const PLURAL_CATEGORIES = [\"zero\", \"one\", \"two\", \"few\", \"many\", \"other\"] as const;\n\n/**\n * Replaces the `{name}` placeholders of a message: `format(\"{count} repositories\", { count: 23 })` → `\"23 repositories\"`.\n * A placeholder without a value (missing, `null` or `undefined`) stays as written, so a gap is visible, never silent.\n */\nexport function format(message: string, values: Values = {}): string {\n return message.replace(PLACEHOLDER, (match, name: string) => {\n const value = Object.hasOwn(values, name) ? values[name] : undefined;\n return value === undefined || value === null ? match : String(value);\n });\n}\n\n/** The placeholder names of a message, sorted and without duplicates: `\"{b} {a} {b}\"` → `[\"a\", \"b\"]`. */\nexport function placeholdersOf(message: string): string[] {\n return [...new Set([...message.matchAll(PLACEHOLDER)].map((m) => m[1] as string))].sort();\n}\n\n/** True when a catalogue value is a plural set: an object of plural categories with an `other` string. */\nexport function isPluralMessage(value: unknown): value is PluralMessage {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n const keys = Object.keys(value);\n return (\n typeof (value as { other?: unknown }).other === \"string\" &&\n keys.every((k) => (PLURAL_CATEGORIES as readonly string[]).includes(k) && typeof (value as Record<string, unknown>)[k] === \"string\")\n );\n}\n","/**\n * The source gate: no user-facing text written in the code. It parses `.tsx` files with the TypeScript compiler API\n * (an optional peer dependency, handed in by the caller) and reports JSX text, user-facing attributes given as\n * literals (`placeholder`, `title`, `aria-label`, `alt`, `label`), messages handed to state setters\n * (`setError(\"…\")`), words in data (`{ label: \"…\" }`) and browser dialogs (`alert`, `confirm`, `prompt`).\n *\n * Words of three letters or more count; the app's brand names are allowed (`allow`). A deliberate exception carries\n * `i18n-ignore` in a comment on the same line or the line above. Next.js `metadata` / `generateMetadata` objects are\n * left out (crawlers read them, not users).\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport type * as TS from \"typescript\";\n\nexport interface ScanOptions {\n /** Folders (or `.tsx` files) to scan. */\n paths: readonly string[];\n /** Words allowed as written: brand names (`[\"Krizaka\"]`). */\n allow?: readonly string[];\n /** Files to leave out, as regular expressions on the path (tests and stories are always left out). */\n skip?: readonly (RegExp | string)[];\n /** Paths are reported relative to this folder (default: the current directory). */\n root?: string;\n}\n\nexport interface Offender {\n file: string;\n line: number;\n /** `text`, `attr <name>`, `set <setter>`, `prop <name>` or `dialog`. */\n kind: string;\n text: string;\n}\n\nconst ATTRS = new Set([\"placeholder\", \"title\", \"aria-label\", \"alt\", \"label\"]);\nconst SETTER = /^set\\w*(Error|Message|Notice|Hint|Status|Feedback|Toast)$/;\nconst PROPS = new Set([\"label\", \"hint\", \"title\", \"placeholder\", \"description\", \"message\", \"text\", \"caption\"]);\nconst ALWAYS_SKIP = [/\\.test\\.tsx?$/, /\\.spec\\.tsx?$/, /\\.stories\\.tsx$/];\nconst SKIP_DIRS = new Set([\"node_modules\", \".next\", \"dist\", \"build\", \"coverage\"]);\n\nfunction* tsxFiles(entry: string): Generator<string> {\n const stat = fs.statSync(entry, { throwIfNoEntry: false });\n if (!stat) return;\n if (stat.isFile()) {\n if (entry.endsWith(\".tsx\")) yield entry;\n return;\n }\n for (const name of fs.readdirSync(entry).sort()) if (!SKIP_DIRS.has(name)) yield* tsxFiles(path.join(entry, name));\n}\n\n/** Scans the sources and returns every hard-coded user-facing string, in file and line order. */\nexport function scanHardcoded(ts: typeof TS, options: ScanOptions): Offender[] {\n const allowed = new Set(options.allow ?? []);\n const skip = [...ALWAYS_SKIP, ...(options.skip ?? []).map((s) => (typeof s === \"string\" ? new RegExp(s) : s))];\n const root = options.root ?? process.cwd();\n const words = (text: string) => (text.match(/[A-Za-zÀ-ÿ]{3,}/g) ?? []).filter((w) => !allowed.has(w));\n\n type Shown = { text: string; node: TS.Node };\n /** The string literals an expression can display: itself, or the branches of conditionals and logical operators. */\n const shownStrings = (expr: TS.Expression): Shown[] => {\n if (ts.isParenthesizedExpression(expr)) return shownStrings(expr.expression);\n if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) return words(expr.text).length ? [{ text: expr.text, node: expr }] : [];\n if (ts.isTemplateExpression(expr)) {\n const parts = [expr.head, ...expr.templateSpans.map((s) => s.literal)].map((p) => p.text);\n return words(parts.join(\" \")).length ? [{ text: parts.join(\"…\"), node: expr }] : [];\n }\n if (ts.isConditionalExpression(expr)) return [...shownStrings(expr.whenTrue), ...shownStrings(expr.whenFalse)];\n if (ts.isBinaryExpression(expr)) {\n const op = expr.operatorToken.kind;\n if (op === ts.SyntaxKind.AmpersandAmpersandToken) return shownStrings(expr.right);\n if (op === ts.SyntaxKind.BarBarToken || op === ts.SyntaxKind.QuestionQuestionToken) return [...shownStrings(expr.right), ...shownStrings(expr.left)];\n }\n return [];\n };\n const isMetadata = (node: TS.Node): boolean => {\n for (let n = node.parent; n; n = n.parent) {\n if (ts.isVariableDeclaration(n) && n.name.getText() === \"metadata\") return true;\n if (ts.isFunctionDeclaration(n) && n.name?.getText() === \"generateMetadata\") return true;\n }\n return false;\n };\n\n const offenders: Offender[] = [];\n for (const entry of options.paths) {\n for (const file of tsxFiles(entry)) {\n if (skip.some((re) => re.test(file.split(path.sep).join(\"/\")))) continue;\n const source = fs.readFileSync(file, \"utf8\");\n const lines = source.split(\"\\n\");\n const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);\n const report = (node: TS.Node, kind: string, text: string) => {\n const line = sf.getLineAndCharacterOfPosition(node.getStart()).line;\n if (lines.slice(Math.max(0, line - 1), line + 1).some((l) => l.includes(\"i18n-ignore\"))) return;\n offenders.push({ file: path.relative(root, file), line: line + 1, kind, text: text.replace(/\\s+/g, \" \").trim().slice(0, 80) });\n };\n const visit = (node: TS.Node): void => {\n if (ts.isJsxText(node)) {\n if (words(node.text).length) report(node, \"text\", node.text);\n } else if (ts.isJsxAttribute(node) && ATTRS.has(node.name.getText()) && node.initializer && ts.isStringLiteral(node.initializer)) {\n if (words(node.initializer.text).length) report(node, `attr ${node.name.getText()}`, node.initializer.text);\n } else if (ts.isJsxExpression(node) && node.expression && (!ts.isJsxAttribute(node.parent) || ATTRS.has(node.parent.name.getText()))) {\n // A string shown as is, or as a branch of `cond ? \"a\" : \"b\"`, `x && \"a\"`, `x ?? \"a\"`, or a template's text.\n for (const lit of shownStrings(node.expression)) report(lit.node, \"text\", lit.text);\n } else if (ts.isCallExpression(node)) {\n const callee = node.expression.getText();\n if (/^(window\\.)?(alert|confirm|prompt)$/.test(callee)) report(node, \"dialog\", callee);\n else if (SETTER.test(callee)) for (const arg of node.arguments) for (const lit of shownStrings(arg)) report(lit.node, `set ${callee}`, lit.text);\n } else if (ts.isPropertyAssignment(node) && PROPS.has(node.name.getText()) && !isMetadata(node)) {\n for (const lit of shownStrings(node.initializer)) report(lit.node, `prop ${node.name.getText()}`, lit.text);\n }\n ts.forEachChild(node, visit);\n };\n visit(sf);\n }\n }\n return offenders;\n}\n","import { main } from \"./cli\";\n\nprocess.exitCode = await main(process.argv.slice(2));\n"],"mappings":"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","names":["path","fs","path","SKIP_DIRS","path"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/check.ts","../src/format.ts","../src/scan.ts","../src/bin.ts"],"sourcesContent":["/**\n * `krizaka-i18n` — the i18n gates of every Krizaka app, one implementation:\n *\n * krizaka-i18n check <dir> [--reference en] [--unused <src>]… [--json]\n * krizaka-i18n scan <path>… [--allow <word,…>]… [--skip <regex>]… [--summary] [--json]\n *\n * Exit 0 when clean, 1 when problems are found, 2 on a usage error.\n */\nimport path from \"node:path\";\nimport { parseArgs } from \"node:util\";\n\nimport { checkMessages, formatReport } from \"./check\";\nimport { type Offender, scanHardcoded } from \"./scan\";\n\nexport const USAGE = `Usage:\n krizaka-i18n check <dir> [--reference en] [--unused <src>]… [--json]\n The catalogues of <dir> (<locale>.json) against the reference: same keys, no empty string, same {placeholders},\n same markup (<b>, <a> only). --unused <src> also fails on keys no source under <src> reads.\n krizaka-i18n scan <path>… [--allow <word,…>]… [--skip <regex>]… [--summary] [--json]\n Hard-coded user-facing strings in .tsx sources (needs the typescript package). --allow lists brand names,\n --skip leaves files out (regular expression on the path), --summary counts per file.\n --json prints a machine-readable report on stdout (for CI).`;\n\nexport interface CliIO {\n stdout: (line: string) => void;\n stderr: (line: string) => void;\n cwd: string;\n /** Loads the TypeScript compiler (an optional peer dependency) for `scan`. */\n loadTypeScript: () => Promise<typeof import(\"typescript\")>;\n}\n\nconst defaultIO: CliIO = {\n stdout: (line) => console.log(line),\n stderr: (line) => console.error(line),\n cwd: process.cwd(),\n loadTypeScript: async () => (await import(\"typescript\")).default,\n};\n\n/** Runs the CLI and returns its exit code. */\nexport async function main(argv: readonly string[], io: CliIO = defaultIO): Promise<number> {\n const [command, ...rest] = argv;\n if (command === undefined || command === \"--help\" || command === \"-h\" || command === \"help\") {\n io.stdout(USAGE);\n return command === undefined ? 2 : 0;\n }\n\n let parsed;\n try {\n parsed = parseArgs({\n args: [...rest],\n allowPositionals: true,\n options: {\n json: { type: \"boolean\" },\n reference: { type: \"string\" },\n unused: { type: \"string\", multiple: true },\n allow: { type: \"string\", multiple: true },\n skip: { type: \"string\", multiple: true },\n summary: { type: \"boolean\" },\n },\n });\n } catch (error) {\n io.stderr(`${(error as Error).message}\\n\\n${USAGE}`);\n return 2;\n }\n const { values, positionals } = parsed;\n const resolve = (p: string) => path.resolve(io.cwd, p);\n\n if (command === \"check\") {\n if (positionals.length !== 1) {\n io.stderr(`check takes one folder of catalogues.\\n\\n${USAGE}`);\n return 2;\n }\n const result = checkMessages({\n dir: resolve(positionals[0] as string),\n reference: values.reference,\n unused: values.unused?.map(resolve),\n });\n if (values.json) io.stdout(JSON.stringify(result, null, 2));\n else (result.ok ? io.stdout : io.stderr)(formatReport(result));\n return result.ok ? 0 : 1;\n }\n\n if (command === \"scan\") {\n if (!positionals.length) {\n io.stderr(`scan takes at least one folder.\\n\\n${USAGE}`);\n return 2;\n }\n let ts;\n try {\n ts = await io.loadTypeScript();\n } catch {\n io.stderr(\"scan needs the typescript package: npm install -D typescript\");\n return 2;\n }\n const offenders = scanHardcoded(ts, {\n paths: positionals.map(resolve),\n allow: values.allow?.flatMap((list) => list.split(\",\")).map((w) => w.trim()).filter(Boolean),\n skip: values.skip,\n root: io.cwd,\n });\n if (values.json) io.stdout(JSON.stringify({ ok: offenders.length === 0, offenders }, null, 2));\n else printScan(offenders, Boolean(values.summary), io);\n return offenders.length ? 1 : 0;\n }\n\n io.stderr(`Unknown command \"${command}\".\\n\\n${USAGE}`);\n return 2;\n}\n\nfunction printScan(offenders: Offender[], summary: boolean, io: CliIO): void {\n if (!offenders.length) {\n io.stdout(\"✓ i18n: no hard-coded user-facing string.\");\n return;\n }\n if (summary) {\n const byFile = new Map<string, number>();\n for (const o of offenders) byFile.set(o.file, (byFile.get(o.file) ?? 0) + 1);\n for (const [file, n] of [...byFile].sort((a, b) => b[1] - a[1])) io.stdout(`${String(n).padStart(4)} ${file}`);\n } else {\n for (const o of offenders) io.stdout(`${o.file}:${o.line} [${o.kind}] ${o.text}`);\n }\n io.stderr(`✗ i18n: ${offenders.length} hard-coded user-facing string(s): move them to the message catalogue.`);\n}\n","/**\n * The catalogue gate: `<dir>/en.json` (the reference) and every other `<dir>/<locale>.json` have exactly the same keys,\n * no empty string, the same `{placeholders}` and the same markup (`<b>`, `<a>`, the only tags a message may carry).\n * Optionally, every key is read somewhere in the sources (`unused`). Run by `krizaka-i18n check <dir>` in each app's\n * lint and CI; it replaces the scripts each app used to carry.\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { isPluralMessage, placeholdersOf } from \"./format\";\nimport type { PluralMessage } from \"./types\";\n\nexport type ProblemKind = \"invalid\" | \"missing\" | \"unknown\" | \"empty\" | \"placeholders\" | \"markup\" | \"plural\" | \"unused\";\n\nexport interface Problem {\n /** The catalogue the problem is in (`\"fr\"`), or the reference for an unused key. */\n locale: string;\n /** The dotted key (`site.nav.title`, `steps[2].title`), or the file for an invalid catalogue. */\n key: string;\n kind: ProblemKind;\n message: string;\n}\n\nexport interface CheckOptions {\n /** The folder of the catalogues (`messages`): one `<locale>.json` per locale. */\n dir: string;\n /** The reference locale (default `en`): the others must match its keys. */\n reference?: string;\n /** Folders (or files) of sources: every key must appear in them (heuristic, see `findUnused`). */\n unused?: readonly string[];\n}\n\nexport interface CheckResult {\n ok: boolean;\n reference: string;\n locales: string[];\n /** The number of messages of the reference (a plural set counts once). */\n messages: number;\n problems: Problem[];\n}\n\ntype Leaf = string | PluralMessage;\n\n/** The leaves of a catalogue by dotted key: strings, plural sets (one leaf) and other scalars; lists as `key[i]`. */\nexport function flatten(value: unknown, prefix = \"\", out = new Map<string, unknown>()): Map<string, unknown> {\n if (Array.isArray(value)) value.forEach((item, i) => flatten(item, `${prefix}[${i}]`, out));\n else if (value && typeof value === \"object\" && !isPluralMessage(value)) {\n for (const [key, item] of Object.entries(value)) flatten(item, prefix ? `${prefix}.${key}` : key, out);\n } else out.set(prefix, value);\n return out;\n}\n\nconst forms = (leaf: Leaf): string[] => (typeof leaf === \"string\" ? [leaf] : (Object.values(leaf) as string[]));\nconst signature = (list: string[]): string => list.sort().join(\",\");\nconst placeholders = (leaf: Leaf): string => signature([...new Set(forms(leaf).flatMap(placeholdersOf))]);\nconst TAG = /<\\/?([A-Za-z][\\w-]*)[^>]*>/g;\nconst tags = (text: string): string[] => [...text.matchAll(TAG)].map((m) => m[0]);\nconst markup = (leaf: Leaf): string => signature([...new Set(forms(leaf).flatMap(tags))]);\nconst ALLOWED_TAGS = new Set([\"<b>\", \"</b>\", \"<a>\", \"</a>\"]);\n\nfunction checkLeaf(locale: string, key: string, value: unknown, problems: Problem[]): value is Leaf {\n if (typeof value !== \"string\" && !isPluralMessage(value)) {\n problems.push({ locale, key, kind: \"invalid\", message: `${locale}: ${key} is not a message (${JSON.stringify(value)})` });\n return false;\n }\n for (const text of forms(value)) {\n if (!text.trim()) problems.push({ locale, key, kind: \"empty\", message: `${locale}: empty ${key}` });\n const bad = tags(text).filter((tag) => !ALLOWED_TAGS.has(tag));\n if (bad.length) problems.push({ locale, key, kind: \"markup\", message: `${locale}: unsupported markup ${bad.join(\" \")} in ${key} (only <b> and <a>)` });\n }\n return true;\n}\n\nfunction load(file: string, problems: Problem[], locale: string): Map<string, unknown> | undefined {\n try {\n return flatten(JSON.parse(fs.readFileSync(file, \"utf8\")));\n } catch (error) {\n problems.push({ locale, key: file, kind: \"invalid\", message: `${locale}: cannot read ${file} (${(error as Error).message})` });\n return undefined;\n }\n}\n\n/** Runs the gate on a folder of catalogues. Never throws for a content problem: it reports. */\nexport function checkMessages(options: CheckOptions): CheckResult {\n const reference = options.reference ?? \"en\";\n const problems: Problem[] = [];\n const files = fs.existsSync(options.dir) ? fs.readdirSync(options.dir).filter((f) => f.endsWith(\".json\")).sort() : [];\n const locales = files.map((f) => f.slice(0, -\".json\".length));\n const result = (messages: number): CheckResult => ({ ok: problems.length === 0, reference, locales, messages, problems });\n\n if (!locales.includes(reference)) {\n problems.push({ locale: reference, key: options.dir, kind: \"invalid\", message: `no ${reference}.json in ${options.dir}` });\n return result(0);\n }\n const ref = load(path.join(options.dir, `${reference}.json`), problems, reference);\n if (!ref) return result(0);\n for (const [key, value] of ref) checkLeaf(reference, key, value, problems);\n\n for (const locale of locales.filter((l) => l !== reference)) {\n const other = load(path.join(options.dir, `${locale}.json`), problems, locale);\n if (!other) continue;\n for (const key of ref.keys()) if (!other.has(key)) problems.push({ locale, key, kind: \"missing\", message: `${locale}: missing ${key}` });\n for (const [key, value] of other) {\n if (!ref.has(key)) {\n problems.push({ locale, key, kind: \"unknown\", message: `${locale}: unknown ${key} (not in ${reference}.json)` });\n continue;\n }\n if (!checkLeaf(locale, key, value, problems)) continue;\n const expected = ref.get(key);\n if (typeof expected !== \"string\" && !isPluralMessage(expected)) continue;\n if (typeof expected !== typeof value) {\n problems.push({ locale, key, kind: \"plural\", message: `${locale}: ${key} must be ${typeof expected === \"string\" ? \"a string\" : \"a plural set\"} as in ${reference}.json` });\n continue;\n }\n if (placeholders(expected) !== placeholders(value)) problems.push({ locale, key, kind: \"placeholders\", message: `${locale}: placeholders differ in ${key}` });\n if (markup(expected) !== markup(value)) problems.push({ locale, key, kind: \"markup\", message: `${locale}: markup differs in ${key}` });\n }\n }\n\n if (options.unused?.length) {\n for (const key of findUnused([...ref.keys()], options.unused)) {\n problems.push({ locale: reference, key, kind: \"unused\", message: `unused ${key} (read nowhere in ${options.unused.join(\", \")})` });\n }\n }\n return result(ref.size);\n}\n\nconst SOURCE = /\\.(?:[cm]?[jt]sx?)$/;\nconst SKIP_DIRS = new Set([\"node_modules\", \".next\", \"dist\", \"build\", \"coverage\", \".turbo\", \".git\"]);\n\nfunction readSources(entry: string, out: string[] = []): string[] {\n const stat = fs.statSync(entry, { throwIfNoEntry: false });\n if (!stat) return out;\n if (stat.isFile()) {\n if (SOURCE.test(entry)) out.push(fs.readFileSync(entry, \"utf8\"));\n return out;\n }\n for (const name of fs.readdirSync(entry)) if (!SKIP_DIRS.has(name)) readSources(path.join(entry, name), out);\n return out;\n}\n\n/** A branch taken whole: `const h = t.home;`, `messages(\"legal\")` — `.home` or `\"home\"` not followed by `.` or a name. */\nconst wholeBranch = (branch: string): RegExp => new RegExp(`[.'\"\\`]${branch.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}(?![\\\\w.$-])`);\n\n/**\n * The keys no source reads. A key counts as read when its dotted path appears in a source (`t(\"home.title\")`,\n * `t.home.title`), when one of its branches is read dynamically (`` `home.${id}` ``, `home[id]`, `\"home.\" + id`), or\n * when a branch is taken whole (`const h = t.home;`, `messages(\"legal\")`).\n * A heuristic: it finds dead keys, it does not prove a key is shown.\n */\nexport function findUnused(keys: readonly string[], sources: readonly string[]): string[] {\n const text = sources.flatMap((entry) => readSources(entry)).join(\"\\n\");\n return keys.filter((full) => {\n const arrayAt = full.search(/\\[\\d+\\]/);\n const key = arrayAt < 0 ? full : full.slice(0, arrayAt);\n if (text.includes(key)) return false;\n const parts = key.split(\".\");\n for (let i = parts.length - 1; i > 0; i--) {\n const branch = parts.slice(0, i).join(\".\");\n if (text.includes(`${branch}.\\${`) || text.includes(`${branch}[`) || text.includes(`${branch}.\" +`) || text.includes(`${branch}.' +`)) return false;\n if (wholeBranch(branch).test(text)) return false;\n }\n return true;\n });\n}\n\n/** The human report of a check, as printed by the CLI. */\nexport function formatReport(result: CheckResult, limit = 50): string {\n if (result.ok) {\n return `✓ i18n: ${result.messages} messages, ${result.locales.length > 1 ? \"every locale complete\" : `${result.reference} only`}`;\n }\n const shown = result.problems.slice(0, limit).map((p) => p.message);\n const more = result.problems.length > limit ? `\\n … and ${result.problems.length - limit} more` : \"\";\n return `✗ i18n: ${result.problems.length} problem(s)\\n ${shown.join(\"\\n \")}${more}`;\n}\n","import type { PluralMessage, Values } from \"./types\";\n\n/** A `{name}` placeholder: letters, digits and `_`. */\nexport const PLACEHOLDER = /\\{(\\w+)\\}/g;\n\n/** The plural categories, in CLDR order (`zero` first: the explicit \"exactly 0\" form). */\nexport const PLURAL_CATEGORIES = [\"zero\", \"one\", \"two\", \"few\", \"many\", \"other\"] as const;\n\n/**\n * Replaces the `{name}` placeholders of a message: `format(\"{count} repositories\", { count: 23 })` → `\"23 repositories\"`.\n * A placeholder without a value (missing, `null` or `undefined`) stays as written, so a gap is visible, never silent.\n */\nexport function format(message: string, values: Values = {}): string {\n return message.replace(PLACEHOLDER, (match, name: string) => {\n const value = Object.hasOwn(values, name) ? values[name] : undefined;\n return value === undefined || value === null ? match : String(value);\n });\n}\n\n/** The placeholder names of a message, sorted and without duplicates: `\"{b} {a} {b}\"` → `[\"a\", \"b\"]`. */\nexport function placeholdersOf(message: string): string[] {\n return [...new Set([...message.matchAll(PLACEHOLDER)].map((m) => m[1] as string))].sort();\n}\n\n/** True when a catalogue value is a plural set: an object of plural categories with an `other` string. */\nexport function isPluralMessage(value: unknown): value is PluralMessage {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n const keys = Object.keys(value);\n return (\n typeof (value as { other?: unknown }).other === \"string\" &&\n keys.every((k) => (PLURAL_CATEGORIES as readonly string[]).includes(k) && typeof (value as Record<string, unknown>)[k] === \"string\")\n );\n}\n","/**\n * The source gate: no user-facing text written in the code. It parses `.tsx` files with the TypeScript compiler API\n * (an optional peer dependency, handed in by the caller) and reports JSX text, user-facing attributes given as\n * literals (`placeholder`, `title`, `aria-label`, `alt`, `label`), messages handed to state setters\n * (`setError(\"…\")`), words in data (`{ label: \"…\" }`) and browser dialogs (`alert`, `confirm`, `prompt`).\n *\n * Words of three letters or more count; the app's brand names are allowed (`allow`). A deliberate exception carries\n * `i18n-ignore` in a comment on the same line or the line above. Next.js `metadata` / `generateMetadata` objects are\n * left out (crawlers read them, not users).\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport type * as TS from \"typescript\";\n\nexport interface ScanOptions {\n /** Folders (or `.tsx` files) to scan. */\n paths: readonly string[];\n /** Words allowed as written: brand names (`[\"Krizaka\"]`). */\n allow?: readonly string[];\n /** Files to leave out, as regular expressions on the path (tests and stories are always left out). */\n skip?: readonly (RegExp | string)[];\n /** Paths are reported relative to this folder (default: the current directory). */\n root?: string;\n}\n\nexport interface Offender {\n file: string;\n line: number;\n /** `text`, `attr <name>`, `set <setter>`, `prop <name>` or `dialog`. */\n kind: string;\n text: string;\n}\n\nconst ATTRS = new Set([\"placeholder\", \"title\", \"aria-label\", \"alt\", \"label\"]);\nconst SETTER = /^set\\w*(Error|Message|Notice|Hint|Status|Feedback|Toast)$/;\nconst PROPS = new Set([\"label\", \"hint\", \"title\", \"placeholder\", \"description\", \"message\", \"text\", \"caption\"]);\nconst ALWAYS_SKIP = [/\\.test\\.tsx?$/, /\\.spec\\.tsx?$/, /\\.stories\\.tsx$/];\nconst SKIP_DIRS = new Set([\"node_modules\", \".next\", \"dist\", \"build\", \"coverage\"]);\n\nfunction* tsxFiles(entry: string): Generator<string> {\n const stat = fs.statSync(entry, { throwIfNoEntry: false });\n if (!stat) return;\n if (stat.isFile()) {\n if (entry.endsWith(\".tsx\")) yield entry;\n return;\n }\n for (const name of fs.readdirSync(entry).sort()) if (!SKIP_DIRS.has(name)) yield* tsxFiles(path.join(entry, name));\n}\n\n/** Scans the sources and returns every hard-coded user-facing string, in file and line order. */\nexport function scanHardcoded(ts: typeof TS, options: ScanOptions): Offender[] {\n const allowed = new Set(options.allow ?? []);\n const skip = [...ALWAYS_SKIP, ...(options.skip ?? []).map((s) => (typeof s === \"string\" ? new RegExp(s) : s))];\n const root = options.root ?? process.cwd();\n const words = (text: string) => (text.match(/[A-Za-zÀ-ÿ]{3,}/g) ?? []).filter((w) => !allowed.has(w));\n\n type Shown = { text: string; node: TS.Node };\n /** The string literals an expression can display: itself, or the branches of conditionals and logical operators. */\n const shownStrings = (expr: TS.Expression): Shown[] => {\n if (ts.isParenthesizedExpression(expr)) return shownStrings(expr.expression);\n if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) return words(expr.text).length ? [{ text: expr.text, node: expr }] : [];\n if (ts.isTemplateExpression(expr)) {\n const parts = [expr.head, ...expr.templateSpans.map((s) => s.literal)].map((p) => p.text);\n return words(parts.join(\" \")).length ? [{ text: parts.join(\"…\"), node: expr }] : [];\n }\n if (ts.isConditionalExpression(expr)) return [...shownStrings(expr.whenTrue), ...shownStrings(expr.whenFalse)];\n if (ts.isBinaryExpression(expr)) {\n const op = expr.operatorToken.kind;\n if (op === ts.SyntaxKind.AmpersandAmpersandToken) return shownStrings(expr.right);\n if (op === ts.SyntaxKind.BarBarToken || op === ts.SyntaxKind.QuestionQuestionToken) return [...shownStrings(expr.right), ...shownStrings(expr.left)];\n }\n return [];\n };\n const isMetadata = (node: TS.Node): boolean => {\n for (let n = node.parent; n; n = n.parent) {\n if (ts.isVariableDeclaration(n) && n.name.getText() === \"metadata\") return true;\n if (ts.isFunctionDeclaration(n) && n.name?.getText() === \"generateMetadata\") return true;\n }\n return false;\n };\n\n const offenders: Offender[] = [];\n for (const entry of options.paths) {\n for (const file of tsxFiles(entry)) {\n if (skip.some((re) => re.test(file.split(path.sep).join(\"/\")))) continue;\n const source = fs.readFileSync(file, \"utf8\");\n const lines = source.split(\"\\n\");\n const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);\n const report = (node: TS.Node, kind: string, text: string) => {\n const line = sf.getLineAndCharacterOfPosition(node.getStart()).line;\n if (lines.slice(Math.max(0, line - 1), line + 1).some((l) => l.includes(\"i18n-ignore\"))) return;\n offenders.push({ file: path.relative(root, file), line: line + 1, kind, text: text.replace(/\\s+/g, \" \").trim().slice(0, 80) });\n };\n const visit = (node: TS.Node): void => {\n if (ts.isJsxText(node)) {\n if (words(node.text).length) report(node, \"text\", node.text);\n } else if (ts.isJsxAttribute(node) && ATTRS.has(node.name.getText()) && node.initializer && ts.isStringLiteral(node.initializer)) {\n if (words(node.initializer.text).length) report(node, `attr ${node.name.getText()}`, node.initializer.text);\n } else if (ts.isJsxExpression(node) && node.expression && (!ts.isJsxAttribute(node.parent) || ATTRS.has(node.parent.name.getText()))) {\n // A string shown as is, or as a branch of `cond ? \"a\" : \"b\"`, `x && \"a\"`, `x ?? \"a\"`, or a template's text.\n for (const lit of shownStrings(node.expression)) report(lit.node, \"text\", lit.text);\n } else if (ts.isCallExpression(node)) {\n const callee = node.expression.getText();\n if (/^(window\\.)?(alert|confirm|prompt)$/.test(callee)) report(node, \"dialog\", callee);\n else if (SETTER.test(callee)) for (const arg of node.arguments) for (const lit of shownStrings(arg)) report(lit.node, `set ${callee}`, lit.text);\n } else if (ts.isPropertyAssignment(node) && PROPS.has(node.name.getText()) && !isMetadata(node)) {\n for (const lit of shownStrings(node.initializer)) report(lit.node, `prop ${node.name.getText()}`, lit.text);\n }\n ts.forEachChild(node, visit);\n };\n visit(sf);\n }\n }\n return offenders;\n}\n","import { main } from \"./cli\";\n\nprocess.exitCode = await main(process.argv.slice(2));\n"],"mappings":"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","names":["path","fs","path","SKIP_DIRS","path"]}
|
package/dist/check.js
CHANGED
|
@@ -96,7 +96,8 @@ var wholeBranch = (branch) => new RegExp(`[.'"\`]${branch.replace(/[.*+?^${}()|[
|
|
|
96
96
|
function findUnused(keys, sources) {
|
|
97
97
|
const text = sources.flatMap((entry) => readSources(entry)).join("\n");
|
|
98
98
|
return keys.filter((full) => {
|
|
99
|
-
const
|
|
99
|
+
const arrayAt = full.search(/\[\d+\]/);
|
|
100
|
+
const key = arrayAt < 0 ? full : full.slice(0, arrayAt);
|
|
100
101
|
if (text.includes(key)) return false;
|
|
101
102
|
const parts = key.split(".");
|
|
102
103
|
for (let i = parts.length - 1; i > 0; i--) {
|
package/dist/check.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/check.ts","../src/cli.ts","../src/scan.ts"],"sourcesContent":["/**\n * The catalogue gate: `<dir>/en.json` (the reference) and every other `<dir>/<locale>.json` have exactly the same keys,\n * no empty string, the same `{placeholders}` and the same markup (`<b>`, `<a>`, the only tags a message may carry).\n * Optionally, every key is read somewhere in the sources (`unused`). Run by `krizaka-i18n check <dir>` in each app's\n * lint and CI; it replaces the scripts each app used to carry.\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { isPluralMessage, placeholdersOf } from \"./format\";\nimport type { PluralMessage } from \"./types\";\n\nexport type ProblemKind = \"invalid\" | \"missing\" | \"unknown\" | \"empty\" | \"placeholders\" | \"markup\" | \"plural\" | \"unused\";\n\nexport interface Problem {\n /** The catalogue the problem is in (`\"fr\"`), or the reference for an unused key. */\n locale: string;\n /** The dotted key (`site.nav.title`, `steps[2].title`), or the file for an invalid catalogue. */\n key: string;\n kind: ProblemKind;\n message: string;\n}\n\nexport interface CheckOptions {\n /** The folder of the catalogues (`messages`): one `<locale>.json` per locale. */\n dir: string;\n /** The reference locale (default `en`): the others must match its keys. */\n reference?: string;\n /** Folders (or files) of sources: every key must appear in them (heuristic, see `findUnused`). */\n unused?: readonly string[];\n}\n\nexport interface CheckResult {\n ok: boolean;\n reference: string;\n locales: string[];\n /** The number of messages of the reference (a plural set counts once). */\n messages: number;\n problems: Problem[];\n}\n\ntype Leaf = string | PluralMessage;\n\n/** The leaves of a catalogue by dotted key: strings, plural sets (one leaf) and other scalars; lists as `key[i]`. */\nexport function flatten(value: unknown, prefix = \"\", out = new Map<string, unknown>()): Map<string, unknown> {\n if (Array.isArray(value)) value.forEach((item, i) => flatten(item, `${prefix}[${i}]`, out));\n else if (value && typeof value === \"object\" && !isPluralMessage(value)) {\n for (const [key, item] of Object.entries(value)) flatten(item, prefix ? `${prefix}.${key}` : key, out);\n } else out.set(prefix, value);\n return out;\n}\n\nconst forms = (leaf: Leaf): string[] => (typeof leaf === \"string\" ? [leaf] : (Object.values(leaf) as string[]));\nconst signature = (list: string[]): string => list.sort().join(\",\");\nconst placeholders = (leaf: Leaf): string => signature([...new Set(forms(leaf).flatMap(placeholdersOf))]);\nconst TAG = /<\\/?([A-Za-z][\\w-]*)[^>]*>/g;\nconst tags = (text: string): string[] => [...text.matchAll(TAG)].map((m) => m[0]);\nconst markup = (leaf: Leaf): string => signature([...new Set(forms(leaf).flatMap(tags))]);\nconst ALLOWED_TAGS = new Set([\"<b>\", \"</b>\", \"<a>\", \"</a>\"]);\n\nfunction checkLeaf(locale: string, key: string, value: unknown, problems: Problem[]): value is Leaf {\n if (typeof value !== \"string\" && !isPluralMessage(value)) {\n problems.push({ locale, key, kind: \"invalid\", message: `${locale}: ${key} is not a message (${JSON.stringify(value)})` });\n return false;\n }\n for (const text of forms(value)) {\n if (!text.trim()) problems.push({ locale, key, kind: \"empty\", message: `${locale}: empty ${key}` });\n const bad = tags(text).filter((tag) => !ALLOWED_TAGS.has(tag));\n if (bad.length) problems.push({ locale, key, kind: \"markup\", message: `${locale}: unsupported markup ${bad.join(\" \")} in ${key} (only <b> and <a>)` });\n }\n return true;\n}\n\nfunction load(file: string, problems: Problem[], locale: string): Map<string, unknown> | undefined {\n try {\n return flatten(JSON.parse(fs.readFileSync(file, \"utf8\")));\n } catch (error) {\n problems.push({ locale, key: file, kind: \"invalid\", message: `${locale}: cannot read ${file} (${(error as Error).message})` });\n return undefined;\n }\n}\n\n/** Runs the gate on a folder of catalogues. Never throws for a content problem: it reports. */\nexport function checkMessages(options: CheckOptions): CheckResult {\n const reference = options.reference ?? \"en\";\n const problems: Problem[] = [];\n const files = fs.existsSync(options.dir) ? fs.readdirSync(options.dir).filter((f) => f.endsWith(\".json\")).sort() : [];\n const locales = files.map((f) => f.slice(0, -\".json\".length));\n const result = (messages: number): CheckResult => ({ ok: problems.length === 0, reference, locales, messages, problems });\n\n if (!locales.includes(reference)) {\n problems.push({ locale: reference, key: options.dir, kind: \"invalid\", message: `no ${reference}.json in ${options.dir}` });\n return result(0);\n }\n const ref = load(path.join(options.dir, `${reference}.json`), problems, reference);\n if (!ref) return result(0);\n for (const [key, value] of ref) checkLeaf(reference, key, value, problems);\n\n for (const locale of locales.filter((l) => l !== reference)) {\n const other = load(path.join(options.dir, `${locale}.json`), problems, locale);\n if (!other) continue;\n for (const key of ref.keys()) if (!other.has(key)) problems.push({ locale, key, kind: \"missing\", message: `${locale}: missing ${key}` });\n for (const [key, value] of other) {\n if (!ref.has(key)) {\n problems.push({ locale, key, kind: \"unknown\", message: `${locale}: unknown ${key} (not in ${reference}.json)` });\n continue;\n }\n if (!checkLeaf(locale, key, value, problems)) continue;\n const expected = ref.get(key);\n if (typeof expected !== \"string\" && !isPluralMessage(expected)) continue;\n if (typeof expected !== typeof value) {\n problems.push({ locale, key, kind: \"plural\", message: `${locale}: ${key} must be ${typeof expected === \"string\" ? \"a string\" : \"a plural set\"} as in ${reference}.json` });\n continue;\n }\n if (placeholders(expected) !== placeholders(value)) problems.push({ locale, key, kind: \"placeholders\", message: `${locale}: placeholders differ in ${key}` });\n if (markup(expected) !== markup(value)) problems.push({ locale, key, kind: \"markup\", message: `${locale}: markup differs in ${key}` });\n }\n }\n\n if (options.unused?.length) {\n for (const key of findUnused([...ref.keys()], options.unused)) {\n problems.push({ locale: reference, key, kind: \"unused\", message: `unused ${key} (read nowhere in ${options.unused.join(\", \")})` });\n }\n }\n return result(ref.size);\n}\n\nconst SOURCE = /\\.(?:[cm]?[jt]sx?)$/;\nconst SKIP_DIRS = new Set([\"node_modules\", \".next\", \"dist\", \"build\", \"coverage\", \".turbo\", \".git\"]);\n\nfunction readSources(entry: string, out: string[] = []): string[] {\n const stat = fs.statSync(entry, { throwIfNoEntry: false });\n if (!stat) return out;\n if (stat.isFile()) {\n if (SOURCE.test(entry)) out.push(fs.readFileSync(entry, \"utf8\"));\n return out;\n }\n for (const name of fs.readdirSync(entry)) if (!SKIP_DIRS.has(name)) readSources(path.join(entry, name), out);\n return out;\n}\n\n/** A branch taken whole: `const h = t.home;`, `messages(\"legal\")` — `.home` or `\"home\"` not followed by `.` or a name. */\nconst wholeBranch = (branch: string): RegExp => new RegExp(`[.'\"\\`]${branch.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}(?![\\\\w.$-])`);\n\n/**\n * The keys no source reads. A key counts as read when its dotted path appears in a source (`t(\"home.title\")`,\n * `t.home.title`), when one of its branches is read dynamically (`` `home.${id}` ``, `home[id]`, `\"home.\" + id`), or\n * when a branch is taken whole (`const h = t.home;`, `messages(\"legal\")`).\n * A heuristic: it finds dead keys, it does not prove a key is shown.\n */\nexport function findUnused(keys: readonly string[], sources: readonly string[]): string[] {\n const text = sources.flatMap((entry) => readSources(entry)).join(\"\\n\");\n return keys.filter((full) => {\n const key = full.replace(/\\[\\d+\\].*$/, \"\");\n if (text.includes(key)) return false;\n const parts = key.split(\".\");\n for (let i = parts.length - 1; i > 0; i--) {\n const branch = parts.slice(0, i).join(\".\");\n if (text.includes(`${branch}.\\${`) || text.includes(`${branch}[`) || text.includes(`${branch}.\" +`) || text.includes(`${branch}.' +`)) return false;\n if (wholeBranch(branch).test(text)) return false;\n }\n return true;\n });\n}\n\n/** The human report of a check, as printed by the CLI. */\nexport function formatReport(result: CheckResult, limit = 50): string {\n if (result.ok) {\n return `✓ i18n: ${result.messages} messages, ${result.locales.length > 1 ? \"every locale complete\" : `${result.reference} only`}`;\n }\n const shown = result.problems.slice(0, limit).map((p) => p.message);\n const more = result.problems.length > limit ? `\\n … and ${result.problems.length - limit} more` : \"\";\n return `✗ i18n: ${result.problems.length} problem(s)\\n ${shown.join(\"\\n \")}${more}`;\n}\n","/**\n * `krizaka-i18n` — the i18n gates of every Krizaka app, one implementation:\n *\n * krizaka-i18n check <dir> [--reference en] [--unused <src>]… [--json]\n * krizaka-i18n scan <path>… [--allow <word,…>]… [--skip <regex>]… [--summary] [--json]\n *\n * Exit 0 when clean, 1 when problems are found, 2 on a usage error.\n */\nimport path from \"node:path\";\nimport { parseArgs } from \"node:util\";\n\nimport { checkMessages, formatReport } from \"./check\";\nimport { type Offender, scanHardcoded } from \"./scan\";\n\nexport const USAGE = `Usage:\n krizaka-i18n check <dir> [--reference en] [--unused <src>]… [--json]\n The catalogues of <dir> (<locale>.json) against the reference: same keys, no empty string, same {placeholders},\n same markup (<b>, <a> only). --unused <src> also fails on keys no source under <src> reads.\n krizaka-i18n scan <path>… [--allow <word,…>]… [--skip <regex>]… [--summary] [--json]\n Hard-coded user-facing strings in .tsx sources (needs the typescript package). --allow lists brand names,\n --skip leaves files out (regular expression on the path), --summary counts per file.\n --json prints a machine-readable report on stdout (for CI).`;\n\nexport interface CliIO {\n stdout: (line: string) => void;\n stderr: (line: string) => void;\n cwd: string;\n /** Loads the TypeScript compiler (an optional peer dependency) for `scan`. */\n loadTypeScript: () => Promise<typeof import(\"typescript\")>;\n}\n\nconst defaultIO: CliIO = {\n stdout: (line) => console.log(line),\n stderr: (line) => console.error(line),\n cwd: process.cwd(),\n loadTypeScript: async () => (await import(\"typescript\")).default,\n};\n\n/** Runs the CLI and returns its exit code. */\nexport async function main(argv: readonly string[], io: CliIO = defaultIO): Promise<number> {\n const [command, ...rest] = argv;\n if (command === undefined || command === \"--help\" || command === \"-h\" || command === \"help\") {\n io.stdout(USAGE);\n return command === undefined ? 2 : 0;\n }\n\n let parsed;\n try {\n parsed = parseArgs({\n args: [...rest],\n allowPositionals: true,\n options: {\n json: { type: \"boolean\" },\n reference: { type: \"string\" },\n unused: { type: \"string\", multiple: true },\n allow: { type: \"string\", multiple: true },\n skip: { type: \"string\", multiple: true },\n summary: { type: \"boolean\" },\n },\n });\n } catch (error) {\n io.stderr(`${(error as Error).message}\\n\\n${USAGE}`);\n return 2;\n }\n const { values, positionals } = parsed;\n const resolve = (p: string) => path.resolve(io.cwd, p);\n\n if (command === \"check\") {\n if (positionals.length !== 1) {\n io.stderr(`check takes one folder of catalogues.\\n\\n${USAGE}`);\n return 2;\n }\n const result = checkMessages({\n dir: resolve(positionals[0] as string),\n reference: values.reference,\n unused: values.unused?.map(resolve),\n });\n if (values.json) io.stdout(JSON.stringify(result, null, 2));\n else (result.ok ? io.stdout : io.stderr)(formatReport(result));\n return result.ok ? 0 : 1;\n }\n\n if (command === \"scan\") {\n if (!positionals.length) {\n io.stderr(`scan takes at least one folder.\\n\\n${USAGE}`);\n return 2;\n }\n let ts;\n try {\n ts = await io.loadTypeScript();\n } catch {\n io.stderr(\"scan needs the typescript package: npm install -D typescript\");\n return 2;\n }\n const offenders = scanHardcoded(ts, {\n paths: positionals.map(resolve),\n allow: values.allow?.flatMap((list) => list.split(\",\")).map((w) => w.trim()).filter(Boolean),\n skip: values.skip,\n root: io.cwd,\n });\n if (values.json) io.stdout(JSON.stringify({ ok: offenders.length === 0, offenders }, null, 2));\n else printScan(offenders, Boolean(values.summary), io);\n return offenders.length ? 1 : 0;\n }\n\n io.stderr(`Unknown command \"${command}\".\\n\\n${USAGE}`);\n return 2;\n}\n\nfunction printScan(offenders: Offender[], summary: boolean, io: CliIO): void {\n if (!offenders.length) {\n io.stdout(\"✓ i18n: no hard-coded user-facing string.\");\n return;\n }\n if (summary) {\n const byFile = new Map<string, number>();\n for (const o of offenders) byFile.set(o.file, (byFile.get(o.file) ?? 0) + 1);\n for (const [file, n] of [...byFile].sort((a, b) => b[1] - a[1])) io.stdout(`${String(n).padStart(4)} ${file}`);\n } else {\n for (const o of offenders) io.stdout(`${o.file}:${o.line} [${o.kind}] ${o.text}`);\n }\n io.stderr(`✗ i18n: ${offenders.length} hard-coded user-facing string(s): move them to the message catalogue.`);\n}\n","/**\n * The source gate: no user-facing text written in the code. It parses `.tsx` files with the TypeScript compiler API\n * (an optional peer dependency, handed in by the caller) and reports JSX text, user-facing attributes given as\n * literals (`placeholder`, `title`, `aria-label`, `alt`, `label`), messages handed to state setters\n * (`setError(\"…\")`), words in data (`{ label: \"…\" }`) and browser dialogs (`alert`, `confirm`, `prompt`).\n *\n * Words of three letters or more count; the app's brand names are allowed (`allow`). A deliberate exception carries\n * `i18n-ignore` in a comment on the same line or the line above. Next.js `metadata` / `generateMetadata` objects are\n * left out (crawlers read them, not users).\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport type * as TS from \"typescript\";\n\nexport interface ScanOptions {\n /** Folders (or `.tsx` files) to scan. */\n paths: readonly string[];\n /** Words allowed as written: brand names (`[\"Krizaka\"]`). */\n allow?: readonly string[];\n /** Files to leave out, as regular expressions on the path (tests and stories are always left out). */\n skip?: readonly (RegExp | string)[];\n /** Paths are reported relative to this folder (default: the current directory). */\n root?: string;\n}\n\nexport interface Offender {\n file: string;\n line: number;\n /** `text`, `attr <name>`, `set <setter>`, `prop <name>` or `dialog`. */\n kind: string;\n text: string;\n}\n\nconst ATTRS = new Set([\"placeholder\", \"title\", \"aria-label\", \"alt\", \"label\"]);\nconst SETTER = /^set\\w*(Error|Message|Notice|Hint|Status|Feedback|Toast)$/;\nconst PROPS = new Set([\"label\", \"hint\", \"title\", \"placeholder\", \"description\", \"message\", \"text\", \"caption\"]);\nconst ALWAYS_SKIP = [/\\.test\\.tsx?$/, /\\.spec\\.tsx?$/, /\\.stories\\.tsx$/];\nconst SKIP_DIRS = new Set([\"node_modules\", \".next\", \"dist\", \"build\", \"coverage\"]);\n\nfunction* tsxFiles(entry: string): Generator<string> {\n const stat = fs.statSync(entry, { throwIfNoEntry: false });\n if (!stat) return;\n if (stat.isFile()) {\n if (entry.endsWith(\".tsx\")) yield entry;\n return;\n }\n for (const name of fs.readdirSync(entry).sort()) if (!SKIP_DIRS.has(name)) yield* tsxFiles(path.join(entry, name));\n}\n\n/** Scans the sources and returns every hard-coded user-facing string, in file and line order. */\nexport function scanHardcoded(ts: typeof TS, options: ScanOptions): Offender[] {\n const allowed = new Set(options.allow ?? []);\n const skip = [...ALWAYS_SKIP, ...(options.skip ?? []).map((s) => (typeof s === \"string\" ? new RegExp(s) : s))];\n const root = options.root ?? process.cwd();\n const words = (text: string) => (text.match(/[A-Za-zÀ-ÿ]{3,}/g) ?? []).filter((w) => !allowed.has(w));\n\n type Shown = { text: string; node: TS.Node };\n /** The string literals an expression can display: itself, or the branches of conditionals and logical operators. */\n const shownStrings = (expr: TS.Expression): Shown[] => {\n if (ts.isParenthesizedExpression(expr)) return shownStrings(expr.expression);\n if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) return words(expr.text).length ? [{ text: expr.text, node: expr }] : [];\n if (ts.isTemplateExpression(expr)) {\n const parts = [expr.head, ...expr.templateSpans.map((s) => s.literal)].map((p) => p.text);\n return words(parts.join(\" \")).length ? [{ text: parts.join(\"…\"), node: expr }] : [];\n }\n if (ts.isConditionalExpression(expr)) return [...shownStrings(expr.whenTrue), ...shownStrings(expr.whenFalse)];\n if (ts.isBinaryExpression(expr)) {\n const op = expr.operatorToken.kind;\n if (op === ts.SyntaxKind.AmpersandAmpersandToken) return shownStrings(expr.right);\n if (op === ts.SyntaxKind.BarBarToken || op === ts.SyntaxKind.QuestionQuestionToken) return [...shownStrings(expr.right), ...shownStrings(expr.left)];\n }\n return [];\n };\n const isMetadata = (node: TS.Node): boolean => {\n for (let n = node.parent; n; n = n.parent) {\n if (ts.isVariableDeclaration(n) && n.name.getText() === \"metadata\") return true;\n if (ts.isFunctionDeclaration(n) && n.name?.getText() === \"generateMetadata\") return true;\n }\n return false;\n };\n\n const offenders: Offender[] = [];\n for (const entry of options.paths) {\n for (const file of tsxFiles(entry)) {\n if (skip.some((re) => re.test(file.split(path.sep).join(\"/\")))) continue;\n const source = fs.readFileSync(file, \"utf8\");\n const lines = source.split(\"\\n\");\n const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);\n const report = (node: TS.Node, kind: string, text: string) => {\n const line = sf.getLineAndCharacterOfPosition(node.getStart()).line;\n if (lines.slice(Math.max(0, line - 1), line + 1).some((l) => l.includes(\"i18n-ignore\"))) return;\n offenders.push({ file: path.relative(root, file), line: line + 1, kind, text: text.replace(/\\s+/g, \" \").trim().slice(0, 80) });\n };\n const visit = (node: TS.Node): void => {\n if (ts.isJsxText(node)) {\n if (words(node.text).length) report(node, \"text\", node.text);\n } else if (ts.isJsxAttribute(node) && ATTRS.has(node.name.getText()) && node.initializer && ts.isStringLiteral(node.initializer)) {\n if (words(node.initializer.text).length) report(node, `attr ${node.name.getText()}`, node.initializer.text);\n } else if (ts.isJsxExpression(node) && node.expression && (!ts.isJsxAttribute(node.parent) || ATTRS.has(node.parent.name.getText()))) {\n // A string shown as is, or as a branch of `cond ? \"a\" : \"b\"`, `x && \"a\"`, `x ?? \"a\"`, or a template's text.\n for (const lit of shownStrings(node.expression)) report(lit.node, \"text\", lit.text);\n } else if (ts.isCallExpression(node)) {\n const callee = node.expression.getText();\n if (/^(window\\.)?(alert|confirm|prompt)$/.test(callee)) report(node, \"dialog\", callee);\n else if (SETTER.test(callee)) for (const arg of node.arguments) for (const lit of shownStrings(arg)) report(lit.node, `set ${callee}`, lit.text);\n } else if (ts.isPropertyAssignment(node) && PROPS.has(node.name.getText()) && !isMetadata(node)) {\n for (const lit of shownStrings(node.initializer)) report(lit.node, `prop ${node.name.getText()}`, lit.text);\n }\n ts.forEachChild(node, visit);\n };\n visit(sf);\n }\n }\n return offenders;\n}\n"],"mappings":"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","names":["path","fs","path","SKIP_DIRS","path"]}
|
|
1
|
+
{"version":3,"sources":["../src/check.ts","../src/cli.ts","../src/scan.ts"],"sourcesContent":["/**\n * The catalogue gate: `<dir>/en.json` (the reference) and every other `<dir>/<locale>.json` have exactly the same keys,\n * no empty string, the same `{placeholders}` and the same markup (`<b>`, `<a>`, the only tags a message may carry).\n * Optionally, every key is read somewhere in the sources (`unused`). Run by `krizaka-i18n check <dir>` in each app's\n * lint and CI; it replaces the scripts each app used to carry.\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { isPluralMessage, placeholdersOf } from \"./format\";\nimport type { PluralMessage } from \"./types\";\n\nexport type ProblemKind = \"invalid\" | \"missing\" | \"unknown\" | \"empty\" | \"placeholders\" | \"markup\" | \"plural\" | \"unused\";\n\nexport interface Problem {\n /** The catalogue the problem is in (`\"fr\"`), or the reference for an unused key. */\n locale: string;\n /** The dotted key (`site.nav.title`, `steps[2].title`), or the file for an invalid catalogue. */\n key: string;\n kind: ProblemKind;\n message: string;\n}\n\nexport interface CheckOptions {\n /** The folder of the catalogues (`messages`): one `<locale>.json` per locale. */\n dir: string;\n /** The reference locale (default `en`): the others must match its keys. */\n reference?: string;\n /** Folders (or files) of sources: every key must appear in them (heuristic, see `findUnused`). */\n unused?: readonly string[];\n}\n\nexport interface CheckResult {\n ok: boolean;\n reference: string;\n locales: string[];\n /** The number of messages of the reference (a plural set counts once). */\n messages: number;\n problems: Problem[];\n}\n\ntype Leaf = string | PluralMessage;\n\n/** The leaves of a catalogue by dotted key: strings, plural sets (one leaf) and other scalars; lists as `key[i]`. */\nexport function flatten(value: unknown, prefix = \"\", out = new Map<string, unknown>()): Map<string, unknown> {\n if (Array.isArray(value)) value.forEach((item, i) => flatten(item, `${prefix}[${i}]`, out));\n else if (value && typeof value === \"object\" && !isPluralMessage(value)) {\n for (const [key, item] of Object.entries(value)) flatten(item, prefix ? `${prefix}.${key}` : key, out);\n } else out.set(prefix, value);\n return out;\n}\n\nconst forms = (leaf: Leaf): string[] => (typeof leaf === \"string\" ? [leaf] : (Object.values(leaf) as string[]));\nconst signature = (list: string[]): string => list.sort().join(\",\");\nconst placeholders = (leaf: Leaf): string => signature([...new Set(forms(leaf).flatMap(placeholdersOf))]);\nconst TAG = /<\\/?([A-Za-z][\\w-]*)[^>]*>/g;\nconst tags = (text: string): string[] => [...text.matchAll(TAG)].map((m) => m[0]);\nconst markup = (leaf: Leaf): string => signature([...new Set(forms(leaf).flatMap(tags))]);\nconst ALLOWED_TAGS = new Set([\"<b>\", \"</b>\", \"<a>\", \"</a>\"]);\n\nfunction checkLeaf(locale: string, key: string, value: unknown, problems: Problem[]): value is Leaf {\n if (typeof value !== \"string\" && !isPluralMessage(value)) {\n problems.push({ locale, key, kind: \"invalid\", message: `${locale}: ${key} is not a message (${JSON.stringify(value)})` });\n return false;\n }\n for (const text of forms(value)) {\n if (!text.trim()) problems.push({ locale, key, kind: \"empty\", message: `${locale}: empty ${key}` });\n const bad = tags(text).filter((tag) => !ALLOWED_TAGS.has(tag));\n if (bad.length) problems.push({ locale, key, kind: \"markup\", message: `${locale}: unsupported markup ${bad.join(\" \")} in ${key} (only <b> and <a>)` });\n }\n return true;\n}\n\nfunction load(file: string, problems: Problem[], locale: string): Map<string, unknown> | undefined {\n try {\n return flatten(JSON.parse(fs.readFileSync(file, \"utf8\")));\n } catch (error) {\n problems.push({ locale, key: file, kind: \"invalid\", message: `${locale}: cannot read ${file} (${(error as Error).message})` });\n return undefined;\n }\n}\n\n/** Runs the gate on a folder of catalogues. Never throws for a content problem: it reports. */\nexport function checkMessages(options: CheckOptions): CheckResult {\n const reference = options.reference ?? \"en\";\n const problems: Problem[] = [];\n const files = fs.existsSync(options.dir) ? fs.readdirSync(options.dir).filter((f) => f.endsWith(\".json\")).sort() : [];\n const locales = files.map((f) => f.slice(0, -\".json\".length));\n const result = (messages: number): CheckResult => ({ ok: problems.length === 0, reference, locales, messages, problems });\n\n if (!locales.includes(reference)) {\n problems.push({ locale: reference, key: options.dir, kind: \"invalid\", message: `no ${reference}.json in ${options.dir}` });\n return result(0);\n }\n const ref = load(path.join(options.dir, `${reference}.json`), problems, reference);\n if (!ref) return result(0);\n for (const [key, value] of ref) checkLeaf(reference, key, value, problems);\n\n for (const locale of locales.filter((l) => l !== reference)) {\n const other = load(path.join(options.dir, `${locale}.json`), problems, locale);\n if (!other) continue;\n for (const key of ref.keys()) if (!other.has(key)) problems.push({ locale, key, kind: \"missing\", message: `${locale}: missing ${key}` });\n for (const [key, value] of other) {\n if (!ref.has(key)) {\n problems.push({ locale, key, kind: \"unknown\", message: `${locale}: unknown ${key} (not in ${reference}.json)` });\n continue;\n }\n if (!checkLeaf(locale, key, value, problems)) continue;\n const expected = ref.get(key);\n if (typeof expected !== \"string\" && !isPluralMessage(expected)) continue;\n if (typeof expected !== typeof value) {\n problems.push({ locale, key, kind: \"plural\", message: `${locale}: ${key} must be ${typeof expected === \"string\" ? \"a string\" : \"a plural set\"} as in ${reference}.json` });\n continue;\n }\n if (placeholders(expected) !== placeholders(value)) problems.push({ locale, key, kind: \"placeholders\", message: `${locale}: placeholders differ in ${key}` });\n if (markup(expected) !== markup(value)) problems.push({ locale, key, kind: \"markup\", message: `${locale}: markup differs in ${key}` });\n }\n }\n\n if (options.unused?.length) {\n for (const key of findUnused([...ref.keys()], options.unused)) {\n problems.push({ locale: reference, key, kind: \"unused\", message: `unused ${key} (read nowhere in ${options.unused.join(\", \")})` });\n }\n }\n return result(ref.size);\n}\n\nconst SOURCE = /\\.(?:[cm]?[jt]sx?)$/;\nconst SKIP_DIRS = new Set([\"node_modules\", \".next\", \"dist\", \"build\", \"coverage\", \".turbo\", \".git\"]);\n\nfunction readSources(entry: string, out: string[] = []): string[] {\n const stat = fs.statSync(entry, { throwIfNoEntry: false });\n if (!stat) return out;\n if (stat.isFile()) {\n if (SOURCE.test(entry)) out.push(fs.readFileSync(entry, \"utf8\"));\n return out;\n }\n for (const name of fs.readdirSync(entry)) if (!SKIP_DIRS.has(name)) readSources(path.join(entry, name), out);\n return out;\n}\n\n/** A branch taken whole: `const h = t.home;`, `messages(\"legal\")` — `.home` or `\"home\"` not followed by `.` or a name. */\nconst wholeBranch = (branch: string): RegExp => new RegExp(`[.'\"\\`]${branch.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}(?![\\\\w.$-])`);\n\n/**\n * The keys no source reads. A key counts as read when its dotted path appears in a source (`t(\"home.title\")`,\n * `t.home.title`), when one of its branches is read dynamically (`` `home.${id}` ``, `home[id]`, `\"home.\" + id`), or\n * when a branch is taken whole (`const h = t.home;`, `messages(\"legal\")`).\n * A heuristic: it finds dead keys, it does not prove a key is shown.\n */\nexport function findUnused(keys: readonly string[], sources: readonly string[]): string[] {\n const text = sources.flatMap((entry) => readSources(entry)).join(\"\\n\");\n return keys.filter((full) => {\n const arrayAt = full.search(/\\[\\d+\\]/);\n const key = arrayAt < 0 ? full : full.slice(0, arrayAt);\n if (text.includes(key)) return false;\n const parts = key.split(\".\");\n for (let i = parts.length - 1; i > 0; i--) {\n const branch = parts.slice(0, i).join(\".\");\n if (text.includes(`${branch}.\\${`) || text.includes(`${branch}[`) || text.includes(`${branch}.\" +`) || text.includes(`${branch}.' +`)) return false;\n if (wholeBranch(branch).test(text)) return false;\n }\n return true;\n });\n}\n\n/** The human report of a check, as printed by the CLI. */\nexport function formatReport(result: CheckResult, limit = 50): string {\n if (result.ok) {\n return `✓ i18n: ${result.messages} messages, ${result.locales.length > 1 ? \"every locale complete\" : `${result.reference} only`}`;\n }\n const shown = result.problems.slice(0, limit).map((p) => p.message);\n const more = result.problems.length > limit ? `\\n … and ${result.problems.length - limit} more` : \"\";\n return `✗ i18n: ${result.problems.length} problem(s)\\n ${shown.join(\"\\n \")}${more}`;\n}\n","/**\n * `krizaka-i18n` — the i18n gates of every Krizaka app, one implementation:\n *\n * krizaka-i18n check <dir> [--reference en] [--unused <src>]… [--json]\n * krizaka-i18n scan <path>… [--allow <word,…>]… [--skip <regex>]… [--summary] [--json]\n *\n * Exit 0 when clean, 1 when problems are found, 2 on a usage error.\n */\nimport path from \"node:path\";\nimport { parseArgs } from \"node:util\";\n\nimport { checkMessages, formatReport } from \"./check\";\nimport { type Offender, scanHardcoded } from \"./scan\";\n\nexport const USAGE = `Usage:\n krizaka-i18n check <dir> [--reference en] [--unused <src>]… [--json]\n The catalogues of <dir> (<locale>.json) against the reference: same keys, no empty string, same {placeholders},\n same markup (<b>, <a> only). --unused <src> also fails on keys no source under <src> reads.\n krizaka-i18n scan <path>… [--allow <word,…>]… [--skip <regex>]… [--summary] [--json]\n Hard-coded user-facing strings in .tsx sources (needs the typescript package). --allow lists brand names,\n --skip leaves files out (regular expression on the path), --summary counts per file.\n --json prints a machine-readable report on stdout (for CI).`;\n\nexport interface CliIO {\n stdout: (line: string) => void;\n stderr: (line: string) => void;\n cwd: string;\n /** Loads the TypeScript compiler (an optional peer dependency) for `scan`. */\n loadTypeScript: () => Promise<typeof import(\"typescript\")>;\n}\n\nconst defaultIO: CliIO = {\n stdout: (line) => console.log(line),\n stderr: (line) => console.error(line),\n cwd: process.cwd(),\n loadTypeScript: async () => (await import(\"typescript\")).default,\n};\n\n/** Runs the CLI and returns its exit code. */\nexport async function main(argv: readonly string[], io: CliIO = defaultIO): Promise<number> {\n const [command, ...rest] = argv;\n if (command === undefined || command === \"--help\" || command === \"-h\" || command === \"help\") {\n io.stdout(USAGE);\n return command === undefined ? 2 : 0;\n }\n\n let parsed;\n try {\n parsed = parseArgs({\n args: [...rest],\n allowPositionals: true,\n options: {\n json: { type: \"boolean\" },\n reference: { type: \"string\" },\n unused: { type: \"string\", multiple: true },\n allow: { type: \"string\", multiple: true },\n skip: { type: \"string\", multiple: true },\n summary: { type: \"boolean\" },\n },\n });\n } catch (error) {\n io.stderr(`${(error as Error).message}\\n\\n${USAGE}`);\n return 2;\n }\n const { values, positionals } = parsed;\n const resolve = (p: string) => path.resolve(io.cwd, p);\n\n if (command === \"check\") {\n if (positionals.length !== 1) {\n io.stderr(`check takes one folder of catalogues.\\n\\n${USAGE}`);\n return 2;\n }\n const result = checkMessages({\n dir: resolve(positionals[0] as string),\n reference: values.reference,\n unused: values.unused?.map(resolve),\n });\n if (values.json) io.stdout(JSON.stringify(result, null, 2));\n else (result.ok ? io.stdout : io.stderr)(formatReport(result));\n return result.ok ? 0 : 1;\n }\n\n if (command === \"scan\") {\n if (!positionals.length) {\n io.stderr(`scan takes at least one folder.\\n\\n${USAGE}`);\n return 2;\n }\n let ts;\n try {\n ts = await io.loadTypeScript();\n } catch {\n io.stderr(\"scan needs the typescript package: npm install -D typescript\");\n return 2;\n }\n const offenders = scanHardcoded(ts, {\n paths: positionals.map(resolve),\n allow: values.allow?.flatMap((list) => list.split(\",\")).map((w) => w.trim()).filter(Boolean),\n skip: values.skip,\n root: io.cwd,\n });\n if (values.json) io.stdout(JSON.stringify({ ok: offenders.length === 0, offenders }, null, 2));\n else printScan(offenders, Boolean(values.summary), io);\n return offenders.length ? 1 : 0;\n }\n\n io.stderr(`Unknown command \"${command}\".\\n\\n${USAGE}`);\n return 2;\n}\n\nfunction printScan(offenders: Offender[], summary: boolean, io: CliIO): void {\n if (!offenders.length) {\n io.stdout(\"✓ i18n: no hard-coded user-facing string.\");\n return;\n }\n if (summary) {\n const byFile = new Map<string, number>();\n for (const o of offenders) byFile.set(o.file, (byFile.get(o.file) ?? 0) + 1);\n for (const [file, n] of [...byFile].sort((a, b) => b[1] - a[1])) io.stdout(`${String(n).padStart(4)} ${file}`);\n } else {\n for (const o of offenders) io.stdout(`${o.file}:${o.line} [${o.kind}] ${o.text}`);\n }\n io.stderr(`✗ i18n: ${offenders.length} hard-coded user-facing string(s): move them to the message catalogue.`);\n}\n","/**\n * The source gate: no user-facing text written in the code. It parses `.tsx` files with the TypeScript compiler API\n * (an optional peer dependency, handed in by the caller) and reports JSX text, user-facing attributes given as\n * literals (`placeholder`, `title`, `aria-label`, `alt`, `label`), messages handed to state setters\n * (`setError(\"…\")`), words in data (`{ label: \"…\" }`) and browser dialogs (`alert`, `confirm`, `prompt`).\n *\n * Words of three letters or more count; the app's brand names are allowed (`allow`). A deliberate exception carries\n * `i18n-ignore` in a comment on the same line or the line above. Next.js `metadata` / `generateMetadata` objects are\n * left out (crawlers read them, not users).\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport type * as TS from \"typescript\";\n\nexport interface ScanOptions {\n /** Folders (or `.tsx` files) to scan. */\n paths: readonly string[];\n /** Words allowed as written: brand names (`[\"Krizaka\"]`). */\n allow?: readonly string[];\n /** Files to leave out, as regular expressions on the path (tests and stories are always left out). */\n skip?: readonly (RegExp | string)[];\n /** Paths are reported relative to this folder (default: the current directory). */\n root?: string;\n}\n\nexport interface Offender {\n file: string;\n line: number;\n /** `text`, `attr <name>`, `set <setter>`, `prop <name>` or `dialog`. */\n kind: string;\n text: string;\n}\n\nconst ATTRS = new Set([\"placeholder\", \"title\", \"aria-label\", \"alt\", \"label\"]);\nconst SETTER = /^set\\w*(Error|Message|Notice|Hint|Status|Feedback|Toast)$/;\nconst PROPS = new Set([\"label\", \"hint\", \"title\", \"placeholder\", \"description\", \"message\", \"text\", \"caption\"]);\nconst ALWAYS_SKIP = [/\\.test\\.tsx?$/, /\\.spec\\.tsx?$/, /\\.stories\\.tsx$/];\nconst SKIP_DIRS = new Set([\"node_modules\", \".next\", \"dist\", \"build\", \"coverage\"]);\n\nfunction* tsxFiles(entry: string): Generator<string> {\n const stat = fs.statSync(entry, { throwIfNoEntry: false });\n if (!stat) return;\n if (stat.isFile()) {\n if (entry.endsWith(\".tsx\")) yield entry;\n return;\n }\n for (const name of fs.readdirSync(entry).sort()) if (!SKIP_DIRS.has(name)) yield* tsxFiles(path.join(entry, name));\n}\n\n/** Scans the sources and returns every hard-coded user-facing string, in file and line order. */\nexport function scanHardcoded(ts: typeof TS, options: ScanOptions): Offender[] {\n const allowed = new Set(options.allow ?? []);\n const skip = [...ALWAYS_SKIP, ...(options.skip ?? []).map((s) => (typeof s === \"string\" ? new RegExp(s) : s))];\n const root = options.root ?? process.cwd();\n const words = (text: string) => (text.match(/[A-Za-zÀ-ÿ]{3,}/g) ?? []).filter((w) => !allowed.has(w));\n\n type Shown = { text: string; node: TS.Node };\n /** The string literals an expression can display: itself, or the branches of conditionals and logical operators. */\n const shownStrings = (expr: TS.Expression): Shown[] => {\n if (ts.isParenthesizedExpression(expr)) return shownStrings(expr.expression);\n if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) return words(expr.text).length ? [{ text: expr.text, node: expr }] : [];\n if (ts.isTemplateExpression(expr)) {\n const parts = [expr.head, ...expr.templateSpans.map((s) => s.literal)].map((p) => p.text);\n return words(parts.join(\" \")).length ? [{ text: parts.join(\"…\"), node: expr }] : [];\n }\n if (ts.isConditionalExpression(expr)) return [...shownStrings(expr.whenTrue), ...shownStrings(expr.whenFalse)];\n if (ts.isBinaryExpression(expr)) {\n const op = expr.operatorToken.kind;\n if (op === ts.SyntaxKind.AmpersandAmpersandToken) return shownStrings(expr.right);\n if (op === ts.SyntaxKind.BarBarToken || op === ts.SyntaxKind.QuestionQuestionToken) return [...shownStrings(expr.right), ...shownStrings(expr.left)];\n }\n return [];\n };\n const isMetadata = (node: TS.Node): boolean => {\n for (let n = node.parent; n; n = n.parent) {\n if (ts.isVariableDeclaration(n) && n.name.getText() === \"metadata\") return true;\n if (ts.isFunctionDeclaration(n) && n.name?.getText() === \"generateMetadata\") return true;\n }\n return false;\n };\n\n const offenders: Offender[] = [];\n for (const entry of options.paths) {\n for (const file of tsxFiles(entry)) {\n if (skip.some((re) => re.test(file.split(path.sep).join(\"/\")))) continue;\n const source = fs.readFileSync(file, \"utf8\");\n const lines = source.split(\"\\n\");\n const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);\n const report = (node: TS.Node, kind: string, text: string) => {\n const line = sf.getLineAndCharacterOfPosition(node.getStart()).line;\n if (lines.slice(Math.max(0, line - 1), line + 1).some((l) => l.includes(\"i18n-ignore\"))) return;\n offenders.push({ file: path.relative(root, file), line: line + 1, kind, text: text.replace(/\\s+/g, \" \").trim().slice(0, 80) });\n };\n const visit = (node: TS.Node): void => {\n if (ts.isJsxText(node)) {\n if (words(node.text).length) report(node, \"text\", node.text);\n } else if (ts.isJsxAttribute(node) && ATTRS.has(node.name.getText()) && node.initializer && ts.isStringLiteral(node.initializer)) {\n if (words(node.initializer.text).length) report(node, `attr ${node.name.getText()}`, node.initializer.text);\n } else if (ts.isJsxExpression(node) && node.expression && (!ts.isJsxAttribute(node.parent) || ATTRS.has(node.parent.name.getText()))) {\n // A string shown as is, or as a branch of `cond ? \"a\" : \"b\"`, `x && \"a\"`, `x ?? \"a\"`, or a template's text.\n for (const lit of shownStrings(node.expression)) report(lit.node, \"text\", lit.text);\n } else if (ts.isCallExpression(node)) {\n const callee = node.expression.getText();\n if (/^(window\\.)?(alert|confirm|prompt)$/.test(callee)) report(node, \"dialog\", callee);\n else if (SETTER.test(callee)) for (const arg of node.arguments) for (const lit of shownStrings(arg)) report(lit.node, `set ${callee}`, lit.text);\n } else if (ts.isPropertyAssignment(node) && PROPS.has(node.name.getText()) && !isMetadata(node)) {\n for (const lit of shownStrings(node.initializer)) report(lit.node, `prop ${node.name.getText()}`, lit.text);\n }\n ts.forEachChild(node, visit);\n };\n visit(sf);\n }\n }\n return offenders;\n}\n"],"mappings":"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","names":["path","fs","path","SKIP_DIRS","path"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krizaka/i18n",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "The i18n engine of every Krizaka app: typed message keys and placeholders from en.json, format(), plurals through @krizaka/intl, <Rich> (<b>, <a>), a React provider, and the krizaka-i18n CLI that checks catalogues and finds hard-coded strings.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"krizaka",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"react",
|
|
13
13
|
"cli"
|
|
14
14
|
],
|
|
15
|
-
"homepage": "https://
|
|
15
|
+
"homepage": "https://ui.krizaka.com/packages/i18n",
|
|
16
16
|
"bugs": "https://github.com/krizaka/krizaka-ui/issues",
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
],
|
|
55
55
|
"sideEffects": false,
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@krizaka/intl": "^0.2.
|
|
57
|
+
"@krizaka/intl": "^0.2.1"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"react": ">=18",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"tsup": "^8.5.1",
|
|
84
84
|
"typescript": "^5.9.0",
|
|
85
85
|
"vitest": "^4.1.11",
|
|
86
|
-
"@krizaka/config": "0.
|
|
86
|
+
"@krizaka/config": "0.4.1"
|
|
87
87
|
},
|
|
88
88
|
"engines": {
|
|
89
89
|
"node": ">=22"
|