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>; |
FWIW, if you're interested in continuing to develop this, I'd love to help in any way I can.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@offirgolan, this is awesome.
I played around with it and found that
"Message without arguments"
didn't quite work as expected, so I (very naively) fixed it by modifyingMessageArguments
like this:I threw together the following wrapper around
FormattedMessage
to validate, to ensure you can omitvalues
when thedefaultMessage
has no arguments:And here's what that looks like in action: