696 lines
23 KiB
JavaScript
696 lines
23 KiB
JavaScript
import { IntlMessageFormat } from 'intl-messageformat';
|
||
import { isValidElement, cloneElement } from 'react';
|
||
import { memoize, strategies } from '@formatjs/fast-memoize';
|
||
|
||
class IntlError extends Error {
|
||
constructor(code, originalMessage) {
|
||
let message = code;
|
||
if (originalMessage) {
|
||
message += ': ' + originalMessage;
|
||
}
|
||
super(message);
|
||
this.code = code;
|
||
if (originalMessage) {
|
||
this.originalMessage = originalMessage;
|
||
}
|
||
}
|
||
}
|
||
|
||
var IntlErrorCode = /*#__PURE__*/function (IntlErrorCode) {
|
||
IntlErrorCode["MISSING_MESSAGE"] = "MISSING_MESSAGE";
|
||
IntlErrorCode["MISSING_FORMAT"] = "MISSING_FORMAT";
|
||
IntlErrorCode["ENVIRONMENT_FALLBACK"] = "ENVIRONMENT_FALLBACK";
|
||
IntlErrorCode["INSUFFICIENT_PATH"] = "INSUFFICIENT_PATH";
|
||
IntlErrorCode["INVALID_MESSAGE"] = "INVALID_MESSAGE";
|
||
IntlErrorCode["INVALID_KEY"] = "INVALID_KEY";
|
||
IntlErrorCode["FORMATTING_ERROR"] = "FORMATTING_ERROR";
|
||
return IntlErrorCode;
|
||
}(IntlErrorCode || {});
|
||
|
||
/**
|
||
* `intl-messageformat` uses separate keys for `date` and `time`, but there's
|
||
* only one native API: `Intl.DateTimeFormat`. Additionally you might want to
|
||
* include both a time and a date in a value, therefore the separation doesn't
|
||
* seem so useful. We offer a single `dateTime` namespace instead, but we have
|
||
* to convert the format before `intl-messageformat` can be used.
|
||
*/
|
||
function convertFormatsToIntlMessageFormat(globalFormats, inlineFormats, timeZone) {
|
||
const mfDateDefaults = IntlMessageFormat.formats.date;
|
||
const mfTimeDefaults = IntlMessageFormat.formats.time;
|
||
const dateTimeFormats = {
|
||
...globalFormats?.dateTime,
|
||
...inlineFormats?.dateTime
|
||
};
|
||
const allFormats = {
|
||
date: {
|
||
...mfDateDefaults,
|
||
...dateTimeFormats
|
||
},
|
||
time: {
|
||
...mfTimeDefaults,
|
||
...dateTimeFormats
|
||
},
|
||
number: {
|
||
...globalFormats?.number,
|
||
...inlineFormats?.number
|
||
}
|
||
// (list is not supported in ICU messages)
|
||
};
|
||
if (timeZone) {
|
||
// The only way to set a time zone with `intl-messageformat` is to merge it into the formats
|
||
// https://github.com/formatjs/formatjs/blob/8256c5271505cf2606e48e3c97ecdd16ede4f1b5/packages/intl/src/message.ts#L15
|
||
['date', 'time'].forEach(property => {
|
||
const formats = allFormats[property];
|
||
for (const [key, value] of Object.entries(formats)) {
|
||
formats[key] = {
|
||
timeZone,
|
||
...value
|
||
};
|
||
}
|
||
});
|
||
}
|
||
return allFormats;
|
||
}
|
||
|
||
function joinPath(...parts) {
|
||
return parts.filter(Boolean).join('.');
|
||
}
|
||
|
||
/**
|
||
* Contains defaults that are used for all entry points into the core.
|
||
* See also `InitializedIntlConfiguration`.
|
||
*/
|
||
|
||
function defaultGetMessageFallback(props) {
|
||
return joinPath(props.namespace, props.key);
|
||
}
|
||
function defaultOnError(error) {
|
||
console.error(error);
|
||
}
|
||
|
||
function createCache() {
|
||
return {
|
||
dateTime: {},
|
||
number: {},
|
||
message: {},
|
||
relativeTime: {},
|
||
pluralRules: {},
|
||
list: {},
|
||
displayNames: {}
|
||
};
|
||
}
|
||
function createMemoCache(store) {
|
||
return {
|
||
create() {
|
||
return {
|
||
get(key) {
|
||
return store[key];
|
||
},
|
||
set(key, value) {
|
||
store[key] = value;
|
||
}
|
||
};
|
||
}
|
||
};
|
||
}
|
||
function memoFn(fn, cache) {
|
||
return memoize(fn, {
|
||
cache: createMemoCache(cache),
|
||
strategy: strategies.variadic
|
||
});
|
||
}
|
||
function memoConstructor(ConstructorFn, cache) {
|
||
return memoFn((...args) => new ConstructorFn(...args), cache);
|
||
}
|
||
function createIntlFormatters(cache) {
|
||
const getDateTimeFormat = memoConstructor(Intl.DateTimeFormat, cache.dateTime);
|
||
const getNumberFormat = memoConstructor(Intl.NumberFormat, cache.number);
|
||
const getPluralRules = memoConstructor(Intl.PluralRules, cache.pluralRules);
|
||
const getRelativeTimeFormat = memoConstructor(Intl.RelativeTimeFormat, cache.relativeTime);
|
||
const getListFormat = memoConstructor(Intl.ListFormat, cache.list);
|
||
const getDisplayNames = memoConstructor(Intl.DisplayNames, cache.displayNames);
|
||
return {
|
||
getDateTimeFormat,
|
||
getNumberFormat,
|
||
getPluralRules,
|
||
getRelativeTimeFormat,
|
||
getListFormat,
|
||
getDisplayNames
|
||
};
|
||
}
|
||
|
||
// Placed here for improved tree shaking. Somehow when this is placed in
|
||
// `formatters.tsx`, then it can't be shaken off from `next-intl`.
|
||
function createMessageFormatter(cache, intlFormatters) {
|
||
const getMessageFormat = memoFn((...args) => new IntlMessageFormat(args[0], args[1], args[2], {
|
||
formatters: intlFormatters,
|
||
...args[3]
|
||
}), cache.message);
|
||
return getMessageFormat;
|
||
}
|
||
function resolvePath(locale, messages, key, namespace) {
|
||
const fullKey = joinPath(namespace, key);
|
||
if (!messages) {
|
||
throw new Error(`No messages available at \`${namespace}\`.` );
|
||
}
|
||
let message = messages;
|
||
key.split('.').forEach(part => {
|
||
const next = message[part];
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||
if (part == null || next == null) {
|
||
throw new Error(`Could not resolve \`${fullKey}\` in messages for locale \`${locale}\`.` );
|
||
}
|
||
message = next;
|
||
});
|
||
return message;
|
||
}
|
||
function prepareTranslationValues(values) {
|
||
// Workaround for https://github.com/formatjs/formatjs/issues/1467
|
||
const transformedValues = {};
|
||
Object.keys(values).forEach(key => {
|
||
let index = 0;
|
||
const value = values[key];
|
||
let transformed;
|
||
if (typeof value === 'function') {
|
||
transformed = chunks => {
|
||
const result = value(chunks);
|
||
return /*#__PURE__*/isValidElement(result) ? /*#__PURE__*/cloneElement(result, {
|
||
key: key + index++
|
||
}) : result;
|
||
};
|
||
} else {
|
||
transformed = value;
|
||
}
|
||
transformedValues[key] = transformed;
|
||
});
|
||
return transformedValues;
|
||
}
|
||
function getMessagesOrError(locale, messages, namespace) {
|
||
try {
|
||
if (!messages) {
|
||
throw new Error(`No messages were configured.` );
|
||
}
|
||
const retrievedMessages = namespace ? resolvePath(locale, messages, namespace) : messages;
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||
if (!retrievedMessages) {
|
||
throw new Error(`No messages for namespace \`${namespace}\` found.` );
|
||
}
|
||
return retrievedMessages;
|
||
} catch (error) {
|
||
const intlError = new IntlError(IntlErrorCode.MISSING_MESSAGE, error.message);
|
||
return intlError;
|
||
}
|
||
}
|
||
function getPlainMessage(candidate, values) {
|
||
// To improve runtime performance, only compile message if:
|
||
return (
|
||
// 1. Values are provided
|
||
values ||
|
||
// 2. There are escaped braces (e.g. "'{name'}")
|
||
/'[{}]/.test(candidate) ||
|
||
// 3. There are missing arguments or tags (dev-only error handling)
|
||
/<|{/.test(candidate) ? undefined // Compile
|
||
: candidate // Don't compile
|
||
);
|
||
}
|
||
function createBaseTranslator(config) {
|
||
const messagesOrError = getMessagesOrError(config.locale, config.messages, config.namespace);
|
||
return createBaseTranslatorImpl({
|
||
...config,
|
||
messagesOrError
|
||
});
|
||
}
|
||
function createBaseTranslatorImpl({
|
||
cache,
|
||
formats: globalFormats,
|
||
formatters,
|
||
getMessageFallback = defaultGetMessageFallback,
|
||
locale,
|
||
messagesOrError,
|
||
namespace,
|
||
onError,
|
||
timeZone
|
||
}) {
|
||
const hasMessagesError = messagesOrError instanceof IntlError;
|
||
function getFallbackFromErrorAndNotify(key, code, message, fallback) {
|
||
const error = new IntlError(code, message);
|
||
onError(error);
|
||
return fallback ?? getMessageFallback({
|
||
error,
|
||
key,
|
||
namespace
|
||
});
|
||
}
|
||
function translateBaseFn(/** Use a dot to indicate a level of nesting (e.g. `namespace.nestedLabel`). */
|
||
key, /** Key value pairs for values to interpolate into the message. */
|
||
values, /** Provide custom formats for numbers, dates and times. */
|
||
formats, _fallback) {
|
||
const fallback = _fallback;
|
||
let message;
|
||
if (hasMessagesError) {
|
||
if (fallback) {
|
||
message = fallback;
|
||
} else {
|
||
onError(messagesOrError);
|
||
return getMessageFallback({
|
||
error: messagesOrError,
|
||
key,
|
||
namespace
|
||
});
|
||
}
|
||
} else {
|
||
const messages = messagesOrError;
|
||
try {
|
||
message = resolvePath(locale, messages, key, namespace);
|
||
} catch (error) {
|
||
if (fallback) {
|
||
message = fallback;
|
||
} else {
|
||
return getFallbackFromErrorAndNotify(key, IntlErrorCode.MISSING_MESSAGE, error.message, fallback);
|
||
}
|
||
}
|
||
}
|
||
if (typeof message === 'object') {
|
||
let code, errorMessage;
|
||
if (Array.isArray(message)) {
|
||
code = IntlErrorCode.INVALID_MESSAGE;
|
||
{
|
||
errorMessage = `Message at \`${joinPath(namespace, key)}\` resolved to an array, but only strings are supported. See https://next-intl.dev/docs/usage/translations#arrays-of-messages`;
|
||
}
|
||
} else {
|
||
code = IntlErrorCode.INSUFFICIENT_PATH;
|
||
{
|
||
errorMessage = `Message at \`${joinPath(namespace, key)}\` resolved to an object, but only strings are supported. Use a \`.\` to retrieve nested messages. See https://next-intl.dev/docs/usage/translations#structuring-messages`;
|
||
}
|
||
}
|
||
return getFallbackFromErrorAndNotify(key, code, errorMessage);
|
||
}
|
||
let messageFormat;
|
||
|
||
// Hot path that avoids creating an `IntlMessageFormat` instance
|
||
const plainMessage = getPlainMessage(message, values);
|
||
if (plainMessage) return plainMessage;
|
||
|
||
// Lazy init the message formatter for better tree
|
||
// shaking in case message formatting is not used.
|
||
if (!formatters.getMessageFormat) {
|
||
formatters.getMessageFormat = createMessageFormatter(cache, formatters);
|
||
}
|
||
try {
|
||
messageFormat = formatters.getMessageFormat(message, locale, convertFormatsToIntlMessageFormat(globalFormats, formats, timeZone), {
|
||
formatters: {
|
||
...formatters,
|
||
getDateTimeFormat(locales, options) {
|
||
// Workaround for https://github.com/formatjs/formatjs/issues/4279
|
||
return formatters.getDateTimeFormat(locales, {
|
||
timeZone,
|
||
...options
|
||
});
|
||
}
|
||
}
|
||
});
|
||
} catch (error) {
|
||
const thrownError = error;
|
||
return getFallbackFromErrorAndNotify(key, IntlErrorCode.INVALID_MESSAGE, thrownError.message + ('originalMessage' in thrownError ? ` (${thrownError.originalMessage})` : '') , fallback);
|
||
}
|
||
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.
|
||
values ? prepareTranslationValues(values) : values);
|
||
if (formattedMessage == null) {
|
||
throw new Error(`Unable to format \`${key}\` in ${namespace ? `namespace \`${namespace}\`` : 'messages'}` );
|
||
}
|
||
|
||
// Limit the function signature to return strings or React elements
|
||
return /*#__PURE__*/isValidElement(formattedMessage) ||
|
||
// Arrays of React elements
|
||
Array.isArray(formattedMessage) || typeof formattedMessage === 'string' ? formattedMessage : String(formattedMessage);
|
||
} catch (error) {
|
||
return getFallbackFromErrorAndNotify(key, IntlErrorCode.FORMATTING_ERROR, error.message, fallback);
|
||
}
|
||
}
|
||
function translateFn(/** Use a dot to indicate a level of nesting (e.g. `namespace.nestedLabel`). */
|
||
key, /** Key value pairs for values to interpolate into the message. */
|
||
values, /** Custom formats for numbers, dates and times. */
|
||
formats, _fallback) {
|
||
const result = translateBaseFn(key, values, formats, _fallback);
|
||
if (typeof result !== 'string') {
|
||
return getFallbackFromErrorAndNotify(key, IntlErrorCode.INVALID_MESSAGE, `The message \`${key}\` in ${namespace ? `namespace \`${namespace}\`` : 'messages'} didn't resolve to a string. If you want to format rich text, use \`t.rich\` instead.` );
|
||
}
|
||
return result;
|
||
}
|
||
translateFn.rich = translateBaseFn;
|
||
|
||
// Augment `translateBaseFn` to return plain strings
|
||
translateFn.markup = (key, values, formats, _fallback) => {
|
||
const result = translateBaseFn(key,
|
||
// @ts-expect-error -- `MarkupTranslationValues` is practically a sub type
|
||
// of `RichTranslationValues` but TypeScript isn't smart enough here.
|
||
values, formats, _fallback);
|
||
if (typeof result !== 'string') {
|
||
const error = new IntlError(IntlErrorCode.FORMATTING_ERROR, "`t.markup` only accepts functions for formatting that receive and return strings.\n\nE.g. t.markup('markup', {b: (chunks) => `<b>${chunks}</b>`})");
|
||
onError(error);
|
||
return getMessageFallback({
|
||
error,
|
||
key,
|
||
namespace
|
||
});
|
||
}
|
||
return result;
|
||
};
|
||
translateFn.raw = key => {
|
||
if (hasMessagesError) {
|
||
onError(messagesOrError);
|
||
return getMessageFallback({
|
||
error: messagesOrError,
|
||
key,
|
||
namespace
|
||
});
|
||
}
|
||
const messages = messagesOrError;
|
||
try {
|
||
return resolvePath(locale, messages, key, namespace);
|
||
} catch (error) {
|
||
return getFallbackFromErrorAndNotify(key, IntlErrorCode.MISSING_MESSAGE, error.message);
|
||
}
|
||
};
|
||
translateFn.has = key => {
|
||
if (hasMessagesError) {
|
||
return false;
|
||
}
|
||
try {
|
||
resolvePath(locale, messagesOrError, key, namespace);
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
};
|
||
return translateFn;
|
||
}
|
||
|
||
/**
|
||
* For the strictly typed messages to work we have to wrap the namespace into
|
||
* a mandatory prefix. See https://stackoverflow.com/a/71529575/343045
|
||
*/
|
||
function resolveNamespace(namespace, namespacePrefix) {
|
||
return namespace === namespacePrefix ? undefined : namespace.slice((namespacePrefix + '.').length);
|
||
}
|
||
|
||
const SECOND = 1;
|
||
const MINUTE = SECOND * 60;
|
||
const HOUR = MINUTE * 60;
|
||
const DAY = HOUR * 24;
|
||
const WEEK = DAY * 7;
|
||
const MONTH = DAY * (365 / 12); // Approximation
|
||
const QUARTER = MONTH * 3;
|
||
const YEAR = DAY * 365;
|
||
const UNIT_SECONDS = {
|
||
second: SECOND,
|
||
seconds: SECOND,
|
||
minute: MINUTE,
|
||
minutes: MINUTE,
|
||
hour: HOUR,
|
||
hours: HOUR,
|
||
day: DAY,
|
||
days: DAY,
|
||
week: WEEK,
|
||
weeks: WEEK,
|
||
month: MONTH,
|
||
months: MONTH,
|
||
quarter: QUARTER,
|
||
quarters: QUARTER,
|
||
year: YEAR,
|
||
years: YEAR
|
||
};
|
||
function resolveRelativeTimeUnit(seconds) {
|
||
const absValue = Math.abs(seconds);
|
||
if (absValue < MINUTE) {
|
||
return 'second';
|
||
} else if (absValue < HOUR) {
|
||
return 'minute';
|
||
} else if (absValue < DAY) {
|
||
return 'hour';
|
||
} else if (absValue < WEEK) {
|
||
return 'day';
|
||
} else if (absValue < MONTH) {
|
||
return 'week';
|
||
} else if (absValue < YEAR) {
|
||
return 'month';
|
||
}
|
||
return 'year';
|
||
}
|
||
function calculateRelativeTimeValue(seconds, unit) {
|
||
// We have to round the resulting values, as `Intl.RelativeTimeFormat`
|
||
// will include fractions like '2.1 hours ago'.
|
||
return Math.round(seconds / UNIT_SECONDS[unit]);
|
||
}
|
||
function createFormatter(props) {
|
||
const {
|
||
_cache: cache = createCache(),
|
||
_formatters: formatters = createIntlFormatters(cache),
|
||
formats,
|
||
locale,
|
||
onError = defaultOnError,
|
||
timeZone: globalTimeZone
|
||
} = props;
|
||
function applyTimeZone(options) {
|
||
if (!options?.timeZone) {
|
||
if (globalTimeZone) {
|
||
options = {
|
||
...options,
|
||
timeZone: globalTimeZone
|
||
};
|
||
} else {
|
||
onError(new IntlError(IntlErrorCode.ENVIRONMENT_FALLBACK, `The \`timeZone\` parameter wasn't provided and there is no global default configured. Consider adding a global default to avoid markup mismatches caused by environment differences. Learn more: https://next-intl.dev/docs/configuration#time-zone` ));
|
||
}
|
||
}
|
||
return options;
|
||
}
|
||
function resolveFormatOrOptions(typeFormats, formatOrOptions, overrides) {
|
||
let options;
|
||
if (typeof formatOrOptions === 'string') {
|
||
const formatName = formatOrOptions;
|
||
options = typeFormats?.[formatName];
|
||
if (!options) {
|
||
const error = new IntlError(IntlErrorCode.MISSING_FORMAT, `Format \`${formatName}\` is not available.` );
|
||
onError(error);
|
||
throw error;
|
||
}
|
||
} else {
|
||
options = formatOrOptions;
|
||
}
|
||
if (overrides) {
|
||
options = {
|
||
...options,
|
||
...overrides
|
||
};
|
||
}
|
||
return options;
|
||
}
|
||
function getFormattedValue(formatOrOptions, overrides, typeFormats, formatter, getFallback) {
|
||
let options;
|
||
try {
|
||
options = resolveFormatOrOptions(typeFormats, formatOrOptions, overrides);
|
||
} catch {
|
||
return getFallback();
|
||
}
|
||
try {
|
||
return formatter(options);
|
||
} catch (error) {
|
||
onError(new IntlError(IntlErrorCode.FORMATTING_ERROR, error.message));
|
||
return getFallback();
|
||
}
|
||
}
|
||
function dateTime(value, formatOrOptions, overrides) {
|
||
return getFormattedValue(formatOrOptions, overrides, formats?.dateTime, options => {
|
||
options = applyTimeZone(options);
|
||
return formatters.getDateTimeFormat(locale, options).format(value);
|
||
}, () => String(value));
|
||
}
|
||
function dateTimeRange(start, end, formatOrOptions, overrides) {
|
||
return getFormattedValue(formatOrOptions, overrides, formats?.dateTime, options => {
|
||
options = applyTimeZone(options);
|
||
return formatters.getDateTimeFormat(locale, options).formatRange(start, end);
|
||
}, () => [dateTime(start), dateTime(end)].join(' – '));
|
||
}
|
||
function number(value, formatOrOptions, overrides) {
|
||
return getFormattedValue(formatOrOptions, overrides, formats?.number, options => formatters.getNumberFormat(locale, options).format(value), () => String(value));
|
||
}
|
||
function getGlobalNow() {
|
||
// Only read when necessary to avoid triggering a `dynamicIO` error
|
||
// unnecessarily (`now` is only needed for `format.relativeTime`)
|
||
if (props.now) {
|
||
return props.now;
|
||
} else {
|
||
onError(new IntlError(IntlErrorCode.ENVIRONMENT_FALLBACK, `The \`now\` parameter wasn't provided to \`relativeTime\` and there is no global default configured, therefore the current time will be used as a fallback. See https://next-intl.dev/docs/usage/dates-times#relative-times-usenow` ));
|
||
return new Date();
|
||
}
|
||
}
|
||
function relativeTime(date, nowOrOptions) {
|
||
try {
|
||
let nowDate, unit;
|
||
const opts = {};
|
||
if (nowOrOptions instanceof Date || typeof nowOrOptions === 'number') {
|
||
nowDate = new Date(nowOrOptions);
|
||
} else if (nowOrOptions) {
|
||
if (nowOrOptions.now != null) {
|
||
nowDate = new Date(nowOrOptions.now);
|
||
} else {
|
||
nowDate = getGlobalNow();
|
||
}
|
||
unit = nowOrOptions.unit;
|
||
opts.style = nowOrOptions.style;
|
||
// @ts-expect-error -- Types are slightly outdated
|
||
opts.numberingSystem = nowOrOptions.numberingSystem;
|
||
}
|
||
if (!nowDate) {
|
||
nowDate = getGlobalNow();
|
||
}
|
||
const dateDate = new Date(date);
|
||
const seconds = (dateDate.getTime() - nowDate.getTime()) / 1000;
|
||
if (!unit) {
|
||
unit = resolveRelativeTimeUnit(seconds);
|
||
}
|
||
|
||
// `numeric: 'auto'` can theoretically produce output like "yesterday",
|
||
// but it only works with integers. E.g. -1 day will produce "yesterday",
|
||
// but -1.1 days will produce "-1.1 days". Rounding before formatting is
|
||
// not desired, as the given dates might cross a threshold were the
|
||
// output isn't correct anymore. Example: 2024-01-08T23:00:00.000Z and
|
||
// 2024-01-08T01:00:00.000Z would produce "yesterday", which is not the
|
||
// case. By using `always` we can ensure correct output. The only exception
|
||
// is the formatting of times <1 second as "now".
|
||
opts.numeric = unit === 'second' ? 'auto' : 'always';
|
||
const value = calculateRelativeTimeValue(seconds, unit);
|
||
return formatters.getRelativeTimeFormat(locale, opts).format(value, unit);
|
||
} catch (error) {
|
||
onError(new IntlError(IntlErrorCode.FORMATTING_ERROR, error.message));
|
||
return String(date);
|
||
}
|
||
}
|
||
function list(value, formatOrOptions, overrides) {
|
||
const serializedValue = [];
|
||
const richValues = new Map();
|
||
|
||
// `formatToParts` only accepts strings, therefore we have to temporarily
|
||
// replace React elements with a placeholder ID that can be used to retrieve
|
||
// the original value afterwards.
|
||
let index = 0;
|
||
for (const item of value) {
|
||
let serializedItem;
|
||
if (typeof item === 'object') {
|
||
serializedItem = String(index);
|
||
richValues.set(serializedItem, item);
|
||
} else {
|
||
serializedItem = String(item);
|
||
}
|
||
serializedValue.push(serializedItem);
|
||
index++;
|
||
}
|
||
return getFormattedValue(formatOrOptions, overrides, formats?.list,
|
||
// @ts-expect-error -- `richValues.size` is used to determine the return type, but TypeScript can't infer the meaning of this correctly
|
||
options => {
|
||
const result = formatters.getListFormat(locale, options).formatToParts(serializedValue).map(part => part.type === 'literal' ? part.value : richValues.get(part.value) || part.value);
|
||
if (richValues.size > 0) {
|
||
return result;
|
||
} else {
|
||
return result.join('');
|
||
}
|
||
}, () => String(value));
|
||
}
|
||
return {
|
||
dateTime,
|
||
number,
|
||
relativeTime,
|
||
list,
|
||
dateTimeRange
|
||
};
|
||
}
|
||
|
||
function validateMessagesSegment(messages, invalidKeyLabels, parentPath) {
|
||
Object.entries(messages).forEach(([key, messageOrMessages]) => {
|
||
if (key.includes('.')) {
|
||
let keyLabel = key;
|
||
if (parentPath) keyLabel += ` (at ${parentPath})`;
|
||
invalidKeyLabels.push(keyLabel);
|
||
}
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||
if (messageOrMessages != null && typeof messageOrMessages === 'object') {
|
||
validateMessagesSegment(messageOrMessages, invalidKeyLabels, joinPath(parentPath, key));
|
||
}
|
||
});
|
||
}
|
||
function validateMessages(messages, onError) {
|
||
const invalidKeyLabels = [];
|
||
validateMessagesSegment(messages, invalidKeyLabels);
|
||
if (invalidKeyLabels.length > 0) {
|
||
onError(new IntlError(IntlErrorCode.INVALID_KEY, `Namespace keys can not contain the character "." as this is used to express nesting. Please remove it or replace it with another character.
|
||
|
||
Invalid ${invalidKeyLabels.length === 1 ? 'key' : 'keys'}: ${invalidKeyLabels.join(', ')}
|
||
|
||
If you're migrating from a flat structure, you can convert your messages as follows:
|
||
|
||
import {set} from "lodash";
|
||
|
||
const input = {
|
||
"one.one": "1.1",
|
||
"one.two": "1.2",
|
||
"two.one.one": "2.1.1"
|
||
};
|
||
|
||
const output = Object.entries(input).reduce(
|
||
(acc, [key, value]) => set(acc, key, value),
|
||
{}
|
||
);
|
||
|
||
// Output:
|
||
//
|
||
// {
|
||
// "one": {
|
||
// "one": "1.1",
|
||
// "two": "1.2"
|
||
// },
|
||
// "two": {
|
||
// "one": {
|
||
// "one": "2.1.1"
|
||
// }
|
||
// }
|
||
// }
|
||
` ));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Enhances the incoming props with defaults.
|
||
*/
|
||
function initializeConfig({
|
||
formats,
|
||
getMessageFallback,
|
||
messages,
|
||
onError,
|
||
...rest
|
||
}) {
|
||
const finalOnError = onError || defaultOnError;
|
||
const finalGetMessageFallback = getMessageFallback || defaultGetMessageFallback;
|
||
{
|
||
if (messages) {
|
||
validateMessages(messages, finalOnError);
|
||
}
|
||
}
|
||
return {
|
||
...rest,
|
||
formats: formats || undefined,
|
||
messages: messages || undefined,
|
||
onError: finalOnError,
|
||
getMessageFallback: finalGetMessageFallback
|
||
};
|
||
}
|
||
|
||
export { IntlError as I, IntlErrorCode as a, createIntlFormatters as b, createFormatter as c, createCache as d, createBaseTranslator as e, defaultGetMessageFallback as f, defaultOnError as g, initializeConfig as i, resolveNamespace as r };
|