Skip to content
Snippets Groups Projects
Select Git revision
  • 566c4be21113085be52517bfd76401a586d702d4
  • master default protected
2 results

i18n.ts

Blame
  • 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)]))]));