UNPKG

ember-intl

Version:

A internationalization toolbox for ambitious applications.

117 lines (100 loc) 3.88 kB
/** * Copyright 2015, Yahoo! Inc. * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. */ import Ember from 'ember'; import memoize from 'fast-memoize'; import { htmlSafe, isHTMLSafe } from '@ember/template'; import type { SafeString } from '@ember/template/-private/handlebars'; import IntlMessageFormat from 'intl-messageformat'; import type { Formats } from '../../types'; import parse from '../utils/parse'; import type { FormatterConfig } from './-base'; import type { TranslationAST } from '../store/translation'; const { Handlebars: { // @ts-expect-error Upstream types are incomplete. Utils: { escapeExpression }, }, } = Ember; function escapeOptions<T extends Record<string, unknown>>(object?: T) { if (typeof object !== 'object') { return; } const escapedOpts = {} as { [K in keyof T]: T[K] extends SafeString ? string : T[K] }; (Object.keys(object) as (keyof T)[]).forEach((key) => { const val = object[key]; if (isHTMLSafe(val)) { // If the option is an instance of Ember SafeString, // we don't want to pass it into the formatter, since the // formatter won't know what to do with it. Instead, we cast // the SafeString to a regular string using `toHTML`. // Since it was already marked as safe we should *not* escape it. // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore escapedOpts[key] = val.toHTML(); } else if (typeof val === 'string') { escapedOpts[key] = escapeExpression(val); } else { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore escapedOpts[key] = val; // copy as-is } }); return escapedOpts; } /** * @private * @hide */ export default class FormatMessage { static readonly type = 'message'; protected readonly config: FormatterConfig; protected readonly readFormatConfig: () => Formats; constructor(config: FormatterConfig) { this.config = config; // NOTE: a fn since we lazily grab the formatter from the config // as it can change at runtime by calling intl.set('formats', {...}); this.readFormatConfig = config.readFormatConfig; } createNativeFormatter = memoize( (ast: TranslationAST, locales: string | string[], formatConfig?: Partial<Formats>) => { return new IntlMessageFormat(ast, locales, formatConfig, { ignoreTag: true, }); } ); // ! Function overloads are not passed through generic types for reasons that // evade my knowledge. ¯\_(ツ)_/¯ // For this reason these types need to be manually copied over to the // `IntlService#formatMessage`. format( locale: string | string[], maybeAst: string | TranslationAST, options?: Partial<Record<string, unknown>> & { htmlSafe?: false } ): string; format( locale: string | string[], maybeAst: string | TranslationAST, options: Partial<Record<string, unknown>> & { htmlSafe: true } ): SafeString; format( locale: string | string[], maybeAst: string | TranslationAST, options?: Partial<Record<string, unknown>> & { htmlSafe?: boolean } ): string | SafeString { let ast = maybeAst as TranslationAST; if (typeof maybeAst === 'string') { // maybe memoize? it's not a typical hot path since we // parse when translations are pushed to ember-intl. // This is only used if inlining a translation i.e., // {{format-message "Hi {name}"}} ast = parse(maybeAst); } const isHTMLSafe = options && options.htmlSafe; const formatterInstance = this.createNativeFormatter(ast, locale, this.readFormatConfig()); const escapedOptions = isHTMLSafe ? escapeOptions(options) : options; const result = formatterInstance.format(escapedOptions) as string; return isHTMLSafe ? htmlSafe(result) : result; } }