This commit is contained in:
Christian Kaisermann 2020-01-23 23:52:38 -03:00
parent 56f683d3e0
commit 1e1db5e981
5 changed files with 409 additions and 0 deletions

3
compiler/bundle.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
deno bundle index.ts compiler.js

125
compiler/compiler.js Normal file
View File

@ -0,0 +1,125 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
// A script preamble that provides the ability to load a single outfile
// TypeScript "bundle" where a main module is loaded which recursively
// instantiates all the other modules in the bundle. This code is used to load
// bundles when creating snapshots, but is also used when emitting bundles from
// Deno cli.
// @ts-nocheck
/**
* @type {(name: string, deps: ReadonlyArray<string>, factory: (...deps: any[]) => void) => void=}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let define;
/**
* @type {(mod: string) => any=}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let instantiate;
/**
* @callback Factory
* @argument {...any[]} args
* @returns {object | void}
*/
/**
* @typedef ModuleMetaData
* @property {ReadonlyArray<string>} dependencies
* @property {(Factory | object)=} factory
* @property {object} exports
*/
(function() {
/**
* @type {Map<string, ModuleMetaData>}
*/
const modules = new Map();
/**
* Bundles in theory can support "dynamic" imports, but for internal bundles
* we can't go outside to fetch any modules that haven't been statically
* defined.
* @param {string[]} deps
* @param {(...deps: any[]) => void} resolve
* @param {(err: any) => void} reject
*/
const require = (deps, resolve, reject) => {
try {
if (deps.length !== 1) {
throw new TypeError("Expected only a single module specifier.");
}
if (!modules.has(deps[0])) {
throw new RangeError(`Module "${deps[0]}" not defined.`);
}
resolve(getExports(deps[0]));
} catch (e) {
if (reject) {
reject(e);
} else {
throw e;
}
}
};
define = (id, dependencies, factory) => {
if (modules.has(id)) {
throw new RangeError(`Module "${id}" has already been defined.`);
}
modules.set(id, {
dependencies,
factory,
exports: {}
});
};
/**
* @param {string} id
* @returns {any}
*/
function getExports(id) {
const module = modules.get(id);
if (!module) {
// because `$deno$/ts_global.d.ts` looks like a real script, it doesn't
// get erased from output as an import, but it doesn't get defined, so
// we don't have a cache for it, so because this is an internal bundle
// we can just safely return an empty object literal.
return {};
}
if (!module.factory) {
return module.exports;
} else if (module.factory) {
const { factory, exports } = module;
delete module.factory;
if (typeof factory === "function") {
const dependencies = module.dependencies.map(id => {
if (id === "require") {
return require;
} else if (id === "exports") {
return exports;
}
return getExports(id);
});
factory(...dependencies);
} else {
Object.assign(exports, factory);
}
return exports;
}
}
instantiate = dep => {
define = undefined;
const result = getExports(dep);
// clean up, or otherwise these end up in the runtime environment
instantiate = undefined;
return result;
};
})();
console.log('foo');
instantiate("file:///Users/kaisermann/Projects/open-source/svelte-i18n/compiler/index");

2
compiler/deps/parser.ts Normal file
View File

@ -0,0 +1,2 @@
// @deno-types="https://unpkg.com/intl-messageformat-parser@3.6.3/lib/index.d.ts"
export * from 'https://unpkg.com/intl-messageformat-parser@3.6.3/lib/index.js'

270
compiler/formatters.ts Normal file
View File

@ -0,0 +1,270 @@
import {
convertNumberSkeletonToNumberFormatOptions,
isArgumentElement,
isDateElement,
isDateTimeSkeleton,
isLiteralElement,
isNumberElement,
isNumberSkeleton,
isPluralElement,
isPoundElement,
isSelectElement,
isTimeElement,
MessageFormatElement,
parseDateTimeSkeleton,
} from './deps/parser.ts'
export interface Formats {
number: Record<string, Intl.NumberFormatOptions>
date: Record<string, Intl.DateTimeFormatOptions>
time: Record<string, Intl.DateTimeFormatOptions>
}
export interface FormatterCache {
number: Record<string, Intl.NumberFormat>
dateTime: Record<string, Intl.DateTimeFormat>
pluralRules: Record<string, Intl.PluralRules>
}
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 const enum PART_TYPE {
literal,
argument,
}
export interface LiteralPart {
type: PART_TYPE.literal
value: string
}
export interface ArgumentPart {
type: PART_TYPE.argument
value: any
}
export type MessageFormatPart = LiteralPart | ArgumentPart
export type PrimitiveType = string | number | boolean | null | undefined | Date
class FormatError extends Error {
public readonly variableId?: string
constructor(msg?: string, variableId?: string) {
super(msg)
this.variableId = variableId
}
}
function mergeLiteral(parts: MessageFormatPart[]): MessageFormatPart[] {
if (parts.length < 2) {
return parts
}
return parts.reduce((all, part) => {
const lastPart = all[all.length - 1]
if (
!lastPart ||
lastPart.type !== PART_TYPE.literal ||
part.type !== PART_TYPE.literal
) {
all.push(part)
} else {
lastPart.value += part.value
}
return all
}, [] as MessageFormatPart[])
}
// TODO(skeleton): add skeleton support
export function formatToParts(
els: MessageFormatElement[],
locales: string | string[],
formatters: Formatters,
formats: Formats,
values?: Record<string, any>,
currentPluralValue?: number,
// For debugging
originalMessage?: string
): MessageFormatPart[] {
// Hot path for straight simple msg translations
if (els.length === 1 && isLiteralElement(els[0])) {
return [
{
type: PART_TYPE.literal,
value: els[0].value,
},
]
}
const result: MessageFormatPart[] = []
for (const el of els) {
// Exit early for string parts.
if (isLiteralElement(el)) {
result.push({
type: PART_TYPE.literal,
value: el.value,
})
continue
}
// TODO: should this part be literal type?
// Replace `#` in plural rules with the actual numeric value.
if (isPoundElement(el)) {
if (typeof currentPluralValue === 'number') {
result.push({
type: PART_TYPE.literal,
value: formatters.getNumberFormat(locales).format(currentPluralValue),
})
}
continue
}
const { value: varName } = el
// Enforce that all required values are provided by the caller.
if (!(values && varName in values)) {
throw new FormatError(
`The intl string context variable "${varName}" was not provided to the string "${originalMessage}"`
)
}
let value = values[varName]
if (isArgumentElement(el)) {
if (!value || typeof value === 'string' || typeof value === 'number') {
value =
typeof value === 'string' || typeof value === 'number'
? String(value)
: ''
}
result.push({
type: PART_TYPE.argument,
value,
})
continue
}
// Recursively format plural and select parts' option — which can be a
// nested pattern structure. The choosing of the option to use is
// abstracted-by and delegated-to the part helper object.
if (isDateElement(el)) {
const style =
typeof el.style === 'string' ? formats.date[el.style] : undefined
result.push({
type: PART_TYPE.literal,
value: formatters
.getDateTimeFormat(locales, style)
.format(value as number),
})
continue
}
if (isTimeElement(el)) {
const style =
typeof el.style === 'string'
? formats.time[el.style]
: isDateTimeSkeleton(el.style)
? parseDateTimeSkeleton(el.style.pattern)
: undefined
result.push({
type: PART_TYPE.literal,
value: formatters
.getDateTimeFormat(locales, style)
.format(value as number),
})
continue
}
if (isNumberElement(el)) {
const style =
typeof el.style === 'string'
? formats.number[el.style]
: isNumberSkeleton(el.style)
? convertNumberSkeletonToNumberFormatOptions(el.style.tokens)
: undefined
result.push({
type: PART_TYPE.literal,
value: formatters
.getNumberFormat(locales, style)
.format(value as number),
})
continue
}
if (isSelectElement(el)) {
const opt = el.options[value as string] || el.options.other
if (!opt) {
throw new RangeError(
`Invalid values for "${
el.value
}": "${value}". Options are "${Object.keys(el.options).join('", "')}"`
)
}
result.push(
...formatToParts(opt.value, locales, formatters, formats, values)
)
continue
}
if (isPluralElement(el)) {
let opt = el.options[`=${value}`]
if (!opt) {
if (!Intl.PluralRules) {
throw new FormatError(`Intl.PluralRules is not available in this environment.
Try polyfilling it using "@formatjs/intl-pluralrules"
`)
}
const rule = formatters
.getPluralRules(locales, { type: el.pluralType })
.select((value as number) - (el.offset || 0))
opt = el.options[rule] || el.options.other
}
if (!opt) {
throw new RangeError(
`Invalid values for "${
el.value
}": "${value}". Options are "${Object.keys(el.options).join('", "')}"`
)
}
result.push(
...formatToParts(
opt.value,
locales,
formatters,
formats,
values,
value - (el.offset || 0)
)
)
continue
}
}
return mergeLiteral(result)
}
export function formatToString(
els: MessageFormatElement[],
locales: string | string[],
formatters: Formatters,
formats: Formats,
values?: Record<string, PrimitiveType>,
// For debugging
originalMessage?: string
): string {
const parts = formatToParts(
els,
locales,
formatters,
formats,
values,
undefined,
originalMessage
)
// Hot path for straight simple msg translations
if (parts.length === 1) {
return parts[0].value
}
return parts.reduce((all, part) => (all += part.value), '')
}

9
compiler/index.ts Normal file
View File

@ -0,0 +1,9 @@
import { parse } from './deps/parser.ts'
const log_json = (o: object) => console.log(JSON.stringify(o, null, 2))
const ast = parse(`{taxableArea, select,
yes {An additional {taxRate, number, percent} tax will be collected.}
other {No taxes apply.}
}`)
log_json(ast)