Skip to content

Commit

Permalink
cached natives
Browse files Browse the repository at this point in the history
  • Loading branch information
eddow committed Jun 23, 2024
1 parent cf9dea4 commit c607eda
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 29 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# 1.1.17

## Modified

- `Intl.*****Format` are now cached

## Added

- Missing key/translation nuance based on fallback
Expand Down
17 changes: 17 additions & 0 deletions docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,20 @@ This partial answer can be conveyed in the answer with the action' results (espe
## Overriding `interpolate`

`I18nClient.interpolate` is called on _each_ translation, and can be used to add a transformation or have a list of "last 20 translations" in the translator's UI

## Native `Intl` helpers

Cached (taking care of locale change)

```ts
class I18nClient {
...

numberFormat(options: Intl.NumberFormatOptions): Intl.NumberFormat
listFormat(options: Intl.ListFormatOptions): Intl.ListFormat
pluralRules(options: Intl.PluralRulesOptions): Intl.PluralRules
relativeTimeFormat(options: Intl.RelativeTimeFormatOptions): Intl.RelativeTimeFormat
displayNames(options: Intl.DisplayNamesOptions): Intl.DisplayNames
dateTimeFormat(options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat
}
```
42 changes: 42 additions & 0 deletions src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ export function removeDuplicates(arr: Locale[]) {
return arr.filter((k) => !done.has(k) && done.add(k))
}

const options2uniqueString = (options: any) =>
Object.entries(options)
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([k, v]) => `${k}:${v}`)
.join('|')

type IntlConstructor<T> = new (locale: string, options?: any) => T

export default class I18nClient implements OmnI18nClient {
internals: Internals = {}
dictionary: ClientDictionary = {}
Expand Down Expand Up @@ -127,6 +135,7 @@ export default class I18nClient implements OmnI18nClient {
this.locales.every((locale, i) => locale == locales[i])
)
return
if (this.locales[0] !== locales[0]) this.nativeIntl = {}
this.locales = locales
this.toLoadZones = this.loadedZones
this.loadedZones = new Set()
Expand Down Expand Up @@ -161,6 +170,8 @@ export default class I18nClient implements OmnI18nClient {
return interpolate({ client: this, key }, text, args)
}

//#region Reports

missing(key: string, fallback?: Translation): string {
this.report(key, fallback !== undefined ? 'Missing translation' : 'Missing key')
return fallback ?? `[${key}]`
Expand All @@ -172,6 +183,37 @@ export default class I18nClient implements OmnI18nClient {
report(key: string, error: string, spec?: object): void {
// To be overridden
}

//#endregion
//#region Natives

private nativeIntl: Record<string, Record<string, any>> = {}
private cachedNative<T>(ctor: IntlConstructor<T>, options: any): T {
const key = ctor.name
if (!this.nativeIntl[key]) this.nativeIntl[key] = {}
const optionsString = options2uniqueString(options || {})
return (this.nativeIntl[key][optionsString] ??= new ctor(this.locales[0], options))
}
numberFormat(options: Intl.NumberFormatOptions) {
return this.cachedNative(Intl.NumberFormat, options)
}
listFormat(options: Intl.ListFormatOptions) {
return this.cachedNative(Intl.ListFormat, options)
}
pluralRules(options: Intl.PluralRulesOptions) {
return this.cachedNative(Intl.PluralRules, options)
}
relativeTimeFormat(options: Intl.RelativeTimeFormatOptions) {
return this.cachedNative(Intl.RelativeTimeFormat, options)
}
displayNames(options: Intl.DisplayNamesOptions) {
return this.cachedNative(Intl.DisplayNames, options)
}
dateTimeFormat(options: Intl.DateTimeFormatOptions) {
return this.cachedNative(Intl.DateTimeFormat, options)
}

//#endregion
}

export function getContext(translator: Translator): TContext {
Expand Down
52 changes: 26 additions & 26 deletions src/client/interpolation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,16 @@ export const processors: Record<string, (...args: any[]) => string> = {
if (!client.internals.ordinals) return client.missing('internals.ordinals')
const num = parseInt(str)
if (isNaN(num)) return client.error(key, 'NaN', { str })
return client.internals.ordinals[
new Intl.PluralRules(client.locales[0], { type: 'ordinal' }).select(num)
].replace('$', str)
return client.internals.ordinals[client.pluralRules({ type: 'ordinal' }).select(num)].replace(
'$',
str
)
},
plural(this: TContext, str: string, designation: string, plural?: string) {
const num = parseInt(str),
{ client, key } = this
if (isNaN(num)) return client.error(key, 'NaN', { str })
const rule = new Intl.PluralRules(client.locales[0], { type: 'cardinal' }).select(num)
const rule = client.pluralRules({ type: 'cardinal' }).select(num)
const rules: string | Record<string, string> = plural
? { one: designation, other: plural }
: designation
Expand All @@ -80,7 +81,7 @@ export const processors: Record<string, (...args: any[]) => string> = {
currency: this.client.currency,
...options
}
return num.toLocaleString(client.locales, options)
return client.numberFormat(options).format(num)
},
date(this: TContext, str: string, options?: any) {
const nbr = parseInt(str),
Expand All @@ -96,7 +97,7 @@ export const processors: Record<string, (...args: any[]) => string> = {
timeZone: client.timeZone,
...options
}
return date.toLocaleString(client.locales, options)
return client.dateTimeFormat(options).format(date)
},
relative(this: TContext, str: string, options?: any) {
const content = /(-?\d+)\s*(\w+)/.exec(str),
Expand All @@ -114,32 +115,29 @@ export const processors: Record<string, (...args: any[]) => string> = {
return client.error(key, 'Invalid date options', { options })
options = formats.date[options]
}
return new Intl.RelativeTimeFormat(client.locales[0], options).format(
nbr,
<Intl.RelativeTimeFormatUnit>unit
)
return client.relativeTimeFormat(options).format(nbr, <Intl.RelativeTimeFormatUnit>unit)
},
region(this: TContext, str: string) {
return (
new Intl.DisplayNames(this.client.locales[0], { type: 'region' }).of(str) ||
this.client.displayNames({ type: 'region' }).of(str) ||
this.client.error(this.key, 'Invalid region', { str })
)
},
language(this: TContext, str: string) {
return (
new Intl.DisplayNames(this.client.locales[0], { type: 'language' }).of(str) ||
this.client.displayNames({ type: 'language' }).of(str) ||
this.client.error(this.key, 'Invalid language', { str })
)
},
script(this: TContext, str: string) {
return (
new Intl.DisplayNames(this.client.locales[0], { type: 'script' }).of(str) ||
this.client.displayNames({ type: 'script' }).of(str) ||
this.client.error(this.key, 'Invalid script', { str })
)
},
currency(this: TContext, str: string) {
return (
new Intl.DisplayNames(this.client.locales[0], { type: 'currency' }).of(str) ||
this.client.displayNames({ type: 'currency' }).of(str) ||
this.client.error(this.key, 'Invalid currency', { str })
)
},
Expand All @@ -155,9 +153,7 @@ export const processors: Record<string, (...args: any[]) => string> = {
args.push(opts)
opts = {}
}
return new Intl.ListFormat(this.client.locales[0], opts).format(
args.map((arg) => makeArray(arg)).flat()
)
return this.client.listFormat(opts).format(args.map((arg) => makeArray(arg)).flat())
},
duration(this: TContext, duration: DurationDescription, options?: DurationOptions) {
const { client, key } = this
Expand Down Expand Up @@ -214,16 +210,20 @@ export const processors: Record<string, (...args: any[]) => string> = {
if (!parts.length)
return empty || client.error(key, 'Empty duration', { duration: cappedDuration })
const translatedParts = parts.map(([value, unit]) =>
new Intl.NumberFormat(this.client.locales[0], {
style: 'unit',
unit,
unitDisplay: style
}).format(value)
this.client
.numberFormat({
style: 'unit',
unit,
unitDisplay: style
})
.format(value)
)
return new Intl.ListFormat(this.client.locales[0], {
style,
type: style === 'narrow' ? 'unit' : 'conjunction'
}).format(translatedParts)
return this.client
.listFormat({
style,
type: style === 'narrow' ? 'unit' : 'conjunction'
})
.format(translatedParts)
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ export interface OmnI18nClient {
onModification?: OnModification
missing(key: string, fallback?: Translation): string
error(key: string, error: string, spec: object): string

numberFormat(options: Intl.NumberFormatOptions): Intl.NumberFormat
listFormat(options: Intl.ListFormatOptions): Intl.ListFormat
pluralRules(options: Intl.PluralRulesOptions): Intl.PluralRules
relativeTimeFormat(options: Intl.RelativeTimeFormatOptions): Intl.RelativeTimeFormat
displayNames(options: Intl.DisplayNamesOptions): Intl.DisplayNames
dateTimeFormat(options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat
}

export interface TContext<Client extends OmnI18nClient = OmnI18nClient> {
Expand Down
6 changes: 3 additions & 3 deletions test/static.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ beforeAll(async () => {
'format.number': { '': '{number::$1}' },
'format.number.engineering': { '': '{number::$1|engineering}' },
'format.price': { '': '{number::$2|style: currency, currency: $1}' },
'format.dateTime': { '': '{date::$1}' },
'format.dateTime': { '': '{date::$1|dateStyle: short, timeStyle: short}' },
'format.medium': { '': '{date::$1|dateStyle: medium}' },
'format.date': { '': '{date::$1|date}' },
'format.time': { '': '{date::$1|time}' },
Expand Down Expand Up @@ -211,8 +211,8 @@ describe('formatting', () => {
const date = new Date('2021-05-01T12:34:56.789Z')
expect(T.en.format.date(date)).toBe('5/1/21')
expect(T.be.format.date(date)).toBe('1/05/21')
expect(T.en.format.dateTime(date)).toBe('5/1/2021, 12:34:56 PM')
expect(T.be.format.dateTime(date)).toBe('01/05/2021 14:34:56')
expect(T.en.format.dateTime(date)).toBe('5/1/21, 12:34 PM')
expect(T.be.format.dateTime(date)).toBe('1/05/21 14:34')
expect(T.en.format.medium(date)).toBe('May 1, 2021')
expect(T.be.format.medium(date)).toBe('1 mai 2021')
expect(T.en.format.time(date)).toBe('12:34 PM')
Expand Down

0 comments on commit c607eda

Please sign in to comment.