Skip to content

Commit

Permalink
feat: caching intl instances for icu strings
Browse files Browse the repository at this point in the history
feat: default options for intl functions
  • Loading branch information
schummar committed Sep 30, 2021
1 parent 2b95b25 commit f6882ed
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 60 deletions.
86 changes: 40 additions & 46 deletions src/react/translator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,19 @@ export function createTranslator<D extends Dict>(options: ReactCreateTranslatorO
type FD = FlattenDict<D>;

const store = new Store(options);
const { sourceLocale, fallbackLocale = [], fallback: defaultFallback, placeholder: defaultPlaceholder, warn } = options;
const {
sourceLocale,
fallbackLocale = [],
fallback: defaultFallback,
placeholder: defaultPlaceholder,
warn,
dateTimeFormatOptions,
displayNamesOptions,
listFormatOptions,
numberFormatOptions,
pluralRulesOptions,
relativeTimeFormatOptions,
} = options;

/////////////////////////////////////////////////////////////////////////////
// hook translator
Expand All @@ -36,7 +48,7 @@ export function createTranslator<D extends Dict>(options: ReactCreateTranslatorO
const t: TranslatorFn<FD, HookTranslatorOptions, string> = (id, ...[values, options]) => {
const fallback = options?.fallback ?? defaultFallback;
const placeholder = options?.placeholder ?? defaultPlaceholder;
return translate({ dicts, sourceDict, id, values, fallback, placeholder, locale, warn }) as any;
return translate({ dicts, sourceDict, id, values, fallback, placeholder, locale, warn, cache: store.cache }) as any;
};

return Object.assign<TranslatorFn<FD>, Omit<HookTranslator<FD>, keyof TranslatorFn<FD>>>(t, {
Expand All @@ -45,32 +57,32 @@ export function createTranslator<D extends Dict>(options: ReactCreateTranslatorO
unknown: t as HookTranslator<FD>['unknown'],

format(template, ...[values]) {
return format(template, values as any, locale);
return format({ template, values: values as any, locale, cache: store.cache });
},

dateTimeFormat(date, options) {
dateTimeFormat(date, options = dateTimeFormatOptions) {
return store.cache.get(Intl.DateTimeFormat, locale, options).format(toDate(date));
},

displayNames(code, options) {
displayNames(code, options = displayNamesOptions) {
// TODO remove cast when DisplayNames is included in standard lib
return store.cache.get((Intl as any).DisplayNames, locale, options).of(code);
},

listFormat(list, options) {
listFormat(list, options = listFormatOptions) {
// TODO remove cast when DisplayNames is included in standard lib
return store.cache.get((Intl as any).ListFormat, locale, options).format(list);
},

numberFormat(number, options) {
numberFormat(number, options = numberFormatOptions) {
return store.cache.get(Intl.NumberFormat, locale, options).format(number);
},

pluralRules(number, options) {
pluralRules(number, options = pluralRulesOptions) {
return store.cache.get(Intl.PluralRules, locale, options).select(number);
},

relativeTimeFormat(value, unit, options) {
relativeTimeFormat(value, unit, options = relativeTimeFormatOptions) {
return store.cache.get(Intl.RelativeTimeFormat, locale, options).format(value, unit);
},
});
Expand Down Expand Up @@ -98,7 +110,7 @@ export function createTranslator<D extends Dict>(options: ReactCreateTranslatorO
const placeholder = options?.placeholder ?? defaultPlaceholder;

const text = useMemo(
() => translate({ dicts, sourceDict, id, values, fallback, placeholder, locale, warn }),
() => translate({ dicts, sourceDict, id, values, fallback, placeholder, locale, warn, cache: store.cache }),
[locale, dicts, sourceDict, id, values, fallback, placeholder],
);
const textArray = castArray(text);
Expand All @@ -117,76 +129,58 @@ export function createTranslator<D extends Dict>(options: ReactCreateTranslatorO
return <TranslatorComponent id={id} values={values} options={options} />;
};

const FormatComponent = ({ template, values }: { template: string; values?: Record<string, unknown> }) => {
const contextLocale = useContext(TranslationContext).locale;
const locale = contextLocale ?? sourceLocale;
const text = useMemo(() => format(template, values, locale), [template, values, locale]);

return <>{text}</>;
};

const createFormatComponent: InlineTranslator<FD>['format'] = (template, ...[values]) => {
return <FormatComponent {...{ template, values: values as any }} />;
};

const RenderComponent = ({ renderFn, dependecies = [renderFn] }: { renderFn: (locale: string) => ReactNode; dependecies?: any[] }) => {
const contextLocale = useContext(TranslationContext).locale;
const locale = contextLocale ?? sourceLocale;
const value = useMemo(() => renderFn(locale), [locale, ...dependecies]);
return <>{value}</>;
};

const createRenderComponent: InlineTranslator<FD>['render'] = (renderFn, dependecies) => {
const render: InlineTranslator<FD>['render'] = (renderFn, dependecies) => {
return <RenderComponent renderFn={renderFn} dependecies={dependecies} />;
};

const t: InlineTranslator<FD> = Object.assign<
TranslatorFn<FD, InlineTranslatorOptions, ReactNode>,
Omit<InlineTranslator<FD>, keyof TranslatorFn<FD, InlineTranslatorOptions, ReactNode>>
>(createTranslatorComponent, {
get locale() {
return createRenderComponent((locale) => locale, []);
},
locale: render((locale) => locale, []),

unknown: createTranslatorComponent as InlineTranslator<FD>['unknown'],

format: createFormatComponent,
format(template, ...[values]) {
return render((locale) => format({ template, values: values as any, locale, cache: store.cache }), [template, hash(values)]);
},

render: createRenderComponent,
render,

dateTimeFormat(date, options) {
return createRenderComponent(
(locale) => store.cache.get(Intl.DateTimeFormat, locale, options).format(toDate(date)),
[date, hash(options)],
);
dateTimeFormat(date, options = dateTimeFormatOptions) {
return render((locale) => store.cache.get(Intl.DateTimeFormat, locale, options).format(toDate(date)), [date, hash(options)]);
},

displayNames(code, options) {
displayNames(code, options = displayNamesOptions) {
// TODO remove cast when DisplayNames is included in standard lib
return createRenderComponent(
(locale) => store.cache.get((Intl as any).DisplayNames, locale, options).of(code),
[code, hash(options)],
);
return render((locale) => store.cache.get((Intl as any).DisplayNames, locale, options).of(code), [code, hash(options)]);
},

listFormat(list, options) {
listFormat(list, options = listFormatOptions) {
// TODO remove cast when DisplayNames is included in standard lib
return createRenderComponent(
return render(
(locale) => store.cache.get((Intl as any).ListFormat, locale, options).format(list),
[list && hash([...list]), hash(options)],
);
},

numberFormat(number, options) {
return createRenderComponent((locale) => store.cache.get(Intl.NumberFormat, locale, options).format(number), [number, hash(options)]);
numberFormat(number, options = numberFormatOptions) {
return render((locale) => store.cache.get(Intl.NumberFormat, locale, options).format(number), [number, hash(options)]);
},

pluralRules(number, options) {
return createRenderComponent((locale) => store.cache.get(Intl.PluralRules, locale, options).select(number), [number, hash(options)]);
pluralRules(number, options = pluralRulesOptions) {
return render((locale) => store.cache.get(Intl.PluralRules, locale, options).select(number), [number, hash(options)]);
},

relativeTimeFormat(value, unit, options) {
return createRenderComponent(
relativeTimeFormat(value, unit, options = relativeTimeFormatOptions) {
return render(
(locale) => store.cache.get(Intl.RelativeTimeFormat, locale, options).format(value, unit),
[value, unit, hash(options)],
);
Expand Down
35 changes: 30 additions & 5 deletions src/translate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { parse } from '@formatjs/icu-messageformat-parser';
import { IntlMessageFormat } from 'intl-messageformat';
import { MaybePromise } from '.';
import { Cache } from './cache';
import { mapPotentialArray } from './mapPotentialArray';
import { FlatDict } from './types';

Expand All @@ -13,6 +14,7 @@ export function translate<F = never>({
placeholder,
locale,
warn,
cache,
}: {
dicts: MaybePromise<FlatDict>[];
sourceDict?: MaybePromise<FlatDict> | null;
Expand All @@ -22,6 +24,7 @@ export function translate<F = never>({
placeholder?: F | ((id: string, sourceTranslation?: string | readonly string[]) => F);
locale: string;
warn?: (locale: string, id: string) => void;
cache: Cache;
}): string | F | (string | F)[] | F {
if (fallback !== undefined) {
dicts = dicts.slice(0, 1);
Expand All @@ -32,7 +35,7 @@ export function translate<F = never>({
if (dict instanceof Promise) {
return mapPotentialArray(
sourceDict && !(sourceDict instanceof Promise)
? translate<string>({ dicts: [sourceDict], sourceDict, id, values, locale })
? translate<string>({ dicts: [sourceDict], sourceDict, id, values, locale, cache })
: undefined,
(sourceTranslation) => {
if (placeholder instanceof Function) {
Expand All @@ -48,7 +51,7 @@ export function translate<F = never>({
if (fallback instanceof Function) {
const sourceTranslation =
sourceDict && !(sourceDict instanceof Promise)
? translate<string>({ dicts: [sourceDict], sourceDict, id, values, locale })
? translate<string>({ dicts: [sourceDict], sourceDict, id, values, locale, cache })
: undefined;
return fallback(id, sourceTranslation);
}
Expand All @@ -58,13 +61,35 @@ export function translate<F = never>({
return id;
}

return mapPotentialArray(template, (template) => format(template, values, locale));
return mapPotentialArray(template, (template) => format({ template, values, locale, cache }));
}

export function format(template: string, values?: Record<string, unknown>, locale?: string): string {
export function format({
template,
values,
locale,
cache,
}: {
template: string;
values?: Record<string, unknown>;
locale?: string;
cache: Cache;
}): string {
try {
const ast = parse(template, { requiresOtherClause: false });
const f = new IntlMessageFormat(ast, locale);
const f = new IntlMessageFormat(ast, locale, undefined, {
formatters: {
getDateTimeFormat(...args) {
return cache.get(Intl.DateTimeFormat, ...args);
},
getNumberFormat(...args) {
return cache.get(Intl.NumberFormat, ...args);
},
getPluralRules(...args) {
return cache.get(Intl.PluralRules, ...args);
},
},
});
const msg = f.format(values);
if (msg instanceof Array) return msg.join(' ');
return String(msg);
Expand Down
29 changes: 20 additions & 9 deletions src/translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,18 @@ import { CreateTranslatorOptions, CreateTranslatorResult, Dict, FlattenDict, Tra
export const createGetTranslator =
<D extends Dict>(
store: Store<D>,
{ fallbackLocale, fallback: globalFallback, warn, sourceLocale }: CreateTranslatorOptions<D>,
{
fallbackLocale,
fallback: globalFallback,
warn,
sourceLocale,
dateTimeFormatOptions,
displayNamesOptions,
listFormatOptions,
numberFormatOptions,
pluralRulesOptions,
relativeTimeFormatOptions,
}: CreateTranslatorOptions<D>,
): ((locale: string) => Promise<Translator<FlattenDict<D>>>) =>
async (locale: string) => {
type FD = FlattenDict<D>;
Expand All @@ -17,7 +28,7 @@ export const createGetTranslator =

const t: TranslatorFn<FD> = (id, ...[values, options]) => {
const fallback = options?.fallback ?? globalFallback;
return translate({ dicts, sourceDict, id, values, fallback, locale, warn }) as any;
return translate({ dicts, sourceDict, id, values, fallback, locale, warn, cache: store.cache }) as any;
};

return Object.assign<TranslatorFn<FD>, Omit<Translator<FD>, keyof TranslatorFn<FD>>>(t, {
Expand All @@ -26,32 +37,32 @@ export const createGetTranslator =
unknown: t as Translator<FD>['unknown'],

format(template, ...[values]) {
return format(template, values as any, locale);
return format({ template, values: values as any, locale, cache: store.cache });
},

dateTimeFormat(date, options) {
dateTimeFormat(date, options = dateTimeFormatOptions) {
return store.cache.get(Intl.DateTimeFormat, locale, options).format(toDate(date));
},

displayNames(code, options) {
displayNames(code, options = displayNamesOptions) {
// TODO remove cast when DisplayNames is included in standard lib
return store.cache.get((Intl as any).DisplayNames, locale, options).of(code);
},

listFormat(list, options) {
listFormat(list, options = listFormatOptions) {
// TODO remove cast when DisplayNames is included in standard lib
return store.cache.get((Intl as any).ListFormat, locale, options).format(list);
},

numberFormat(number, options) {
numberFormat(number, options = numberFormatOptions) {
return store.cache.get(Intl.NumberFormat, locale, options).format(number);
},

pluralRules(number, options) {
pluralRules(number, options = pluralRulesOptions) {
return store.cache.get(Intl.PluralRules, locale, options).select(number);
},

relativeTimeFormat(value, unit, options) {
relativeTimeFormat(value, unit, options = relativeTimeFormatOptions) {
return store.cache.get(Intl.RelativeTimeFormat, locale, options).format(value, unit);
},
});
Expand Down
12 changes: 12 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ export interface CreateTranslatorOptions<D extends Dict> {
warn?: (locale: string, id: string) => void;
/** Configure cache for intl instances */
cacheOptions?: CacheOptions;
/** Default options */
dateTimeFormatOptions?: Intl.DateTimeFormatOptions;
/** Default options */
displayNamesOptions?: DisplayNamesOptions;
/** Default options */
listFormatOptions?: ListFormatOptions;
/** Default options */
numberFormatOptions?: Intl.NumberFormatOptions;
/** Default options */
pluralRulesOptions?: Intl.PluralRulesOptions;
/** Default options */
relativeTimeFormatOptions?: Intl.RelativeTimeFormatOptions;
}

export interface CreateTranslatorResult<D extends FlatDict> {
Expand Down
12 changes: 12 additions & 0 deletions test/react.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const createContext = () =>
sourceLocale: 'en',
dicts: { de: dictDe, es: dictEs },
fallback: () => '-',
dateTimeFormatOptions: { dateStyle: 'medium', timeStyle: 'medium' },
});
const test = anyTest as TestInterface<ReturnType<typeof createContext>>;

Expand Down Expand Up @@ -178,6 +179,17 @@ forCases(
},
);

forCases(
'dateTimeFormat default options',
(t) => t.dateTimeFormat(date),
async (t, div) => {
t.is(div.textContent, 'Feb 2, 2000, 3:04:05 AM');

fireEvent.click(div);
t.is(div.textContent, '02.02.2000, 03:04:05');
},
);

forCases(
'displayNames',
(t) => t.displayNames('de', { type: 'language' }),
Expand Down

0 comments on commit f6882ed

Please sign in to comment.