@hi18n/core
Version:
Message internationalization meets immutability and type-safety - core runtime
472 lines (412 loc) • 12.4 kB
JavaScript
;
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