refactor progress
This commit is contained in:
39
packages/i18n/src/runtime/BuildTemplates.tsx
Normal file
39
packages/i18n/src/runtime/BuildTemplates.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {I18nConfig} from '@fluxer/i18n/src/runtime/I18nTypes';
|
||||
|
||||
export function buildTemplates<TKey extends string, TValue, TVariables>(
|
||||
record: Record<string, unknown>,
|
||||
config: I18nConfig<TKey, TValue, TVariables>,
|
||||
filePath: string,
|
||||
): Map<TKey, TValue> {
|
||||
const templates = new Map<TKey, TValue>();
|
||||
|
||||
for (const [key, value] of Object.entries(record)) {
|
||||
const template = config.parseTemplate(value, key);
|
||||
if (template !== null) {
|
||||
templates.set(key as TKey, template);
|
||||
} else {
|
||||
config.onWarning?.(`Skipping invalid template in ${filePath}: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
return templates;
|
||||
}
|
||||
30
packages/i18n/src/runtime/CompileTemplate.tsx
Normal file
30
packages/i18n/src/runtime/CompileTemplate.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {TemplateCompiler} from '@fluxer/i18n/src/runtime/I18nTypes';
|
||||
import type MessageFormat from '@messageformat/core';
|
||||
|
||||
export function compileTemplate<TValue, TVariables>(
|
||||
compiler: TemplateCompiler<TValue, TVariables>,
|
||||
template: TValue,
|
||||
variables: TVariables,
|
||||
mf: MessageFormat,
|
||||
): TValue {
|
||||
return compiler(template, variables, mf);
|
||||
}
|
||||
88
packages/i18n/src/runtime/CreateI18n.tsx
Normal file
88
packages/i18n/src/runtime/CreateI18n.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import {hasLocaleFile} from '@fluxer/i18n/src/io/LocaleFilePath';
|
||||
import {parseYamlRecord} from '@fluxer/i18n/src/io/ParseYamlRecord';
|
||||
import {buildTemplates} from '@fluxer/i18n/src/runtime/BuildTemplates';
|
||||
import {getTemplate} from '@fluxer/i18n/src/runtime/GetTemplate';
|
||||
import type {I18nConfig, I18nResult, I18nState, TemplateCompiler} from '@fluxer/i18n/src/runtime/I18nTypes';
|
||||
|
||||
interface I18nModule<TKey extends string, TValue, TVariables> {
|
||||
getTemplate(key: TKey, locale: string | null, variables: TVariables): I18nResult<TKey, TValue>;
|
||||
hasLocale(locale: string): boolean;
|
||||
getLoadedLocales(): Set<string>;
|
||||
reset(): void;
|
||||
getState(): I18nState<TKey, TValue, TVariables>;
|
||||
}
|
||||
|
||||
export function createI18n<TKey extends string, TValue, TVariables>(
|
||||
config: I18nConfig<TKey, TValue, TVariables>,
|
||||
compile: TemplateCompiler<TValue, TVariables>,
|
||||
): I18nModule<TKey, TValue, TVariables> {
|
||||
const onWarning = config.onWarning ?? console.warn;
|
||||
|
||||
const state: I18nState<TKey, TValue, TVariables> = {
|
||||
loadedLocales: new Set(),
|
||||
templatesByLocale: new Map(),
|
||||
messageFormatCache: new Map(),
|
||||
config: {...config, onWarning},
|
||||
};
|
||||
|
||||
loadDefaultLocale();
|
||||
|
||||
function loadDefaultLocale(): void {
|
||||
if (!fs.existsSync(state.config.defaultMessagesFile)) {
|
||||
onWarning(`Default messages bundle not found: ${state.config.defaultMessagesFile}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(state.config.defaultMessagesFile, 'utf8');
|
||||
const parsed = parseYamlRecord(raw);
|
||||
const templates = buildTemplates(parsed, state.config, state.config.defaultMessagesFile);
|
||||
|
||||
state.templatesByLocale.set(state.config.defaultLocale, templates);
|
||||
state.loadedLocales.add(state.config.defaultLocale);
|
||||
}
|
||||
|
||||
return {
|
||||
getTemplate(key: TKey, locale: string | null, variables: TVariables): I18nResult<TKey, TValue> {
|
||||
return getTemplate(state, key, locale, variables, compile);
|
||||
},
|
||||
|
||||
hasLocale(locale: string): boolean {
|
||||
return hasLocaleFile(locale, state.config.localesPath, state.config.defaultLocale);
|
||||
},
|
||||
|
||||
getLoadedLocales(): Set<string> {
|
||||
return new Set(state.loadedLocales);
|
||||
},
|
||||
|
||||
reset(): void {
|
||||
state.loadedLocales.clear();
|
||||
state.templatesByLocale.clear();
|
||||
state.messageFormatCache.clear();
|
||||
loadDefaultLocale();
|
||||
},
|
||||
|
||||
getState(): I18nState<TKey, TValue, TVariables> {
|
||||
return state;
|
||||
},
|
||||
};
|
||||
}
|
||||
44
packages/i18n/src/runtime/GetEffectiveLocale.tsx
Normal file
44
packages/i18n/src/runtime/GetEffectiveLocale.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {hasLocaleFile} from '@fluxer/i18n/src/io/LocaleFilePath';
|
||||
import type {I18nState} from '@fluxer/i18n/src/runtime/I18nTypes';
|
||||
|
||||
export function getEffectiveLocale<TKey extends string, TValue, TVariables>(
|
||||
state: I18nState<TKey, TValue, TVariables>,
|
||||
locale: string | null | undefined,
|
||||
): string {
|
||||
if (!locale) {
|
||||
return state.config.defaultLocale;
|
||||
}
|
||||
|
||||
const normalizeLocale = state.config.normalizeLocale ?? ((locale: string) => locale);
|
||||
const normalizedLocale = normalizeLocale(locale);
|
||||
|
||||
if (normalizedLocale === state.config.defaultLocale) {
|
||||
return state.config.defaultLocale;
|
||||
}
|
||||
|
||||
if (!hasLocaleFile(normalizedLocale, state.config.localesPath, state.config.defaultLocale)) {
|
||||
state.config.onWarning?.(`Unsupported locale, falling back to ${state.config.defaultLocale}: ${locale}`);
|
||||
return state.config.defaultLocale;
|
||||
}
|
||||
|
||||
return normalizedLocale;
|
||||
}
|
||||
93
packages/i18n/src/runtime/GetTemplate.tsx
Normal file
93
packages/i18n/src/runtime/GetTemplate.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {compileTemplate} from '@fluxer/i18n/src/runtime/CompileTemplate';
|
||||
import {getEffectiveLocale} from '@fluxer/i18n/src/runtime/GetEffectiveLocale';
|
||||
import type {I18nResult, I18nState, TemplateCompiler} from '@fluxer/i18n/src/runtime/I18nTypes';
|
||||
import {loadLocaleIfNotLoaded} from '@fluxer/i18n/src/runtime/LoadLocale';
|
||||
import MessageFormat from '@messageformat/core';
|
||||
|
||||
export function getTemplate<TKey extends string, TValue, TVariables>(
|
||||
state: I18nState<TKey, TValue, TVariables>,
|
||||
key: TKey,
|
||||
locale: string | null,
|
||||
variables: TVariables,
|
||||
compile: TemplateCompiler<TValue, TVariables>,
|
||||
): I18nResult<TKey, TValue> {
|
||||
const effectiveLocale = getEffectiveLocale(state, locale);
|
||||
|
||||
loadLocaleIfNotLoaded(state, effectiveLocale);
|
||||
|
||||
const sourceTemplate = state.templatesByLocale.get(state.config.defaultLocale)?.get(key);
|
||||
if (!sourceTemplate) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
kind: 'missing-template',
|
||||
key,
|
||||
message: `Missing template ${key}`,
|
||||
},
|
||||
locale: effectiveLocale,
|
||||
};
|
||||
}
|
||||
|
||||
const translatedTemplate = state.templatesByLocale.get(effectiveLocale)?.get(key);
|
||||
const template = translatedTemplate ?? sourceTemplate;
|
||||
const validationError = state.config.validateVariables?.(key, template, variables);
|
||||
|
||||
if (validationError) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
kind: 'invalid-variables',
|
||||
key,
|
||||
message: validationError,
|
||||
},
|
||||
locale: effectiveLocale,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const compiled = compileTemplate(compile, template, variables, getMessageFormat(state, effectiveLocale));
|
||||
return {ok: true, value: compiled, locale: effectiveLocale};
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
kind: 'compile-failed',
|
||||
key,
|
||||
message: error instanceof Error ? error.message : 'Failed to compile template',
|
||||
},
|
||||
locale: effectiveLocale,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getMessageFormat<TKey extends string, TValue, TVariables>(
|
||||
state: I18nState<TKey, TValue, TVariables>,
|
||||
locale: string,
|
||||
): MessageFormat {
|
||||
const cached = state.messageFormatCache.get(locale);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const mf = new MessageFormat(locale);
|
||||
state.messageFormatCache.set(locale, mf);
|
||||
return mf;
|
||||
}
|
||||
55
packages/i18n/src/runtime/I18nTypes.tsx
Normal file
55
packages/i18n/src/runtime/I18nTypes.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type MessageFormat from '@messageformat/core';
|
||||
|
||||
export type I18nErrorKind = 'missing-template' | 'invalid-variables' | 'compile-failed';
|
||||
|
||||
export interface I18nError<TKey extends string> {
|
||||
kind: I18nErrorKind;
|
||||
key: TKey;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type I18nResult<TKey extends string, TValue> =
|
||||
| {ok: true; value: TValue; locale: string}
|
||||
| {ok: false; error: I18nError<TKey>; locale: string};
|
||||
|
||||
export interface I18nState<TKey extends string, TValue, TVariables> {
|
||||
loadedLocales: Set<string>;
|
||||
templatesByLocale: Map<string, Map<TKey, TValue>>;
|
||||
messageFormatCache: Map<string, MessageFormat>;
|
||||
config: I18nConfig<TKey, TValue, TVariables>;
|
||||
}
|
||||
|
||||
export interface I18nConfig<TKey extends string, TValue, TVariables> {
|
||||
localesPath: string;
|
||||
defaultLocale: string;
|
||||
defaultMessagesFile: string;
|
||||
normalizeLocale?: (locale: string) => string;
|
||||
parseTemplate: (value: unknown, key: string) => TValue | null;
|
||||
onWarning?: (message: string) => void;
|
||||
validateVariables?: (key: TKey, template: TValue, variables: TVariables) => string | null;
|
||||
}
|
||||
|
||||
export type TemplateCompiler<TValue, TVariables> = (
|
||||
template: TValue,
|
||||
variables: TVariables,
|
||||
mf: MessageFormat,
|
||||
) => TValue;
|
||||
62
packages/i18n/src/runtime/LoadLocale.tsx
Normal file
62
packages/i18n/src/runtime/LoadLocale.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import {localeFilePath} from '@fluxer/i18n/src/io/LocaleFilePath';
|
||||
import {parseYamlRecord} from '@fluxer/i18n/src/io/ParseYamlRecord';
|
||||
import {buildTemplates} from '@fluxer/i18n/src/runtime/BuildTemplates';
|
||||
import type {I18nState} from '@fluxer/i18n/src/runtime/I18nTypes';
|
||||
|
||||
export function loadLocaleIfNotLoaded<TKey extends string, TValue, TVariables>(
|
||||
state: I18nState<TKey, TValue, TVariables>,
|
||||
locale: string,
|
||||
): void {
|
||||
if (locale === state.config.defaultLocale) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.loadedLocales.has(locale)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = localeFilePath(locale, state.config.localesPath);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
state.config.onWarning?.(
|
||||
`Locale file not found for ${locale}: ${filePath}. Falling back to ${state.config.defaultLocale}.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
const parsed = parseYamlRecord(raw);
|
||||
const templates = buildTemplates(parsed, state.config, filePath);
|
||||
|
||||
let localeMap = state.templatesByLocale.get(locale);
|
||||
if (!localeMap) {
|
||||
localeMap = new Map();
|
||||
state.templatesByLocale.set(locale, localeMap);
|
||||
}
|
||||
|
||||
for (const [key, template] of templates) {
|
||||
localeMap.set(key, template);
|
||||
}
|
||||
|
||||
state.loadedLocales.add(locale);
|
||||
}
|
||||
180
packages/i18n/src/runtime/tests/CreateI18n.test.tsx
Normal file
180
packages/i18n/src/runtime/tests/CreateI18n.test.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import {createI18n} from '@fluxer/i18n/src/runtime/CreateI18n';
|
||||
import type {I18nResult} from '@fluxer/i18n/src/runtime/I18nTypes';
|
||||
import {beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
type TestKey = 'greeting' | 'farewell' | 'with_vars';
|
||||
|
||||
function writeFile(filePath: string, content: string): void {
|
||||
fs.mkdirSync(path.dirname(filePath), {recursive: true});
|
||||
fs.writeFileSync(filePath, content, 'utf8');
|
||||
}
|
||||
|
||||
function unwrapResult(result: I18nResult<TestKey, string>): string {
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
return result.value;
|
||||
}
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
|
||||
describe('createI18n', () => {
|
||||
let tempDir: string;
|
||||
let localesPath: string;
|
||||
let messagesFile: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fluxer-i18n-'));
|
||||
localesPath = path.join(tempDir, 'locales');
|
||||
messagesFile = path.join(localesPath, 'messages.yaml');
|
||||
writeFile(
|
||||
messagesFile,
|
||||
["'farewell': 'Bye'", "'greeting': 'Hello {name}'", "'with_vars': 'Hello {name}, you have {count}'", ''].join(
|
||||
'\n',
|
||||
),
|
||||
);
|
||||
writeFile(path.join(localesPath, 'fr.yaml'), ["'greeting': 'Salut {name}'", ''].join('\n'));
|
||||
});
|
||||
|
||||
it('returns compiled templates from the default locale', () => {
|
||||
const i18n = createI18n<TestKey, string, Record<string, unknown>>(
|
||||
{
|
||||
localesPath,
|
||||
defaultLocale: 'en-US',
|
||||
defaultMessagesFile: messagesFile,
|
||||
parseTemplate: (value) => (typeof value === 'string' ? value : null),
|
||||
},
|
||||
(template, variables, mf) => String(mf.compile(template)(variables)),
|
||||
);
|
||||
|
||||
const result = i18n.getTemplate('greeting', 'en-US', {name: 'Taylor'});
|
||||
|
||||
expect(unwrapResult(result)).toBe('Hello Taylor');
|
||||
});
|
||||
|
||||
it('loads locale files on demand', () => {
|
||||
const i18n = createI18n<TestKey, string, Record<string, unknown>>(
|
||||
{
|
||||
localesPath,
|
||||
defaultLocale: 'en-US',
|
||||
defaultMessagesFile: messagesFile,
|
||||
parseTemplate: (value) => (typeof value === 'string' ? value : null),
|
||||
},
|
||||
(template, variables, mf) => String(mf.compile(template)(variables)),
|
||||
);
|
||||
|
||||
const result = i18n.getTemplate('greeting', 'fr', {name: 'Taylor'});
|
||||
|
||||
expect(unwrapResult(result)).toBe('Salut Taylor');
|
||||
});
|
||||
|
||||
it('returns a missing-template error result', () => {
|
||||
const i18n = createI18n<TestKey, string, Record<string, unknown>>(
|
||||
{
|
||||
localesPath,
|
||||
defaultLocale: 'en-US',
|
||||
defaultMessagesFile: messagesFile,
|
||||
parseTemplate: (value) => (typeof value === 'string' ? value : null),
|
||||
},
|
||||
(template, variables, mf) => String(mf.compile(template)(variables)),
|
||||
);
|
||||
|
||||
const result = i18n.getTemplate('missing' as TestKey, 'en-US', {name: 'Taylor'});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error.kind).toBe('missing-template');
|
||||
}
|
||||
});
|
||||
|
||||
it('returns invalid-variables error when validation fails', () => {
|
||||
const i18n = createI18n<TestKey, string, Record<string, unknown> | undefined>(
|
||||
{
|
||||
localesPath,
|
||||
defaultLocale: 'en-US',
|
||||
defaultMessagesFile: messagesFile,
|
||||
parseTemplate: (value) => (typeof value === 'string' ? value : null),
|
||||
validateVariables: (key, template, variables) => {
|
||||
if (variables) {
|
||||
return null;
|
||||
}
|
||||
if (template.includes('{')) {
|
||||
return `Missing variables for ${key}`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
(template, variables, mf) => String(mf.compile(template)(variables ?? {})),
|
||||
);
|
||||
|
||||
const result = i18n.getTemplate('with_vars', 'en-US', undefined);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error.kind).toBe('invalid-variables');
|
||||
}
|
||||
});
|
||||
|
||||
it('returns compile-failed when the compiler throws', () => {
|
||||
const i18n = createI18n<TestKey, string, Record<string, unknown>>(
|
||||
{
|
||||
localesPath,
|
||||
defaultLocale: 'en-US',
|
||||
defaultMessagesFile: messagesFile,
|
||||
parseTemplate: (value) => (typeof value === 'string' ? value : null),
|
||||
},
|
||||
() => {
|
||||
throw new Error('compile failed');
|
||||
},
|
||||
);
|
||||
|
||||
const result = i18n.getTemplate('greeting', 'en-US', {name: 'Taylor'});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error.kind).toBe('compile-failed');
|
||||
}
|
||||
});
|
||||
|
||||
it('returns default locale for unsupported locales', () => {
|
||||
const warnings: Array<string> = [];
|
||||
const i18n = createI18n<TestKey, string, Record<string, unknown>>(
|
||||
{
|
||||
localesPath,
|
||||
defaultLocale: 'en-US',
|
||||
defaultMessagesFile: messagesFile,
|
||||
parseTemplate: (value) => (typeof value === 'string' ? value : null),
|
||||
onWarning: (message) => {
|
||||
warnings.push(message);
|
||||
},
|
||||
},
|
||||
(template, variables, mf) => String(mf.compile(template)(variables)),
|
||||
);
|
||||
|
||||
const result = i18n.getTemplate('greeting', 'pt-BR', {name: 'Taylor'});
|
||||
|
||||
expect(unwrapResult(result)).toBe('Hello Taylor');
|
||||
expect(warnings).toContain('Unsupported locale, falling back to en-US: pt-BR');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user