Various build shenenigans (#5)
This PR extends the build pipeline of the www service to support locale-prefixed static builds (EN/RU) with build-time SSG output injection, while also refactoring UI typography to a tokenized Tailwind v4 @theme-driven approach. Changes: - Add build-time SSG plugin to @ermnvldmr/rsbuild-config and wire multi-locale builds (/en/, /ru/) into services/www. - Introduce new @ermnvldmr/i18n package (locale constants, path helpers, keysets, pluralization) and apply it to www navigation/text. - Refactor ui typography to a shared getTypographyClassNames utility + Tailwind token definitions, with stl/cn updated for correct Tailwind-merge behavior.
This commit is contained in:
1 parent
ac9c455e5a
commit
b84ef775b8
68 files changed
+2991
-397
No files matched your search
@@ -0,0 +1,132 @@
|
||||
import { LOCALE } from './locale.js';
|
||||
import { pluralize, type PluralForms } from './pluralize.js';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
/** A translation entry with a plain string per locale. */
|
||||
interface PlainTranslation { en: string; ru: string }
|
||||
|
||||
/** A translation entry with plural forms per locale. */
|
||||
interface PluralTranslation { en: PluralForms; ru: PluralForms }
|
||||
|
||||
/** Union of all supported translation shapes. */
|
||||
type Translation = PlainTranslation | PluralTranslation;
|
||||
|
||||
/** A map of translation keys to their translation entries. */
|
||||
type Keyset = Record<string, Translation>;
|
||||
|
||||
/**
|
||||
* Returns `true` when the translation entry uses plural forms.
|
||||
*
|
||||
* @param t - The translation entry to inspect.
|
||||
* @returns Type predicate narrowing `t` to {@link PluralTranslation}.
|
||||
* @example
|
||||
* isPluralTranslation({ en: 'hello', ru: 'привет' }) // → false
|
||||
*/
|
||||
function isPluralTranslation(t: Translation): t is PluralTranslation {
|
||||
return typeof t.en === 'object';
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces `{key}` placeholders in `template` with values from `params`.
|
||||
*
|
||||
* @param template - String containing `{key}` placeholders.
|
||||
* @param params - Map of placeholder names to string/number values.
|
||||
* @returns The interpolated string.
|
||||
* @example
|
||||
* interpolate('Hello {name}!', { name: 'World' }) // → 'Hello World!'
|
||||
*/
|
||||
function interpolate(template: string, params: Record<string, string | number>): string {
|
||||
return template.replace(/\{(\w+)\}/g, (_, key: string) =>
|
||||
key in params ? String(params[key]) : `{${key}}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits `template` on `{key}` placeholders and substitutes ReactNode values.
|
||||
*
|
||||
* @param template - String containing `{key}` placeholders.
|
||||
* @param params - Map of placeholder names to ReactNode values.
|
||||
* @returns A single string when all parts are strings, otherwise a ReactNode array.
|
||||
* @example
|
||||
* interpolateRaw('Click {link}', { link: <a href="/">here</a> })
|
||||
*/
|
||||
function interpolateRaw(template: string, params: Record<string, ReactNode>): ReactNode {
|
||||
const parts = template.split(/(\{[^}]+\})/g);
|
||||
const result = parts.map((part) => {
|
||||
const match = /^\{(\w+)\}$/.exec(part);
|
||||
if (match) {
|
||||
const key = match[1];
|
||||
return key in params ? params[key] : part;
|
||||
}
|
||||
return part;
|
||||
});
|
||||
if (result.every((p): p is string => typeof p === 'string')) {
|
||||
return result.join('');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a typed `t` and `tRaw` pair bound to `keyset`.
|
||||
*
|
||||
* @param keyset - An object mapping translation keys to their locale entries.
|
||||
* @returns `{ t, tRaw }` translation helpers.
|
||||
* @example
|
||||
* const { t } = createKeyset({ greeting: { en: 'Hello', ru: 'Привет' } });
|
||||
* t('greeting') // → 'Hello' (in EN build)
|
||||
*/
|
||||
export function createKeyset<const K extends Keyset>(keyset: K) {
|
||||
/** String union of all keys defined in `keyset`. */
|
||||
type Keys = keyof K & string;
|
||||
|
||||
/**
|
||||
* Returns the translated string for `key`, with optional interpolation and pluralisation.
|
||||
*
|
||||
* @param key - A key defined in the keyset.
|
||||
* @param params - Optional interpolation/pluralisation params.
|
||||
* @returns The translated string for the current build locale.
|
||||
* @example
|
||||
* t('greeting') // → 'Hello'
|
||||
* t('items {count}', { count: 3 }) // → '3 items'
|
||||
*/
|
||||
function t(key: Keys, params?: Record<string, string | number>): string {
|
||||
const entry = keyset[key];
|
||||
|
||||
if (isPluralTranslation(entry)) {
|
||||
const forms = entry[LOCALE];
|
||||
const count = typeof params?.count === 'number' ? params.count : 0;
|
||||
const template = pluralize(count, forms);
|
||||
return params ? interpolate(template, params) : template;
|
||||
}
|
||||
|
||||
const template = entry[LOCALE];
|
||||
return params ? interpolate(template, params) : template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translated ReactNode for `key`, allowing ReactNode interpolation values.
|
||||
*
|
||||
* @param key - A key defined in the keyset.
|
||||
* @param params - Optional interpolation params; values may be any ReactNode.
|
||||
* @returns The translated string or ReactNode array for the current build locale.
|
||||
* @example
|
||||
* tRaw('click {link}', { link: <a href="/">here</a> })
|
||||
*/
|
||||
function tRaw(key: Keys, params?: Record<string, ReactNode>): ReactNode {
|
||||
const entry = keyset[key];
|
||||
|
||||
if (isPluralTranslation(entry)) {
|
||||
const forms = entry[LOCALE];
|
||||
const countParam = params?.count;
|
||||
const count = typeof countParam === 'number' ? countParam : 0;
|
||||
const template = pluralize(count, forms);
|
||||
return params ? interpolateRaw(template, params) : template;
|
||||
}
|
||||
|
||||
const template = entry[LOCALE];
|
||||
return params ? interpolateRaw(template, params) : template;
|
||||
}
|
||||
|
||||
return { t, tRaw };
|
||||
}
|
||||
Reference in new issue
Block a user