@intl-t/core
Version:
A Fully-Typed Node-Based i18n Translation Library
383 lines (382 loc) • 14 kB
JavaScript
import { getLocales } from "@intl-t/core/dynamic";
import { hydration, isClient, enabledEval, disabledEval } from "@intl-t/core/global";
import { injectVariables } from "@intl-t/format";
export let TranslationBase = (enabledEval ? Function : Object);
class TranslationProxy extends TranslationBase {
__call__;
name = "Translation";
constructor(__call__) {
super();
this.__call__ = __call__;
return new Proxy(this, {
apply(target, _thisArg, argArray) {
return __call__.apply(target, argArray).t;
},
construct(target, [settings]) {
settings.settings = { ...settings.settings, ...target.settings };
return new TranslationNode(settings);
},
get(target, p, receiver) {
let val = Reflect.get(target, p, receiver);
let src;
if (val !== undefined) {
if (typeof val !== "function" || !(p in TranslationNode.prototype))
return val;
src = target;
}
else {
if (Array.isArray(target.node))
src = target.children.map((c) => target[c]);
else if (p in String.prototype)
src = target.base;
else
src = target.node || "";
val = src[p];
}
if (typeof val === "function" && !val.t)
val = val.bind(src);
return val;
},
});
}
}
export class TranslationNode extends TranslationProxy {
settings;
t = this;
translation = this;
translationNode = this;
node;
variables;
locale;
path;
key;
children;
__node__;
global;
g;
parent;
use = enabledEval ? this : this.call;
get = this.use;
static Node = TranslationNode;
static createTranslation = createTranslation;
static createTranslationSettings = createTranslationSettings;
static injectVariables = injectVariables;
static getChildren = getChildren;
static Proxy = TranslationProxy;
static context = null;
static t = null;
static setLocale = undefined;
static getLocale = function () {
return this.defaultLocale;
};
static Provider = function (...args) {
return this[this.settings.locale](...args);
};
T = new Proxy(enabledEval ? this : this.call.bind(this), {
apply(target, _, args) {
if (disabledEval)
target = target();
return TranslationNode.Provider.apply(target, args);
},
get(target, p, receiver) {
if (disabledEval)
target = target();
const t = Reflect.get(target, p, receiver);
return t?.T || t;
},
});
Tr = this.T;
Trans = this.T;
Translation = this.T;
TranslationProvider = this.T;
void = void 0;
static hook = function (...args) {
return this.current(...args);
};
hook = new Proxy(enabledEval ? this : this.call.bind(this), {
apply(target, _, argArray) {
if (disabledEval)
target = target();
return TranslationNode.hook.apply(target, argArray);
},
get(target, p, receiver) {
if (disabledEval)
target = target();
const t = Reflect.get(target, p, receiver);
return t?.hook || t;
},
});
useTranslation = this.hook;
useTranslations = this.hook;
getTranslation = this.hook;
getTranslations = this.hook;
useLocale = this.hook;
constructor(params) {
super((...args) => this.call(...args));
this.settings = params.settings ??= createTranslationSettings(params);
const { settings = params.settings, locale = settings.locale, node = settings.tree[locale], variables = node?.values || {}, path = [], key = path.at(-1), parent = settings, preload = false, } = params;
this.node = node;
this.variables = variables;
this.locale = locale;
this.path = path;
this.key = key;
this.parent = parent;
this.children = [];
this.global = parent.global || this;
this.g = this.global;
const descriptors = {};
const t = this;
this.getNode(preload);
settings.allowedLocales.forEach(locale => {
if (locale === t.locale) {
parent[locale] ??= t;
t[locale] = t;
if (typeof node !== "function" && node)
return;
return (descriptors[locale] = {
configurable: true,
enumerable: false,
get() {
Object.defineProperty(t, locale, { value: t, configurable: true, enumerable: false });
if (settings.preload && t == settings.t && t.hasOwnProperty("then"))
delete settings.t.then;
return (t.node === node && t.getNode(t[Symbol.for("preload")] ?? true), t);
},
});
}
descriptors[locale] = {
get() {
const node = parent[locale]?.[key] || parent[locale];
const value = node instanceof TranslationNode
? node
: new TranslationNode({
settings,
locale,
variables,
parent,
node: settings.locales[locale],
preload: t[Symbol.for("preload")] ?? true,
});
Object.defineProperty(t, locale, { value, configurable: true, enumerable: false });
return value;
},
configurable: true,
enumerable: false,
};
});
if (settings.preload && !settings.t)
descriptors.then = {
value(cb) {
return new Promise((r, c) => getLocales(settings.getLocale, settings.allowedLocales)
.then(locales => ((settings.locales = locales), delete t.then, r(t), cb?.(t)))
.catch(c));
},
configurable: true,
};
TranslationNode.t ??= settings.t ??= t;
Object.defineProperties(this, descriptors);
}
call(...path) {
const variables = path.at(-1)?.__proto__ === Object.prototype ? path.pop() : undefined;
if (typeof path[0] === "object")
path = path[0];
else if (path.length === 1)
path = path[0]?.trim().split(this.settings.ps);
this.set(variables);
path = path?.filter?.(Boolean);
if (!path?.length)
return this;
this.getNode();
const t = path.reduce((o, key, index) => o?.[key] ??
(() => {
const value = new TranslationNode({
node: o?.[this.settings.mainLocale]?.node?.[key] || path.slice(0, index + 1).join(o.settings.ps),
settings: o.settings,
locale: o.locale,
parent: o,
path: [...o.path, key],
});
Object.defineProperty(o, key, { value, configurable: true, enumerable: false });
return value;
})(), this);
t.set(variables);
return t;
}
set(variables) {
if (variables)
Object.assign(this.variables, variables);
return this;
}
setSource(source) {
this.node = source;
this.__node__ = void 0;
return this.getNode();
}
setNode(node) {
if (!this)
return;
if (this.__node__ === node)
return node;
this.node = this.__node__ = node;
this.setChildren();
}
getNode(load = true) {
let node = (this.node ||= this.settings.getLocale.bind(null, this.locale));
if (load && typeof node === "function")
node = node((this.settings.hydrate ??= true));
if (node instanceof Promise)
node.then(this.setNode);
else
this.setNode(node);
return (this.node = node);
}
addChildren(children = []) {
const t = this;
const settings = t.settings;
const locale = t.locale;
const descriptors = {};
children.forEach(child => {
const path = [...t.path, child];
descriptors[child] = {
get() {
const value = new TranslationNode({
node: t.node[child] || null,
settings,
locale,
parent: t,
path,
});
Object.defineProperty(t, child, { value, configurable: true, enumerable: false });
return value;
},
configurable: true,
enumerable: false,
};
});
Object.defineProperties(t, descriptors);
return children;
}
setChildren(children = getChildren(this.node)) {
this.children = children;
return this.addChildren(children);
}
get base() {
const node = this.getNode();
return TranslationNode.injectVariables(node ? String(typeof node !== "object" ? node : node.base) : this.path.join(this.settings.ps), this.values, this.settings);
}
getChildren() {
return getChildren(this.node);
}
getLocale() {
return this.settings.locale;
}
setLocale(locale = this.settings.locale) {
if (typeof locale === "function")
locale = locale(this.currentLocale);
this.settings.setLocale(locale) || (this.settings.locale = locale);
return this.current;
}
get values() {
return { ...this.parent.values, ...this.variables };
}
get child() {
return this.children[0];
}
get currentLocale() {
return this.settings.locale;
}
get current() {
this[Symbol.for("preload")] = false;
const t = this[this.currentLocale] || this;
this[Symbol.for("preload")] = null;
if (disabledEval)
return new Proxy(t.call, {
get(_target, p, receiver) {
return Reflect.get(t, p, receiver);
},
});
return t;
}
get mainLocale() {
return this.settings.mainLocale;
}
get allowedLocales() {
return this.settings.allowedLocales;
}
get locales() {
return this.allowedLocales;
}
get id() {
return (this[Symbol.for("id")] ??= this.path.join(this.settings.ps));
}
get keys() {
return this.child;
}
[Symbol.toStringTag]() {
return "Translation";
}
toString() {
return String(this.base);
}
get raw() {
return this.toString();
}
get promise() {
return this.then ? new Promise((r, c) => this.then?.(r).catch(c)) : null;
}
get then() {
const t = this;
let node = (this.node ||= this.settings?.getLocale(this.locale));
if (typeof node === "function")
node = this.node = node((this.settings.hydrate ??= true));
return node instanceof Promise
? cb => new Promise((r, c) => node.then(node => (t.setNode(node), r(t), cb?.(t))).catch(c))
: (this.setNode(node), null);
}
*[Symbol.iterator]() {
if (Array.isArray(this.node))
return yield* this.children.map(child => this[child]);
yield this.base;
}
toJSON() {
return typeof this.node === "object" ? this.node : this.base;
}
}
export const Translation = TranslationNode;
export function createTranslationSettings(settings = {}) {
if (typeof settings.locales === "function")
((settings.getLocale = settings.locales), (settings.locales = void 0), (settings.preload = !isClient));
settings.locales ??= {};
settings.allowedLocales ??= Object.keys(settings.locales);
settings.mainLocale ??= settings.defaultLocale ??= settings.allowedLocales[0];
settings.defaultLocale ??= settings.mainLocale;
settings.allowedLocale ??= settings.mainLocale;
settings.currentLocale ??= TranslationNode.context?.locale || settings.defaultLocale;
settings.locale ??= settings.currentLocale;
settings.setLocale ??= TranslationNode.setLocale;
settings.tree ??= settings.locales;
settings.variables ??= {};
settings.hydration ??= hydration;
settings.ps ??= settings.pathSeparator ??= ".";
if (TranslationNode.context?.source)
settings.locales[TranslationNode.context.locale] = TranslationNode.context.source;
const gls = settings.getLocale;
settings.getLocale = l => (settings.locales[l] ??= gls?.(l, (settings.hydrate ??= true)));
settings.setLocale ??= TranslationNode.setLocale || (l => (settings.locale = l));
return (settings.settings = settings);
}
export function createTranslation(settings = {}) {
return new Translation(createTranslationSettings(settings));
}
export const invalidKeys = ["base", "values", "children", "parent", "node", "path", "settings", "key", "default", "catch", "then"];
export function getChildren(node) {
return (node?.children ||
(typeof node !== "object" || !node
? []
: Object.keys(node).filter(key => !invalidKeys.includes(key))));
}
export function getT() {
return TranslationNode.t;
}
export const getTranslation = ((...args) => getT().current(...args));
export default TranslationNode;