545 lines
21 KiB
JavaScript
545 lines
21 KiB
JavaScript
'use strict';
|
||
|
||
var IntlMessageFormat = require('intl-messageformat');
|
||
var React = require('react');
|
||
var initializeConfig = require('./initializeConfig-BhfMSHP7.js');
|
||
|
||
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
||
|
||
var IntlMessageFormat__default = /*#__PURE__*/_interopDefault(IntlMessageFormat);
|
||
|
||
function setTimeZoneInFormats(formats, timeZone) {
|
||
if (!formats) return formats;
|
||
|
||
// 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
|
||
return Object.keys(formats).reduce((acc, key) => {
|
||
acc[key] = {
|
||
timeZone,
|
||
...formats[key]
|
||
};
|
||
return acc;
|
||
}, {});
|
||
}
|
||
|
||
/**
|
||
* `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(formats, timeZone) {
|
||
const formatsWithTimeZone = timeZone ? {
|
||
...formats,
|
||
dateTime: setTimeZoneInFormats(formats.dateTime, timeZone)
|
||
} : formats;
|
||
const mfDateDefaults = IntlMessageFormat__default.default.formats.date;
|
||
const defaultDateFormats = timeZone ? setTimeZoneInFormats(mfDateDefaults, timeZone) : mfDateDefaults;
|
||
const mfTimeDefaults = IntlMessageFormat__default.default.formats.time;
|
||
const defaultTimeFormats = timeZone ? setTimeZoneInFormats(mfTimeDefaults, timeZone) : mfTimeDefaults;
|
||
return {
|
||
...formatsWithTimeZone,
|
||
date: {
|
||
...defaultDateFormats,
|
||
...formatsWithTimeZone.dateTime
|
||
},
|
||
time: {
|
||
...defaultTimeFormats,
|
||
...formatsWithTimeZone.dateTime
|
||
}
|
||
};
|
||
}
|
||
|
||
// 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 = initializeConfig.memoFn(function () {
|
||
return new IntlMessageFormat__default.default(arguments.length <= 0 ? undefined : arguments[0], arguments.length <= 1 ? undefined : arguments[1], arguments.length <= 2 ? undefined : arguments[2], {
|
||
formatters: intlFormatters,
|
||
...(arguments.length <= 3 ? undefined : arguments[3])
|
||
});
|
||
}, cache.message);
|
||
return getMessageFormat;
|
||
}
|
||
function resolvePath(locale, messages, key, namespace) {
|
||
const fullKey = initializeConfig.joinPath(namespace, key);
|
||
if (!messages) {
|
||
throw new Error("No messages available at `".concat(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 `".concat(fullKey, "` in messages for locale `").concat(locale, "`.") );
|
||
}
|
||
message = next;
|
||
});
|
||
return message;
|
||
}
|
||
function prepareTranslationValues(values) {
|
||
if (Object.keys(values).length === 0) return undefined;
|
||
|
||
// 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__*/React.isValidElement(result) ? /*#__PURE__*/React.cloneElement(result, {
|
||
key: key + index++
|
||
}) : result;
|
||
};
|
||
} else {
|
||
transformed = value;
|
||
}
|
||
transformedValues[key] = transformed;
|
||
});
|
||
return transformedValues;
|
||
}
|
||
function getMessagesOrError(locale, messages, namespace) {
|
||
let onError = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : initializeConfig.defaultOnError;
|
||
try {
|
||
if (!messages) {
|
||
throw new Error("No messages were configured on the provider." );
|
||
}
|
||
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 `".concat(namespace, "` found.") );
|
||
}
|
||
return retrievedMessages;
|
||
} catch (error) {
|
||
const intlError = new initializeConfig.IntlError(initializeConfig.IntlErrorCode.MISSING_MESSAGE, error.message);
|
||
onError(intlError);
|
||
return intlError;
|
||
}
|
||
}
|
||
function getPlainMessage(candidate, values) {
|
||
if (values) return undefined;
|
||
const unescapedMessage = candidate.replace(/'([{}])/gi, '$1');
|
||
|
||
// Placeholders can be in the message if there are default values,
|
||
// or if the user has forgotten to provide values. In the latter
|
||
// case we need to compile the message to receive an error.
|
||
const hasPlaceholders = /<|{/.test(unescapedMessage);
|
||
if (!hasPlaceholders) {
|
||
return unescapedMessage;
|
||
}
|
||
return undefined;
|
||
}
|
||
function createBaseTranslator(config) {
|
||
const messagesOrError = getMessagesOrError(config.locale, config.messages, config.namespace, config.onError);
|
||
return createBaseTranslatorImpl({
|
||
...config,
|
||
messagesOrError
|
||
});
|
||
}
|
||
function createBaseTranslatorImpl(_ref) {
|
||
let {
|
||
cache,
|
||
defaultTranslationValues,
|
||
formats: globalFormats,
|
||
formatters,
|
||
getMessageFallback = initializeConfig.defaultGetMessageFallback,
|
||
locale,
|
||
messagesOrError,
|
||
namespace,
|
||
onError,
|
||
timeZone
|
||
} = _ref;
|
||
const hasMessagesError = messagesOrError instanceof initializeConfig.IntlError;
|
||
function getFallbackFromErrorAndNotify(key, code, message) {
|
||
const error = new initializeConfig.IntlError(code, message);
|
||
onError(error);
|
||
return 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) {
|
||
if (hasMessagesError) {
|
||
// We have already warned about this during render
|
||
return getMessageFallback({
|
||
error: messagesOrError,
|
||
key,
|
||
namespace
|
||
});
|
||
}
|
||
const messages = messagesOrError;
|
||
let message;
|
||
try {
|
||
message = resolvePath(locale, messages, key, namespace);
|
||
} catch (error) {
|
||
return getFallbackFromErrorAndNotify(key, initializeConfig.IntlErrorCode.MISSING_MESSAGE, error.message);
|
||
}
|
||
if (typeof message === 'object') {
|
||
let code, errorMessage;
|
||
if (Array.isArray(message)) {
|
||
code = initializeConfig.IntlErrorCode.INVALID_MESSAGE;
|
||
{
|
||
errorMessage = "Message at `".concat(initializeConfig.joinPath(namespace, key), "` resolved to an array, but only strings are supported. See https://next-intl.dev/docs/usage/messages#arrays-of-messages");
|
||
}
|
||
} else {
|
||
code = initializeConfig.IntlErrorCode.INSUFFICIENT_PATH;
|
||
{
|
||
errorMessage = "Message at `".concat(initializeConfig.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/messages#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, initializeConfig.IntlErrorCode.INVALID_MESSAGE, thrownError.message + ('originalMessage' in thrownError ? " (".concat(thrownError.originalMessage, ")") : '') );
|
||
}
|
||
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
|
||
}));
|
||
if (formattedMessage == null) {
|
||
throw new Error("Unable to format `".concat(key, "` in ").concat(namespace ? "namespace `".concat(namespace, "`") : 'messages') );
|
||
}
|
||
|
||
// Limit the function signature to return strings or React elements
|
||
return /*#__PURE__*/React.isValidElement(formattedMessage) ||
|
||
// Arrays of React elements
|
||
Array.isArray(formattedMessage) || typeof formattedMessage === 'string' ? formattedMessage : String(formattedMessage);
|
||
} catch (error) {
|
||
return getFallbackFromErrorAndNotify(key, initializeConfig.IntlErrorCode.FORMATTING_ERROR, error.message);
|
||
}
|
||
}
|
||
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, /** Provide custom formats for numbers, dates and times. */
|
||
formats) {
|
||
const result = translateBaseFn(key, values, formats);
|
||
if (typeof result !== 'string') {
|
||
return getFallbackFromErrorAndNotify(key, initializeConfig.IntlErrorCode.INVALID_MESSAGE, "The message `".concat(key, "` in ").concat(namespace ? "namespace `".concat(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) => {
|
||
const result = translateBaseFn(key,
|
||
// @ts-expect-error -- `MarkupTranslationValues` is practically a sub type
|
||
// of `RichTranslationValues` but TypeScript isn't smart enough here.
|
||
values, formats);
|
||
|
||
// When only string chunks are provided to the parser, only
|
||
// strings should be returned here. Note that we need a runtime
|
||
// check for this since rich text values could be accidentally
|
||
// inherited from `defaultTranslationValues`.
|
||
if (typeof result !== 'string') {
|
||
const error = new initializeConfig.IntlError(initializeConfig.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) {
|
||
// We have already warned about this during render
|
||
return getMessageFallback({
|
||
error: messagesOrError,
|
||
key,
|
||
namespace
|
||
});
|
||
}
|
||
const messages = messagesOrError;
|
||
try {
|
||
return resolvePath(locale, messages, key, namespace);
|
||
} catch (error) {
|
||
return getFallbackFromErrorAndNotify(key, initializeConfig.IntlErrorCode.MISSING_MESSAGE, error.message);
|
||
}
|
||
};
|
||
translateFn.has = key => {
|
||
if (hasMessagesError) {
|
||
return false;
|
||
}
|
||
try {
|
||
resolvePath(locale, messagesOrError, key, namespace);
|
||
return true;
|
||
} catch (_unused) {
|
||
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(_ref) {
|
||
let {
|
||
_cache: cache = initializeConfig.createCache(),
|
||
_formatters: formatters = initializeConfig.createIntlFormatters(cache),
|
||
formats,
|
||
locale,
|
||
now: globalNow,
|
||
onError = initializeConfig.defaultOnError,
|
||
timeZone: globalTimeZone
|
||
} = _ref;
|
||
function applyTimeZone(options) {
|
||
var _options;
|
||
if (!((_options = options) !== null && _options !== void 0 && _options.timeZone)) {
|
||
if (globalTimeZone) {
|
||
options = {
|
||
...options,
|
||
timeZone: globalTimeZone
|
||
};
|
||
} else {
|
||
onError(new initializeConfig.IntlError(initializeConfig.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) {
|
||
let options;
|
||
if (typeof formatOrOptions === 'string') {
|
||
const formatName = formatOrOptions;
|
||
options = typeFormats === null || typeFormats === void 0 ? void 0 : typeFormats[formatName];
|
||
if (!options) {
|
||
const error = new initializeConfig.IntlError(initializeConfig.IntlErrorCode.MISSING_FORMAT, "Format `".concat(formatName, "` is not available. You can configure it on the provider or provide custom options.") );
|
||
onError(error);
|
||
throw error;
|
||
}
|
||
} else {
|
||
options = formatOrOptions;
|
||
}
|
||
return options;
|
||
}
|
||
function getFormattedValue(formatOrOptions, typeFormats, formatter, getFallback) {
|
||
let options;
|
||
try {
|
||
options = resolveFormatOrOptions(typeFormats, formatOrOptions);
|
||
} catch (_unused) {
|
||
return getFallback();
|
||
}
|
||
try {
|
||
return formatter(options);
|
||
} catch (error) {
|
||
onError(new initializeConfig.IntlError(initializeConfig.IntlErrorCode.FORMATTING_ERROR, error.message));
|
||
return getFallback();
|
||
}
|
||
}
|
||
function dateTime(/** If a number is supplied, this is interpreted as a UTC timestamp. */
|
||
value,
|
||
/** If a time zone is supplied, the `value` is converted to that time zone.
|
||
* Otherwise the user time zone will be used. */
|
||
formatOrOptions) {
|
||
return getFormattedValue(formatOrOptions, formats === null || formats === void 0 ? void 0 : formats.dateTime, options => {
|
||
options = applyTimeZone(options);
|
||
return formatters.getDateTimeFormat(locale, options).format(value);
|
||
}, () => String(value));
|
||
}
|
||
function dateTimeRange(/** If a number is supplied, this is interpreted as a UTC timestamp. */
|
||
start, /** If a number is supplied, this is interpreted as a UTC timestamp. */
|
||
end,
|
||
/** If a time zone is supplied, the values are converted to that time zone.
|
||
* Otherwise the user time zone will be used. */
|
||
formatOrOptions) {
|
||
return getFormattedValue(formatOrOptions, formats === null || formats === void 0 ? void 0 : formats.dateTime, options => {
|
||
options = applyTimeZone(options);
|
||
return formatters.getDateTimeFormat(locale, options).formatRange(start, end);
|
||
}, () => [dateTime(start), dateTime(end)].join(' – '));
|
||
}
|
||
function number(value, formatOrOptions) {
|
||
return getFormattedValue(formatOrOptions, formats === null || formats === void 0 ? void 0 : formats.number, options => formatters.getNumberFormat(locale, options).format(value), () => String(value));
|
||
}
|
||
function getGlobalNow() {
|
||
if (globalNow) {
|
||
return globalNow;
|
||
} else {
|
||
onError(new initializeConfig.IntlError(initializeConfig.IntlErrorCode.ENVIRONMENT_FALLBACK, "The `now` 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#now" ));
|
||
return new Date();
|
||
}
|
||
}
|
||
function relativeTime(/** The date time that needs to be formatted. */
|
||
date, /** The reference point in time to which `date` will be formatted in relation to. */
|
||
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 initializeConfig.IntlError(initializeConfig.IntlErrorCode.FORMATTING_ERROR, error.message));
|
||
return String(date);
|
||
}
|
||
}
|
||
function list(value, formatOrOptions) {
|
||
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, formats === null || formats === void 0 ? void 0 : 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
|
||
};
|
||
}
|
||
|
||
exports.createBaseTranslator = createBaseTranslator;
|
||
exports.createFormatter = createFormatter;
|
||
exports.resolveNamespace = resolveNamespace;
|