release: WRNexusJS 0.4.0

This commit is contained in:
2026-07-27 12:42:18 +05:30
parent 8b728a3e5d
commit 30e5721e84
250 changed files with 10065 additions and 3923 deletions
+127
View File
@@ -0,0 +1,127 @@
import type { Messages, ResolvedI18n } from "./index.ts";
export function flattenMessages(
messages: Messages,
prefix = "",
output: Record<string, string> = {},
): Record<string, string> {
for (const [key, value] of Object.entries(messages)) {
const path = prefix ? `${prefix}.${key}` : key;
if (typeof value === "string") output[path] = value;
else if (value && typeof value === "object" && !Array.isArray(value))
flattenMessages(value as Messages, path, output);
}
return output;
}
export function localeFallbacks(locale: string, fallback = "en"): string[] {
const normalized = locale.replace(/_/g, "-");
const values = [normalized];
const base = normalized.split("-")[0]!;
if (base !== normalized) values.push(base);
if (!values.includes(fallback)) values.push(fallback);
return values;
}
export function translationCoverage(i18n: ResolvedI18n): Record<
string,
{
translated: number;
total: number;
percentage: number;
missing: string[];
extra: string[];
}
> {
const canonical = flattenMessages(i18n.messages[i18n.default] ?? {});
const canonicalKeys = new Set(Object.keys(canonical));
const total = canonicalKeys.size;
const output: Record<
string,
{ translated: number; total: number; percentage: number; missing: string[]; extra: string[] }
> = {};
for (const lang of i18n.langs) {
const messages = flattenMessages(i18n.messages[lang] ?? {});
const keys = new Set(Object.keys(messages));
const missing = [...canonicalKeys].filter((key) => !keys.has(key)).sort();
const extra = [...keys].filter((key) => !canonicalKeys.has(key)).sort();
const translated = total - missing.length;
output[lang] = {
translated,
total,
percentage: total ? Math.round((translated / total) * 10000) / 100 : 100,
missing,
extra,
};
}
return output;
}
export interface LocaleFormatter {
number(value: number, options?: Intl.NumberFormatOptions): string;
currency(
value: number,
currency: string,
options?: Omit<Intl.NumberFormatOptions, "style" | "currency">,
): string;
date(value: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
relative(
value: number,
unit: Intl.RelativeTimeFormatUnit,
options?: Intl.RelativeTimeFormatOptions,
): string;
list(values: string[], options?: Intl.ListFormatOptions): string;
}
export function createLocaleFormatter(locale: string, timeZone?: string): LocaleFormatter {
const numberCache = new Map<string, Intl.NumberFormat>();
const dateCache = new Map<string, Intl.DateTimeFormat>();
const key = (value: unknown) => JSON.stringify(value ?? {});
return {
number(value, options) {
const cacheKey = key(options);
let format = numberCache.get(cacheKey);
if (!format) {
format = new Intl.NumberFormat(locale, options);
numberCache.set(cacheKey, format);
}
return format.format(value);
},
currency(value, currency, options) {
return new Intl.NumberFormat(locale, { ...options, style: "currency", currency }).format(
value,
);
},
date(value, options = { dateStyle: "medium" }) {
const resolved = { ...options, ...(timeZone ? { timeZone } : {}) };
const cacheKey = key(resolved);
let format = dateCache.get(cacheKey);
if (!format) {
format = new Intl.DateTimeFormat(locale, resolved);
dateCache.set(cacheKey, format);
}
return format.format(value instanceof Date ? value : new Date(value));
},
relative: (value, unit, options) =>
new Intl.RelativeTimeFormat(locale, options).format(value, unit),
list: (values, options) => new Intl.ListFormat(locale, options).format(values),
};
}
/** Lightweight plural templates: `{count, plural, one {# item} other {# items}}`. */
export function formatMessage(
template: string,
params: Record<string, string | number>,
locale: string,
): string {
const plural = /\{(\w+),\s*plural,\s*one\s*\{([^{}]*)\}\s*other\s*\{([^{}]*)\}\s*\}/g;
let result = template.replace(plural, (_match, name: string, one: string, other: string) => {
const value = Number(params[name] ?? 0);
const selected = new Intl.PluralRules(locale).select(value) === "one" ? one : other;
return selected.replace(/#/g, String(value));
});
result = result.replace(/\{(\w+)\}/g, (_match, name: string) =>
name in params ? String(params[name]) : `{${name}}`,
);
return result;
}