core-plugin-api: implemented typed formatting options for TranslationFunction
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -145,44 +145,52 @@ describe('TranslationFunction', () => {
|
||||
f('deep', { replace: { a: { b: { c: '' } } } });
|
||||
});
|
||||
|
||||
it('should ignore formatting', () => {
|
||||
it('should support formatting', () => {
|
||||
const f = (() => {}) as TranslationFunction<{
|
||||
none: '=';
|
||||
simple: '= {{bar, number}}';
|
||||
multiple: '= {{bar , whatever }} {{ baz, someFormat(pretty: "yes")}}';
|
||||
deep: '= {{x.y,derp}} {{ x.z, derp}} {{ a.b.c, derp }}';
|
||||
none: '{{x}}';
|
||||
number: '{{x, number}}';
|
||||
numberOptions: '{{x, number(minimumFractionDigits: 2)}}';
|
||||
currency: '{{x, currency}}';
|
||||
datetime: '{{x, dateTime}}';
|
||||
relativeTimeOptions: '{{x, relativeTime(quarter)}}';
|
||||
list: '{{x, list}}';
|
||||
}>;
|
||||
expect(f).toBeDefined();
|
||||
|
||||
f('none', { replace: { x: 'x' } });
|
||||
f('number', { replace: { x: 1 } });
|
||||
f('number', {
|
||||
replace: { x: 1 },
|
||||
formatParams: { x: { minimumFractionDigits: 2 } },
|
||||
});
|
||||
f('numberOptions', { replace: { x: 1 } });
|
||||
f('currency', { replace: { x: 1 } });
|
||||
f('datetime', { replace: { x: new Date() } });
|
||||
f('relativeTimeOptions', { replace: { x: 1 } });
|
||||
f('relativeTimeOptions', {
|
||||
replace: { x: 1 },
|
||||
formatParams: { x: { style: 'short' } },
|
||||
});
|
||||
f('list', { replace: { x: ['a', 'b', 'c'] } });
|
||||
// @ts-expect-error
|
||||
f('none', { replace: { unknown: 1 } });
|
||||
f('simple', { replace: { bar: '' } });
|
||||
f('none', { replace: { x: 1 } });
|
||||
// @ts-expect-error
|
||||
f('simple');
|
||||
f('number', { replace: { x: '1' } });
|
||||
// @ts-expect-error
|
||||
f('simple', { replace: {} });
|
||||
f('numberOptions', { replace: { x: '1' } });
|
||||
// @ts-expect-error
|
||||
f('simple', { replace: { wrong: '' } });
|
||||
f('multiple', { replace: { bar: '', baz: '' } });
|
||||
f('currency', { replace: { x: '1' } });
|
||||
// @ts-expect-error
|
||||
f('multiple', { replace: { bar: '' } });
|
||||
f('datetime', { replace: { x: '1' } });
|
||||
// @ts-expect-error
|
||||
f('multiple', { replace: { baz: '' } });
|
||||
f('relativeTimeOptions', { replace: { x: '1' } });
|
||||
f('relativeTimeOptions', {
|
||||
replace: { x: 1 },
|
||||
// @ts-expect-error
|
||||
formatParams: { x: { minimumFractionDigits: 2 } },
|
||||
});
|
||||
// @ts-expect-error
|
||||
f('multiple');
|
||||
// @ts-expect-error
|
||||
f('multiple', {});
|
||||
// @ts-expect-error
|
||||
f('multiple', { replace: {} });
|
||||
f('deep', { replace: { x: { y: '', z: '' }, a: { b: { c: '' } } } });
|
||||
// @ts-expect-error
|
||||
f('deep');
|
||||
// @ts-expect-error
|
||||
f('deep', { replace: { x: { y: '', z: '' }, a: { b: '' } } });
|
||||
// @ts-expect-error
|
||||
f('deep', { replace: { x: { y: '', z: '' } } });
|
||||
// @ts-expect-error
|
||||
f('deep', { replace: { a: { b: { c: '' } } } });
|
||||
f('list', { replace: { x: [1, 2, 3] } });
|
||||
});
|
||||
|
||||
it('should support nesting', () => {
|
||||
|
||||
@@ -24,7 +24,10 @@ import { TranslationRef } from '../../translation';
|
||||
* @alpha
|
||||
*/
|
||||
interface BaseOptions {
|
||||
interpolation?: {};
|
||||
interpolation?: {
|
||||
/** Whether to HTML escape provided values, defaults to false */
|
||||
escapeValue?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,13 +35,34 @@ interface BaseOptions {
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export type TranslationPlural =
|
||||
| 'zero'
|
||||
| 'one'
|
||||
| 'two'
|
||||
| 'few'
|
||||
| 'many'
|
||||
| 'other';
|
||||
type TranslationPlural = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
|
||||
|
||||
/**
|
||||
* A mapping of i18n formatting types to their corresponding types and options.
|
||||
* @internal
|
||||
*/
|
||||
type I18nextFormatMap = {
|
||||
number: {
|
||||
type: number;
|
||||
options: Intl.NumberFormatOptions;
|
||||
};
|
||||
currency: {
|
||||
type: number;
|
||||
options: Intl.NumberFormatOptions;
|
||||
};
|
||||
datetime: {
|
||||
type: Date;
|
||||
options: Intl.DateTimeFormatOptions;
|
||||
};
|
||||
relativetime: {
|
||||
type: number;
|
||||
options: Intl.RelativeTimeFormatOptions;
|
||||
};
|
||||
list: {
|
||||
type: string[];
|
||||
options: Intl.ListFormatOptions;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts all pluralized keys from the message map.
|
||||
@@ -92,18 +116,75 @@ type Trim<T> = T extends ` ${infer U}`
|
||||
: T;
|
||||
|
||||
/**
|
||||
* Extracts all option keys from a message string.
|
||||
* Extracts the key and format from a replacement string.
|
||||
*
|
||||
* e.g. 'foo {{bar}} {{baz, number}}' -> 'bar' | 'baz'
|
||||
* e.g. 'foo, number' -> { foo: number }, 'foo' -> { foo: undefined }
|
||||
*/
|
||||
type ExtractFormat<Replacement extends string> =
|
||||
Replacement extends `${infer Key},${infer FullFormat}`
|
||||
? {
|
||||
[key in Trim<Key>]: Lowercase<
|
||||
Trim<
|
||||
FullFormat extends `${infer Format}(${string})${string}`
|
||||
? Format
|
||||
: FullFormat
|
||||
>
|
||||
>;
|
||||
}
|
||||
: { [key in Trim<Replacement>]: undefined };
|
||||
|
||||
/**
|
||||
* Expand the keys in a flat map to nested objects.
|
||||
*
|
||||
* e.g. { 'a.b': 'foo', 'a.c': 'bar' } -> { a: { b: 'foo', c: 'bar' }
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
type OptionKeysFromMessage<TMessage> =
|
||||
TMessage extends `${string}{{${infer Key},${string}}}${infer Tail}` // ignore formatting, e.g. {{foo, number}}
|
||||
? Trim<Key> | OptionKeysFromMessage<Tail>
|
||||
: TMessage extends `${string}{{${infer Key}}}${infer Tail}` // no formatting, e.g. {{foo}}
|
||||
? Trim<Key> | OptionKeysFromMessage<Tail>
|
||||
: never;
|
||||
type ExpandKeys<TMap extends {}> = {
|
||||
[Key in keyof TMap as Key extends `${infer Prefix}.${string}`
|
||||
? Prefix
|
||||
: Key]: Key extends `${string}.${infer Rest}`
|
||||
? ExpandKeys<{ [key in Rest]: TMap[Key] }>
|
||||
: TMap[Key];
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts all option keys and their format from a message string.
|
||||
*
|
||||
* e.g. 'foo {{bar}} {{baz, number}}' -> { 'bar': undefined, 'baz': 'number' }
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
type ReplaceFormatsFromMessage<TMessage> =
|
||||
TMessage extends `${string}{{${infer Replacement}}}${infer Tail}` // no formatting, e.g. {{foo}}
|
||||
? ExpandKeys<ExtractFormat<Replacement>> & ReplaceFormatsFromMessage<Tail>
|
||||
: {};
|
||||
|
||||
/**
|
||||
* Generates the replace options structure
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
type ReplaceOptionsFromFormats<TFormats extends {}> = {
|
||||
[Key in keyof TFormats]: TFormats[Key] extends keyof I18nextFormatMap
|
||||
? I18nextFormatMap[TFormats[Key]]['type']
|
||||
: TFormats[Key] extends {}
|
||||
? Expand<ReplaceOptionsFromFormats<TFormats[Key]>>
|
||||
: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates the formatParams options structure
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
type ReplaceFormatParamsFromFormats<TFormats extends {}> = {
|
||||
[Key in keyof TFormats]?: TFormats[Key] extends keyof I18nextFormatMap
|
||||
? I18nextFormatMap[TFormats[Key]]['options']
|
||||
: TFormats[Key] extends {}
|
||||
? Expand<ReplaceFormatParamsFromFormats<TFormats[Key]>>
|
||||
: undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts all nesting keys from a message string.
|
||||
@@ -158,23 +239,6 @@ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
|
||||
? I
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Extracts all options from a message string.
|
||||
*
|
||||
* e.g. 'foo {{bar}} {{x.y.z}}' -> { bar: <value>, x: { y: { z: <value> } } }
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
type ReplaceOptionsFromKeys<TKeys extends string> = UnionToIntersection<
|
||||
TKeys extends `${infer Prefix}.${infer Rest}`
|
||||
? {
|
||||
[key in Prefix]: ReplaceOptionsFromKeys<Rest>;
|
||||
}
|
||||
: {
|
||||
[key in TKeys & string]: string | number | string[];
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Collects different types of options into a single object
|
||||
*
|
||||
@@ -182,9 +246,14 @@ type ReplaceOptionsFromKeys<TKeys extends string> = UnionToIntersection<
|
||||
*/
|
||||
type CollectOptions<
|
||||
TCount extends { count?: number },
|
||||
TReplaceOptions extends {},
|
||||
TFormats extends {},
|
||||
> = TCount &
|
||||
(keyof TReplaceOptions extends never ? {} : { replace: TReplaceOptions });
|
||||
(keyof TFormats extends never
|
||||
? {}
|
||||
: {
|
||||
replace: Expand<ReplaceOptionsFromFormats<TFormats>>;
|
||||
formatParams?: Expand<ReplaceFormatParamsFromFormats<TFormats>>;
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper type to only require options argument if needed
|
||||
@@ -201,12 +270,10 @@ type TranslationFunctionOptions<
|
||||
TPluralKeys extends keyof TMessages, // All keys in the message map that are pluralized
|
||||
TMessages extends { [key in string]: string }, // Collapsed message map with normalized keys and union values
|
||||
> = OptionArgs<
|
||||
Expand<
|
||||
CollectOptions<
|
||||
TKeys & TPluralKeys extends never ? {} : { count: number },
|
||||
ExpandRecursive<
|
||||
ReplaceOptionsFromKeys<OptionKeysFromMessage<TMessages[TKeys]>>
|
||||
>
|
||||
CollectOptions<
|
||||
TKeys & TPluralKeys extends never ? {} : { count: number },
|
||||
ExpandRecursive<
|
||||
UnionToIntersection<ReplaceFormatsFromMessage<TMessages[TKeys]>>
|
||||
>
|
||||
>
|
||||
>;
|
||||
|
||||
Reference in New Issue
Block a user