UNPKG

@hyperse/translator

Version:

Translates messages from the given namespace by using the ICU syntax.

633 lines (619 loc) 18.9 kB
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); } function defaultPlainMessageCheck(message) { return !/<|{/.test(message); } // 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, plainMessageCheck) { if (values) return void 0; const unescapedMessage = candidate.replace(/'([{}])/gi, "$1"); const checkFunction = plainMessageCheck || ((message) => !/<|{/.test(message)); const isPlain = checkFunction(unescapedMessage); if (isPlain) { 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, plainMessageCheck, 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, plainMessageCheck ); 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, plainMessageCheck = defaultPlainMessageCheck, ...rest }) { return createTranslatorImpl( { ...rest, cache: _cache, formatters: _formatters, onError, getMessageFallback, plainMessageCheck, 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, defaultGetMessageFallback, defaultOnError, defaultPlainMessageCheck }; //# sourceMappingURL=index.mjs.map //# sourceMappingURL=index.mjs.map