mirror of
https://github.com/cupcakearmy/svelte-i18n.git
synced 2024-11-16 18:10:43 +01:00
wip
This commit is contained in:
parent
56f683d3e0
commit
1e1db5e981
3
compiler/bundle.sh
Executable file
3
compiler/bundle.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
deno bundle index.ts compiler.js
|
125
compiler/compiler.js
Normal file
125
compiler/compiler.js
Normal 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
2
compiler/deps/parser.ts
Normal 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
270
compiler/formatters.ts
Normal 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
9
compiler/index.ts
Normal 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)
|
Loading…
Reference in New Issue
Block a user