Last active
July 9, 2025 06:57
-
-
Save temoncher/d6faa4a7af6f45a96be4baaa190b598a to your computer and use it in GitHub Desktop.
ICU format parser in typescript 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
| // https://unicode-org.github.io/icu/userguide/format_parse/messages/ | |
| type Trim<S extends string> = | |
| S extends ` ${infer R}` | `${infer R} ` | `\n${infer R}` | `${infer R}\n` ? Trim<R> : S; | |
| type AccumulateBeforeClosing<S extends string, TStack extends unknown[] = [], R extends string = ''> = S extends `${infer TBeforeClosing}}${infer TAfterClosing}` | |
| ? S extends `${infer TBeforeOpening}{${infer TAfterOpening}` | |
| ? TBeforeOpening extends `${TBeforeClosing}${string}` | |
| ? TStack['length'] extends 0 | |
| ? `${R}${TBeforeClosing}` | |
| : TStack extends [...infer TStart, infer TLast] | |
| ? AccumulateBeforeClosing<TAfterClosing, TStart, `${R}${TBeforeClosing}}`> | |
| : never // can't happen | |
| : AccumulateBeforeClosing<TAfterOpening, [...TStack, undefined], `${R}${TBeforeOpening}{`> | |
| : TStack['length'] extends 0 | |
| ? `${R}${TBeforeClosing}` | |
| : TStack extends [...infer TStart, infer TLast] | |
| ? AccumulateBeforeClosing<TAfterClosing, TStart, `${R}${TBeforeClosing}}`> | |
| : never // can't happen | |
| : `${R}${S}` | |
| type ICUTypes = 'number' | 'number, currency' | 'date' | 'time' | |
| type DetectICU<S extends string> = S extends `${infer TIdentifier}, ${ICUTypes}` | |
| ? TIdentifier | |
| : S extends `${infer TSelectIdentifier}, select,${infer RestSelect}` | |
| ? S extends `${infer TPluralIdentifier}, plural,${infer RestPlural}` | |
| /** | |
| * In case we got nested select and plural | |
| * we pick smallest of TSelectIdentifier/TPluralIdentifier, because otherwise we catch something bigger than identifier | |
| */ | |
| ? (TSelectIdentifier extends `${TPluralIdentifier}${string}` ? [TPluralIdentifier, RestPlural] : [TSelectIdentifier, RestSelect]) extends [infer TIdentifier, infer Rest extends string] | |
| ? TIdentifier | ParseInterpolation<ParseOneLevelOfInterpolation<Rest>> | |
| : never // can't happen | |
| : TSelectIdentifier | ParseInterpolation<ParseOneLevelOfInterpolation<RestSelect>> | |
| : S extends `${infer TPluralIdentifier}, plural,${infer RestPlural}` | |
| ? TPluralIdentifier | ParseInterpolation<ParseOneLevelOfInterpolation<RestPlural>> | |
| : S | |
| type ParseOneLevelOfInterpolation<S extends string> = S extends `${string}{${infer TAfterOpening}` | |
| ? AccumulateBeforeClosing<TAfterOpening> extends infer TInside extends string | |
| ? TAfterOpening extends `${TInside}}${infer TAfterClosing}` | |
| ? Trim<TInside> extends infer TTrimmedInside extends string | |
| ? TTrimmedInside | ParseOneLevelOfInterpolation<TAfterClosing> | |
| : never // can't happen | |
| : never // means no interpolation strings inside S (because no "}") | |
| : never // can't happen | |
| : never; // means no interpolation strings inside S (because no "{") | |
| type ParseInterpolation<S extends string> = DetectICU<ParseOneLevelOfInterpolation<S>> | |
| type Test = ParseInterpolation<"I have {apples_count, plural, zero {# {apple_color} apples} one {# {apple_color} apple} other {# many {qualification} {apple_color} apples}} in my {color} backpack">; | |
| // ^? | |
| type Test2 = ParseInterpolation<'Hello {user}, you have {count, number} messages.'>; | |
| type test_text = `{num_guests, plural, offset:1 | |
| =0 {{host} does not give a party.} | |
| =1 {{host} invites {guest} to their party.} | |
| =2 {{host} invites {guest} and one other person to their party.} | |
| other {{host} invites {guest} and # other people to their party.}}}`; | |
| type Test99 = ParseInterpolation<test_text> | |
| // ^? | |
| type large_test_text = `{gender_of_host, select, | |
| female { | |
| {num_guests, plural, offset:1 | |
| =0 {{host} does not give a party.} | |
| =1 {{host} invites {guest} to her party.} | |
| =2 {{host} invites {guest} and one other person to her party.} | |
| other {{host} invites {guest} and # other people to her party.}}} | |
| male { | |
| {num_guests, plural, offset:1 | |
| =0 {{host} does not give a party.} | |
| =1 {{host} invites {guest} to his party.} | |
| =2 {{host} invites {guest} and one other person to his party.} | |
| other {{host} invites {guest} and # other people to his party.}}} | |
| other { | |
| {num_guests, plural, offset:1 | |
| =0 {{host} does not give a party.} | |
| =1 {{host} invites {guest} to their party.} | |
| =2 {{host} invites {guest} and one other person to their party.} | |
| other {{host} invites {guest} and # other people to their party.}}}}`; | |
| type Test3 = ParseInterpolation<large_test_text> | |
| // ^? | |
| type Test4 = ParseInterpolation<' {foo} '> | |
| // ^? | |
| type Test5 = ParseInterpolation<'{foo} '> | |
| // ^? | |
| type Test6 = ParseInterpolation<' {foo}'> | |
| // ^? | |
| type Test7 = ParseInterpolation<'{foo}'> | |
| // ^? | |
| type Test8 = ParseInterpolation<'{foo}' | ' {bar} {kek}'> | |
| // ^? |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment