Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/use-intl/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@
],
"dependencies": {
"@formatjs/ecma402-abstract": "^1.11.4",
"intl-messageformat": "^9.3.18"
"@messageformat/runtime": "^3.0.1",
"icu-to-json": "0.0.20"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
Expand Down
5 changes: 5 additions & 0 deletions packages/use-intl/src/core/MessageFormat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type {compile} from 'icu-to-json/compiler';

type MessageFormat = ReturnType<typeof compile>;

export default MessageFormat;
7 changes: 3 additions & 4 deletions packages/use-intl/src/core/MessageFormatCache.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// eslint-disable-next-line import/no-named-as-default -- False positive
import type IntlMessageFormat from 'intl-messageformat';
import MessageFormat from './MessageFormat';

type MessageFormatCache = Map<
/** Format: `${locale}.${namespace}.${key}.${message}` */
string,
IntlMessageFormat
string, // Could simplify the key here
MessageFormat
>;

export default MessageFormatCache;
33 changes: 16 additions & 17 deletions packages/use-intl/src/core/createBaseTranslator.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// eslint-disable-next-line import/no-named-as-default -- False positive
import IntlMessageFormat from 'intl-messageformat';
// import IntlMessageFormat from 'intl-messageformat';
import {run, evaluateAst} from 'icu-to-json';
import {compile} from 'icu-to-json/compiler';
import {
cloneElement,
isValidElement,
Expand All @@ -11,13 +13,14 @@ import AbstractIntlMessages from './AbstractIntlMessages';
import Formats from './Formats';
import {InitializedIntlConfig} from './IntlConfig';
import IntlError, {IntlErrorCode} from './IntlError';
import MessageFormat from './MessageFormat';
import MessageFormatCache from './MessageFormatCache';
import TranslationValues, {
MarkupTranslationValues,
RichTranslationValues
} from './TranslationValues';
import convertFormatsToIntlMessageFormat from './convertFormatsToIntlMessageFormat';
import {defaultGetMessageFallback, defaultOnError} from './defaults';
import getFormatters from './getFormatters';
import MessageKeys from './utils/MessageKeys';
import NestedKeyOf from './utils/NestedKeyOf';
import NestedValueOf from './utils/NestedValueOf';
Expand Down Expand Up @@ -224,7 +227,7 @@ function createBaseTranslatorImpl<

const cacheKey = joinPath([locale, namespace, key, String(message)]);

let messageFormat: IntlMessageFormat;
let messageFormat: MessageFormat;
if (messageFormatCache?.has(cacheKey)) {
messageFormat = messageFormatCache.get(cacheKey)!;
} else {
Expand Down Expand Up @@ -252,18 +255,12 @@ function createBaseTranslatorImpl<
}

// Hot path that avoids creating an `IntlMessageFormat` instance
// TODO: We can get rid of this with icu-to-json
const plainMessage = getPlainMessage(message as string, values);
if (plainMessage) return plainMessage;

try {
messageFormat = new IntlMessageFormat(
message,
locale,
convertFormatsToIntlMessageFormat(
{...globalFormats, ...formats},
timeZone
)
);
messageFormat = compile(message);
} catch (error) {
return getFallbackFromErrorAndNotify(
key,
Expand All @@ -276,14 +273,16 @@ function createBaseTranslatorImpl<
}

try {
const formattedMessage = messageFormat.format(
// @ts-expect-error `intl-messageformat` expects a different format
// for rich text elements since a recent minor update. This
// needs to be evaluated in detail, possibly also in regards
// to be able to format to parts.
prepareTranslationValues({...defaultTranslationValues, ...values})
const evaluated = evaluateAst(
messageFormat.json,
locale,
{...defaultTranslationValues, ...values},
getFormatters(timeZone, formats, globalFormats)
);

const isRichText = evaluated.length > 1;
const formattedMessage = isRichText ? evaluated : evaluated.join('');

if (formattedMessage == null) {
throw new Error(
process.env.NODE_ENV !== 'production'
Expand Down
122 changes: 122 additions & 0 deletions packages/use-intl/src/core/getFormatters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import Formats from './Formats';

// TODO: Move to a scoped cache to avoid memory leaks?
const numberFormats: Record<string, Intl.NumberFormat> = {};

// TODO: time & date vs dateTime. Maybe we should just use dateTime?

// Copied from intl-messageformat
const defaults = {
number: {
integer: {maximumFractionDigits: 0},
currency: {style: 'currency'},
percent: {style: 'percent'}
},
date: {
short: {month: 'numeric', day: 'numeric', year: '2-digit'},
medium: {month: 'short', day: 'numeric', year: 'numeric'},
long: {month: 'long', day: 'numeric', year: 'numeric'},
full: {weekday: 'long', month: 'long', day: 'numeric', year: 'numeric'}
},
time: {
short: {hour: 'numeric', minute: 'numeric'},
medium: {hour: 'numeric', minute: 'numeric', second: 'numeric'},
long: {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZoneName: 'short'
},
full: {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZoneName: 'short'
}
}
} as const;

function formatNumber(
locale: string | Array<string>,
opt: Intl.NumberFormatOptions
) {
const key = String(locale) + JSON.stringify(opt);
if (!numberFormats[key]) {
numberFormats[key] = new Intl.NumberFormat(locale, opt);
}
return numberFormats[key];
}

export default function getFormatters(
timeZone?: string,
formats?: Partial<Formats>,
globalFormats?: Partial<Formats>
) {
const formatters = {
date(
value: number | string,
locale: string,
formatName?: keyof typeof defaults.date
) {
const allFormats = {
...defaults.date,
...globalFormats?.dateTime
};

const options: Intl.DateTimeFormatOptions = {timeZone};

if (formatName && formatName in allFormats) {
Object.assign(options, allFormats[formatName]);
}

// TODO: Use Intl.DateTimeFormat and caching?
return new Date(value).toLocaleDateString(locale, options);
},
time(
value: number | string,
locale: string,
formatName?: keyof typeof defaults.time
) {
const allFormats = {
...defaults.time,
...globalFormats?.dateTime
};

const options: Intl.DateTimeFormatOptions = {timeZone};

if (formatName && formatName in allFormats) {
Object.assign(options, allFormats[formatName]);
}

// TODO: Use Intl.DateTimeFormat and caching?
return new Date(value).toLocaleTimeString(locale, options);
},
numberFmt(
value: number,
locale: string,
arg: string,
defaultCurrency: string
) {
const allFormats = {
...defaults.number,
...globalFormats?.number,
...formats?.number
};

// Based on https://github.com/messageformat/messageformat/blob/main/packages/runtime/src/fmt/number.ts
const [formatName, currency] = (arg && arg.split(':')) || [];

const options: Intl.NumberFormatOptions = {currency};

if (formatName && formatName in allFormats) {
Object.assign(options, allFormats[formatName]);
}

// TODO: Caching?
const format = new Intl.NumberFormat(locale, options);
return format.format(value);
}
};

return formatters;
}
Loading