chrome-devtools-frontend
Version:
Chrome DevTools UI
287 lines (247 loc) • 7.67 kB
text/typescript
/*
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;
}