prosebook 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +357 -26
- package/dist/bind-BIZNv8nU.js +62 -0
- package/dist/bind-BIZNv8nU.js.map +1 -0
- package/dist/bind-ZS86jHXk.d.ts +31 -0
- package/dist/bind-ZS86jHXk.d.ts.map +1 -0
- package/dist/bun.d.ts +12 -3
- package/dist/bun.d.ts.map +1 -1
- package/dist/bun.js +77 -26
- package/dist/bun.js.map +1 -1
- package/dist/{bundle-KD-DiiFa.d.ts → bundle-CLHY_97D.d.ts} +3 -3
- package/dist/bundle-CLHY_97D.d.ts.map +1 -0
- package/dist/bundle-Ut6Y2X6N.js +217 -0
- package/dist/bundle-Ut6Y2X6N.js.map +1 -0
- package/dist/cli.js +55 -12
- package/dist/cli.js.map +1 -1
- package/dist/{config-BPSmoYel.js → config-7T4hX_f7.js} +13 -5
- package/dist/{config-BPSmoYel.js.map → config-7T4hX_f7.js.map} +1 -1
- package/dist/{config-DdMkKt_N.d.ts → config-CzaJJQ60.d.ts} +19 -5
- package/dist/{config-DdMkKt_N.d.ts.map → config-CzaJJQ60.d.ts.map} +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/{copy-B3V_QRkT.d.ts → copy-BXn0aYQn.d.ts} +37 -3
- package/dist/copy-BXn0aYQn.d.ts.map +1 -0
- package/dist/copy-Bv6S-Hr2.js +235 -0
- package/dist/copy-Bv6S-Hr2.js.map +1 -0
- package/dist/endpoints-Dhgowovz.js +217 -0
- package/dist/endpoints-Dhgowovz.js.map +1 -0
- package/dist/generate-1BeQAd1O.d.ts +72 -0
- package/dist/generate-1BeQAd1O.d.ts.map +1 -0
- package/dist/{generate-9pR3i5Bj.js → generate-CKv86Iki.js} +1724 -627
- package/dist/generate-CKv86Iki.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/next-loader.d.ts +9 -2
- package/dist/next-loader.d.ts.map +1 -1
- package/dist/next-loader.js +12 -3
- package/dist/next-loader.js.map +1 -1
- package/dist/next.d.ts +11 -3
- package/dist/next.d.ts.map +1 -1
- package/dist/next.js +94 -21
- package/dist/next.js.map +1 -1
- package/dist/preact/index.d.ts +34 -0
- package/dist/preact/index.d.ts.map +1 -0
- package/dist/preact/index.js +72 -0
- package/dist/preact/index.js.map +1 -0
- package/dist/react/index.d.ts +5 -14
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +9 -33
- package/dist/react/index.js.map +1 -1
- package/dist/react/server.d.ts +1 -1
- package/dist/react/server.js +1 -1
- package/dist/{sarif-CWyTAQ0y.js → sarif-BnwYC78_.js} +8 -98
- package/dist/sarif-BnwYC78_.js.map +1 -0
- package/dist/{scan-Bx0_yDSi.js → scan-C0ap4Mpp.js} +145 -20
- package/dist/scan-C0ap4Mpp.js.map +1 -0
- package/dist/solid/index.d.ts +29 -0
- package/dist/solid/index.d.ts.map +1 -0
- package/dist/solid/index.js +48 -0
- package/dist/solid/index.js.map +1 -0
- package/dist/svelte/Text.svelte +13 -0
- package/dist/svelte/Text.svelte.d.ts +20 -0
- package/dist/svelte/index.d.ts +31 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +35 -0
- package/dist/svelte/index.js.map +1 -0
- package/dist/tools.d.ts +44 -65
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +5 -5
- package/dist/vite.d.ts +13 -3
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +82 -25
- package/dist/vite.js.map +1 -1
- package/dist/vue/index.d.ts +43 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/index.js +76 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +63 -7
- package/src/bun.ts +77 -20
- package/src/cli.ts +62 -8
- package/src/index.ts +1 -1
- package/src/next-loader.ts +17 -5
- package/src/next.ts +120 -16
- package/src/preact/index.ts +91 -0
- package/src/react/index.tsx +12 -42
- package/src/runtime/bind.ts +96 -0
- package/src/runtime/copy.ts +148 -24
- package/src/solid/index.ts +67 -0
- package/src/svelte/Text.svelte +13 -0
- package/src/svelte/Text.svelte.d.ts +20 -0
- package/src/svelte/index.ts +65 -0
- package/src/tools/bundle.ts +143 -2
- package/src/tools/catalog.ts +10 -1
- package/src/tools/chunks.ts +142 -0
- package/src/tools/config.ts +25 -7
- package/src/tools/entry.ts +19 -20
- package/src/tools/exchange.ts +275 -0
- package/src/tools/extract.ts +2 -2
- package/src/tools/generate.ts +7 -1
- package/src/tools/index.ts +2 -1
- package/src/tools/native/android.ts +186 -0
- package/src/tools/native/apple.ts +201 -0
- package/src/tools/native/date.ts +219 -0
- package/src/tools/native/index.ts +17 -5
- package/src/tools/native/kotlin.ts +47 -6
- package/src/tools/native/model.ts +67 -29
- package/src/tools/native/printf.ts +137 -0
- package/src/tools/native/rust.ts +86 -5
- package/src/tools/native/swift.ts +62 -6
- package/src/tools/scan.ts +146 -25
- package/src/tools/todo.ts +6 -1
- package/src/vite.ts +121 -28
- package/src/vue/index.ts +117 -0
- package/dist/bundle-DboItAre.js +0 -111
- package/dist/bundle-DboItAre.js.map +0 -1
- package/dist/bundle-KD-DiiFa.d.ts.map +0 -1
- package/dist/compile-xF3F4oIp.d.ts +0 -16
- package/dist/compile-xF3F4oIp.d.ts.map +0 -1
- package/dist/copy-B3V_QRkT.d.ts.map +0 -1
- package/dist/copy-Bo_FIoLy.js +0 -154
- package/dist/copy-Bo_FIoLy.js.map +0 -1
- package/dist/endpoints-CnUfCkIk.js +0 -96
- package/dist/endpoints-CnUfCkIk.js.map +0 -1
- package/dist/generate-9pR3i5Bj.js.map +0 -1
- package/dist/sarif-CWyTAQ0y.js.map +0 -1
- package/dist/scan-Bx0_yDSi.js.map +0 -1
|
@@ -9,7 +9,8 @@ import { chainOf } from '#src/runtime/locale.ts';
|
|
|
9
9
|
import type { Catalog, Message, Problem } from '#src/tools/catalog.ts';
|
|
10
10
|
import type { Entry } from '#src/tools/compile.ts';
|
|
11
11
|
import type { NativeTarget } from '#src/tools/config.ts';
|
|
12
|
-
import { numberOptions, optionArgsOf, placeholderOf, selectionOf, TYPE, type MessageFormatElement } from '#src/tools/icu.ts';
|
|
12
|
+
import { dateOptions, numberOptions, optionArgsOf, placeholderOf, selectionOf, TYPE, type MessageFormatElement } from '#src/tools/icu.ts';
|
|
13
|
+
import { dateSpec, type DateSpec } from './date.ts';
|
|
13
14
|
import { docText } from './emit.ts';
|
|
14
15
|
import { numberSpec, roundingOf, type NumberSpec, type Rounding } from './number.ts';
|
|
15
16
|
import { pluralRules, type PluralRules } from './plurals.ts';
|
|
@@ -19,6 +20,8 @@ export type Part =
|
|
|
19
20
|
| { type: 'string'; arg: string }
|
|
20
21
|
/** The argument less `offset`, in format `format`; `fallback` where it isn't a number. */
|
|
21
22
|
| { type: 'number'; arg: string; offset: number; format: number; fallback: string }
|
|
23
|
+
/** The argument, a date, in date format `format`. */
|
|
24
|
+
| { type: 'date'; arg: string; format: number }
|
|
22
25
|
| { type: 'select'; arg: string; arms: Arm[]; other: Part[] }
|
|
23
26
|
| {
|
|
24
27
|
type: 'plural';
|
|
@@ -45,7 +48,7 @@ export interface NativeMessage {
|
|
|
45
48
|
words: string[];
|
|
46
49
|
/** The source text, its context, and what's asked of it. */
|
|
47
50
|
doc: string[];
|
|
48
|
-
args: { name: string; type: 'string' | 'number'; doc?: string }[];
|
|
51
|
+
args: { name: string; type: 'string' | 'number' | 'date'; doc?: string }[];
|
|
49
52
|
/** A tree per locale that renders it its own way; every locale is in one. */
|
|
50
53
|
bodies: { locales: string[]; parts: Part[] }[];
|
|
51
54
|
}
|
|
@@ -56,13 +59,30 @@ export interface NativeModel {
|
|
|
56
59
|
source: string;
|
|
57
60
|
messages: NativeMessage[];
|
|
58
61
|
formats: NumberSpec[];
|
|
62
|
+
dates: DateSpec[];
|
|
59
63
|
rules: { name: string; rules: PluralRules }[];
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
/**
|
|
67
|
+
* The words a target names `key` by, less the prefix that took it in (longest first): `tray.quit-app`
|
|
68
|
+
* is quit, app under `['tray']`. Nothing when the target doesn't take the key.
|
|
69
|
+
*/
|
|
70
|
+
export function wordsFor(target: NativeTarget, key: string): string[] | undefined {
|
|
71
|
+
return restOf(target, key)
|
|
72
|
+
?.split(/[.\-_]/)
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `key` less the prefix that took it in, as written: `tray.quit-app` is `quit-app` under `['tray']`. */
|
|
77
|
+
export function restOf(target: NativeTarget, key: string): string | undefined {
|
|
78
|
+
const prefix = target.keys === undefined ? '' : [...target.keys].sort((a, b) => b.length - a.length).find((p) => key.startsWith(`${p}.`));
|
|
79
|
+
if (prefix === undefined) return undefined;
|
|
80
|
+
return key.slice(prefix === '' ? 0 : prefix.length + 1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The locale whose text `locale` shows for `key`: its own, else the first down its fallback chain that has it. */
|
|
84
|
+
export function foundIn(key: string, locale: string, locales: string[], asts: Map<string, Map<string, MessageFormatElement[]>>) {
|
|
85
|
+
return chainOf(locale, locales, locales[0]!).find((l) => asts.get(l)?.has(key));
|
|
66
86
|
}
|
|
67
87
|
|
|
68
88
|
/** Values a file declares once each, numbered by what they hold, so locales that format alike share one. */
|
|
@@ -90,21 +110,18 @@ export function nativeModel(
|
|
|
90
110
|
const source = locales[0]!;
|
|
91
111
|
const asts = new Map([...entries].map(([locale, list]) => [locale, new Map(list)]));
|
|
92
112
|
const formats = new Numbered<NumberSpec>();
|
|
113
|
+
const dates = new Numbered<DateSpec>();
|
|
93
114
|
const rules = new Numbered<{ name: string; rules: PluralRules }>();
|
|
94
115
|
const problems: Problem[] = [];
|
|
95
116
|
const messages: NativeMessage[] = [];
|
|
96
117
|
|
|
97
118
|
for (const m of catalog.messages) {
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
100
|
-
const words = m.key
|
|
101
|
-
.slice(prefix === '' ? 0 : prefix.length + 1)
|
|
102
|
-
.split(/[.\-_]/)
|
|
103
|
-
.filter(Boolean);
|
|
119
|
+
const words = wordsFor(target, m.key);
|
|
120
|
+
if (words === undefined) continue;
|
|
104
121
|
const bodies = new Map<string, { locales: string[]; parts: Part[] }>();
|
|
105
122
|
let failed = false;
|
|
106
123
|
for (const locale of locales) {
|
|
107
|
-
const found =
|
|
124
|
+
const found = foundIn(m.key, locale, locales, asts);
|
|
108
125
|
if (found === undefined) continue;
|
|
109
126
|
const body = bodies.get(found);
|
|
110
127
|
if (body !== undefined) {
|
|
@@ -112,7 +129,7 @@ export function nativeModel(
|
|
|
112
129
|
continue;
|
|
113
130
|
}
|
|
114
131
|
try {
|
|
115
|
-
bodies.set(found, { locales: [locale], parts: parts(asts.get(found)!.get(m.key)!, found, formats, rules, null) });
|
|
132
|
+
bodies.set(found, { locales: [locale], parts: parts(asts.get(found)!.get(m.key)!, found, { formats, dates, rules }, null) });
|
|
116
133
|
} catch (e) {
|
|
117
134
|
failed = true;
|
|
118
135
|
problems.push({
|
|
@@ -130,16 +147,16 @@ export function nativeModel(
|
|
|
130
147
|
doc: docOf(m),
|
|
131
148
|
args: [...m.args].map(([name, type]) => ({
|
|
132
149
|
name,
|
|
133
|
-
type: type === 'number' ? 'number' : 'string',
|
|
150
|
+
type: type === 'number' ? 'number' : type === 'Date | number' ? 'date' : 'string',
|
|
134
151
|
doc: m.argDocs.has(name) ? docText(m.argDocs.get(name)!) : undefined,
|
|
135
152
|
})),
|
|
136
153
|
bodies: [...bodies.values()],
|
|
137
154
|
});
|
|
138
155
|
}
|
|
139
|
-
return { model: { locales, source, messages, formats: formats.list, rules: rules.list }, problems };
|
|
156
|
+
return { model: { locales, source, messages, formats: formats.list, dates: dates.list, rules: rules.list }, problems };
|
|
140
157
|
}
|
|
141
158
|
|
|
142
|
-
function fileOf(m: Message, locale: string, source: string): string {
|
|
159
|
+
export function fileOf(m: Message, locale: string, source: string): string {
|
|
143
160
|
return locale === source ? m.file : (m.translations.get(locale)?.file ?? m.file);
|
|
144
161
|
}
|
|
145
162
|
|
|
@@ -151,11 +168,38 @@ function docOf(m: Message): string[] {
|
|
|
151
168
|
return lines.map(docText);
|
|
152
169
|
}
|
|
153
170
|
|
|
154
|
-
const LEAVE_OUT = "or leave the key out of this target's `keys`";
|
|
171
|
+
export const LEAVE_OUT = "or leave the key out of this target's `keys`";
|
|
155
172
|
|
|
156
173
|
/** Specs by locale and options, kept across runs: taking a format apart checks it against `Intl` dozens of times. */
|
|
157
174
|
const SPECS = new Map<string, NumberSpec | Error>();
|
|
158
175
|
|
|
176
|
+
/** What a file declares, numbered as the messages use it. */
|
|
177
|
+
interface Declared {
|
|
178
|
+
formats: Numbered<NumberSpec>;
|
|
179
|
+
dates: Numbered<DateSpec>;
|
|
180
|
+
rules: Numbered<{ name: string; rules: PluralRules }>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Date specs as `SPECS` keeps number specs. */
|
|
184
|
+
const DATE_SPECS = new Map<string, DateSpec | Error>();
|
|
185
|
+
|
|
186
|
+
function date(dates: Numbered<DateSpec>, locale: string, options: Intl.DateTimeFormatOptions | undefined): number {
|
|
187
|
+
const key = JSON.stringify([locale, options ?? {}]);
|
|
188
|
+
let spec = DATE_SPECS.get(key);
|
|
189
|
+
if (spec === undefined) {
|
|
190
|
+
try {
|
|
191
|
+
spec = dateSpec(locale, options);
|
|
192
|
+
} catch (e) {
|
|
193
|
+
spec = new Error(`native code can't format this date in ${locale}: ${(e as Error).message}. Format it another way, ${LEAVE_OUT}`, {
|
|
194
|
+
cause: e,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
DATE_SPECS.set(key, spec);
|
|
198
|
+
}
|
|
199
|
+
if (spec instanceof Error) throw spec;
|
|
200
|
+
return dates.add(spec);
|
|
201
|
+
}
|
|
202
|
+
|
|
159
203
|
function format(formats: Numbered<NumberSpec>, locale: string, options: Intl.NumberFormatOptions | undefined): number {
|
|
160
204
|
const key = JSON.stringify([locale, options ?? {}]);
|
|
161
205
|
let spec = SPECS.get(key);
|
|
@@ -175,13 +219,8 @@ function format(formats: Numbered<NumberSpec>, locale: string, options: Intl.Num
|
|
|
175
219
|
|
|
176
220
|
type Pound = { arg: string; offset: number } | null;
|
|
177
221
|
|
|
178
|
-
function parts(
|
|
179
|
-
|
|
180
|
-
locale: string,
|
|
181
|
-
formats: Numbered<NumberSpec>,
|
|
182
|
-
rules: Numbered<{ name: string; rules: PluralRules }>,
|
|
183
|
-
pound: Pound,
|
|
184
|
-
): Part[] {
|
|
222
|
+
function parts(elements: MessageFormatElement[], locale: string, declared: Declared, pound: Pound): Part[] {
|
|
223
|
+
const { formats, dates, rules } = declared;
|
|
185
224
|
const out: Part[] = [];
|
|
186
225
|
const text = (value: string) => {
|
|
187
226
|
const last = out.at(-1);
|
|
@@ -195,7 +234,7 @@ function parts(
|
|
|
195
234
|
if (isolate !== undefined) text(isolate[1]);
|
|
196
235
|
};
|
|
197
236
|
const arms = (options: Record<string, { value: MessageFormatElement[] }>, own: Pound) =>
|
|
198
|
-
Object.entries(options).map(([key, option]) => ({ key, parts: parts(option.value, locale,
|
|
237
|
+
Object.entries(options).map(([key, option]) => ({ key, parts: parts(option.value, locale, declared, own) }));
|
|
199
238
|
|
|
200
239
|
for (const el of elements) {
|
|
201
240
|
if (Object.keys(optionArgsOf(el) ?? {}).length > 0) {
|
|
@@ -204,9 +243,8 @@ function parts(
|
|
|
204
243
|
);
|
|
205
244
|
}
|
|
206
245
|
if (isDateElement(el) || isTimeElement(el)) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
);
|
|
246
|
+
placed(el, el.value, { type: 'date', arg: el.value, format: date(dates, locale, dateOptions(el)) });
|
|
247
|
+
continue;
|
|
210
248
|
}
|
|
211
249
|
if (isNumberElement(el)) {
|
|
212
250
|
const fallback = placeholderOf(el, el.value).fallback;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Messages as printf-style format strings, for the targets whose platform fills values in itself:
|
|
3
|
+
* Android's resources and Apple's `.strings` and `.stringsdict` files. Text and plain `{name}` placeholders, as `%N$s`
|
|
4
|
+
* with N the argument's place in the message's arguments, so a translation can put them in another
|
|
5
|
+
* order; anything `Intl` would format fails with the fix.
|
|
6
|
+
*/
|
|
7
|
+
import { isDateElement, isNumberElement, isTimeElement } from '@formatjs/icu-messageformat-parser';
|
|
8
|
+
|
|
9
|
+
import type { PluralElement } from '@formatjs/icu-messageformat-parser';
|
|
10
|
+
import type { Message } from '#src/tools/catalog.ts';
|
|
11
|
+
|
|
12
|
+
import { numberOptions, placeholderOf, selectionOf, TYPE, type MessageFormatElement } from '#src/tools/icu.ts';
|
|
13
|
+
import { docText } from './emit.ts';
|
|
14
|
+
import { LEAVE_OUT } from './model.ts';
|
|
15
|
+
|
|
16
|
+
export interface Printf {
|
|
17
|
+
/** Who fills values in, and with what, as problems name them: `Android`, `String.format`. */
|
|
18
|
+
platform: string;
|
|
19
|
+
formatter: string;
|
|
20
|
+
/** What holds the text, as problems name it: `Android resources`. */
|
|
21
|
+
holder: string;
|
|
22
|
+
/** A string argument's conversion: `s`, or `@` for Apple's objects. */
|
|
23
|
+
string: string;
|
|
24
|
+
/** A count's conversion: `d`, or `lld` for Apple's 64-bit `Int`. */
|
|
25
|
+
count: string;
|
|
26
|
+
/** Text as the file holds it; `formatted` when the string has arguments, so `%` must be doubled. */
|
|
27
|
+
escape: (text: string, formatted: boolean) => string;
|
|
28
|
+
/** The problem with a plural the target can't hold where it is. */
|
|
29
|
+
plural: (name: string) => string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The context and what each argument holds, on one line, for the comment above an entry. */
|
|
33
|
+
export function noteOf(m: Message): string {
|
|
34
|
+
const lines = [
|
|
35
|
+
m.context,
|
|
36
|
+
...[...m.args.keys()].map((a) => (m.argDocs.has(a) ? `${a}: ${m.argDocs.get(a)!.replace(/\.$/, '')}.` : undefined)),
|
|
37
|
+
];
|
|
38
|
+
return docText(lines.filter((l) => l !== undefined && l !== '').join(' '));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** What a plural's arms may hold as its count: `#`, or the count as a plain `:number` or `:integer`. */
|
|
42
|
+
function isCount(el: MessageFormatElement, count: string): boolean {
|
|
43
|
+
if (el.type === TYPE.pound) return true;
|
|
44
|
+
if (!isNumberElement(el) || el.value !== count) return false;
|
|
45
|
+
const options = Object.entries(numberOptions(el) ?? {});
|
|
46
|
+
return options.length === 0 || (options.length === 1 && options[0]![0] === 'maximumFractionDigits' && options[0]![1] === 0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** What a platform's plurals are called in problems, and what they can't hold. */
|
|
50
|
+
export interface PluralHolder {
|
|
51
|
+
/** `Android`, as in "Android selects on the count as an integer". */
|
|
52
|
+
platform: string;
|
|
53
|
+
/** `Android's <plurals>`. */
|
|
54
|
+
plurals: string;
|
|
55
|
+
/** Why an `=N` arm has no place, after "have none for one number". */
|
|
56
|
+
exact: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Throws, with the fix, where a plural asks for more than a platform's plurals pick on: one count,
|
|
61
|
+
* as an integer, by its cardinal category.
|
|
62
|
+
*/
|
|
63
|
+
export function checkPlural(plural: PluralElement, holder: PluralHolder): void {
|
|
64
|
+
const named = `{${plural.value}}`;
|
|
65
|
+
if (plural.pluralType === 'ordinal') {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`${named} is an ordinal, and ${holder.plurals} are cardinal only: write a key per case and pick one in code, ${LEAVE_OUT}`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (plural.offset !== 0)
|
|
71
|
+
throw new Error(`${named} has an offset, which ${holder.plurals} don't: count without it, and say the rest in each arm, ${LEAVE_OUT}`);
|
|
72
|
+
|
|
73
|
+
const selection = selectionOf(plural);
|
|
74
|
+
if (selection !== undefined && (selection.fn.name !== 'integer' || Object.keys(selection.fn.options).length > 0)) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`${named} selects with :${selection.fn.name} and its options, and ${holder.platform} selects on the count as an integer: select with a bare :number or :integer, ${LEAVE_OUT}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const exact = Object.keys(plural.options).filter((k) => k.startsWith('='));
|
|
80
|
+
if (exact.length > 0) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`${named} has the arm ${exact.join(', ')}, and ${holder.plurals} have none for one number${holder.exact}: say it in a category's arm, or write a key for it and pick that in code, ${LEAVE_OUT}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** `elements` as a format string over `args`; inside a plural, its `count` is `%N$d` (or `lld`). */
|
|
88
|
+
export function formatString(elements: MessageFormatElement[], args: string[], printf: Printf, count?: string): string {
|
|
89
|
+
const formatted = args.length > 0;
|
|
90
|
+
const place = (el: MessageFormatElement, name: string, conversion: string) => {
|
|
91
|
+
const at = args.indexOf(name);
|
|
92
|
+
if (at < 0) throw new Error(`{${name}} is not an argument of this message: say it in the source text too`);
|
|
93
|
+
const { isolate } = placeholderOf(el, name);
|
|
94
|
+
const spec = `%${at + 1}$${conversion}`;
|
|
95
|
+
return isolate === undefined ? spec : `${printf.escape(isolate[0], formatted)}${spec}${printf.escape(isolate[1], formatted)}`;
|
|
96
|
+
};
|
|
97
|
+
let out = '';
|
|
98
|
+
for (const el of elements) {
|
|
99
|
+
if (count !== undefined && isCount(el, count)) {
|
|
100
|
+
out += place(el, count, printf.count);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (isDateElement(el) || isTimeElement(el)) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`{${el.value}} is a date, which ${printf.holder} don't format: pass it formatted, as a plain {${el.value}}, ${LEAVE_OUT}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (isNumberElement(el)) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`{${el.value}} is a formatted number, and ${printf.platform} fills values in with ${printf.formatter}, not Intl: pass it formatted, as a plain {${el.value}}, ${LEAVE_OUT}`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
switch (el.type) {
|
|
114
|
+
case TYPE.literal:
|
|
115
|
+
out += printf.escape(el.value, formatted);
|
|
116
|
+
break;
|
|
117
|
+
case TYPE.argument:
|
|
118
|
+
out += place(el, el.value, printf.string);
|
|
119
|
+
break;
|
|
120
|
+
case TYPE.pound:
|
|
121
|
+
// Outside a plural, `#` is text.
|
|
122
|
+
out += '#';
|
|
123
|
+
break;
|
|
124
|
+
case TYPE.select:
|
|
125
|
+
throw new Error(
|
|
126
|
+
`{${el.value}} is a select, which ${printf.holder} can't hold: write a key per case and pick one in code, ${LEAVE_OUT}`,
|
|
127
|
+
);
|
|
128
|
+
case TYPE.plural:
|
|
129
|
+
throw new Error(printf.plural(el.value));
|
|
130
|
+
case TYPE.tag:
|
|
131
|
+
throw new Error(`<${el.value}> is a tag, and ${printf.holder} here hold plain text: say it without the tag, ${LEAVE_OUT}`);
|
|
132
|
+
default:
|
|
133
|
+
throw new Error(`prosebook: no ${printf.platform} writer for ICU element type ${(el as MessageFormatElement).type}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
package/src/tools/native/rust.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* a private `rt` module, so no argument a message names can shadow it.
|
|
4
4
|
*/
|
|
5
5
|
import type { NativeTarget } from '#src/tools/config.ts';
|
|
6
|
+
import { pieceExpr, type DateSpec } from './date.ts';
|
|
6
7
|
import { checkNames, conditionCode, float, leading, pascal, snake, stringLiteral, wordsOf } from './emit.ts';
|
|
7
8
|
import type { NativeMessage, NativeModel, Part } from './model.ts';
|
|
8
9
|
import type { NumberSpec } from './number.ts';
|
|
@@ -50,6 +51,7 @@ function value(part: Exclude<Part, { type: 'text' }>, indent: string): string {
|
|
|
50
51
|
const of = part.offset === 0 ? argName(part.arg) : `${argName(part.arg)} - ${float(part.offset)}`;
|
|
51
52
|
return `rt::number(${of}, &rt::F${part.format}, ${lit(part.fallback)})`;
|
|
52
53
|
}
|
|
54
|
+
if (part.type === 'date') return `rt::D${part.format}(&${argName(part.arg)})`;
|
|
53
55
|
if (part.type === 'select') {
|
|
54
56
|
const arms = part.arms.map((a) => `${inner}${lit(a.key)} => ${expr(a.parts, inner)},\n`).join('');
|
|
55
57
|
return `match ${argName(part.arg)} {\n${arms}${inner}_ => ${expr(part.other, inner)},\n${indent}}`;
|
|
@@ -74,7 +76,9 @@ function method(m: NativeMessage, name: string, enumName: string): string {
|
|
|
74
76
|
const isStatic = m.bodies.every((b) => b.parts.every((p) => p.type === 'text'));
|
|
75
77
|
if (isStatic && m.args.length === 0)
|
|
76
78
|
return `${head} pub fn ${name}(self) -> &'static str {\n match self {\n${arms((parts) => lit(parts[0]?.type === 'text' ? parts[0].value : ''))} }\n }\n`;
|
|
77
|
-
const params = m.args
|
|
79
|
+
const params = m.args
|
|
80
|
+
.map((a) => `, ${argName(a.name)}: ${a.type === 'number' ? 'impl Number' : a.type === 'date' ? 'DateTime' : '&str'}`)
|
|
81
|
+
.join('');
|
|
78
82
|
const argDocs = m.args.filter((a) => a.doc !== undefined).map((a) => `* \`${argName(a.name)}\`: ${a.doc}`);
|
|
79
83
|
const docs = argDocs.length > 0 ? `${head} ///\n${doc(argDocs, ' ')}` : head;
|
|
80
84
|
const numbers = m.args
|
|
@@ -107,6 +111,81 @@ function formatConst(spec: NumberSpec, i: number): string {
|
|
|
107
111
|
return ` pub const F${i}: Format = Format {\n${fields.map((f) => ` ${f},\n`).join('')} };\n`;
|
|
108
112
|
}
|
|
109
113
|
|
|
114
|
+
/** A date format as a function of the date. */
|
|
115
|
+
function dateFn(spec: DateSpec, i: number): string {
|
|
116
|
+
const digits = spec.digits === undefined ? 'None' : `Some(G${i})`;
|
|
117
|
+
const at = (field: string) => (field === 'weekday' ? 't.weekday() as i64' : `t.${field} as i64`);
|
|
118
|
+
const lines = spec.pieces.map((piece) => {
|
|
119
|
+
if (piece.type === 'text') return `s += ${lit(piece.value)};`;
|
|
120
|
+
if (piece.type === 'name') return `s += [${piece.names.map(lit).join(', ')}][(${pieceExpr(piece, at)}) as usize];`;
|
|
121
|
+
return `s += &pad(${pieceExpr(piece, at)}, ${piece.pad}, ${digits});`;
|
|
122
|
+
});
|
|
123
|
+
const table = spec.digits === undefined ? '' : ` const G${i}: [&str; 10] = [${spec.digits.map(lit).join(', ')}];\n`;
|
|
124
|
+
const body = lines.map((l) => ` ${l}\n`).join('');
|
|
125
|
+
return `${table} pub fn D${i}(t: &super::DateTime) -> String {\n let mut s = String::new();\n${body} s\n }\n`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const DATE = `
|
|
129
|
+
/// \`value\` written with at least \`width\` digits, in \`digits\` where the locale has its own.
|
|
130
|
+
fn pad(value: i64, width: usize, digits: Option<[&'static str; 10]>) -> String {
|
|
131
|
+
let text = format!("{:0width$}", value, width = width);
|
|
132
|
+
match digits {
|
|
133
|
+
Some(digits) => text.bytes().map(|b| if b.is_ascii_digit() { digits[(b - b'0') as usize] } else { "-" }).collect(),
|
|
134
|
+
None => text,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
`;
|
|
138
|
+
|
|
139
|
+
const DATE_TIME = `
|
|
140
|
+
/// A date and time as a clock shows it, in the time zone it is shown in.
|
|
141
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
142
|
+
pub struct DateTime {
|
|
143
|
+
pub year: i32,
|
|
144
|
+
/// 1 to 12.
|
|
145
|
+
pub month: u8,
|
|
146
|
+
pub day: u8,
|
|
147
|
+
/// 0 to 23.
|
|
148
|
+
pub hour: u8,
|
|
149
|
+
pub minute: u8,
|
|
150
|
+
pub second: u8,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
impl DateTime {
|
|
154
|
+
/// The time \`seconds\` after the Unix epoch, on a clock \`offset\` seconds ahead of UTC: the
|
|
155
|
+
/// local time's offset where it is shown, which Rust's standard library can't look up.
|
|
156
|
+
pub fn from_unix(seconds: i64, offset: i32) -> DateTime {
|
|
157
|
+
let local = seconds + offset as i64;
|
|
158
|
+
let (days, time) = (local.div_euclid(86_400), local.rem_euclid(86_400));
|
|
159
|
+
// Howard Hinnant's civil_from_days: the Gregorian date \`days\` after 1970-01-01.
|
|
160
|
+
let z = days + 719_468;
|
|
161
|
+
let (era, doe) = (z.div_euclid(146_097), z.rem_euclid(146_097));
|
|
162
|
+
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
|
|
163
|
+
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
|
164
|
+
let mp = (5 * doy + 2) / 153;
|
|
165
|
+
let month = if mp < 10 { mp + 3 } else { mp - 9 };
|
|
166
|
+
DateTime {
|
|
167
|
+
year: (yoe + era * 400 + if month <= 2 { 1 } else { 0 }) as i32,
|
|
168
|
+
month: month as u8,
|
|
169
|
+
day: (doy - (153 * mp + 2) / 5 + 1) as u8,
|
|
170
|
+
hour: (time / 3600) as u8,
|
|
171
|
+
minute: (time / 60 % 60) as u8,
|
|
172
|
+
second: (time % 60) as u8,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// The day of the week, 0 for Sunday.
|
|
177
|
+
pub fn weekday(self) -> u8 {
|
|
178
|
+
// days_from_civil, its inverse.
|
|
179
|
+
let m = self.month as i64;
|
|
180
|
+
let y = self.year as i64 - if m <= 2 { 1 } else { 0 };
|
|
181
|
+
let (era, yoe) = (y.div_euclid(400), y.rem_euclid(400));
|
|
182
|
+
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + self.day as i64 - 1;
|
|
183
|
+
let days = era * 146_097 + yoe * 365 + yoe / 4 - yoe / 100 + doy - 719_468;
|
|
184
|
+
(days + 4).rem_euclid(7) as u8
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
`;
|
|
188
|
+
|
|
110
189
|
const NUMBER = `
|
|
111
190
|
/// A number format, taken apart when the file was generated.
|
|
112
191
|
pub struct Format {
|
|
@@ -300,6 +379,8 @@ export function rustModule(model: NativeModel, target: NativeTarget & { name: st
|
|
|
300
379
|
const tags = model.locales.map((l) => ` ${name}::${variant(l)} => ${lit(l)},\n`).join('');
|
|
301
380
|
const methods = model.messages.map((m, i) => method(m, names[i]![1], name)).join('\n');
|
|
302
381
|
const usesNumbers = model.messages.some((m) => m.args.some((a) => a.type === 'number'));
|
|
382
|
+
const usesDates = model.dates.length > 0;
|
|
383
|
+
const usesFormats = model.formats.length + model.rules.length > 0;
|
|
303
384
|
const rules = model.rules
|
|
304
385
|
.map(({ name: rule, rules }, i) => {
|
|
305
386
|
const checks = rules.map(
|
|
@@ -310,17 +391,17 @@ export function rustModule(model: NativeModel, target: NativeTarget & { name: st
|
|
|
310
391
|
})
|
|
311
392
|
.join('\n');
|
|
312
393
|
const runtime =
|
|
313
|
-
|
|
394
|
+
!usesFormats && !usesDates
|
|
314
395
|
? ''
|
|
315
396
|
: `
|
|
316
397
|
/// What formats the messages, generated with them.
|
|
317
|
-
mod rt {${NUMBER}${model.rules.length > 0 ? PLURAL : ''}
|
|
318
|
-
${model.formats.map(formatConst).join('\n')}${rules === '' ? '' : `\n${rules}`}}
|
|
398
|
+
mod rt {${usesFormats ? NUMBER : ''}${model.rules.length > 0 ? PLURAL : ''}${usesDates ? DATE : ''}
|
|
399
|
+
${[...model.formats.map(formatConst), ...model.dates.map(dateFn)].join('\n')}${rules === '' ? '' : `\n${rules}`}}
|
|
319
400
|
`;
|
|
320
401
|
|
|
321
402
|
return `${banner}
|
|
322
403
|
#![allow(dead_code, non_snake_case, clippy::all)]
|
|
323
|
-
${usesNumbers ? NUMBER_TRAIT : ''}
|
|
404
|
+
${usesNumbers ? NUMBER_TRAIT : ''}${usesDates ? DATE_TIME : ''}
|
|
324
405
|
/// A locale the catalog is written or translated in, and its messages.
|
|
325
406
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
326
407
|
pub enum ${name} {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* message, and a file-private enum holding what formats them.
|
|
4
4
|
*/
|
|
5
5
|
import type { NativeTarget } from '#src/tools/config.ts';
|
|
6
|
+
import { pieceExpr, type DateSpec } from './date.ts';
|
|
6
7
|
import { camel, checkNames, conditionCode, float, leading, stringLiteral, wordsOf } from './emit.ts';
|
|
7
8
|
import type { NativeMessage, NativeModel, Part } from './model.ts';
|
|
8
9
|
import type { NumberSpec } from './number.ts';
|
|
@@ -36,6 +37,7 @@ function value(part: Exclude<Part, { type: 'text' }>, indent: string, rt: string
|
|
|
36
37
|
const of = part.offset === 0 ? argName(part.arg) : `${argName(part.arg)} - ${float(part.offset)}`;
|
|
37
38
|
return `${rt}.number(${of}, ${rt}.f${part.format}, ${lit(part.fallback)})`;
|
|
38
39
|
}
|
|
40
|
+
if (part.type === 'date') return `${rt}.d${part.format}(${argName(part.arg)})`;
|
|
39
41
|
if (part.type === 'select') {
|
|
40
42
|
const arms = part.arms.map((a) => `${inner}case ${lit(a.key)}: return ${expr(a.parts, inner, rt)}\n`).join('');
|
|
41
43
|
return `{ () -> String in\n${inner}switch ${argName(part.arg)} {\n${arms}${inner}default: return ${expr(part.other, inner, rt)}\n${inner}}\n${indent}}()`;
|
|
@@ -66,10 +68,17 @@ function member(m: NativeMessage, name: string, rt: string, number: NumberProtoc
|
|
|
66
68
|
const head = doc(argDocs.length > 0 ? [...m.doc, '', ...argDocs] : m.doc, ' ');
|
|
67
69
|
const body = ` switch self {\n${cases((parts) => expr(parts, ' ', rt))} }\n`;
|
|
68
70
|
if (m.args.length === 0) return `${head} public var ${name}: String {\n${body} }\n`;
|
|
69
|
-
const params = m.args
|
|
71
|
+
const params = m.args
|
|
72
|
+
.map((a) => `${argName(a.name)}: ${a.type === 'number' ? `some ${number.type}` : a.type === 'date' ? 'Date' : 'String'}`)
|
|
73
|
+
.join(', ');
|
|
70
74
|
const numbers = m.args
|
|
71
|
-
.
|
|
72
|
-
|
|
75
|
+
.map((a) =>
|
|
76
|
+
a.type === 'number'
|
|
77
|
+
? ` let ${argName(a.name)} = ${argName(a.name)}.${number.value}\n`
|
|
78
|
+
: a.type === 'date'
|
|
79
|
+
? ` let ${argName(a.name)} = ${rt}.clock(${argName(a.name)})\n`
|
|
80
|
+
: '',
|
|
81
|
+
)
|
|
73
82
|
.join('');
|
|
74
83
|
return `${head} public func ${name}(${params}) -> String {\n${numbers}${body} }\n`;
|
|
75
84
|
}
|
|
@@ -97,6 +106,51 @@ function formatConst(spec: NumberSpec, i: number): string {
|
|
|
97
106
|
return ` static let f${i} = Format(\n${fields.map((f) => ` ${f}`).join(',\n')}\n )\n`;
|
|
98
107
|
}
|
|
99
108
|
|
|
109
|
+
/** A date format as a function of the clock's fields. */
|
|
110
|
+
function dateFn(spec: DateSpec, i: number): string {
|
|
111
|
+
const digits = spec.digits === undefined ? 'nil' : `g${i}`;
|
|
112
|
+
const at = (field: string) => `t.${field}`;
|
|
113
|
+
const lines = spec.pieces.map((piece) => {
|
|
114
|
+
if (piece.type === 'text') return `s += ${lit(piece.value)}`;
|
|
115
|
+
if (piece.type === 'name') return `s += [${piece.names.map(lit).join(', ')}][${pieceExpr(piece, at)}]`;
|
|
116
|
+
return `s += pad(${pieceExpr(piece, at)}, ${piece.pad}, ${digits})`;
|
|
117
|
+
});
|
|
118
|
+
const table = spec.digits === undefined ? '' : ` static let g${i} = [${spec.digits.map(lit).join(', ')}]\n`;
|
|
119
|
+
const body = lines.map((l) => ` ${l}\n`).join('');
|
|
120
|
+
return `${table} static func d${i}(_ t: Clock) -> String {\n var s = ""\n${body} return s\n }\n`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const DATE = `
|
|
124
|
+
/// A date as the clock shows it in the device's time zone; \`weekday\` is 0 for Sunday.
|
|
125
|
+
struct Clock {
|
|
126
|
+
let year: Int
|
|
127
|
+
let month: Int
|
|
128
|
+
let day: Int
|
|
129
|
+
let hour: Int
|
|
130
|
+
let minute: Int
|
|
131
|
+
let second: Int
|
|
132
|
+
let weekday: Int
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
static func clock(_ date: Date) -> Clock {
|
|
136
|
+
var calendar = Calendar(identifier: .gregorian)
|
|
137
|
+
calendar.timeZone = .current
|
|
138
|
+
let c = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second, .weekday], from: date)
|
|
139
|
+
return Clock(
|
|
140
|
+
year: c.year ?? 0, month: c.month ?? 1, day: c.day ?? 1, hour: c.hour ?? 0, minute: c.minute ?? 0,
|
|
141
|
+
second: c.second ?? 0, weekday: (c.weekday ?? 1) - 1
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// \`value\` written with at least \`width\` digits, in \`digits\` where the locale has its own.
|
|
146
|
+
private static func pad(_ value: Int, _ width: Int, _ digits: [String]?) -> String {
|
|
147
|
+
var text = String(value)
|
|
148
|
+
while text.count < width { text = "0" + text }
|
|
149
|
+
guard let digits else { return text }
|
|
150
|
+
return text.map { c in c.wholeNumberValue.map { digits[$0] } ?? String(c) }.joined()
|
|
151
|
+
}
|
|
152
|
+
`;
|
|
153
|
+
|
|
100
154
|
const NUMBER = `
|
|
101
155
|
/// A number format, taken apart when the file was generated.
|
|
102
156
|
struct Format {
|
|
@@ -265,6 +319,8 @@ export function swiftFile(model: NativeModel, target: NativeTarget & { name: str
|
|
|
265
319
|
const number = { type: `${name}Number`, value: `${name.charAt(0).toLowerCase()}${name.slice(1)}Value` };
|
|
266
320
|
const members = model.messages.map((m, i) => member(m, names[i]![1], rt, number)).join('\n');
|
|
267
321
|
const usesNumbers = model.messages.some((m) => m.args.some((a) => a.type === 'number'));
|
|
322
|
+
const usesDates = model.dates.length > 0;
|
|
323
|
+
const usesFormats = model.formats.length + model.rules.length > 0;
|
|
268
324
|
const numberProtocol = !usesNumbers
|
|
269
325
|
? ''
|
|
270
326
|
: `
|
|
@@ -285,12 +341,12 @@ ${NUMBER_TYPES.map((t) => `extension ${t}: ${number.type} { public var ${number.
|
|
|
285
341
|
})
|
|
286
342
|
.join('\n');
|
|
287
343
|
const runtime =
|
|
288
|
-
|
|
344
|
+
!usesFormats && !usesDates
|
|
289
345
|
? ''
|
|
290
346
|
: `
|
|
291
347
|
/// What formats the messages, generated with them.
|
|
292
|
-
private enum ${rt} {${NUMBER}${model.rules.length > 0 ? PLURAL : ''}
|
|
293
|
-
${model.formats.map(formatConst).join('\n')}${rules === '' ? '' : `\n${rules}`}}
|
|
348
|
+
private enum ${rt} {${usesFormats ? NUMBER : ''}${model.rules.length > 0 ? PLURAL : ''}${usesDates ? DATE : ''}
|
|
349
|
+
${[...model.formats.map(formatConst), ...model.dates.map(dateFn)].join('\n')}${rules === '' ? '' : `\n${rules}`}}
|
|
294
350
|
`;
|
|
295
351
|
|
|
296
352
|
return `${banner}
|