Files
WRNexusJS/packages/i18n/src/tooling.ts
T
Clintchiz 586a6db8ff
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.0
2026-08-02 23:18:51 +05:30

105 lines
3.4 KiB
TypeScript

import { readFileSync } from "node:fs";
import type { Messages } from "./index.ts";
export interface ExtractedTranslationKey {
key: string;
file?: string;
offset: number;
}
export function extractTranslationKeys(source: string, file?: string): ExtractedTranslationKey[] {
const found = new Map<string, ExtractedTranslationKey>();
const patterns = [
/(?:\b(?:t|\$t)|\bi18n\.t)\s*\(\s*(["'])([^"']+)\1/g,
/\bdata-i18n\s*=\s*(["'])([^"']+)\1/g,
/\{t:([A-Za-z0-9_.:-]+)\}/g,
];
for (const pattern of patterns) {
for (const match of source.matchAll(pattern)) {
const key = (match[2] ?? match[1])!.trim();
if (key && !found.has(key)) found.set(key, { key, file, offset: match.index });
}
}
return [...found.values()].sort((left, right) => left.key.localeCompare(right.key));
}
export function extractTranslationKeysFromFiles(
files: Iterable<string>,
): ExtractedTranslationKey[] {
const found = new Map<string, ExtractedTranslationKey>();
for (const file of files) {
for (const item of extractTranslationKeys(readFileSync(file, "utf8"), file)) {
found.set(`${item.file}:${item.key}`, item);
}
}
return [...found.values()].sort(
(left, right) =>
String(left.file).localeCompare(String(right.file)) || left.key.localeCompare(right.key),
);
}
export function flattenMessageKeys(messages: Messages, prefix = ""): string[] {
const keys: string[] = [];
for (const [name, value] of Object.entries(messages)) {
const key = prefix ? `${prefix}.${name}` : name;
if (value && typeof value === "object" && !Array.isArray(value)) {
keys.push(...flattenMessageKeys(value as Messages, key));
} else keys.push(key);
}
return keys.sort();
}
export function auditLocaleKeys(
messages: Record<string, Messages>,
referenceLocale: string,
): Record<string, { missing: string[]; extra: string[] }> {
const reference = new Set(flattenMessageKeys(messages[referenceLocale] ?? {}));
const result: Record<string, { missing: string[]; extra: string[] }> = {};
for (const [locale, value] of Object.entries(messages)) {
const keys = new Set(flattenMessageKeys(value));
result[locale] = {
missing: [...reference].filter((key) => !keys.has(key)).sort(),
extra: [...keys].filter((key) => !reference.has(key)).sort(),
};
}
return result;
}
const ACCENTS: Record<string, string> = {
a: "à",
e: "ë",
i: "ï",
o: "ô",
u: "ü",
A: "À",
E: "Ë",
I: "Ï",
O: "Ô",
U: "Ü",
};
export function pseudoLocalize(value: string, options: { rtl?: boolean } = {}): string {
const parts = value.split(/(\{[^{}]+\}|<[^>]+>)/g);
const transformed = parts
.map((part) =>
/^\{[^{}]+\}$|^<[^>]+>$/.test(part)
? part
: part.replace(/[aeiouAEIOU]/g, (character) => ACCENTS[character] ?? character),
)
.join("");
return options.rtl ? `\u202e[${transformed}]\u202c` : `[${transformed}~~~]`;
}
export function createPseudoLocale(messages: Messages, options: { rtl?: boolean } = {}): Messages {
const output: Messages = Object.create(null) as Messages;
for (const [key, value] of Object.entries(messages)) {
output[key] =
typeof value === "string"
? pseudoLocalize(value, options)
: value && typeof value === "object" && !Array.isArray(value)
? createPseudoLocale(value as Messages, options)
: value;
}
return output;
}