/** * @module lib/translation * This module contains a translation system that supports flat and nested JSON objects and value transformation functions - [see the documentation for more info](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#translation) */ import type { Stringifiable } from "@sv443-network/coreutils"; /** * Translation object to pass to {@linkcode tr.addTranslations()} * Can be a flat object of identifier keys and the translation text as the value, or an infinitely nestable object containing the same. * * @example * // Flat object: * const tr_en: TrObject = { * hello: "Hello, %1!", * foo: "Foo", * }; * * // Nested object: * const tr_de: TrObject = { * hello: "Hallo, %1!", * foo: { * bar: "Foo bar", * }, * }; */ export interface TrObject { [key: string]: string | TrObject; } /** Properties for the transform function that transforms a matched translation string into something else */ export type TransformFnProps = { /** The current language - empty string if not set yet */ language: string; /** The matches as returned by `RegExp.exec()` */ matches: RegExpExecArray[]; /** The translation key */ trKey: TTrKey; /** Translation value before any transformations */ trValue: string; /** Current value, possibly in-between transformations */ currentValue: string; /** Arguments passed to the translation function */ trArgs: (Stringifiable | Record)[]; }; /** Function that transforms a matched translation string into another string */ export type TransformFn = (props: TransformFnProps) => Stringifiable; /** Transform pattern and function in tuple form */ export type TransformTuple = [RegExp, TransformFn]; /** * Pass a recursive or flat translation object to this generic type to get all keys in the object. * @example ```ts * type Keys = TrKeys<{ a: { b: "foo" }, c: "bar" }>; * // result: type Keys = "a.b" | "c" * ``` */ export type TrKeys = { [K in keyof TTrObj]: K extends string | number | boolean | null | undefined ? TTrObj[K] extends object ? TrKeys : `${P}${K}` : never; }[keyof TTrObj]; /** All translations loaded into memory */ declare const trans: { [language: string]: TrObject; }; /** * Returns the translated text for the specified key in the specified language. * If the key is not found in the specified previously registered translation, the key itself is returned. * * ⚠️ Remember to register a language with {@linkcode tr.addTranslations()} before using this function, otherwise it will always return the key itself. * @param language Language code or name to use for the translation * @param key Key of the translation to return * @param args Optional arguments to be passed to the translated text. They will replace placeholders in the format `%n`, where `n` is the 1-indexed argument number */ declare function trFor(language: string, key: TTrKey, ...args: (Stringifiable | Record)[]): string; /** * Prepares a translation function for a specific language. * @example ```ts * tr.addTranslations("en", { * hello: "Hello, %1!", * }); * const t = tr.useTr("en"); * t("hello", "John"); // "Hello, John!" * ``` */ declare function useTr(language: string): (key: TTrKey, ...args: (Stringifiable | Record)[]) => ReturnType>; /** * Checks if a translation exists given its {@linkcode key} in the specified {@linkcode language} or the set fallback language. * If the given language was not registered with {@linkcode tr.addTranslations()}, this function will return `false`. * @param key Key of the translation to check for * @param language Language code or name to check in - defaults to the currently active language (set by {@linkcode tr.setLanguage()}) * @returns Whether the translation key exists in the specified language - always returns `false` if no language is given and no active language was set */ declare function hasKey(language: string | undefined, key: TTrKey): boolean; /** * Registers a new language and its translations - if the language already exists, it will be wholly replaced by the new one. If merging is necessary, it will have to be done before calling this function. * * The translations are a key-value pair where the key is the translation key and the value is the translated text. * The translations can also be infinitely nested objects, resulting in a dot-separated key. * @param language Arbitrary language code or name to register for these translations. These should ideally stick to a standard like [IETF BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag) (the standard used by JavaScript), but could really be anything. * @param translations Translations for the specified language * @example ```ts * tr.addTranslations("en", { * hello: "Hello, %1!", * foo: { * bar: "Foo bar", * }, * }); * ``` */ declare function addTranslations(language: string, translations: TrObject): void; /** * Returns the translation object for the specified language or currently active one. * If the language is not registered with {@linkcode tr.addTranslations()}, this function will return `undefined`. * @param language Language code or name to get translations for - defaults to the currently active language (set by {@linkcode tr.setLanguage()}) * @returns Translations for the specified language */ declare function getTranslations(language?: string): TrObject | undefined; /** * Returns all translations currently loaded into memory, indexed by language. * @param asCopy Set to `false` to get a reference to the actual translations object instead of a copy (default). Might be useful for modifying translations in-memory without using {@linkcode tr.addTranslations()} to replace the entire object. */ declare function getAllTranslations(asCopy?: boolean): typeof trans; /** * The fallback language to use when a translation key is not found in the currently active language. * Leave undefined to disable fallbacks and just return the translation key if translations are not found. */ declare function setFallbackLanguage(fallbackLanguage?: string): void; /** Returns the fallback language set by {@linkcode tr.setFallbackLanguage()} */ declare function getFallbackLanguage(): string | undefined; /** * Adds a transform function that gets called after resolving a translation for any language. * Use it to enable dynamic values in translations, for example to insert custom global values from the application or to denote a section that could be encapsulated by rich text. * Each function will receive the RegExpMatchArray [see MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match) and the current language as arguments. * After all %n-formatted values have been injected, the transform functions will be called sequentially in the order they were added. * @example * ```ts * import { tr, type TrKeys } from "@sv443-network/userutils"; * * const transEn = { * "headline": { * "basic": "Hello, ${USERNAME}", * "html": "Hello, ${USERNAME}
You have ${UNREAD_NOTIFS} unread notifications." * } * } as const; * * tr.addTranslations("en", transEn); * * // replace ${PATTERN} with predefined values * tr.addTransform(/\$\{([A-Z_]+)\}/g, ({ matches }) => { * switch(matches?.[1]) { * default: * return `[UNKNOWN: ${matches?.[1]}]`; * // these would be grabbed from elsewhere in the application, like a DataStore, global state or variable: * case "USERNAME": * return "JohnDoe45"; * case "UNREAD_NOTIFS": * return 5; * } * }); * * // replace ... with ... * tr.addTransform(/(.*?)<\/c>/g, ({ matches }) => { * const color = matches?.[1]; * const content = matches?.[2]; * * return `${content}`; * }); * * const t = tr.use>("en"); * * t("headline.basic"); // "Hello, JohnDoe45" * t("headline.html"); // "Hello, JohnDoe45
You have 5 unread notifications." * ``` * @param args A tuple containing the regular expression to match and the transform function to call if the pattern is found in a translation string */ declare function addTransform(transform: TransformTuple): void; /** * Deletes the first transform function from the list of registered transform functions. * @param patternOrFn A reference to the regular expression of the transform function, a string matching the original pattern, or a reference to the transform function to delete * @returns Returns true if the transform function was found and deleted, false if it wasn't found */ declare function deleteTransform(patternOrFn: RegExp | TransformFn): boolean; declare const tr: { for: (language: string, key: TTrKey, ...args: (Stringifiable | Record)[]) => ReturnType>; use: (language: string) => ReturnType>; hasKey: (language: string | undefined, key: TTrKey) => ReturnType>; addTranslations: typeof addTranslations; getTranslations: typeof getTranslations; getAllTranslations: typeof getAllTranslations; deleteTranslations: (language: string) => boolean; setFallbackLanguage: typeof setFallbackLanguage; getFallbackLanguage: typeof getFallbackLanguage; addTransform: typeof addTransform; deleteTransform: typeof deleteTransform; /** Collection of predefined transform functions that can be added via {@linkcode tr.addTransform()} */ transforms: { /** * This transform will replace placeholders matching `${key}` with the value of the passed argument(s). * The arguments can be passed in keyed object form or positionally via the spread operator: * - Keyed: If the first argument is an object and `key` is found in it, the value will be used for the replacement. * - Positional: If the first argument is not an object or has a `toString()` method that returns something that doesn't start with `[object`, the values will be positionally inserted in the order they were passed. * * @example ```ts * tr.addTranslations("en", { * "greeting": "Hello, ${user}!\nYou have ${notifs} notifications.", * }); * tr.addTransform(tr.transforms.templateLiteral); * * const t = tr.use("en"); * * // both calls return the same result: * t("greeting", { user: "John", notifs: 5 }); // "Hello, John!\nYou have 5 notifications." * t("greeting", "John", 5); // "Hello, John!\nYou have 5 notifications." * * // when a key isn't found in the object, it will be left as-is: * t("greeting", { user: "John" }); // "Hello, John!\nYou have ${notifs} notifications." * ``` */ templateLiteral: TransformTuple; /** * This transform will replace placeholders matching `{{key}}` with the value of the passed argument(s). * This format is commonly used in i18n libraries. Note that advanced syntax is not supported, only simple key replacement. * The arguments can be passed in keyed object form or positionally via the spread operator: * - Keyed: If the first argument is an object and `key` is found in it, the value will be used for the replacement. * - Positional: If the first argument is not an object or has a `toString()` method that returns something that doesn't start with `[object`, the values will be positionally inserted in the order they were passed. * * @example ```ts * tr.addTranslations("en", { * "greeting": "Hello, {{user}}!\nYou have {{notifs}} notifications.", * }); * tr.addTransform(tr.transforms.i18n); * * const t = tr.use("en"); * * // both calls return the same result: * t("greeting", { user: "Alice", notifs: 5 }); // "Hello, Alice!\nYou have 5 notifications." * t("greeting", "Alice", 5); // "Hello, Alice!\nYou have 5 notifications." * * // when a key isn't found in the object, it will be left as-is: * t("greeting", { user: "Alice" }); // "Hello, Alice!\nYou have {{notifs}} notifications." * ``` */ i18n: TransformTuple; /** * This transform will replace `%n` placeholders with the value of the passed arguments. * The `%n` placeholders are 1-indexed, meaning `%1` will be replaced by the first argument (index 0), `%2` by the second (index 1), and so on. * Objects will be stringified via `String()` before being inserted. * * @example ```ts * tr.addTranslations("en", { * "greeting": "Hello, %1!\nYou have %2 notifications.", * }); * tr.addTransform(tr.transforms.percent); * * const t = tr.use("en"); * * // arguments are inserted in the order they're passed: * t("greeting", "Bob", 5); // "Hello, Bob!\nYou have 5 notifications." * * // when a value isn't found, the placeholder will be left as-is: * t("greeting", "Bob"); // "Hello, Bob!\nYou have %2 notifications." * ``` */ percent: TransformTuple; }; }; export { tr };