UNPKG

chrome-devtools-frontend

Version:
287 lines (247 loc) • 7.67 kB
/* Copyright (c) 2014, Yahoo! Inc. All rights reserved. Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. */ import { ArgumentElement, MessageFormatPattern, MessageTextElement, PluralFormat as ParserPluralFormat, SelectFormat as ParserSelectFormat } from 'intl-messageformat-parser'; export interface Formats { number: Record<string, Intl.NumberFormatOptions>; date: Record<string, Intl.DateTimeFormatOptions>; time: Record<string, Intl.DateTimeFormatOptions>; } export interface Formatters { getNumberFormat( ...args: ConstructorParameters<typeof Intl.NumberFormat> ): Intl.NumberFormat; getDateTimeFormat( ...args: ConstructorParameters<typeof Intl.DateTimeFormat> ): Intl.DateTimeFormat; getPluralRules( ...args: ConstructorParameters<typeof Intl.PluralRules> ): Intl.PluralRules; } export type Pattern = | string | PluralOffsetString | PluralFormat | SelectFormat | StringFormat; export default class Compiler { private locales: string | string[] = []; private formats: Formats = { number: {}, date: {}, time: {} }; private pluralNumberFormat: Intl.NumberFormat | null = null; private currentPlural: ArgumentElement | null | undefined = null; private pluralStack: Array<ArgumentElement | null | undefined> = []; private formatters: Formatters; constructor( locales: string | string[], formats: Formats, formatters: Formatters ) { this.locales = locales; this.formats = formats; this.formatters = formatters; } compile(ast: MessageFormatPattern): Pattern[] { this.pluralStack = []; this.currentPlural = null; this.pluralNumberFormat = null; return this.compileMessage(ast); } compileMessage(ast: MessageFormatPattern) { if (!(ast && ast.type === 'messageFormatPattern')) { throw new Error('Message AST is not of type: "messageFormatPattern"'); } const { elements } = ast; const pattern = elements .filter<MessageTextElement | ArgumentElement>( (el): el is MessageTextElement | ArgumentElement => el.type === 'messageTextElement' || el.type === 'argumentElement' ) .map(el => el.type === 'messageTextElement' ? this.compileMessageText(el) : this.compileArgument(el) ); if (pattern.length !== elements.length) { throw new Error('Message element does not have a valid type'); } return pattern; } compileMessageText(element: MessageTextElement) { // When this `element` is part of plural sub-pattern and its value contains // an unescaped '#', use a `PluralOffsetString` helper to properly output // the number with the correct offset in the string. if (this.currentPlural && /(^|[^\\])#/g.test(element.value)) { // Create a cache a NumberFormat instance that can be reused for any // PluralOffsetString instance in this message. if (!this.pluralNumberFormat) { this.pluralNumberFormat = new Intl.NumberFormat(this.locales); } return new PluralOffsetString( this.currentPlural.id, (this.currentPlural.format as ParserPluralFormat).offset, this.pluralNumberFormat, element.value ); } // Unescape the escaped '#'s in the message text. return element.value.replace(/\\#/g, '#'); } compileArgument(element: ArgumentElement) { const { format, id } = element; const { formatters } = this; if (!format) { return new StringFormat(id); } const { formats, locales } = this; switch (format.type) { case 'numberFormat': return { id, format: formatters.getNumberFormat( locales, formats.number[format.style] ).format }; case 'dateFormat': return { id, format: formatters.getDateTimeFormat( locales, formats.date[format.style] ).format }; case 'timeFormat': return { id, format: formatters.getDateTimeFormat( locales, formats.time[format.style] ).format }; case 'pluralFormat': return new PluralFormat( id, format.offset, this.compileOptions(element), formatters.getPluralRules(locales, { type: format.ordinal ? 'ordinal' : 'cardinal' }) ); case 'selectFormat': return new SelectFormat(id, this.compileOptions(element)); default: throw new Error('Message element does not have a valid format type'); } } compileOptions(element: ArgumentElement) { const format = element.format as ParserPluralFormat | ParserSelectFormat; const { options } = format; // Save the current plural element, if any, then set it to a new value when // compiling the options sub-patterns. This conforms the spec's algorithm // for handling `"#"` syntax in message text. this.pluralStack.push(this.currentPlural); this.currentPlural = format.type === 'pluralFormat' ? element : null; const optionsHash = options.reduce( (all: Record<string, Array<Pattern>>, option) => { // Compile the sub-pattern and save it under the options's selector. all[option.selector] = this.compileMessage(option.value); return all; }, {} ); // Pop the plural stack to put back the original current plural value. this.currentPlural = this.pluralStack.pop(); return optionsHash; } } // -- Compiler Helper Classes -------------------------------------------------- abstract class Formatter { public id: string; constructor(id: string) { this.id = id; } abstract format(value: string | number): string; } class StringFormat extends Formatter { format(value: number | string) { if (!value && typeof value !== 'number') { return ''; } return typeof value === 'string' ? value : String(value); } } class PluralFormat { public id: string; private offset: number; private options: Record<string, Pattern[]>; private pluralRules: Intl.PluralRules; constructor( id: string, offset: number, options: Record<string, Pattern[]>, pluralRules: Intl.PluralRules ) { this.id = id; this.offset = offset; this.options = options; this.pluralRules = pluralRules; } getOption(value: number) { const { options } = this; const option = options['=' + value] || options[this.pluralRules.select(value - this.offset)]; return option || options.other; } } export class PluralOffsetString extends Formatter { private offset: number; private numberFormat: Intl.NumberFormat; private string: string; constructor( id: string, offset: number, numberFormat: Intl.NumberFormat, string: string ) { super(id); this.offset = offset; this.numberFormat = numberFormat; this.string = string; } format(value: number) { const number = this.numberFormat.format(value - this.offset); return this.string .replace(/(^|[^\\])#/g, '$1' + number) .replace(/\\#/g, '#'); } } export class SelectFormat { public id: string; private options: Record<string, Pattern[]>; constructor(id: string, options: Record<string, Pattern[]>) { this.id = id; this.options = options; } getOption(value: string) { const { options } = this; return options[value] || options.other; } } export function isSelectOrPluralFormat( f: any ): f is SelectFormat | PluralFormat { return !!f.options; }