Skip to content

Instantly share code, notes, and snippets.

@offirgolan
Last active July 11, 2023 04:20
Show Gist options
  • Save offirgolan/51134b82f526aafd9a9dd9d112e3cc14 to your computer and use it in GitHub Desktop.
Save offirgolan/51134b82f526aafd9a9dd9d112e3cc14 to your computer and use it in GitHub Desktop.
Extract ICU Message Argument Types
/**
* 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>;
@jrnail23
Copy link

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