feat: homepage

This commit is contained in:
tresabhi committed 2025-01-06 20:09:11 -06:00
1 parent 1d44ecab18
commit d9688f6051
18 files changed
+303 -153

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
export * from './strings';
export * from './translator';
+46
View File
@@ -0,0 +1,46 @@
import { merge } from 'lodash-es';
import type en from '../../lang/en.json';
import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from '../blitzkit';
import { TranslationTree } from './translator';
const DEBUG_MISSING_I18N = import.meta.env.DEBUG_MISSING_I18N;
const files = import.meta.glob('../../lang/*.json', { eager: true });
export const localizedStrings: Record<string, typeof en> = {};
function nuke(object: TranslationTree) {
for (const key in object) {
const typedKey = key as keyof typeof object;
if (typeof object[typedKey] === 'string') {
const value = object[typedKey];
object[typedKey] = redact(value as string);
} else nuke(object[typedKey] as TranslationTree);
}
}
SUPPORTED_LOCALES.forEach((locale) => {
const defaultStrings = files[`../../lang/${DEFAULT_LOCALE}.json`];
const strings = files[`../../lang/${locale}.json`];
const mergedStrings = merge({}, defaultStrings, strings);
if (DEBUG_MISSING_I18N === 'true') nuke(mergedStrings);
localizedStrings[locale] = mergedStrings as typeof en;
});
function redact(string: string) {
const matches = [...string.matchAll(/%s\d+/g)];
const characters = string
.split('')
.map((char) => (char === ' ' ? ' ' : '█')) as string[];
matches.forEach((match) => {
const start = match.index;
const matchText = match[0];
for (let i = 0; i < matchText.length; i++) {
characters[start + i] = matchText[i];
}
});
return characters.join('');
}
+90
View File
@@ -0,0 +1,90 @@
export type TranslationTree = { [key: string]: TranslationNode };
export type TranslationNode = string | TranslationTree;
type NestedKeys<Type> = Type extends object
? {
[Key in keyof Type]: Key extends string
? Type[Key] extends object
? '$' extends keyof Type[Key]
? `${Key}` | `${Key}.${NestedKeys<Type[Key]>}`
: `${Key}.${NestedKeys<Type[Key]>}`
: Key
: never;
}[keyof Type]
: never;
export class Translator<
Locale extends string,
DefaultLocale extends Locale,
Strings extends TranslationTree,
Path extends NestedKeys<Strings>,
> {
private readonly defaultStrings: Map<string, string>;
constructor(
private strings: Record<Locale, Strings>,
private defaultLocale: DefaultLocale,
) {
this.defaultStrings = this.flatten(strings[defaultLocale]);
}
private flatten(strings: TranslationTree) {
const translations = new Map<string, string>();
function traverse(parent: string, tree: Record<string, TranslationNode>) {
for (const key in tree) {
const path = `${parent}${key}`;
if (typeof tree[key] === 'string') {
translations.set(path, tree[key]);
} else {
traverse(`${path}.`, tree[key]);
}
}
}
traverse('', strings);
return translations;
}
locale(locale: Locale) {
if (this.strings[locale] === undefined) {
throw new TypeError(`Locale "${locale}" is not defined`);
}
const strings = this.strings[locale];
const defaultStrings = this.defaultStrings;
const defaultLocale = this.defaultLocale;
const translations = this.flatten(strings);
function translate(path: Path, literals?: string[]) {
let translation = translations.get(path) ?? translations.get(`${path}.$`);
if (translation === undefined) {
translation =
defaultStrings.get(path) ?? defaultStrings.get(`${path}.$`);
if (translation === undefined) {
throw new Error(
`Undefined translation for "${path}" in locale "${locale}"; no fallback found in "${defaultLocale}"`,
);
}
console.warn(
`Undefined translation for "${path}" in locale "${locale}"; falling back to "${defaultLocale}"`,
);
}
literals?.forEach((literal, index) => {
translation = translation!.replaceAll(`%s${index + 1}`, literal);
});
return translation;
}
return { translate, t: translate };
}
}