Skip to content

Commit

Permalink
feat(i18n): enhance type-safety for translateString
Browse files Browse the repository at this point in the history
  • Loading branch information
zyf722 committed May 6, 2024
1 parent 4f90713 commit 12bdeea
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 25 deletions.
34 changes: 20 additions & 14 deletions src/livecodes/core.ts
Expand Up @@ -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';
Expand All @@ -149,10 +149,10 @@ declare global {
interface Window {
deps: {
showMode: typeof showMode;
translateString: (
key: I18nKeyType,
value: string,
interpolation?: I18nInterpolationType,
translateString: <Key extends I18nKeyType>(
key: Key,
value: I18nValueType<Key>,
...args: I18nOptionalInterpolation<I18nValueType<Key>>
) => string;
};
}
Expand Down Expand Up @@ -3988,16 +3988,19 @@ const handleI18n = () => {
});
};

const translateStringMock = (
_key: I18nKeyType,
value: string,
interpolation?: I18nInterpolationType,
const translateStringMock = <Key extends I18nKeyType>(
_key: Key,
value: I18nValueType<Key>,
...args: I18nOptionalInterpolation<I18nValueType<Key>>
) => {
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 = () => {
Expand Down Expand Up @@ -4560,8 +4563,11 @@ const createApi = (): API => {
const initApp = async (config: Partial<Config>, baseUrl: string) => {
window.deps = {
showMode,
translateString: (key: I18nKeyType, value: string, interpolation?: I18nInterpolationType) =>
translateString(i18n, key, value, interpolation),
translateString: <Key extends I18nKeyType>(
key: Key,
value: I18nValueType<Key>,
...args: I18nOptionalInterpolation<I18nValueType<Key>> // @ts-ignore
) => translateString(i18n, key, value, args[0]),
};
await initializePlayground({ config, baseUrl }, async () => {
basicHandlers();
Expand Down
85 changes: 74 additions & 11 deletions 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';
Expand Down Expand Up @@ -142,23 +142,86 @@ export const translate = (
});
};

export type I18nKeyType = ParseKeys<keyof CustomTypeOptions['resources']>;
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<Base, Key extends string> = Key extends `${infer Start}.${infer Rest}`
? Start extends keyof Base
? Rest extends string
? GetTranslation<Base[Start], Rest>
: never
: never
: Key extends keyof Base
? Base[Key]
: never;

type InferKeys<Base, isNs extends boolean = false, KeyStr extends string = ''> = {
[K in keyof Base]: Base[K] extends object
? KeyStr extends '' // Whether it is of first level (namespace)
? InferKeys<Base[K], true, `${K & string}`>
: KeyStr extends CustomTypeOptions['defaultNS'] // Default namespace and colon can be omited
?
| InferKeys<Base[K], false, `${KeyStr}${isNs extends true ? ':' : '.'}${K & string}`>
| InferKeys<Base[K], false, `${K & string}`>
: InferKeys<Base[K], false, `${KeyStr}${isNs extends true ? ':' : '.'}${K & string}`>
: `${KeyStr}${KeyStr extends '' ? '' : '.'}${K & string}`;
}[keyof Base];

type ExtractInterpolations<Value> =
Value extends `${infer _First}{{${infer Interpolation}}}${infer Rest}`
? Interpolation | ExtractInterpolations<Rest>
: never;

interface I18nOptions {
isHTML?: boolean;
}

export const translateString = (
declare const emptyObjectSymbol: unique symbol;
export interface EmptyObject {
[emptyObjectSymbol]?: never;
}

export type I18nOptionalInterpolation<T> =
I18nInterpolationType<T> extends EmptyObject
? [I18nOptions?]
: [I18nInterpolationType<T> & I18nOptions];

export type I18nKeyType = InferKeys<CustomTypeOptions['resources'], true>;
export type I18nValueType<K extends I18nKeyType> = GetTranslation<
GetNamespaceBase<CustomTypeOptions['resources'], K>,
K
>;
export type I18nInterpolationType<Value> = {
[K in ExtractInterpolations<Value>]?: string | number;
};

export const translateString = <Key extends I18nKeyType>(
i18n: typeof import('./i18n').default | undefined,
key: I18nKeyType,
value: string,
interpolation?: I18nInterpolationType,
key: Key,
value: I18nValueType<Key>,
...args: I18nOptionalInterpolation<I18nValueType<Key>>
) => {
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) => {
Expand Down

0 comments on commit 12bdeea

Please sign in to comment.