UNPKG

@solid-primitives/i18n

Version:

Library of primitives for providing internationalization support.

165 lines (164 loc) 4.78 kB
const isDict = (value) => value != null && ((value = Object.getPrototypeOf(value)), value === Array.prototype || value === Object.prototype); const isRecordDict = (value) => value != null && Object.getPrototypeOf(value) === Object.prototype; function visitDict(flat_dict, dict, path) { for (const [key, value] of Object.entries(dict)) { const key_path = `${path}.${key}`; flat_dict[key_path] = value; isDict(value) && visitDict(flat_dict, value, key_path); } } /** * Flatten a nested dictionary into a flat dictionary. * * This way each nested property is available as a flat key. * * @example * ```ts * const dict = { * a: { * foo: "foo", * b: { bar: 1 } * } * } * * const flat_dict = flatten(dict); * * flat_dict === { * a: { * foo: "foo", * b: { bar: 1 } * }, * "a.foo": "foo", * "a.b": { bar: 1 }, * "a.b.bar": 1, * } * ``` */ export function flatten(dict) { const flat_dict = { ...dict }; for (const [key, value] of Object.entries(dict)) { isDict(value) && visitDict(flat_dict, value, key); } return flat_dict; } /** * Prefix all *(own)* keys in the dictionary with the provided prefix. * * Useful for namespacing a dictionary when combining multiple dictionaries. * * @example * ```ts * const dict = { * hello: "hello", * goodbye: "goodbye", * food: { meat: "meat" }, * } * * const prefixed_dict = prefix(dict, "greetings"); * * prefixed_dict === { * "greetings.hello": "hello", * "greetings.goodbye": "goodbye", * "greetings.food": { meat: "meat" }, * } * ``` */ export const prefix = (dict, prefix) => { prefix += "."; const result = {}; for (const [key, value] of Object.entries(dict)) { result[prefix + key] = value; } return result; }; /** * Identity function that returns the same string branded as {@link Template} with the arguments needed to resolve the template. * * @example * ```ts * const template = i18n.template<{ name: string }>("hello {{ name }}!"); * * // same as * const template = "hello {{ name }}!" as Template<{ name: string }>; * ``` */ export const template = (source) => source; /** * Simple template resolver that replaces `{{ key }}` with the value of `args.key`. * * @example * ```ts * resolveTemplate("hello {{ name }}!", { name: "John" }); * // => "hello John!" * ``` */ export const resolveTemplate = (string, args) => { if (args) for (const [key, value] of Object.entries(args)) string = string.replace(new RegExp(`{{\\s*${key}\\s*}}`, "g"), value); return string; }; /** * Template resolver that does nothing. It's used as a fallback when no template resolver is provided. */ export const identityResolveTemplate = (v => v); export function translator(dict, resolveTemplate = identityResolveTemplate) { return (path, ...args) => { if (path[0] === ".") path = path.slice(1); const value = dict()?.[path]; switch (typeof value) { case "function": return value(...args); case "string": return resolveTemplate(value, args[0]); default: return value; } }; } export function scopedTranslator(translator, scope) { return (path, ...args) => translator(`${scope}.${path}`, ...args); } export function chainedTranslator(init_dict, translate, path = "") { const result = { ...init_dict }; for (const [key, value] of Object.entries(init_dict)) { const key_path = `${path}.${key}`; result[key] = isRecordDict(value) ? chainedTranslator(value, translate, key_path) : (...args) => translate(key_path, // @ts-expect-error ...args); } return result; } export function proxyTranslator(translate, path = "") { return new Proxy(translate.bind(void 0, path), new Traps(translate, path)); } class Traps { translate; path; constructor(translate, path) { this.translate = translate; this.path = path; } get(target, prop) { if (typeof prop !== "string") return Reflect.get(target, prop); return proxyTranslator(this.translate, `${this.path}.${prop}`); } has(target, prop) { if (typeof prop !== "string") return Reflect.has(target, prop); return proxyTranslator(this.translate, `${this.path}.${prop}`) !== undefined; } getOwnPropertyDescriptor(target, prop) { if (typeof prop !== "string") return Reflect.getOwnPropertyDescriptor(target, prop); return { enumerable: true, get: () => proxyTranslator(this.translate, `${this.path}.${prop}`), }; } }