@solid-primitives/i18n
Version:
Library of primitives for providing internationalization support.
165 lines (164 loc) • 4.78 kB
JavaScript
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}`),
};
}
}