105 lines
3.4 KiB
TypeScript
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;
|
|
}
|