release: WRNexusJS 0.4.0
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/i18n",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -170,3 +170,11 @@ export const I18N_RUNTIME = String.raw`
|
||||
else bind(document);
|
||||
})();
|
||||
`.trim();
|
||||
export {
|
||||
flattenMessages,
|
||||
localeFallbacks,
|
||||
translationCoverage,
|
||||
createLocaleFormatter,
|
||||
formatMessage,
|
||||
} from "./advanced.ts";
|
||||
export type { LocaleFormatter } from "./advanced.ts";
|
||||
|
||||
Reference in New Issue
Block a user