UNPKG

@solid-primitives/i18n

Version:

Library of primitives for providing internationalization support.

263 lines (262 loc) 9.8 kB
export type BaseRecordDict = { readonly [K: string | number]: unknown; }; export type BaseArrayDict = readonly unknown[]; export type BaseDict = BaseRecordDict | BaseArrayDict; type JoinPath<A, B> = A extends string | number ? B extends string | number ? `${A}.${B}` : A : B extends string | number ? B : ""; type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; /** * Flatten a nested dictionary into a flat dictionary. * * @example * ```ts * type Dict = { * a: { * foo: string; * b: { bar: number } * } * } * * type FlatDict = Flatten<Dict>; * * type FlatDict = { * a: { * foo: string; * b: { bar: number } * }, * "a.foo": string; * "a.b": { bar: number} , * "a.b.bar": number; * } * ``` */ export type Flatten<T extends BaseDict, P = {}> = number extends T ? BaseRecordDict : T extends (infer V)[] ? /* array */ { readonly [K in JoinPath<P, number>]?: V; } & (V extends BaseDict ? Partial<Flatten<V, JoinPath<P, number>>> : {}) : /* record */ UnionToIntersection<{ [K in keyof T]: T[K] extends BaseDict ? Flatten<T[K], JoinPath<P, K>> : never; }[keyof T]> & { readonly [K in keyof T as JoinPath<P, K>]: T[K]; }; /** * 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 declare function flatten<T extends BaseDict>(dict: T): Flatten<T>; export type Prefixed<T extends BaseRecordDict, P extends string> = { readonly [K in keyof T as `${P}.${K & (string | number)}`]: T[K]; }; /** * 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 declare const prefix: <T extends BaseRecordDict, P extends string>(dict: T, prefix: P) => Prefixed<T, P>; export type BaseTemplateArgs = Record<string, string | number | boolean>; /** * A string branded with arguments needed to resolve the template. */ export type Template<T extends BaseTemplateArgs> = string & { __template_args: T; }; export type TemplateArgs<T extends Template<any>> = T extends Template<infer R> ? R : never; /** * 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 declare const template: <T extends BaseTemplateArgs>(source: string) => Template<T>; /** * Resolve a {@link Template} with the provided {@link TemplateArgs}. */ export type TemplateResolver<O = string> = <T extends string>(template: T, ...args: ResolveArgs<T, O>) => O; /** * Simple template resolver that replaces `{{ key }}` with the value of `args.key`. * * @example * ```ts * resolveTemplate("hello {{ name }}!", { name: "John" }); * // => "hello John!" * ``` */ export declare const resolveTemplate: TemplateResolver; /** * Template resolver that does nothing. It's used as a fallback when no template resolver is provided. */ export declare const identityResolveTemplate: TemplateResolver; export type Resolved<T, O> = T extends (...args: any[]) => infer R ? R : T extends O ? O : T; export type ResolveArgs<T, O> = T extends (...args: infer A) => any ? A : T extends Template<infer R> ? [args: R] : T extends O ? [args?: BaseTemplateArgs] : []; export type Resolver<T, O> = (...args: ResolveArgs<T, O>) => Resolved<T, O>; export type NullableResolver<T, O> = (...args: ResolveArgs<T, O>) => Resolved<T, O> | undefined; export type Translator<T extends BaseRecordDict, O = string> = <K extends keyof T>(path: K, ...args: ResolveArgs<T[K], O>) => Resolved<T[K], O>; export type NullableTranslator<T extends BaseRecordDict, O = string> = <K extends keyof T>(path: K, ...args: ResolveArgs<T[K], O>) => Resolved<T[K], O> | undefined; /** * Create a translator function that will resolve the path in the dictionary and return the value. * * If the value is a function it will call it with the provided arguments. * * If the value is a string it will resolve the template using {@link resolveTemplate} with the provided arguments. * * Otherwise it will return the value as is. * * @param dict A function that returns the dictionary to use for translation. Will be called on each translation. * @param resolveTemplate A function that will resolve the template. Defaults to {@link identityResolveTemplate}. * * @example * ```ts * const dict = { * hello: "hello {{ name }}!", * goodbye: (name: string) => `goodbye ${name}!`, * food: { * meat: "meat", * } * } * const flat_dict = i18n.flatten(dict); * * const t = i18n.translator(() => flat_dict, i18n.resolveTemplate); * * t("hello", { name: "John" }) // => "hello John!" * t("goodbye", "John") // => "goodbye John!" * t("food.meat") // => "meat" * ``` */ export declare function translator<T extends BaseRecordDict, O = string>(dict: () => T, resolveTemplate?: TemplateResolver<O>): Translator<T, O>; export declare function translator<T extends BaseRecordDict, O = string>(dict: () => T | undefined, resolveTemplate?: TemplateResolver<O>): NullableTranslator<T, O>; export type Scopes<T extends string> = { [K in T]: K extends `${infer S}.${infer R}` ? S | `${S}.${Scopes<R>}` : never; }[T]; export type Scoped<T extends BaseRecordDict, S extends Scopes<keyof T & string>> = { readonly [K in keyof T as K extends `${S}.${infer R}` ? R : never]: T[K]; }; /** * Scopes the provided {@link Translator} to the given {@link scope}. * * @example * ```ts * const dict = { * greetings: { * hello: "hello {{ name }}!", * hi: "hi {{ name }}!", * }, * goodbye: (name: string) => `goodbye ${name}!`, * } * const flat_dict = i18n.flatten(dict); * * const t = i18n.translator(() => flat_dict, i18n.resolveTemplate); * * const greetings = i18n.scopedTranslator(t, "greetings"); * * greetings("hello", { name: "John" }) // => "hello John!" * greetings("hi", { name: "John" }) // => "hi John!" * greetings("goodbye", "John") // => undefined * ``` */ export declare function scopedTranslator<T extends BaseRecordDict, O, S extends Scopes<keyof T & string>>(translator: Translator<T, O>, scope: S): Translator<Scoped<T, S>, O>; export declare function scopedTranslator<T extends BaseRecordDict, O, S extends Scopes<keyof T & string>>(translator: NullableTranslator<T, O>, scope: S): NullableTranslator<Scoped<T, S>, O>; export type ChainedTranslator<T extends BaseRecordDict, O = string> = { readonly [K in keyof T]: T[K] extends BaseRecordDict ? ChainedTranslator<T[K], O> : Resolver<T[K], O>; }; export type NullableChainedTranslator<T extends BaseRecordDict, O = string> = { readonly [K in keyof T]: T[K] extends BaseRecordDict ? NullableChainedTranslator<T[K], O> : NullableResolver<T[K], O>; }; /** * Create an object-chained translator that will resolve the path in the dictionary and return the value. * * @param init_dict The initial dictionary used for getting the structure of nested objects. * @param translate {@link Translator} function that will resolve the path in the dictionary and return the value. * * @example * ```ts * const dict = { * greetings: { * hello: "hello {{ name }}!", * hi: "hi!", * }, * goodbye: (name: string) => `goodbye ${name}!`, * } * const flat_dict = i18n.flatten(dict); * * const t = i18n.translator(() => flat_dict, i18n.resolveTemplate); * * const chained = i18n.chainedTranslator(dict, t); * * chained.greetings.hello({ name: "John" }) // => "hello John!" * chained.greetings.hi() // => "hi!" * chained.goodbye("John") // => "goodbye John!" * ``` */ export declare function chainedTranslator<T extends BaseRecordDict, O>(init_dict: T, translate: Translator<T, O>, path?: string): ChainedTranslator<T, O>; export declare function chainedTranslator<T extends BaseRecordDict, O>(init_dict: T, translate: NullableTranslator<T, O>, path?: string): NullableChainedTranslator<T, O>; /** * Create an object-chained translator *(implemented using a Proxy)* that will resolve the path in the dictionary and return the value. * * @param translate {@link Translator} function that will resolve the path in the dictionary and return the value. * * @example * ```ts * const dict = { * greetings: { * hello: "hello {{ name }}!", * hi: "hi!", * }, * goodbye: (name: string) => `goodbye ${name}!`, * } * const flat_dict = i18n.flatten(dict); * * const t = i18n.translator(() => flat_dict, i18n.resolveTemplate); * * const proxy = i18n.proxyTranslator(t); * * proxy.greetings.hello({ name: "John" }) // => "hello John!" * proxy.greetings.hi() // => "hi!" * proxy.goodbye("John") // => "goodbye John!" * ``` */ export declare function proxyTranslator<T extends BaseRecordDict, O>(translate: Translator<T, O>, path?: string): ChainedTranslator<T, O>; export declare function proxyTranslator<T extends BaseRecordDict, O>(translate: NullableTranslator<T, O>, path?: string): NullableChainedTranslator<T, O>; export {};