@hyperse/translator
Version:
Translates messages from the given namespace by using the ICU syntax.
622 lines (608 loc) • 18.5 kB
JavaScript
import { isValidElement, cloneElement } from 'react';
import { IntlMessageFormat } from 'intl-messageformat';
import { memoize, strategies } from '@formatjs/fast-memoize';
// src/createBaseTranslator.ts
// src/utils/joinPath.ts
function joinPath(...parts) {
return parts.filter(Boolean).join(".");
}
// src/defaults.ts
function defaultGetMessageFallback(props) {
return joinPath(props.namespace, props.key);
}
function defaultOnError(error) {
console.error(error);
}
// src/IntlError.ts
var IntlError = class extends Error {
constructor(code, originalMessage) {
let message = code;
if (originalMessage) {
message += ": " + originalMessage;
}
super(message);
this.code = code;
if (originalMessage) {
this.originalMessage = originalMessage;
}
}
};
function setTimeZoneInFormats(formats, timeZone) {
if (!formats) return formats;
return Object.keys(formats).reduce(
(acc, key) => {
acc[key] = {
timeZone,
...formats[key]
};
return acc;
},
{}
);
}
function convertFormatsToIntlMessageFormat(formats, timeZone) {
const formatsWithTimeZone = timeZone ? { ...formats, dateTime: setTimeZoneInFormats(formats.dateTime, timeZone) } : formats;
const mfDateDefaults = IntlMessageFormat.formats.date;
const defaultDateFormats = timeZone ? setTimeZoneInFormats(mfDateDefaults, timeZone) : mfDateDefaults;
const mfTimeDefaults = IntlMessageFormat.formats.time;
const defaultTimeFormats = timeZone ? setTimeZoneInFormats(mfTimeDefaults, timeZone) : mfTimeDefaults;
return {
...formatsWithTimeZone,
date: {
...defaultDateFormats,
...formatsWithTimeZone == null ? void 0 : formatsWithTimeZone.dateTime
},
time: {
...defaultTimeFormats,
...formatsWithTimeZone == null ? void 0 : formatsWithTimeZone.dateTime
}
};
}
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
};
}
// src/utils/createMessageFormatter.ts
function createMessageFormatter(cache, intlFormatters) {
const getMessageFormat = memoFn(
(...args) => new IntlMessageFormat(args[0], args[1], args[2], {
formatters: intlFormatters,
...args[3]
}),
cache.message
);
return getMessageFormat;
}
// src/utils/resolvePath.ts
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];
if (part == null || next == null) {
throw new Error(
`Could not resolve \`${fullKey}\` in messages for locale \`${locale}\`.`
);
}
message = next;
});
return message;
}
// src/utils/getMessagesOrError.ts
function getMessagesOrError(locale, messages, namespace, onError = defaultOnError) {
try {
if (!messages) {
throw new Error(
process.env.NODE_ENV !== "production" ? `No messages were configured on the provider.` : void 0
);
}
const retrievedMessages = namespace ? resolvePath(locale, messages, namespace) : messages;
if (!retrievedMessages) {
throw new Error(
process.env.NODE_ENV !== "production" ? `No messages for namespace \`${namespace}\` found.` : namespace
);
}
return retrievedMessages;
} catch (error) {
const intlError = new IntlError(
"MISSING_MESSAGE" /* MISSING_MESSAGE */,
error.message
);
onError(intlError);
return intlError;
}
}
// src/utils/getPlainMessage.ts
function getPlainMessage(candidate, values) {
if (values) return void 0;
const unescapedMessage = candidate.replace(/'([{}])/gi, "$1");
const hasPlaceholders = /<|{/.test(unescapedMessage);
if (!hasPlaceholders) {
return unescapedMessage;
}
return void 0;
}
function prepareTranslationValues(values) {
if (Object.keys(values).length === 0) return void 0;
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 isValidElement(result) ? cloneElement(result, { key: key + index++ }) : result;
};
} else {
transformed = value;
}
transformedValues[key] = transformed;
});
return transformedValues;
}
// src/createBaseTranslator.ts
function createBaseTranslator(config) {
const messagesOrError = getMessagesOrError(
config.locale,
config.messages,
config.namespace,
config.onError
);
return createBaseTranslatorImpl({
...config,
messagesOrError
});
}
function createBaseTranslatorImpl({
cache,
defaultTranslationValues,
formats: globalFormats,
formatters,
getMessageFallback = defaultGetMessageFallback,
locale,
messagesOrError,
namespace,
onError,
timeZone
}) {
function getFallbackFromErrorAndNotify(key, code, message) {
const error = new IntlError(code, message);
onError(error);
return getMessageFallback({ error, key, namespace });
}
function translateBaseFn(key, values, formats) {
if (messagesOrError instanceof IntlError) {
return getMessageFallback({
error: messagesOrError,
key,
namespace
});
}
const messages = messagesOrError;
let message;
try {
message = resolvePath(locale, messages, key, namespace);
} catch (error) {
return getFallbackFromErrorAndNotify(
key,
"MISSING_MESSAGE" /* MISSING_MESSAGE */,
error.message
);
}
if (typeof message === "object") {
let code, errorMessage;
if (Array.isArray(message)) {
code = "INVALID_MESSAGE" /* INVALID_MESSAGE */;
if (process.env.NODE_ENV !== "production") {
errorMessage = `Message at \`${joinPath(
namespace,
key
)}\` resolved to an array, but only strings are supported.`;
}
} else {
code = "INSUFFICIENT_PATH" /* INSUFFICIENT_PATH */;
if (process.env.NODE_ENV !== "production") {
errorMessage = `Message at \`${joinPath(
namespace,
key
)}\` resolved to an object, but only strings are supported. Use a \`.\` to retrieve nested messages.`;
}
}
return getFallbackFromErrorAndNotify(key, code, errorMessage);
}
let messageFormat;
const plainMessage = getPlainMessage(message, values);
if (plainMessage) return plainMessage;
if (!formatters.getMessageFormat) {
formatters.getMessageFormat = createMessageFormatter(cache, formatters);
}
try {
messageFormat = formatters.getMessageFormat(
message,
locale,
convertFormatsToIntlMessageFormat(
{ ...globalFormats, ...formats },
timeZone
),
{
formatters: {
...formatters,
getDateTimeFormat(locales, options) {
return formatters.getDateTimeFormat(locales, {
timeZone,
...options
});
}
}
}
);
} catch (error) {
const thrownError = error;
return getFallbackFromErrorAndNotify(
key,
"INVALID_MESSAGE" /* INVALID_MESSAGE */,
process.env.NODE_ENV !== "production" ? thrownError.message + ("originalMessage" in thrownError ? ` (${thrownError.originalMessage})` : "") : thrownError.message
);
}
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(
process.env.NODE_ENV !== "production" ? `Unable to format \`${key}\` in ${namespace ? `namespace \`${namespace}\`` : "messages"}` : void 0
);
}
return isValidElement(formattedMessage) || // Arrays of React elements
Array.isArray(formattedMessage) || typeof formattedMessage === "string" ? formattedMessage : String(formattedMessage);
} catch (error) {
return getFallbackFromErrorAndNotify(
key,
"FORMATTING_ERROR" /* FORMATTING_ERROR */,
error.message
);
}
}
function translateFn(key, values, formats) {
const result = translateBaseFn(key, values, formats);
if (typeof result !== "string") {
return getFallbackFromErrorAndNotify(
key,
"INVALID_MESSAGE" /* INVALID_MESSAGE */,
`The message \`${key}\` in ${namespace ? `namespace \`${namespace}\`` : "messages"} didn't resolve to a string.`
);
}
return result;
}
translateFn.rich = translateBaseFn;
return translateFn;
}
// src/utils/resolveNamespace.ts
function resolveNamespace(namespace, namespacePrefix) {
return namespace === namespacePrefix ? void 0 : namespace.slice((namespacePrefix + ".").length);
}
// src/createTranslatorImpl.ts
function createTranslatorImpl({ messages, namespace, ...rest }, namespacePrefix) {
messages = messages[namespacePrefix];
namespace = resolveNamespace(namespace, namespacePrefix);
const translator = createBaseTranslator({
...rest,
namespace,
messages
});
function base(...args) {
return translator(...args);
}
base.rich = function(...args) {
return translator.rich(...args);
};
return base;
}
// src/createTranslator.ts
function createTranslator({
_cache = createCache(),
_formatters = createIntlFormatters(_cache),
messages,
namespace,
onError = defaultOnError,
getMessageFallback = defaultGetMessageFallback,
...rest
}) {
return createTranslatorImpl(
{
...rest,
cache: _cache,
formatters: _formatters,
onError,
getMessageFallback,
messages: { "!": messages },
namespace: namespace ? `!.${namespace}` : "!"
},
"!"
);
}
// src/formatter/createFormatter.ts
var SECOND = 1;
var MINUTE = SECOND * 60;
var HOUR = MINUTE * 60;
var DAY = HOUR * 24;
var WEEK = DAY * 7;
var MONTH = DAY * (365 / 12);
var QUARTER = MONTH * 3;
var YEAR = DAY * 365;
var 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) {
return Math.round(seconds / UNIT_SECONDS[unit]);
}
function createFormatter({
_cache: cache = createCache(),
_formatters: formatters = createIntlFormatters(cache),
formats,
locale,
now: globalNow,
onError = defaultOnError,
timeZone: globalTimeZone
}) {
function applyTimeZone(options) {
if (!(options == null ? void 0 : options.timeZone)) {
if (globalTimeZone) {
options = { ...options, timeZone: globalTimeZone };
} else {
onError(
new IntlError(
"ENVIRONMENT_FALLBACK" /* ENVIRONMENT_FALLBACK */,
process.env.NODE_ENV !== "production" ? `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-docs.vercel.app/docs/configuration#time-zone` : void 0
)
);
}
}
return options;
}
function resolveFormatOrOptions(typeFormats, formatOrOptions) {
let options;
if (typeof formatOrOptions === "string") {
const formatName = formatOrOptions;
options = typeFormats == null ? void 0 : typeFormats[formatName];
if (!options) {
const error = new IntlError(
"MISSING_FORMAT" /* MISSING_FORMAT */,
process.env.NODE_ENV !== "production" ? `Format \`${formatName}\` is not available. You can configure it on the provider or provide custom options.` : void 0
);
onError(error);
throw error;
}
} else {
options = formatOrOptions;
}
return options;
}
function getFormattedValue(formatOrOptions, typeFormats, formatter, getFallback) {
let options;
try {
options = resolveFormatOrOptions(typeFormats, formatOrOptions);
} catch {
return getFallback();
}
try {
return formatter(options);
} catch (error) {
onError(
new IntlError("FORMATTING_ERROR" /* FORMATTING_ERROR */, error.message)
);
return getFallback();
}
}
function dateTime(value, formatOrOptions) {
return getFormattedValue(
formatOrOptions,
formats == null ? void 0 : formats.dateTime,
(options) => {
options = applyTimeZone(options);
return formatters.getDateTimeFormat(locale, options).format(value);
},
() => String(value)
);
}
function dateTimeRange(start, end, formatOrOptions) {
return getFormattedValue(
formatOrOptions,
formats == null ? void 0 : formats.dateTime,
(options) => {
options = applyTimeZone(options);
return formatters.getDateTimeFormat(locale, options).formatRange(start, end);
},
() => [dateTime(start), dateTime(end)].join("\u2009\u2013\u2009")
);
}
function number(value, formatOrOptions) {
return getFormattedValue(
formatOrOptions,
formats == null ? void 0 : formats.number,
(options) => formatters.getNumberFormat(locale, options).format(value),
() => String(value)
);
}
function getGlobalNow() {
if (globalNow) {
return globalNow;
} else {
onError(
new IntlError(
"ENVIRONMENT_FALLBACK" /* ENVIRONMENT_FALLBACK */,
process.env.NODE_ENV !== "production" ? `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-docs.vercel.app/docs/configuration#now` : void 0
)
);
return /* @__PURE__ */ 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;
opts.numberingSystem = nowOrOptions.numberingSystem;
}
if (!nowDate) {
nowDate = getGlobalNow();
}
const dateDate = new Date(date);
const seconds = (dateDate.getTime() - nowDate.getTime()) / 1e3;
if (!unit) {
unit = resolveRelativeTimeUnit(seconds);
}
opts.numeric = unit === "second" ? "auto" : "always";
const value = calculateRelativeTimeValue(seconds, unit);
return formatters.getRelativeTimeFormat(locale, opts).format(value, unit);
} catch (error) {
onError(
new IntlError("FORMATTING_ERROR" /* FORMATTING_ERROR */, error.message)
);
return String(date);
}
}
function list(value, formatOrOptions) {
const serializedValue = [];
const richValues = /* @__PURE__ */ new Map();
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 ? 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 };
}
export { createFormatter, createTranslator };
//# sourceMappingURL=index.mjs.map
//# sourceMappingURL=index.mjs.map