diff --git a/src/livecodes/core.ts b/src/livecodes/core.ts index 2ef58d6aa..5b5e40877 100644 --- a/src/livecodes/core.ts +++ b/src/livecodes/core.ts @@ -139,7 +139,7 @@ import { import { customEvents } from './events/custom-events'; import { populateConfig } from './import/utils'; import { permanentUrlService } from './services/permanent-url'; -import type { I18nInterpolationType, I18nKeyType } from './i18n/utils'; +import type { I18nKeyType, I18nValueType, I18nOptionalInterpolation } from './i18n/utils'; // eslint-disable-next-line no-duplicate-imports import { translate, translateString } from './i18n/utils'; @@ -149,10 +149,10 @@ declare global { interface Window { deps: { showMode: typeof showMode; - translateString: ( - key: I18nKeyType, - value: string, - interpolation?: I18nInterpolationType, + translateString: ( + key: Key, + value: I18nValueType, + ...args: I18nOptionalInterpolation> ) => string; }; } @@ -3988,16 +3988,19 @@ const handleI18n = () => { }); }; -const translateStringMock = ( - _key: I18nKeyType, - value: string, - interpolation?: I18nInterpolationType, +const translateStringMock = ( + _key: Key, + value: I18nValueType, + ...args: I18nOptionalInterpolation> ) => { - if (!interpolation) return value; + const interpolation = args[0]; + + if (!interpolation) return value as string; + let result: string = value as string; for (const [k, v] of Object.entries(interpolation)) { - value = value.replaceAll(`{{${k}}}`, v as string); + result = result.replaceAll(`{{${k}}}`, v as string); } - return value; + return result; }; const basicHandlers = () => { @@ -4560,8 +4563,11 @@ const createApi = (): API => { const initApp = async (config: Partial, baseUrl: string) => { window.deps = { showMode, - translateString: (key: I18nKeyType, value: string, interpolation?: I18nInterpolationType) => - translateString(i18n, key, value, interpolation), + translateString: ( + key: Key, + value: I18nValueType, + ...args: I18nOptionalInterpolation> // @ts-ignore + ) => translateString(i18n, key, value, args[0]), }; await initializePlayground({ config, baseUrl }, async () => { basicHandlers(); diff --git a/src/livecodes/i18n/utils.ts b/src/livecodes/i18n/utils.ts index 071e88ee1..b8be82ada 100644 --- a/src/livecodes/i18n/utils.ts +++ b/src/livecodes/i18n/utils.ts @@ -1,4 +1,4 @@ -import type { ParseKeys, CustomTypeOptions } from 'i18next'; +import type { CustomTypeOptions } from 'i18next'; // eslint-disable-next-line import/no-internal-modules import { predefinedValues } from '../utils/utils'; @@ -142,23 +142,86 @@ export const translate = ( }); }; -export type I18nKeyType = ParseKeys; -export interface I18nInterpolationType { - [key: string]: string | number; +type GetNamespaceBase< + Base extends CustomTypeOptions['resources'], + Key extends string, +> = Key extends `${infer Namespace}:${infer _Rest}` + ? Namespace extends keyof Base + ? Base[Namespace] + : never + : Base[CustomTypeOptions['defaultNS']]; + +type GetTranslation = Key extends `${infer Start}.${infer Rest}` + ? Start extends keyof Base + ? Rest extends string + ? GetTranslation + : never + : never + : Key extends keyof Base + ? Base[Key] + : never; + +type InferKeys = { + [K in keyof Base]: Base[K] extends object + ? KeyStr extends '' // Whether it is of first level (namespace) + ? InferKeys + : KeyStr extends CustomTypeOptions['defaultNS'] // Default namespace and colon can be omited + ? + | InferKeys + | InferKeys + : InferKeys + : `${KeyStr}${KeyStr extends '' ? '' : '.'}${K & string}`; +}[keyof Base]; + +type ExtractInterpolations = + Value extends `${infer _First}{{${infer Interpolation}}}${infer Rest}` + ? Interpolation | ExtractInterpolations + : never; + +interface I18nOptions { + isHTML?: boolean; } -export const translateString = ( +declare const emptyObjectSymbol: unique symbol; +export interface EmptyObject { + [emptyObjectSymbol]?: never; +} + +export type I18nOptionalInterpolation = + I18nInterpolationType extends EmptyObject + ? [I18nOptions?] + : [I18nInterpolationType & I18nOptions]; + +export type I18nKeyType = InferKeys; +export type I18nValueType = GetTranslation< + GetNamespaceBase, + K +>; +export type I18nInterpolationType = { + [K in ExtractInterpolations]?: string | number; +}; + +export const translateString = ( i18n: typeof import('./i18n').default | undefined, - key: I18nKeyType, - value: string, - interpolation?: I18nInterpolationType, + key: Key, + value: I18nValueType, + ...args: I18nOptionalInterpolation> ) => { - if (!i18n) return value; - return i18n.t(key, { + if (!i18n) return value as string; + + const interpolation = args[0]; + const translation = i18n.t(key, { ...interpolation, ...predefinedValues, - defaultValue: value, + defaultValue: value as string, }) as string; + + if (!interpolation || !interpolation.isHTML) { + return translation; + } else { + const { elements } = abstractifyHTML(value as string); + return unabstractifyHTML(translation, elements); + } }; export const dispatchTranslationEvent = (elem: HTMLElement) => {