From ce69897d87e27b40144dec9720979a681e4b9dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sta=C5=9B=20Ma=C5=82olepszy?= Date: Wed, 18 Aug 2021 00:21:45 +0200 Subject: [PATCH 1/5] (third) Add formatToParts --- .../stasm/third/example/example_glossary.ts | 11 ++- .../stasm/third/example/example_list.ts | 8 +- .../stasm/third/example/example_number.ts | 9 ++- .../stasm/third/example/example_phrases.ts | 12 ++- experiments/stasm/third/impl/registry.ts | 13 ++- experiments/stasm/third/impl/runtime.ts | 79 ++++++++++++++----- 6 files changed, 107 insertions(+), 25 deletions(-) diff --git a/experiments/stasm/third/example/example_glossary.ts b/experiments/stasm/third/example/example_glossary.ts index 59cd36efc6..8fb20b3cc7 100644 --- a/experiments/stasm/third/example/example_glossary.ts +++ b/experiments/stasm/third/example/example_glossary.ts @@ -1,6 +1,6 @@ import {Argument, Message, Parameter} from "../impl/model.js"; import {REGISTRY} from "../impl/registry.js"; -import {formatMessage, FormattingContext, StringValue} from "../impl/runtime.js"; +import {formatMessage, FormattingContext, formatToParts, StringValue} from "../impl/runtime.js"; import {get_term} from "./glossary.js"; REGISTRY["NOUN"] = function get_noun( @@ -140,6 +140,15 @@ console.log("==== English ===="); color: new StringValue("red"), }) ); + + console.log( + Array.of( + ...formatToParts(message, { + item: new StringValue("t-shirt"), + color: new StringValue("red"), + }) + ) + ); } { diff --git a/experiments/stasm/third/example/example_list.ts b/experiments/stasm/third/example/example_list.ts index 99dbd2d5dd..01a368528f 100644 --- a/experiments/stasm/third/example/example_list.ts +++ b/experiments/stasm/third/example/example_list.ts @@ -2,6 +2,7 @@ import {Argument, Message, Parameter} from "../impl/model.js"; import {REGISTRY} from "../impl/registry.js"; import { formatMessage, + FormattedPart, FormattingContext, PluralValue, RuntimeValue, @@ -20,7 +21,12 @@ class Person { class PeopleValue extends RuntimeValue> { format(ctx: FormattingContext): string { - throw new RangeError("Must be formatted via PEOPLE_LIST."); + // TODO(stasm): Implement this. + throw new Error("Not implemented yet."); + } + *formatToParts(ctx: FormattingContext): IterableIterator { + // TODO(stasm): Implement this. + throw new Error("Not implemented yet."); } } diff --git a/experiments/stasm/third/example/example_number.ts b/experiments/stasm/third/example/example_number.ts index b242e74e36..7810bc7bd8 100644 --- a/experiments/stasm/third/example/example_number.ts +++ b/experiments/stasm/third/example/example_number.ts @@ -1,5 +1,5 @@ import {Message} from "../impl/model.js"; -import {formatMessage, NumberValue} from "../impl/runtime.js"; +import {formatMessage, formatToParts, NumberValue} from "../impl/runtime.js"; console.log("==== English ===="); @@ -78,4 +78,11 @@ console.log("==== French ===="); payloadSize: new NumberValue(1.23), }) ); + console.log( + Array.of( + ...formatToParts(message, { + payloadSize: new NumberValue(1.23), + }) + ) + ); } diff --git a/experiments/stasm/third/example/example_phrases.ts b/experiments/stasm/third/example/example_phrases.ts index 82c5c52146..662629ca7c 100644 --- a/experiments/stasm/third/example/example_phrases.ts +++ b/experiments/stasm/third/example/example_phrases.ts @@ -1,5 +1,5 @@ import {Message} from "../impl/model.js"; -import {formatMessage, NumberValue, StringValue} from "../impl/runtime.js"; +import {formatMessage, formatToParts, NumberValue, StringValue} from "../impl/runtime.js"; console.log("==== English ===="); @@ -90,6 +90,16 @@ console.log("==== English ===="); photoCount: new NumberValue(34), }) ); + + console.log( + Array.of( + ...formatToParts(message, { + userName: new StringValue("Mary"), + userGender: new StringValue("feminine"), + photoCount: new NumberValue(34), + }) + ) + ); } console.log("==== polski ===="); diff --git a/experiments/stasm/third/impl/registry.ts b/experiments/stasm/third/impl/registry.ts index 7ccd60b21f..962cd16335 100644 --- a/experiments/stasm/third/impl/registry.ts +++ b/experiments/stasm/third/impl/registry.ts @@ -1,5 +1,12 @@ import {Argument, Parameter} from "./model.js"; -import {FormattingContext, NumberValue, PluralValue, RuntimeValue, StringValue} from "./runtime.js"; +import { + FormattingContext, + NumberValue, + PatternValue, + PluralValue, + RuntimeValue, + StringValue, +} from "./runtime.js"; export type RegistryFunc = ( ctx: FormattingContext, @@ -31,7 +38,7 @@ function get_phrase( ctx: FormattingContext, args: Array, opts: Record -): StringValue { +): PatternValue { let phrase_name = ctx.toRuntimeValue(args[0]); if (!(phrase_name instanceof StringValue)) { throw new TypeError(); @@ -39,7 +46,7 @@ function get_phrase( let phrase = ctx.message.phrases[phrase_name.value]; let variant = ctx.selectVariant(phrase.variants, phrase.selectors); - return new StringValue(ctx.formatPattern(variant.value)); + return new PatternValue(variant.value); } function format_number( diff --git a/experiments/stasm/third/impl/runtime.ts b/experiments/stasm/third/impl/runtime.ts index 5c8d5ad871..c2b35bc1fb 100644 --- a/experiments/stasm/third/impl/runtime.ts +++ b/experiments/stasm/third/impl/runtime.ts @@ -1,14 +1,16 @@ -import { - Message, - Parameter, - PatternElement, - Phrase, - Selector, - StringLiteral, - Variant, -} from "./model.js"; +import {Message, Parameter, PatternElement, Selector, StringLiteral, Variant} from "./model.js"; import {REGISTRY} from "./registry.js"; +export interface FormattedPart { + type: string; + value: string; +} + +export interface OpaquePart { + type: "opaque"; + value: unknown; +} + // A value passed in as a variable to format() or to which literals are resolved // at runtime. There are 4 built-in runtime value types in this implementation: // StringValue, NumberValue, PluralValue, and BooleanValue. Other @@ -23,12 +25,17 @@ export abstract class RuntimeValue { } abstract format(ctx: FormattingContext): string; + abstract formatToParts(ctx: FormattingContext): IterableIterator; } export class StringValue extends RuntimeValue { format(ctx: FormattingContext): string { return this.value; } + + *formatToParts(ctx: FormattingContext): IterableIterator { + yield {type: "literal", value: this.value}; + } } export class NumberValue extends RuntimeValue { @@ -43,6 +50,10 @@ export class NumberValue extends RuntimeValue { // TODO(stasm): Cache NumberFormat. return new Intl.NumberFormat(ctx.locale, this.opts).format(this.value); } + + *formatToParts(ctx: FormattingContext): IterableIterator { + yield* new Intl.NumberFormat(ctx.locale, this.opts).formatToParts(this.value); + } } export class PluralValue extends RuntimeValue { @@ -58,12 +69,32 @@ export class PluralValue extends RuntimeValue { let pr = new Intl.PluralRules(ctx.locale, this.opts); return pr.select(this.value); } + + *formatToParts(ctx: FormattingContext): IterableIterator { + throw new TypeError("Pluralvalue is not formattable to parts."); + } } export class BooleanValue extends RuntimeValue { format(ctx: FormattingContext): string { throw new TypeError("BooleanValue is not formattable."); } + + *formatToParts(ctx: FormattingContext): IterableIterator { + throw new TypeError("BooleanValue is not formattable to parts."); + } +} + +export class PatternValue extends RuntimeValue> { + format(ctx: FormattingContext): string { + return ctx.formatPattern(this.value); + } + + *formatToParts(ctx: FormattingContext): IterableIterator { + for (let value of ctx.resolvePattern(this.value)) { + yield* value.formatToParts(ctx); + } + } } // Resolution context for a single formatMessage() call. @@ -81,12 +112,15 @@ export class FormattingContext { this.visited = new WeakSet(); } - formatPhrase(phrase: Phrase): string { - let variant = this.selectVariant(phrase.variants, phrase.selectors); - return this.formatPattern(variant.value); + formatPattern(pattern: Array): string { + let output = ""; + for (let value of this.resolvePattern(pattern)) { + output += value.format(this); + } + return output; } - formatPattern(pattern: Array): string { + *resolvePattern(pattern: Array): IterableIterator> { if (this.visited.has(pattern)) { throw new RangeError("Recursive reference to a variant value."); } @@ -96,17 +130,15 @@ export class FormattingContext { for (let element of pattern) { switch (element.type) { case "StringLiteral": - result += element.value; + yield new StringValue(element.value); continue; case "VariableReference": { - let value = this.vars[element.name]; - result += value.format(this); + yield this.vars[element.name]; continue; } case "FunctionCall": { let callable = REGISTRY[element.name]; - let value = callable(this, element.args, element.opts); - result += value.format(this); + yield callable(this, element.args, element.opts); continue; } } @@ -209,3 +241,14 @@ export function formatMessage( let variant = ctx.selectVariant(message.variants, message.selectors); return ctx.formatPattern(variant.value); } + +export function* formatToParts( + message: Message, + vars: Record> +): IterableIterator { + let ctx = new FormattingContext(message.lang, message, vars); + let variant = ctx.selectVariant(message.variants, message.selectors); + for (let value of ctx.resolvePattern(variant.value)) { + yield* value.formatToParts(ctx); + } +} From 96ce5c29cf28ef673058f19f8f173c18f41fa3a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sta=C5=9B=20Ma=C5=82olepszy?= Date: Wed, 18 Aug 2021 01:14:49 +0200 Subject: [PATCH 2/5] (third) ListValue.formatToParts --- .../stasm/third/example/example_list.ts | 63 ++++++++++++++----- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/experiments/stasm/third/example/example_list.ts b/experiments/stasm/third/example/example_list.ts index 01a368528f..286bd355a4 100644 --- a/experiments/stasm/third/example/example_list.ts +++ b/experiments/stasm/third/example/example_list.ts @@ -4,6 +4,7 @@ import { formatMessage, FormattedPart, FormattingContext, + formatToParts, PluralValue, RuntimeValue, StringValue, @@ -19,14 +20,26 @@ class Person { } } -class PeopleValue extends RuntimeValue> { +// TODO(stasm): Move this to impl/runtime.ts? +class ListValue extends RuntimeValue> { + private opts: Record; // ListFormatOptions + + constructor(value: Array, opts: Record = {}) { + super(value); + this.opts = opts; + } + format(ctx: FormattingContext): string { - // TODO(stasm): Implement this. - throw new Error("Not implemented yet."); + // TODO(stasm): Cache ListFormat. + // @ts-ignore + let lf = new Intl.ListFormat(ctx.locale, this.opts); + return lf.format(this.value); } + *formatToParts(ctx: FormattingContext): IterableIterator { - // TODO(stasm): Implement this. - throw new Error("Not implemented yet."); + // @ts-ignore + let lf = new Intl.ListFormat(ctx.locale, this.opts); + yield* lf.formatToParts(this.value); } } @@ -36,7 +49,7 @@ REGISTRY["PLURAL_LEN"] = function ( opts: Record ): PluralValue { let elements = ctx.toRuntimeValue(args[0]); - if (!(elements instanceof PeopleValue)) { + if (!(elements instanceof ListValue)) { throw new TypeError(); } @@ -47,13 +60,13 @@ REGISTRY["PEOPLE_LIST"] = function ( ctx: FormattingContext, args: Array, opts: Record -): StringValue { +): ListValue { if (ctx.locale !== "ro") { throw new Error("Only Romanian supported"); } let elements = ctx.toRuntimeValue(args[0]); - if (!(elements instanceof PeopleValue)) { + if (!(elements instanceof ListValue)) { throw new TypeError(); } @@ -76,14 +89,21 @@ REGISTRY["PEOPLE_LIST"] = function ( break; } - // @ts-ignore - let lf = new Intl.ListFormat(ctx.locale, { - // TODO(stasm): Type-check these. + let list_style = ctx.toRuntimeValue(opts["STYLE"]); + if (!(list_style instanceof StringValue)) { + throw new TypeError(); + } + + let list_type = ctx.toRuntimeValue(opts["TYPE"]); + if (!(list_type instanceof StringValue)) { + throw new TypeError(); + } + + return new ListValue(names, { // TODO(stasm): Add default options. - style: ctx.toRuntimeValue(opts["STYLE"]).value, - type: ctx.toRuntimeValue(opts["TYPE"]).value, + style: list_style.value, + type: list_type.value, }); - return new StringValue(lf.format(names)); function decline(name: string): string { let declension = ctx.toRuntimeValue(opts["CASE"]); @@ -172,13 +192,24 @@ console.log("==== Romanian ===="); }; console.log( formatMessage(message, { - names: new PeopleValue([ + names: new ListValue([ new Person("Maria", "Stanescu"), new Person("Ileana", "Zamfir"), new Person("Petre", "Belu"), ]), }) ); + console.log( + Array.of( + ...formatToParts(message, { + names: new ListValue([ + new Person("Maria", "Stanescu"), + new Person("Ileana", "Zamfir"), + new Person("Petre", "Belu"), + ]), + }) + ) + ); } { @@ -241,7 +272,7 @@ console.log("==== Romanian ===="); }; console.log( formatMessage(message, { - names: new PeopleValue([ + names: new ListValue([ new Person("Maria", "Stanescu"), new Person("Ileana", "Zamfir"), new Person("Petre", "Belu"), From 1e584256df0672faef1e1fea04d2a4a06f8f97c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sta=C5=9B=20Ma=C5=82olepszy?= Date: Wed, 18 Aug 2021 01:16:46 +0200 Subject: [PATCH 3/5] =?UTF-8?q?(third)=20Rename=20RuntimeValue.format=20?= =?UTF-8?q?=E2=86=92=20formatToString?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stasm/third/example/example_list.ts | 2 +- experiments/stasm/third/impl/runtime.ts | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/experiments/stasm/third/example/example_list.ts b/experiments/stasm/third/example/example_list.ts index 286bd355a4..40a231352e 100644 --- a/experiments/stasm/third/example/example_list.ts +++ b/experiments/stasm/third/example/example_list.ts @@ -29,7 +29,7 @@ class ListValue extends RuntimeValue> { this.opts = opts; } - format(ctx: FormattingContext): string { + formatToString(ctx: FormattingContext): string { // TODO(stasm): Cache ListFormat. // @ts-ignore let lf = new Intl.ListFormat(ctx.locale, this.opts); diff --git a/experiments/stasm/third/impl/runtime.ts b/experiments/stasm/third/impl/runtime.ts index c2b35bc1fb..d1e9ac64bb 100644 --- a/experiments/stasm/third/impl/runtime.ts +++ b/experiments/stasm/third/impl/runtime.ts @@ -24,12 +24,12 @@ export abstract class RuntimeValue { this.value = value; } - abstract format(ctx: FormattingContext): string; + abstract formatToString(ctx: FormattingContext): string; abstract formatToParts(ctx: FormattingContext): IterableIterator; } export class StringValue extends RuntimeValue { - format(ctx: FormattingContext): string { + formatToString(ctx: FormattingContext): string { return this.value; } @@ -46,7 +46,7 @@ export class NumberValue extends RuntimeValue { this.opts = opts; } - format(ctx: FormattingContext): string { + formatToString(ctx: FormattingContext): string { // TODO(stasm): Cache NumberFormat. return new Intl.NumberFormat(ctx.locale, this.opts).format(this.value); } @@ -64,7 +64,7 @@ export class PluralValue extends RuntimeValue { this.opts = opts; } - format(ctx: FormattingContext): string { + formatToString(ctx: FormattingContext): string { // TODO(stasm): Cache PluralRules. let pr = new Intl.PluralRules(ctx.locale, this.opts); return pr.select(this.value); @@ -76,7 +76,7 @@ export class PluralValue extends RuntimeValue { } export class BooleanValue extends RuntimeValue { - format(ctx: FormattingContext): string { + formatToString(ctx: FormattingContext): string { throw new TypeError("BooleanValue is not formattable."); } @@ -86,7 +86,7 @@ export class BooleanValue extends RuntimeValue { } export class PatternValue extends RuntimeValue> { - format(ctx: FormattingContext): string { + formatToString(ctx: FormattingContext): string { return ctx.formatPattern(this.value); } @@ -115,7 +115,7 @@ export class FormattingContext { formatPattern(pattern: Array): string { let output = ""; for (let value of this.resolvePattern(pattern)) { - output += value.format(this); + output += value.formatToString(this); } return output; } @@ -173,7 +173,7 @@ export class FormattingContext { let value = this.vars[selector.expr.name]; resolved_selectors.push({ value: value.value, - string: value.format(this), + string: value.formatToString(this), default: selector.default.value, }); break; @@ -183,7 +183,7 @@ export class FormattingContext { let value = callable(this, selector.expr.args, selector.expr.opts); resolved_selectors.push({ value: value.value, - string: value.format(this), + string: value.formatToString(this), default: selector.default.value, }); break; From 757690c74580dbfd0a0e5de1e35e74c9dd33e3a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sta=C5=9B=20Ma=C5=82olepszy?= Date: Wed, 18 Aug 2021 01:32:28 +0200 Subject: [PATCH 4/5] (third) Add example_opaque --- experiments/stasm/third/README.md | 1 + .../stasm/third/example/example_list.ts | 2 +- .../stasm/third/example/example_opaque.ts | 53 +++++++++++++++++++ experiments/stasm/third/package.json | 2 +- 4 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 experiments/stasm/third/example/example_opaque.ts diff --git a/experiments/stasm/third/README.md b/experiments/stasm/third/README.md index 8646c241d2..d57489a9ed 100644 --- a/experiments/stasm/third/README.md +++ b/experiments/stasm/third/README.md @@ -73,6 +73,7 @@ Run each example individually: npm run example phrases npm run example list npm run example number + npm run example opaque ## References diff --git a/experiments/stasm/third/example/example_list.ts b/experiments/stasm/third/example/example_list.ts index 40a231352e..8e6fa44f23 100644 --- a/experiments/stasm/third/example/example_list.ts +++ b/experiments/stasm/third/example/example_list.ts @@ -20,7 +20,7 @@ class Person { } } -// TODO(stasm): Move this to impl/runtime.ts? +// TODO(stasm): This is generic enough that it could be in impl/runtime.ts. class ListValue extends RuntimeValue> { private opts: Record; // ListFormatOptions diff --git a/experiments/stasm/third/example/example_opaque.ts b/experiments/stasm/third/example/example_opaque.ts new file mode 100644 index 0000000000..505d5d4fb0 --- /dev/null +++ b/experiments/stasm/third/example/example_opaque.ts @@ -0,0 +1,53 @@ +import {Message} from "../impl/model.js"; +import {FormattingContext, formatToParts, OpaquePart, RuntimeValue} from "../impl/runtime.js"; + +// We want to pass it into the translation and get it back out unformatted, in +// the correct position in the sentence, via formatToParts. +class SomeUnstringifiableClass {} + +// TODO(stasm): This is generic enough that it could be in impl/runtime.ts. +class WrappedValue extends RuntimeValue { + formatToString(ctx: FormattingContext): string { + throw new Error("Method not implemented."); + } + *formatToParts(ctx: FormattingContext): IterableIterator { + yield {type: "opaque", value: this.value}; + } +} + +console.log("==== English ===="); + +{ + // "Ready? Then {$submitButton}!" + let message: Message = { + lang: "en", + id: "submit", + phrases: {}, + selectors: [ + { + expr: null, + default: {type: "StringLiteral", value: "default"}, + }, + ], + variants: [ + { + keys: [{type: "StringLiteral", value: "default"}], + value: [ + {type: "StringLiteral", value: "Ready? Then "}, + { + type: "VariableReference", + name: "submitButton", + }, + {type: "StringLiteral", value: "!"}, + ], + }, + ], + }; + console.log( + Array.of( + ...formatToParts(message, { + submitButton: new WrappedValue(new SomeUnstringifiableClass()), + }) + ) + ); +} diff --git a/experiments/stasm/third/package.json b/experiments/stasm/third/package.json index 4f0d2b283c..89e6b87ecd 100644 --- a/experiments/stasm/third/package.json +++ b/experiments/stasm/third/package.json @@ -12,7 +12,7 @@ }, "scripts": { "example": "tsc && node example/index.js", - "start": "tsc && (node example/index.js glossary; node example/index.js list; node example/index.js phrases; node example/index.js number)", + "start": "tsc && (node example/index.js glossary; node example/index.js list; node example/index.js phrases; node example/index.js number; node example/index.js opaque)", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Staś Małolepszy ", From fc963f6571e5148c7140b50d37721ca1faa739a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sta=C5=9B=20Ma=C5=82olepszy?= Date: Wed, 18 Aug 2021 05:35:46 +0200 Subject: [PATCH 5/5] (third) Add typings for Intl.ListFormat --- .../stasm/third/example/example_list.ts | 6 ++--- experiments/stasm/third/impl/intl.d.ts | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 experiments/stasm/third/impl/intl.d.ts diff --git a/experiments/stasm/third/example/example_list.ts b/experiments/stasm/third/example/example_list.ts index 8e6fa44f23..c760b44d92 100644 --- a/experiments/stasm/third/example/example_list.ts +++ b/experiments/stasm/third/example/example_list.ts @@ -22,22 +22,20 @@ class Person { // TODO(stasm): This is generic enough that it could be in impl/runtime.ts. class ListValue extends RuntimeValue> { - private opts: Record; // ListFormatOptions + private opts: Intl.ListFormatOptions; - constructor(value: Array, opts: Record = {}) { + constructor(value: Array, opts: Intl.ListFormatOptions = {}) { super(value); this.opts = opts; } formatToString(ctx: FormattingContext): string { // TODO(stasm): Cache ListFormat. - // @ts-ignore let lf = new Intl.ListFormat(ctx.locale, this.opts); return lf.format(this.value); } *formatToParts(ctx: FormattingContext): IterableIterator { - // @ts-ignore let lf = new Intl.ListFormat(ctx.locale, this.opts); yield* lf.formatToParts(this.value); } diff --git a/experiments/stasm/third/impl/intl.d.ts b/experiments/stasm/third/impl/intl.d.ts new file mode 100644 index 0000000000..ed0c4a7a81 --- /dev/null +++ b/experiments/stasm/third/impl/intl.d.ts @@ -0,0 +1,25 @@ +declare namespace Intl { + interface ListFormatOptions { + // I added `string` to avoid having to validate the exact values of options. + localeMatcher?: string | "best fit" | "lookup"; + type?: string | "conjunction" | "disjunction | unit"; + style?: string | "long" | "short" | "narrow"; + } + + type ListFormatPartTypes = "literal" | "element"; + + interface ListFormatPart { + type: ListFormatPartTypes; + value: string; + } + + interface ListFormat { + format(value?: Array): string; + formatToParts(value?: Array): ListFormatPart[]; + } + + var ListFormat: { + new (locales?: string | string[], options?: ListFormatOptions): ListFormat; + (locales?: string | string[], options?: ListFormatOptions): ListFormat; + }; +}