mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(i18n): convert to opaque keys (#1848)
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { mkdirSync, readdirSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import en from '../src/core/i18n/locales/en.ts';
|
||||
import { LOCALES, localeAliases } from '../src/core/i18n/locales.ts';
|
||||
import { flattenEntries } from './i18n-utils.ts';
|
||||
|
||||
const GENERATED_HEADER = '/** Generated by packages/core/scripts/generate-i18n-locales.ts — do not edit. */\n';
|
||||
|
||||
@@ -10,6 +11,7 @@ const coreRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const coreLocalesDir = resolve(coreRoot, 'src/core/i18n/locales');
|
||||
const htmlLocalesDir = resolve(coreRoot, '../html/src/i18n/locales');
|
||||
const reactLocalesDir = resolve(coreRoot, '../react/src/i18n/locales');
|
||||
const textDir = resolve(coreRoot, 'src/core/i18n/text');
|
||||
|
||||
const localeTags = [...LOCALES, ...localeAliases(LOCALES)] as const;
|
||||
const PLATFORM_LOCALE_TAGS = ['en', ...localeTags] as const;
|
||||
@@ -20,10 +22,7 @@ function importBinding(tag: string): string {
|
||||
|
||||
function objectEntry(tag: string): string {
|
||||
const binding = importBinding(tag);
|
||||
if (tag === binding) {
|
||||
return ` ${tag},`;
|
||||
}
|
||||
return ` '${tag}': ${binding},`;
|
||||
return tag === binding ? ` ${tag},` : ` '${tag}': ${binding},`;
|
||||
}
|
||||
|
||||
function propertyKey(key: string): string {
|
||||
@@ -35,20 +34,21 @@ function generateLoadLocaleTs(): string {
|
||||
.map((tag) => ` ${propertyKey(tag.trim().replaceAll('_', '-').toLowerCase())}: () => import('./locales/${tag}'),`)
|
||||
.join('\n');
|
||||
|
||||
return `${GENERATED_HEADER}import { findLocaleKeys, getCanonicalLocaleKey, hasRegisteredLocale } from './registry';
|
||||
import type { Translations } from './types';
|
||||
return `${GENERATED_HEADER}import { flattenTranslations } from './utils';
|
||||
import { findLocaleKeys, getCanonicalLocaleKey, hasRegisteredLocale } from './registry';
|
||||
import type { FlatTranslations, Translations } from './params';
|
||||
|
||||
const loaders = {
|
||||
${entries}
|
||||
} as const satisfies Record<string, () => Promise<{ default: Partial<Translations> }>>;
|
||||
} as const satisfies Record<string, () => Promise<{ default: Translations }>>;
|
||||
|
||||
/** Lazy-import a shipped locale pack when the tag is not already in the registry. */
|
||||
export async function loadLocale(tag: string): Promise<Partial<Translations> | undefined> {
|
||||
export async function loadLocale(tag: string): Promise<Partial<FlatTranslations> | undefined> {
|
||||
if (hasRegisteredLocale(tag)) return undefined;
|
||||
for (const chainTag of findLocaleKeys(tag)) {
|
||||
if (hasRegisteredLocale(chainTag)) return undefined;
|
||||
const load = loaders[getCanonicalLocaleKey(chainTag) as keyof typeof loaders];
|
||||
if (load) return (await load()).default;
|
||||
if (load) return flattenTranslations((await load()).default);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -61,7 +61,7 @@ function generateCoreAllTs(): string {
|
||||
|
||||
const objectLines = [' en,', ...localeTags.map(objectEntry)].join('\n');
|
||||
|
||||
return `${GENERATED_HEADER}import type { Translations } from '../types';
|
||||
return `${GENERATED_HEADER}import type { Translations } from '../params';
|
||||
${imports}
|
||||
|
||||
/** Every built-in locale pack keyed by BCP 47 tag. */
|
||||
@@ -86,6 +86,61 @@ function generatePlatformAllReExport(): string {
|
||||
`;
|
||||
}
|
||||
|
||||
function exportName(key: string): string {
|
||||
const name = key.slice(key.indexOf('.') + 1);
|
||||
return name === 'default' ? 'defaultText' : `${name}Text`;
|
||||
}
|
||||
|
||||
function stringLiteral(value: string): string {
|
||||
return `'${value.replaceAll('\\', '\\\\').replaceAll("'", "\\'").replaceAll('\n', '\\n')}'`;
|
||||
}
|
||||
|
||||
function generateTextModule(entries: [string, string][]): string {
|
||||
const namespace = entries[0][0].slice(0, entries[0][0].indexOf('.'));
|
||||
const prefix = `${namespace}.`;
|
||||
const exports = entries
|
||||
.map(
|
||||
([key, text]) =>
|
||||
`export const ${exportName(key)} = {\n key: \`\${prefix}${key.slice(prefix.length)}\`,\n text: ${stringLiteral(text)},\n} as const satisfies Text;`
|
||||
)
|
||||
.join('\n\n');
|
||||
|
||||
return `${GENERATED_HEADER}import type { Text } from '../text';\n\nconst prefix = '${prefix}';\n\n${exports}\n`;
|
||||
}
|
||||
|
||||
async function validateLocaleCompleteness(): Promise<void> {
|
||||
const englishKeys = new Set(flattenEntries(en).map(([key]) => key));
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const tag of localeTags) {
|
||||
const { default: locale } = await import(pathToFileURL(resolve(coreLocalesDir, `${tag}.ts`)).href);
|
||||
const localeKeys = new Set(flattenEntries(locale ?? {}).map(([key]) => key));
|
||||
const missing = [...englishKeys].filter((key) => !localeKeys.has(key));
|
||||
if (missing.length) errors.push(`${tag}: ${missing.join(', ')}`);
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
throw new Error(`Locale packs are missing English keys:\n${errors.join('\n')}`);
|
||||
}
|
||||
}
|
||||
|
||||
function generateTextModules(): void {
|
||||
rmSync(textDir, { recursive: true, force: true });
|
||||
mkdirSync(textDir, { recursive: true });
|
||||
|
||||
const namespaces = new Map<string, [string, string][]>();
|
||||
for (const [key, text] of flattenEntries(en)) {
|
||||
const namespace = key.slice(0, key.indexOf('.'));
|
||||
const entries = namespaces.get(namespace) ?? [];
|
||||
entries.push([key, text]);
|
||||
namespaces.set(namespace, entries);
|
||||
}
|
||||
|
||||
for (const [namespace, entries] of namespaces) {
|
||||
writeGenerated(resolve(textDir, `${namespace}.ts`), generateTextModule(entries));
|
||||
}
|
||||
}
|
||||
|
||||
function generatePlatformRegisterTs(tag: string): string {
|
||||
return `${GENERATED_HEADER}import { registerI18n } from '@videojs/core/i18n';
|
||||
import translations from '@videojs/core/i18n/locales/${tag}';
|
||||
@@ -139,9 +194,11 @@ function syncPlatformLocaleDir(dir: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
await validateLocaleCompleteness();
|
||||
writeGenerated(resolve(coreLocalesDir, 'all.ts'), generateCoreAllTs());
|
||||
writeGenerated(resolve(coreRoot, 'src/core/i18n/load-locale.ts'), generateLoadLocaleTs());
|
||||
generateTextModules();
|
||||
syncPlatformLocaleDir(htmlLocalesDir);
|
||||
syncPlatformLocaleDir(reactLocalesDir);
|
||||
|
||||
console.log('[generate-i18n-locales] Updated core all.ts, load-locale.ts, and html/react locale re-exports');
|
||||
console.log('[generate-i18n-locales] Updated locale packs, text descriptors, and platform re-exports');
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import en from '../src/core/i18n/locales/en.ts';
|
||||
import { flattenEntries } from './i18n-utils.ts';
|
||||
|
||||
const GENERATED_HEADER = '/** Generated by packages/core/scripts/generate-i18n-types.ts — do not edit. */\n';
|
||||
const output = resolve(dirname(fileURLToPath(import.meta.url)), '../src/core/i18n/params.generated.ts');
|
||||
|
||||
function parameterNames(text: string): string[] {
|
||||
return [...text.matchAll(/\{([^{}]+)\}/g)]
|
||||
.map(([, name]) => name)
|
||||
.filter((name, index, names) => names.indexOf(name) === index);
|
||||
}
|
||||
|
||||
function generate(): string {
|
||||
const entries = flattenEntries(en);
|
||||
const params = entries
|
||||
.map(([key, text]) => {
|
||||
const names = parameterNames(text);
|
||||
const value = names.length ? `{ ${names.map((name) => `${name}: string | number`).join('; ')} }` : 'never';
|
||||
return ` '${key}': ${value};`;
|
||||
})
|
||||
.join('\n');
|
||||
return `${GENERATED_HEADER}
|
||||
/** Generated translation parameter contract from the English catalogue. */
|
||||
export interface TranslationParams {
|
||||
${params}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
writeFileSync(output, generate());
|
||||
console.log('[generate-i18n-types] Updated generated translation types');
|
||||
@@ -0,0 +1,5 @@
|
||||
import { flatten } from '@videojs/utils/object';
|
||||
|
||||
export function flattenEntries(value: Record<string, unknown>): [string, string][] {
|
||||
return Object.entries(flatten(value)) as [string, string][];
|
||||
}
|
||||
Reference in New Issue
Block a user