Select Git revision
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
i18n.ts 4.96 KiB
import { writable } from "svelte/store";
import de from "./de";
import en from "./en";
import { browser, dev } from "$app/environment";
export type LocalizedString = string;
export type TranslationFunction = ((...args: unknown[])=>LocalizedString&{raw:string});
export type Translation<T=TranslationFunction> = ReplaceString<typeof de, T>;
type ReplaceString<T, R = LocalizedString> = {
[K in keyof T]:
T[K] extends string ? R :
ReplaceString<T[K], R>
};
export const L: {[K in Locale]: Translation} = {
de: proxify(generateLObject(de, "de")),
en: proxify(generateLObject(en, "en")),
};
export const locales = ["de", "en"] as const;
export type Locale = typeof locales[number];
export const defaultLocale: Locale = "de";
let currentLocale: Locale = defaultLocale;
export const locale = writable<Locale>(currentLocale);
export const LL = writable<Translation>(L[currentLocale]);
export default LL;
export function setLocale(newLocale: Locale) {
currentLocale = newLocale;
locale.set(newLocale);
LL.set(L[newLocale]);
if(browser){
document.cookie = `lang=${newLocale};path=/;SameSite=Strict`;
document.documentElement.lang = newLocale;
}
}
type Placeholder = {start: number, end: number, name: string, options: string[]};
function findPlaceholders(input: string): Placeholder[] {
const result = [] as Placeholder[];
let startIdx = -1;
let currentOption = -1;
let currentOptions = [] as string[];
let inOption = false;
let currentName = "";
for(let i = 0; i < input.length; i++) {
if(startIdx === -1) {
if(input[i] === "{") startIdx = i;
}else{
if(input[i] === "}"){
if(inOption) {
currentOptions.push(input.slice(currentOption, i));
inOption = false;
}else{
currentName = input.slice(startIdx+1, i);
}
result.push({start: startIdx, end: i+1, name: currentName, options: currentOptions});
startIdx = -1;
currentOptions = [];
currentOption = -1;
currentName = "";
}else if(input[i] === "|"){
if(inOption){
currentOptions.push(input.slice(currentOption, i));
currentOption = i+1;
}else{
currentName = input.slice(startIdx+1, i);
currentOption = i+1;
inOption = true;
}
}
}
}
return result;
}
export function LLL(input: LocalizedString, ...args: unknown[]): LocalizedString {
return formatLocaleString(input, currentLocale, ...args);
}
function formatLocaleString(input: string, locale: Locale = currentLocale, ...args: unknown[]): LocalizedString {
if(typeof input !== "string"){
console.trace("input is not a string", input);
return "";
}
for(let i = 0; i < args.length; i++) {
const arg = args[i];
const toHandle = {} as Record<string, unknown>;
if(typeof arg === "object" && arg !== null) {
for(const key in arg) {
toHandle[key] = arg[key];
}
} else {
toHandle[i] = arg;
}
const placeholders = findPlaceholders(input);
for(let i = placeholders.length-1; i >= 0; i--) {
const {start, end, name, options} = placeholders[i];
const value = toHandle[name];
if(value === undefined) continue;
const formatted = options.reduce((acc, option)=>{
if(formatters[locale][option]) return formatters[locale][option](acc);
else return acc;
}, value);
input = input.slice(0, start) + formatted + input.slice(end);
}
}
return input;
}
const nothingProxy: ()=>()=>string = (path: string)=>new Proxy(()=>{
if(dev) console.trace("Trying to call non-existing translation function", path);
return "";
}, {
get: (_, prop: string)=>nothingProxy(path+"."+prop),
has: ()=>false,
});
function proxify<T extends object>(t: T, path=""): T {
//return t;
return new Proxy(t, {
get: (target, prop)=>{
if(typeof target[prop] === "function") return target[prop];
else if(typeof target[prop] === "object") return proxify(target[prop], path+"."+prop);
else return nothingProxy(path+"."+prop);
},
});
}
function generateLObject(obj: typeof de, locale: Locale): Translation {
const result = {} as Translation;
for(const key in obj){
if(typeof obj[key] === "string"){
result[key] = (...args: unknown[])=>formatLocaleString(obj[key], locale, ...args);
result[key].raw = obj[key];
}else{
result[key] = generateLObject(obj[key], locale);
}
}
return result;
}
const formatterBuilders: Record<string, (lang: Locale)=>(arg: any)=>unknown> = {
dateLong: (lang: Locale)=>new Intl.DateTimeFormat(lang, {year: "numeric", month: "long", day: "2-digit"}).format,
dateRangeLong: (lang: Locale)=>(value: [number|Date, number|Date])=>new Intl.DateTimeFormat(lang, {year: "numeric", month: "long", day: "2-digit"}).formatRange(value[0], value[1]),
dateWithWeekday: (lang: Locale)=>new Intl.DateTimeFormat(lang, {weekday: "short", year: "numeric", month: "long", day: "2-digit"}).format,
sanitize: ()=>(input: string)=>input.replace(/[^a-zA-Z0-9_ ()äöüÄÖÜß-]/g, "-"),
lowercase: ()=>(input: string)=>input.toLowerCase(),
}
const formatters = Object.fromEntries(locales.map((lang)=>[lang, Object.fromEntries(Object.entries(formatterBuilders).map(([key, builder])=>[key, builder(lang)]))]));