UNPKG

@hi18n/core

Version:

Message internationalization meets immutability and type-safety - core runtime

472 lines (412 loc) 12.4 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default; Object.defineProperty(exports, "__esModule", { value: true }); var _exportNames = { msg: true, translationId: true, Book: true, Catalog: true, getTranslator: true, preloadCatalogs: true, getDefaultTimeZone: true }; exports.Catalog = exports.Book = void 0; exports.getDefaultTimeZone = getDefaultTimeZone; exports.getTranslator = getTranslator; exports.msg = msg; exports.preloadCatalogs = preloadCatalogs; exports.translationId = translationId; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _msgfmtEval = require("./msgfmt-eval.js"); var _msgfmtParser = require("./msgfmt-parser.js"); var _errors = require("./errors.js"); Object.keys(_errors).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; if (key in exports && exports[key] === _errors[key]) return; Object.defineProperty(exports, key, { enumerable: true, get: function () { return _errors[key]; } }); }); var _errorHandling = require("./error-handling.js"); Object.keys(_errorHandling).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; if (key in exports && exports[key] === _errorHandling[key]) return; Object.defineProperty(exports, key, { enumerable: true, get: function () { return _errorHandling[key]; } }); }); /* eslint-disable @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any */ /** * Infers the appropriate type for the translated message. * * At runtime, it just returns the first argument. * * @param s the translated message * @returns the first argument * * @since 0.1.0 (`@hi18n/core`) * * @example * ```ts * export default new Book<Vocabulary>({ * "example/greeting": msg("Hello, {name}!"), * }); * ``` */ function msg(s) { return s; } /** * Same as {@link msg} but can be used to indicate an untranslated state. * * At runtime, it just returns the first argument. * * @param s the translated message * @returns the first argument * * @since 0.1.3 (`@hi18n/core`) * * @example * ```ts * export default new Book<Vocabulary>({ * "example/greeting": msg.todo("Hello, {name}!"), * }); * ``` */ msg.todo = function todo(s) { return s; }; /** * Marks a translation id as dynamically used with {@link CompoundTranslatorFunction.dynamic t.dynamic}. * * At runtime, it just returns the second argument. * * @param book the book the id is linked to. Just discarded at runtime. * @param id the translation id. * @returns the second argument * * @since 0.1.1 (`@hi18n/core`) * * @example * ```ts * const menus = [ * { * url: "https://example.com/home", * titleId: translationId(book, "example/navigation/home"), * }, * { * url: "https://example.com/map", * titleId: translationId(book, "example/navigation/map"), * }, * ]; * * const { t } = getTranslator(book, "en"); * t.dynamic(menus[i].titleId); * ``` */ function translationId(book, id) { const _book = book; return id; } /** * A set of translated messages, containing translations for all supported locales. * * In other words, a book is a set of {@link Catalog}s for all languages. * * @since 0.1.0 (`@hi18n/core`) * * @example * ```ts * type Vocabulary = { * "example/greeting": Message<{ name: string }>; * }; * export const book = new Book<Vocabulary>({ * en: catalogEn, * ja: catalogJa, * }); * ``` * * @example You can use `import()` to lazy-load catalogs. * Note that you need to use extra setup to avoid the * "Catalog not loaded" error. * * ```ts * type Vocabulary = { * "example/greeting": Message<{ name: string }>; * }; * export const book = new Book<Vocabulary>({ * en: () => import("./en"), * ja: () => import("./ja"), * }); * ``` */ class Book { constructor(catalogs) { let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; (0, _defineProperty2.default)(this, "catalogs", void 0); (0, _defineProperty2.default)(this, "_loaders", void 0); (0, _defineProperty2.default)(this, "_handleError", void 0); (0, _defineProperty2.default)(this, "_implicitLocale", void 0); this.catalogs = {}; this._loaders = catalogs; this._handleError = options.handleError; this._implicitLocale = options.implicitLocale; for (const [locale, catalog] of Object.entries(catalogs)) { // Skip lazy-loaded catalogs if (typeof catalog === "function") continue; this.catalogs[locale] = catalog; // @ts-expect-error deliberately breaking privacy if (catalog._looseLocale) { catalog.locale = locale; } else if (catalog.locale !== locale) { throw new Error("Locale mismatch: expected ".concat(locale, ", got ").concat(catalog.locale)); } } if (this._implicitLocale != null && !hasOwn(catalogs, this._implicitLocale)) { throw new Error("Invalid implicitLocale: ".concat(this._implicitLocale)); } } /** * Load a catalog for specific locale. * * Consider using {@link preloadCatalogs} instead. * * @param locale locale to load * * @since 0.1.9 (`@hi18n/core`) */ async loadCatalog(locale) { const loader = this._loaders[locale]; if (typeof loader !== "function") return; const { default: catalog } = await loader(); // @ts-expect-error deliberately breaking privacy if (catalog._looseLocale) { catalog.locale = locale; } else if (catalog.locale !== locale) { throw new Error("Locale mismatch: expected ".concat(locale, ", got ").concat(catalog.locale)); } if (this.catalogs[locale] != null) return; this.catalogs[locale] = catalog; } handleError(e, level) { var _this$_handleError; ((_this$_handleError = this._handleError) !== null && _this$_handleError !== void 0 ? _this$_handleError : _errorHandling.defaultErrorHandler)(e, level); } } /** * @since 0.1.7 (`@hi18n/core`) */ exports.Book = Book; /** * A set of translated messages for a specific locale. * * @since 0.1.0 (`@hi18n/core`) * * @example * ```ts * type Vocabulary = { * "example/greeting": Message<{ name: string }>; * }; * export default new Catalog<Vocabulary>("en", { * "example/greeting": msg("Hello, {name}!"), * }); * ``` */ class Catalog { // TODO: make it non-nullish and readonly in 0.2.0 // TODO: remove it in 0.2.0 /** * @since 0.1.6 (`@hi18n/core`) */ /** * @deprecated deprecated from 0.1.6. Please specify the locale. * @since 0.1.0 (`@hi18n/core`) */ constructor(locale, data) { (0, _defineProperty2.default)(this, "locale", void 0); (0, _defineProperty2.default)(this, "data", void 0); (0, _defineProperty2.default)(this, "_looseLocale", false); (0, _defineProperty2.default)(this, "_compiled", {}); if (typeof locale === "object") { // For backwards-compatibility // TODO: remove it in 0.2.0 this.data = locale; this._looseLocale = true; } else { this.locale = locale; this.data = data; } } getCompiledMessage(id) { if (!hasOwn(this._compiled, id)) { if (!hasOwn(this.data, id)) { throw new _errors.MissingTranslationError(); } const msg = this.data[id]; this._compiled[id] = (0, _msgfmtParser.parseMessage)(msg); } return this._compiled[id]; } } /** * An object returned from {@link getTranslator}. * * @since 0.1.0 (`@hi18n/core`) */ exports.Catalog = Catalog; /** * Retrieves the translation helpers from the book and the locales. * * @param book the "book" (i.e. the set of translations) containing the desired messages. * @param locales a locale or a list of locale in the order of preference (the latter being not supported yet) * @param options.throwPromise if true, it throws a Promise instance instead of an error. Used for React Suspense integration. * @returns A set of translation helpers * * @since 0.1.0 (`@hi18n/core`) * * @example * ```ts * const { t } = getTranslator(book, "en"); * t("example/greeting-simple"); // => "Hello!" * ``` */ function getTranslator(book, locales) { let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; const locale = selectLocale(book, locales); const catalog = book.catalogs[locale]; if (catalog == null) { if (options.throwPromise) { throw book.loadCatalog(locale); } else { throw new Error("Catalog not loaded: ".concat(locale)); } } const t = function (id) { let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; try { return compileAndEvaluateMessage(catalog, locale, id, { timeZone: options["timeZone"], params: options, handleError: book._handleError }); } catch (e) { if (!(e instanceof Error)) throw e; book.handleError(e, "error"); return "[".concat(id, "]"); } }; t.dynamic = t; t.todo = id => { return "[TODO: ".concat(id, "]"); }; return { t: t, translateWithComponents: (id, interpolator, options) => { try { return compileAndEvaluateMessage(catalog, locale, id, { timeZone: options["timeZone"], params: options, handleError: book._handleError, collect: interpolator.collect, wrap: interpolator.wrap }); } catch (e) { if (!(e instanceof Error)) throw e; book.handleError(e, "error"); return "[".concat(id, "]"); } } }; } /** * options for {@link getTranslator} * * @since 0.1.9 (`@hi18n/core`) */ /** * Starts loading and waits for catalogs so that {@link getTranslator} does not error * with "Catalog not loaded" error. * * It is a wrapper for {@link Book.loadCatalog}. * * @param book same as {@link getTranslator}'s `book` parameter. * @param locales same as {@link getTranslator}'s `locales` parameter. * * @since 0.1.9 (`@hi18n/core`) */ async function preloadCatalogs(book, locales) { const locale = selectLocale(book, locales); const catalog = book.catalogs[locale]; if (catalog == null) { await book.loadCatalog(locale); } } // To ensure deterministic behavior, // this function picks the locale whether the catalog has already been loaded or not. function selectLocale(book, locales) { const localesArray = Array.isArray(locales) ? locales : [locales]; const filteredLocales = []; for (const locale of localesArray) { if (hasOwn(book._loaders, locale)) { filteredLocales.push(locale); } } if (filteredLocales.length === 0) { const error = localesArray.length === 0 ? new _errors.NoLocaleError() : new _errors.MissingLocaleError({ locale: localesArray[0], availableLocales: Object.keys(book._loaders) }); if (book._implicitLocale != null) { book.handleError(error, "error"); filteredLocales.push(book._implicitLocale); } else { throw error; } } return filteredLocales[0]; } function compileAndEvaluateMessage(catalog, locale, id, options) { try { return (0, _msgfmtEval.evaluateMessage)(catalog.getCompiledMessage(id), { id, locale, ...options }); } catch (e) { if (!(e instanceof Error)) throw e; throw new _errors.MessageError({ cause: e, id, locale }); } } /** * A convenience helper to get the default time zone. * If you need more sophisticated guess for old browsers, * consider using other libraries like `moment.tz.guess`. * * @returns the default time zone, if anything is found. Otherwise the string "UTC" * * @since 0.1.3 (`@hi18n/core`) */ function getDefaultTimeZone() { if (typeof Intl !== "undefined" && Intl.DateTimeFormat) { const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; if (typeof timeZone === "string") return timeZone; } return "UTC"; } function hasOwn(o, s) { return Object.prototype.hasOwnProperty.call(o, s); } //# sourceMappingURL=index.js.map