Last active
July 11, 2023 04:20
-
-
Save offirgolan/51134b82f526aafd9a9dd9d112e3cc14 to your computer and use it in GitHub Desktop.
Extract ICU Message Argument Types
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Utility type to replace a string with another. | |
*/ | |
type Replace<S extends string, R extends string, W extends string> = | |
S extends `${infer BS}${R}${infer AS}` | |
? Replace<`${BS}${W}${AS}`, R, W> | |
: S | |
/** | |
* Utility type to remove all spaces and new lines from the provided string. | |
*/ | |
type StripWhitespace<S extends string> = Replace<Replace<S, '\n', ''>, ' ', ''>; | |
/** | |
* Utility type to remove escaped characters. | |
* | |
* @example "'{word}" -> "word}" | |
* @example "foo '{word1} {word2}'" -> "foo " | |
*/ | |
type StripEscaped<S extends string> = | |
S extends `${infer A}'${string}'${infer B}` ? StripEscaped<`${A}${B}`> : | |
S extends `${infer A}'${string}${infer B}` ? StripEscaped<`${A}${B}`> : | |
S; | |
/** | |
* Extract ICU message arguments from the given string. | |
*/ | |
type ExtractArguments<S extends string> = | |
/* Handle {arg0,selectordinal,...}} since it has nested {} */ | |
S extends `${infer A}{${infer B}}}${infer C}` | |
? ExtractArguments<A> | _ExtractComplexArguments<B> | ExtractArguments<C> : | |
/* Handle remaining arguments {arg0}, {arg0, number}, {arg0, date, short}, etc. */ | |
S extends `${infer A}{${infer B}}${infer C}` | |
? ExtractArguments<A> | B | ExtractArguments<C> : | |
never; | |
/** | |
* Handle complex type argument extraction (i.e plural, select, and selectordinal) which | |
* can have nested arguments. | |
*/ | |
type _ExtractComplexArguments<S extends string> = | |
/* Handle arg0,plural,... */ | |
S extends `${infer A},plural,${infer B}` | |
? ExtractArguments<`{${A},plural}`> | _ExtractNestedArguments<`${B}}`> : | |
/* Handle arg0,select,... */ | |
S extends `${infer A},select,${infer B}` | |
? ExtractArguments<`{${A},select}`> | _ExtractNestedArguments<`${B}}`> : | |
/* Handle arg0,selectordinal,... */ | |
S extends `${infer A},selectordinal,${infer B}` | |
? ExtractArguments<`{${A},selectordinal}`> | _ExtractNestedArguments<`${B}}`> : | |
never | |
/** | |
* Extract nested arguments from complex types such as plural, select, and selectordinal. | |
*/ | |
type _ExtractNestedArguments<S extends string> = S extends `${infer A}{${infer B}}${infer C}` | |
? _ExtractNestedArguments<A> | ExtractArguments<`${B}}`> | _ExtractNestedArguments<C> : | |
never; | |
/** | |
* Normalize extract arguments to either `name` or `name,type`. | |
*/ | |
type NormalizeArguments<TArg extends string> = | |
/* Handle "name,type,other args" */ | |
TArg extends `${infer Name},${infer Type},${string}` ? `${Name},${Type}` : | |
/* Handle "name,type" */ | |
TArg extends `${infer Name},${infer Type}` ? `${Name},${Type}` : | |
/* Handle "name" */ | |
TArg; | |
/** | |
* Convert ICU type to TS type. | |
*/ | |
type Value<T extends string> = | |
T extends 'number' | 'plural' | 'selectordinal' ? number : | |
T extends 'date' | 'time' ? Date : | |
string; | |
/** | |
* Create an object mapping the extracted key to its type. | |
*/ | |
type ArgumentsMap<S extends string> = { | |
[key in S extends `${infer Key},${string}` ? Key : S]: Extract<S, `${key},${string}`> extends `${string},${infer V}` ? Value<V>: string; | |
} | |
/** | |
* Create an object mapping all ICU message arguments to their types. | |
*/ | |
type MessageArguments<T extends string> = ArgumentsMap<NormalizeArguments<ExtractArguments<StripEscaped<StripWhitespace<T>>>>>; | |
/* ======================= */ | |
const message1 = '{name00} Foo bar {name0} baz {name1, number} bars {name2, number, ::currency} foos{name3, date, short}' | |
const message2 = `{name00} Foo bar {name0} baz {name1, number} bars {name2, number, ::currency} You have {numPhotos, plural, | |
=0 {no photos {nested, date, short}.} | |
=1 {one photo.} | |
other {# photos.} | |
}. {gender, select, | |
male {He {nested1, number}} | |
female {She} | |
other {They} | |
} will respond shortly. It's my cat's {year, selectordinal, | |
one {#st {nested2}} | |
two {#nd} | |
few {#rd} | |
other {#th} | |
} birthday!` | |
const message3 = "Message without arguments"; | |
const message4 = "{count, plural, =0 {} =1 {We accept {foo}.} other {We accept {bar} and {foo}.}}"; | |
const message5 = `{gender, select, | |
male {He {nested1, number}} | |
female {She} | |
other {They} | |
} will respond shortly.` | |
const message6 = `It's my cat's {year, selectordinal, | |
one {#st {nested2}} | |
two {#nd} | |
few {#rd} | |
other {#th} | |
} birthday!` | |
const message7 = `{name00} Foo bar {name0} baz {name1, number} This '{isn''t}' obvious. '{name2, number, ::currency}' foos'{name3, date, short}` | |
const message8 = `Our price is <boldThis>{price, number, ::currency/USD precision-integer}</boldThis> | |
with <link>{pct, number, ::percent} discount</link>` | |
type Arguments1 = MessageArguments<typeof message1>; | |
type Arguments2 = MessageArguments<typeof message2>; | |
type Arguments3 = MessageArguments<typeof message3>; | |
type Arguments4 = MessageArguments<typeof message4>; | |
type Arguments5 = MessageArguments<typeof message5>; | |
type Arguments6 = MessageArguments<typeof message6>; | |
type Arguments7 = MessageArguments<typeof message7>; | |
type Arguments8 = MessageArguments<typeof message8>; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
FWIW, if you're interested in continuing to develop this, I'd love to help in any way I can.