prosebook 0.3.0 → 0.4.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 +74 -3
- package/dist/bun.d.ts +2 -2
- package/dist/bun.js +3 -3
- package/dist/{bundle-KD-DiiFa.d.ts → bundle-CpUIWb9j.d.ts} +3 -3
- package/dist/bundle-CpUIWb9j.d.ts.map +1 -0
- package/dist/cli.js +3 -3
- package/dist/{compile-xF3F4oIp.d.ts → compile-ClK9htKr.d.ts} +2 -2
- package/dist/{compile-xF3F4oIp.d.ts.map → compile-ClK9htKr.d.ts.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-BPSmoYel.js → config-nWZf9QmF.js} +9 -3
- package/dist/{config-BPSmoYel.js.map → config-nWZf9QmF.js.map} +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/{endpoints-CnUfCkIk.js → endpoints-BgG_5Vpm.js} +2 -2
- package/dist/{endpoints-CnUfCkIk.js.map → endpoints-BgG_5Vpm.js.map} +1 -1
- package/dist/{generate-9pR3i5Bj.js → generate-BW3N8EGb.js} +590 -275
- package/dist/generate-BW3N8EGb.js.map +1 -0
- package/dist/next.d.ts +2 -2
- package/dist/next.js +3 -3
- package/dist/{sarif-CWyTAQ0y.js → sarif-B5nrMqrT.js} +3 -3
- package/dist/{sarif-CWyTAQ0y.js.map → sarif-B5nrMqrT.js.map} +1 -1
- package/dist/tools.d.ts +2 -2
- package/dist/tools.js +3 -3
- package/dist/vite.d.ts +2 -2
- package/dist/vite.js +3 -3
- package/package.json +1 -1
- package/src/tools/config.ts +24 -6
- package/src/tools/native/android.ts +201 -0
- package/src/tools/native/apple.ts +139 -0
- package/src/tools/native/index.ts +17 -5
- package/src/tools/native/model.ts +25 -13
- package/src/tools/native/printf.ts +95 -0
- package/dist/bundle-KD-DiiFa.d.ts.map +0 -1
- package/dist/generate-9pR3i5Bj.js.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as chainOf } from "./locale-DpQA1pqH.js";
|
|
2
2
|
import { a as formatsOf, c as parseMessage$1, d as staticLiteral, f as tagsOf, i as formatProblems, l as placeholderOf, n as argsOf, o as numberOptions, p as walk, s as optionArgsOf, t as TYPE$1, u as selectionOf } from "./icu-Dej0mNsF.js";
|
|
3
3
|
import { d as localeModule, l as compileLocale } from "./scan-Bx0_yDSi.js";
|
|
4
|
-
import { p as lint, u as applyEdits } from "./config-
|
|
4
|
+
import { p as lint, u as applyEdits } from "./config-nWZf9QmF.js";
|
|
5
5
|
import { dirname, join, relative, resolve } from "node:path";
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { SKELETON_TYPE, TYPE, isDateElement, isNumberElement, isTimeElement, parse } from "@formatjs/icu-messageformat-parser";
|
|
@@ -33,7 +33,7 @@ const CATEGORY_ORDER = [
|
|
|
33
33
|
function print$1(elements, inPlural) {
|
|
34
34
|
return elements.map((el, i) => {
|
|
35
35
|
switch (el.type) {
|
|
36
|
-
case TYPE.literal: return escape(el.value, inPlural, elements[i + 1] === void 0);
|
|
36
|
+
case TYPE.literal: return escape$2(el.value, inPlural, elements[i + 1] === void 0);
|
|
37
37
|
case TYPE.argument: return `{${el.value}}`;
|
|
38
38
|
case TYPE.number:
|
|
39
39
|
case TYPE.date:
|
|
@@ -66,7 +66,7 @@ function print$1(elements, inPlural) {
|
|
|
66
66
|
* Literal text, quoted where ICU would read it as syntax. An apostrophe starts a quote only before a
|
|
67
67
|
* special character, so one before ordinary text (`don't`) stays single.
|
|
68
68
|
*/
|
|
69
|
-
function escape(text, inPlural, last) {
|
|
69
|
+
function escape$2(text, inPlural, last) {
|
|
70
70
|
const special = (c) => c === "{" || c === "}" || c === "<" || inPlural && c === "#";
|
|
71
71
|
let out = "";
|
|
72
72
|
for (let i = 0; i < text.length; i++) {
|
|
@@ -2371,246 +2371,6 @@ function checkNames(language, named, taken) {
|
|
|
2371
2371
|
seen.set(name, key);
|
|
2372
2372
|
}
|
|
2373
2373
|
}
|
|
2374
|
-
// src/tools/native/kotlin.ts
|
|
2375
|
-
const KEYWORDS$2 = new Set("as break class continue do else false for fun if in interface is null object package return super this throw true try typealias typeof val var when while".split(" "));
|
|
2376
|
-
const ident$2 = (name) => KEYWORDS$2.has(name) ? `\`${name}\`` : name;
|
|
2377
|
-
const lit$2 = (value) => stringLiteral(value, "kotlin");
|
|
2378
|
-
const entry = (tag) => leading(upper(wordsOf(tag)));
|
|
2379
|
-
const argName$2 = (name) => ident$2(leading(camel(wordsOf(name))));
|
|
2380
|
-
function doc$3(lines, indent) {
|
|
2381
|
-
return `${indent}/**\n${lines.map((l) => l.replaceAll("*/", "*\\/")).map((l) => l === "" ? `${indent} *` : `${indent} * ${l}`).join("\n")}\n${indent} */\n`;
|
|
2382
|
-
}
|
|
2383
|
-
/** The parts as an expression of type `String`, with `rt` the runtime object. */
|
|
2384
|
-
function expr$2(parts, indent, rt) {
|
|
2385
|
-
if (parts.length === 0) return "\"\"";
|
|
2386
|
-
return parts.map((p) => p.type === "text" ? lit$2(p.value) : value$2(p, indent, rt)).join(" + ");
|
|
2387
|
-
}
|
|
2388
|
-
function value$2(part, indent, rt) {
|
|
2389
|
-
const inner = `${indent} `;
|
|
2390
|
-
if (part.type === "string") return argName$2(part.arg);
|
|
2391
|
-
if (part.type === "number") return `${rt}.number(${part.offset === 0 ? argName$2(part.arg) : `${argName$2(part.arg)} - ${float(part.offset)}`}, ${rt}.F${part.format}, ${lit$2(part.fallback)})`;
|
|
2392
|
-
if (part.type === "select") {
|
|
2393
|
-
const arms = part.arms.map((a) => `${inner}${lit$2(a.key)} -> ${expr$2(a.parts, inner, rt)}\n`).join("");
|
|
2394
|
-
return `when (${argName$2(part.arg)}) {\n${arms}${inner}else -> ${expr$2(part.other, inner, rt)}\n${indent}}`;
|
|
2395
|
-
}
|
|
2396
|
-
if (part.arms.length + part.exact.length === 0) return expr$2(part.other, indent, rt);
|
|
2397
|
-
const arg = argName$2(part.arg);
|
|
2398
|
-
const on = part.on === "round" ? `${rt}.round(${arg})` : part.on === "percent" ? `${arg} * 100.0` : arg;
|
|
2399
|
-
const { minFrac, maxFrac } = part.rounding;
|
|
2400
|
-
const exact = part.exact.map((e) => `${float(e.value)} to ${lit$2(`=${e.value}`)}`).join(", ");
|
|
2401
|
-
const arms = [...part.exact.map((e) => ({
|
|
2402
|
-
key: `=${e.value}`,
|
|
2403
|
-
parts: e.parts
|
|
2404
|
-
})), ...part.arms].map((a) => `${inner}${lit$2(a.key)} -> ${expr$2(a.parts, inner, rt)}\n`).join("");
|
|
2405
|
-
return `when (${`${rt}.plural(${on}, ${float(part.offset)}, listOf(${exact}), ${rt}::r${part.rules}, ${minFrac}, ${maxFrac})`}) {\n${arms}${inner}else -> ${expr$2(part.other, inner, rt)}\n${indent}}`;
|
|
2406
|
-
}
|
|
2407
|
-
function member$1(m, name, rt) {
|
|
2408
|
-
const branches = m.bodies.map((b) => ` ${b.locales.map(entry).join(", ")} -> ${expr$2(b.parts, " ", rt)}\n`).join("");
|
|
2409
|
-
const argDocs = m.args.filter((a) => a.doc !== void 0).map((a) => `@param ${argName$2(a.name)} ${a.doc}`);
|
|
2410
|
-
const head = doc$3(argDocs.length > 0 ? [
|
|
2411
|
-
...m.doc,
|
|
2412
|
-
"",
|
|
2413
|
-
...argDocs
|
|
2414
|
-
] : m.doc, " ");
|
|
2415
|
-
const body = `when (this) {\n${branches} }`;
|
|
2416
|
-
if (m.args.length === 0) return `${head} val ${name}: String\n get() = ${body}\n`;
|
|
2417
|
-
return `${head} fun ${name}(${m.args.map((a) => `${argName$2(a.name)}: ${a.type === "number" ? "Number" : "String"}`).join(", ")}): String {\n${m.args.filter((a) => a.type === "number").map((a) => ` val ${argName$2(a.name)} = ${argName$2(a.name)}.toDouble()\n`).join("")} return ${body}\n }\n`;
|
|
2418
|
-
}
|
|
2419
|
-
function formatConst$2(spec, i) {
|
|
2420
|
-
const affix = ([prefix, suffix]) => `${lit$2(prefix)} to ${lit$2(suffix)}`;
|
|
2421
|
-
return ` val F${i} = Format(\n${[
|
|
2422
|
-
spec.minInt,
|
|
2423
|
-
spec.minFrac,
|
|
2424
|
-
spec.maxFrac,
|
|
2425
|
-
spec.shift,
|
|
2426
|
-
spec.grouping.primary,
|
|
2427
|
-
spec.grouping.secondary,
|
|
2428
|
-
spec.grouping.min,
|
|
2429
|
-
lit$2(spec.decimal),
|
|
2430
|
-
lit$2(spec.group),
|
|
2431
|
-
spec.digits === void 0 ? "null" : `listOf(${spec.digits.map(lit$2).join(", ")})`,
|
|
2432
|
-
affix(spec.positive),
|
|
2433
|
-
affix(spec.negative),
|
|
2434
|
-
affix(spec.zero),
|
|
2435
|
-
affix(spec.negativeZero),
|
|
2436
|
-
lit$2(spec.infinity),
|
|
2437
|
-
lit$2(spec.negativeInfinity)
|
|
2438
|
-
].map((f) => ` ${f}`).join(",\n")},\n )\n`;
|
|
2439
|
-
}
|
|
2440
|
-
const NUMBER$2 = `
|
|
2441
|
-
/** A number format, taken apart when the file was generated. */
|
|
2442
|
-
class Format(
|
|
2443
|
-
val minInt: Int,
|
|
2444
|
-
val minFrac: Int,
|
|
2445
|
-
val maxFrac: Int,
|
|
2446
|
-
val shift: Int,
|
|
2447
|
-
val primary: Int,
|
|
2448
|
-
val secondary: Int,
|
|
2449
|
-
val minGroup: Int,
|
|
2450
|
-
val decimal: String,
|
|
2451
|
-
val group: String,
|
|
2452
|
-
val digits: List<String>?,
|
|
2453
|
-
val positive: Pair<String, String>,
|
|
2454
|
-
val negative: Pair<String, String>,
|
|
2455
|
-
val zero: Pair<String, String>,
|
|
2456
|
-
val negativeZero: Pair<String, String>,
|
|
2457
|
-
val infinity: String,
|
|
2458
|
-
val negativeInfinity: String,
|
|
2459
|
-
)
|
|
2460
|
-
|
|
2461
|
-
/** \`value\` as \`format\` writes it, or \`fallback\` where it isn't a number. */
|
|
2462
|
-
fun number(value: Double, format: Format, fallback: String): String {
|
|
2463
|
-
if (value.isNaN()) return fallback
|
|
2464
|
-
if (value.isInfinite()) return if (value > 0) format.infinity else format.negativeInfinity
|
|
2465
|
-
val (written, frac) = digits(Math.abs(value), format.shift, format.minFrac, format.maxFrac)
|
|
2466
|
-
val zero = (written + frac).all { it == '0' }
|
|
2467
|
-
val negative = value.toRawBits() < 0
|
|
2468
|
-
val (prefix, suffix) = if (zero) (if (negative) format.negativeZero else format.zero) else (if (negative) format.negative else format.positive)
|
|
2469
|
-
val int = written.padStart(maxOf(format.minInt, 1), '0')
|
|
2470
|
-
val grouped = format.minGroup > 0 && int.length >= format.primary + format.minGroup
|
|
2471
|
-
val out = StringBuilder(prefix)
|
|
2472
|
-
for ((i, digit) in int.withIndex()) {
|
|
2473
|
-
val left = int.length - i
|
|
2474
|
-
if (grouped && i > 0 && (left == format.primary || (left > format.primary && (left - format.primary) % format.secondary == 0))) {
|
|
2475
|
-
out.append(format.group)
|
|
2476
|
-
}
|
|
2477
|
-
out.append(shown(digit, format))
|
|
2478
|
-
}
|
|
2479
|
-
if (frac.isNotEmpty()) {
|
|
2480
|
-
out.append(format.decimal)
|
|
2481
|
-
for (digit in frac) out.append(shown(digit, format))
|
|
2482
|
-
}
|
|
2483
|
-
return out.append(suffix).toString()
|
|
2484
|
-
}
|
|
2485
|
-
|
|
2486
|
-
private fun shown(digit: Char, format: Format): String = format.digits?.get(digit - '0') ?: digit.toString()
|
|
2487
|
-
|
|
2488
|
-
/**
|
|
2489
|
-
* The shortest digits that read back as \`value\`, as \`Intl\` rounds them: moved \`shift\` places
|
|
2490
|
-
* right, rounded half away from zero to \`max\` fraction digits, and trailing zeros cut to \`min\`.
|
|
2491
|
-
*/
|
|
2492
|
-
fun digits(value: Double, shift: Int, min: Int, max: Int): Pair<String, String> {
|
|
2493
|
-
// \`toString\` writes the shortest digits that read back, with an exponent past a size.
|
|
2494
|
-
val plain = java.math.BigDecimal(value.toString()).toPlainString()
|
|
2495
|
-
val int = plain.substringBefore('.')
|
|
2496
|
-
val all = (int + plain.substringAfter('.', "")).map { it - '0' }.toMutableList()
|
|
2497
|
-
var point = int.length + shift
|
|
2498
|
-
while (all.size <= point + max) all.add(0)
|
|
2499
|
-
val next = all[point + max]
|
|
2500
|
-
while (all.size > point + max) all.removeAt(all.size - 1)
|
|
2501
|
-
if (next >= 5) {
|
|
2502
|
-
var at = all.size - 1
|
|
2503
|
-
while (at >= 0 && all[at] == 9) all[at--] = 0
|
|
2504
|
-
if (at < 0) {
|
|
2505
|
-
all.add(0, 1)
|
|
2506
|
-
point += 1
|
|
2507
|
-
} else {
|
|
2508
|
-
all[at] += 1
|
|
2509
|
-
}
|
|
2510
|
-
}
|
|
2511
|
-
val frac = all.subList(point, all.size).toMutableList()
|
|
2512
|
-
while (frac.size > min && frac.last() == 0) frac.removeAt(frac.size - 1)
|
|
2513
|
-
return all.subList(0, point).dropWhile { it == 0 }.joinToString("") to frac.joinToString("")
|
|
2514
|
-
}
|
|
2515
|
-
`;
|
|
2516
|
-
const PLURAL$2 = `
|
|
2517
|
-
/** CLDR's plural operands of a number as written. */
|
|
2518
|
-
class Operands(val n: Double, val i: Double, val v: Double, val w: Double, val f: Double, val t: Double, val c: Double, val e: Double)
|
|
2519
|
-
|
|
2520
|
-
/** The arm a plural takes: the \`=N\` that \`value\` equals, else the category of \`value\` less \`offset\`. */
|
|
2521
|
-
fun plural(value: Double, offset: Double, exact: List<Pair<Double, String>>, rules: (Operands) -> String, min: Int, max: Int): String =
|
|
2522
|
-
exact.firstOrNull { it.first == value }?.second ?: category(rules, value - offset, min, max)
|
|
2523
|
-
|
|
2524
|
-
/** The category \`rules\` give \`value\`, rounded to between \`min\` and \`max\` fraction digits. */
|
|
2525
|
-
private fun category(rules: (Operands) -> String, value: Double, min: Int, max: Int): String {
|
|
2526
|
-
if (!value.isFinite()) return "other"
|
|
2527
|
-
val (written, frac) = digits(Math.abs(value), 0, min, max)
|
|
2528
|
-
val int = written.ifEmpty { "0" }
|
|
2529
|
-
val trimmed = frac.trimEnd('0')
|
|
2530
|
-
val read = { s: String -> s.toDoubleOrNull() ?: 0.0 }
|
|
2531
|
-
return rules(
|
|
2532
|
-
Operands(
|
|
2533
|
-
n = read(if (frac.isEmpty()) int else "$int.$frac"),
|
|
2534
|
-
i = read(int),
|
|
2535
|
-
v = frac.length.toDouble(),
|
|
2536
|
-
w = trimmed.length.toDouble(),
|
|
2537
|
-
f = read(frac),
|
|
2538
|
-
t = read(trimmed),
|
|
2539
|
-
c = 0.0,
|
|
2540
|
-
e = 0.0,
|
|
2541
|
-
),
|
|
2542
|
-
)
|
|
2543
|
-
}
|
|
2544
|
-
|
|
2545
|
-
/** An integer in the range, as CLDR's ranges hold. */
|
|
2546
|
-
private fun within(value: Double, from: Double, to: Double): Boolean = value % 1.0 == 0.0 && value >= from && value <= to
|
|
2547
|
-
|
|
2548
|
-
/** \`Math.round\`: half up, toward positive infinity. */
|
|
2549
|
-
fun round(value: Double): Double {
|
|
2550
|
-
val floor = Math.floor(value)
|
|
2551
|
-
return if (value - floor >= 0.5) floor + 1 else floor
|
|
2552
|
-
}
|
|
2553
|
-
`;
|
|
2554
|
-
function kotlinFile(model, target, banner) {
|
|
2555
|
-
const name = target.name;
|
|
2556
|
-
const rt = `${name}Runtime`;
|
|
2557
|
-
const names = model.messages.map((m) => [m.key, ident$2(leading(camel(m.words)))]);
|
|
2558
|
-
checkNames("kotlin", names, [
|
|
2559
|
-
"tag",
|
|
2560
|
-
"name",
|
|
2561
|
-
"ordinal",
|
|
2562
|
-
"entries",
|
|
2563
|
-
"negotiate",
|
|
2564
|
-
"SOURCE"
|
|
2565
|
-
]);
|
|
2566
|
-
const entries = model.locales.map((l) => ` /** \`${l}\`${l === model.source ? ", the source" : ""}. */\n ${entry(l)}(${lit$2(l)})`).join(",\n");
|
|
2567
|
-
const members = model.messages.map((m, i) => member$1(m, names[i][1], rt)).join("\n");
|
|
2568
|
-
const rules = model.rules.map(({ name: rule, rules }, i) => {
|
|
2569
|
-
return ` /** ${rule} */\n fun r${i}(o: Operands): String {\n${rules.map(([category, condition]) => ` if (${conditionCode(condition, {
|
|
2570
|
-
field: (op) => `o.${op}`,
|
|
2571
|
-
mod: (v, by) => `${v} % ${by}`,
|
|
2572
|
-
within: (v, from, to) => `within(${v}, ${from}, ${to})`
|
|
2573
|
-
})}) return ${lit$2(category)}\n`).join("")} return "other"\n }\n`;
|
|
2574
|
-
}).join("\n");
|
|
2575
|
-
const runtime = model.formats.length + model.rules.length === 0 ? "" : `
|
|
2576
|
-
/** What formats the messages, generated with them. */
|
|
2577
|
-
private object ${rt} {${NUMBER$2}${model.rules.length > 0 ? PLURAL$2 : ""}
|
|
2578
|
-
${model.formats.map(formatConst$2).join("\n")}${rules === "" ? "" : `\n${rules}`}}
|
|
2579
|
-
`;
|
|
2580
|
-
return `${banner}
|
|
2581
|
-
@file:Suppress("unused", "NAME_SHADOWING", "KotlinConstantConditions", "ConvertTwoComparisonsToRangeCheck")
|
|
2582
|
-
${target.package === void 0 ? "" : `\npackage ${target.package}\n`}
|
|
2583
|
-
/** A locale the catalog is written or translated in, and its messages. */
|
|
2584
|
-
enum class ${name}(val tag: String) {
|
|
2585
|
-
${entries};
|
|
2586
|
-
|
|
2587
|
-
${members}
|
|
2588
|
-
companion object {
|
|
2589
|
-
/** The locale the catalog is written in, which every message falls back to. */
|
|
2590
|
-
val SOURCE: ${name} = ${entry(model.source)}
|
|
2591
|
-
|
|
2592
|
-
/**
|
|
2593
|
-
* The best locale for what was asked, most preferred first: each tag tries an exact match, then
|
|
2594
|
-
* its parents (\`pt-BR\` finds \`pt\`), then any locale of its language. The source if none is.
|
|
2595
|
-
*/
|
|
2596
|
-
fun negotiate(requested: List<String>): ${name} {
|
|
2597
|
-
fun language(tag: String) = tag.substringBefore('-').lowercase()
|
|
2598
|
-
for (want in requested) {
|
|
2599
|
-
var tag = want
|
|
2600
|
-
while (true) {
|
|
2601
|
-
entries.firstOrNull { it.tag.equals(tag, ignoreCase = true) }?.let { return it }
|
|
2602
|
-
val cut = tag.lastIndexOf('-')
|
|
2603
|
-
if (cut < 0) break
|
|
2604
|
-
tag = tag.substring(0, cut)
|
|
2605
|
-
}
|
|
2606
|
-
entries.firstOrNull { language(it.tag) == language(want) }?.let { return it }
|
|
2607
|
-
}
|
|
2608
|
-
return SOURCE
|
|
2609
|
-
}
|
|
2610
|
-
}
|
|
2611
|
-
}
|
|
2612
|
-
${runtime}`;
|
|
2613
|
-
}
|
|
2614
2374
|
// src/tools/native/number.ts
|
|
2615
2375
|
const NUMBER_PARTS = /* @__PURE__ */ new Set([
|
|
2616
2376
|
"integer",
|
|
@@ -4015,10 +3775,22 @@ function parseRelation(source) {
|
|
|
4015
3775
|
* tree per locale it renders in, with every fallback resolved here, so the generated code has no
|
|
4016
3776
|
* lookup to do. Number formats and plural rules are read from `Intl` and CLDR once, and numbered.
|
|
4017
3777
|
*/
|
|
4018
|
-
/**
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
3778
|
+
/**
|
|
3779
|
+
* The words a target names `key` by, less the prefix that took it in (longest first): `tray.quit-app`
|
|
3780
|
+
* is quit, app under `['tray']`. Nothing when the target doesn't take the key.
|
|
3781
|
+
*/
|
|
3782
|
+
function wordsFor(target, key) {
|
|
3783
|
+
return restOf(target, key)?.split(/[.\-_]/).filter(Boolean);
|
|
3784
|
+
}
|
|
3785
|
+
/** `key` less the prefix that took it in, as written: `tray.quit-app` is `quit-app` under `['tray']`. */
|
|
3786
|
+
function restOf(target, key) {
|
|
3787
|
+
const prefix = target.keys === void 0 ? "" : [...target.keys].sort((a, b) => b.length - a.length).find((p) => key.startsWith(`${p}.`));
|
|
3788
|
+
if (prefix === void 0) return void 0;
|
|
3789
|
+
return key.slice(prefix === "" ? 0 : prefix.length + 1);
|
|
3790
|
+
}
|
|
3791
|
+
/** The locale whose text `locale` shows for `key`: its own, else the first down its fallback chain that has it. */
|
|
3792
|
+
function foundIn(key, locale, locales, asts) {
|
|
3793
|
+
return chainOf(locale, locales, locales[0]).find((l) => asts.get(l)?.has(key));
|
|
4022
3794
|
}
|
|
4023
3795
|
/** Values a file declares once each, numbered by what they hold, so locales that format alike share one. */
|
|
4024
3796
|
var Numbered = class {
|
|
@@ -4042,13 +3814,12 @@ function nativeModel(target, catalog, locales, entries) {
|
|
|
4042
3814
|
const problems = [];
|
|
4043
3815
|
const messages = [];
|
|
4044
3816
|
for (const m of catalog.messages) {
|
|
4045
|
-
const
|
|
4046
|
-
if (
|
|
4047
|
-
const words = m.key.slice(prefix === "" ? 0 : prefix.length + 1).split(/[.\-_]/).filter(Boolean);
|
|
3817
|
+
const words = wordsFor(target, m.key);
|
|
3818
|
+
if (words === void 0) continue;
|
|
4048
3819
|
const bodies = /* @__PURE__ */ new Map();
|
|
4049
3820
|
let failed = false;
|
|
4050
3821
|
for (const locale of locales) {
|
|
4051
|
-
const found =
|
|
3822
|
+
const found = foundIn(m.key, locale, locales, asts);
|
|
4052
3823
|
if (found === void 0) continue;
|
|
4053
3824
|
const body = bodies.get(found);
|
|
4054
3825
|
if (body !== void 0) {
|
|
@@ -4230,18 +4001,557 @@ function parts(elements, locale, formats, rules, pound) {
|
|
|
4230
4001
|
}
|
|
4231
4002
|
return out;
|
|
4232
4003
|
}
|
|
4233
|
-
// src/tools/native/
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4004
|
+
// src/tools/native/printf.ts
|
|
4005
|
+
/**
|
|
4006
|
+
* Messages as printf-style format strings, for the targets whose platform fills values in itself:
|
|
4007
|
+
* Android's resources and Apple's `.strings` files. Text and plain `{name}` placeholders, as `%N$s`
|
|
4008
|
+
* with N the argument's place in the message's arguments, so a translation can put them in another
|
|
4009
|
+
* order; anything `Intl` would format fails with the fix.
|
|
4010
|
+
*/
|
|
4011
|
+
/** The context and what each argument holds, on one line, for the comment above an entry. */
|
|
4012
|
+
function noteOf(m) {
|
|
4013
|
+
return docText([m.context, ...[...m.args.keys()].map((a) => m.argDocs.has(a) ? `${a}: ${m.argDocs.get(a).replace(/\.$/, "")}.` : void 0)].filter((l) => l !== void 0 && l !== "").join(" "));
|
|
4014
|
+
}
|
|
4015
|
+
/** What a plural's arms may hold as its count: `#`, or the count as a plain `:number` or `:integer`. */
|
|
4016
|
+
function isCount(el, count) {
|
|
4017
|
+
if (el.type === TYPE$1.pound) return true;
|
|
4018
|
+
if (!isNumberElement(el) || el.value !== count) return false;
|
|
4019
|
+
const options = Object.entries(numberOptions(el) ?? {});
|
|
4020
|
+
return options.length === 0 || options.length === 1 && options[0][0] === "maximumFractionDigits" && options[0][1] === 0;
|
|
4021
|
+
}
|
|
4022
|
+
/** `elements` as a format string over `args`; inside a plural, its `count` is `%N$d`. */
|
|
4023
|
+
function formatString(elements, args, printf, count) {
|
|
4024
|
+
const formatted = args.length > 0;
|
|
4025
|
+
const place = (el, name, conversion) => {
|
|
4026
|
+
const at = args.indexOf(name);
|
|
4027
|
+
if (at < 0) throw new Error(`{${name}} is not an argument of this message: say it in the source text too`);
|
|
4028
|
+
const { isolate } = placeholderOf(el, name);
|
|
4029
|
+
const spec = `%${at + 1}$${conversion}`;
|
|
4030
|
+
return isolate === void 0 ? spec : `${printf.escape(isolate[0], formatted)}${spec}${printf.escape(isolate[1], formatted)}`;
|
|
4031
|
+
};
|
|
4032
|
+
let out = "";
|
|
4033
|
+
for (const el of elements) {
|
|
4034
|
+
if (count !== void 0 && isCount(el, count)) {
|
|
4035
|
+
out += place(el, count, "d");
|
|
4036
|
+
continue;
|
|
4037
|
+
}
|
|
4038
|
+
if (isDateElement(el) || isTimeElement(el)) throw new Error(`{${el.value}} is a date, which ${printf.holder} don't format: pass it formatted, as a plain {${el.value}}, ${LEAVE_OUT}`);
|
|
4039
|
+
if (isNumberElement(el)) throw new Error(`{${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}`);
|
|
4040
|
+
switch (el.type) {
|
|
4041
|
+
case TYPE$1.literal:
|
|
4042
|
+
out += printf.escape(el.value, formatted);
|
|
4043
|
+
break;
|
|
4044
|
+
case TYPE$1.argument:
|
|
4045
|
+
out += place(el, el.value, printf.string);
|
|
4046
|
+
break;
|
|
4047
|
+
case TYPE$1.pound:
|
|
4048
|
+
out += "#";
|
|
4049
|
+
break;
|
|
4050
|
+
case TYPE$1.select: throw new Error(`{${el.value}} is a select, which ${printf.holder} can't hold: write a key per case and pick one in code, ${LEAVE_OUT}`);
|
|
4051
|
+
case TYPE$1.plural: throw new Error(printf.plural(el.value));
|
|
4052
|
+
case TYPE$1.tag: throw new Error(`<${el.value}> is a tag, and ${printf.holder} here hold plain text: say it without the tag, ${LEAVE_OUT}`);
|
|
4053
|
+
default: throw new Error(`prosebook: no ${printf.platform} writer for ICU element type ${el.type}`);
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
return out;
|
|
4057
|
+
}
|
|
4058
|
+
// src/tools/native/android.ts
|
|
4059
|
+
/**
|
|
4060
|
+
* The catalog as Android string resources: `values/<name>.xml` for the source and
|
|
4061
|
+
* `values-<qualifier>/<name>.xml` for every other locale, each holding what that locale shows, every
|
|
4062
|
+
* fallback resolved. An app with the Kotlin target still needs these: its manifest, shortcuts and
|
|
4063
|
+
* other XML resources name strings as `@string/…`, which only a resource answers.
|
|
4064
|
+
*
|
|
4065
|
+
* Android fills values in with `String.format` and picks a plural's arm itself, from its own CLDR
|
|
4066
|
+
* rules, so this takes a narrower subset than the other targets, written straight from the ICU tree:
|
|
4067
|
+
* text, plain `{name}` placeholders, and one cardinal plural, with the text around it in each arm.
|
|
4068
|
+
*/
|
|
4069
|
+
const KEYWORDS$3 = new Set("abstract assert boolean break byte case catch char class const continue default do double else enum extends false final finally float for goto if implements import instanceof int interface long native new null package private protected public return short static strictfp super switch synchronized this throw throws transient true try void volatile while".split(" "));
|
|
4070
|
+
/**
|
|
4071
|
+
* A locale as Android's resource qualifier: `de`, `pt-rBR`, and BCP 47's own form, `b+sr+Latn`,
|
|
4072
|
+
* past what the old form holds (a script, a numeric region, a variant).
|
|
4073
|
+
*/
|
|
4074
|
+
function qualifierOf(locale) {
|
|
4075
|
+
const [language = "", ...rest] = locale.split(/[-_]/);
|
|
4076
|
+
const subtags = [language.toLowerCase(), ...rest.map((s) => s.length === 4 && /^[a-z]+$/i.test(s) ? s.charAt(0).toUpperCase() + s.slice(1).toLowerCase() : s.length === 2 ? s.toUpperCase() : s.toLowerCase())];
|
|
4077
|
+
if (!(/^[a-z]{2,3}$/.test(subtags[0]) && (rest.length === 0 || rest.length === 1 && /^[A-Z]{2}$/.test(subtags[1])))) return `b+${subtags.join("+")}`;
|
|
4078
|
+
return rest.length === 0 ? subtags[0] : `${subtags[0]}-r${subtags[1]}`;
|
|
4079
|
+
}
|
|
4080
|
+
/** Whitespace as aapt2 reads it: collapsed to one space, and trimmed at the ends, outside double quotes. */
|
|
4081
|
+
const LOOSE = /^[ \t\n\v\f\r]|[ \t\n\v\f\r]$|[ \t\n\v\f\r]{2}/;
|
|
4082
|
+
/**
|
|
4083
|
+
* Text as a resource holds it: XML's escapes, then Android's (`\`, `'`, `"`, a newline or a tab).
|
|
4084
|
+
* In a string that is formatted, `%` is doubled, so `String.format` writes one.
|
|
4085
|
+
*/
|
|
4086
|
+
function escape$1(text, formatted) {
|
|
4087
|
+
const out = text.replaceAll("\\", "\\\\").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("'", "\\'").replaceAll("\"", "\\\"").replaceAll("\n", "\\n").replaceAll(" ", "\\t");
|
|
4088
|
+
return formatted ? out.replaceAll("%", "%%") : out;
|
|
4089
|
+
}
|
|
4090
|
+
/**
|
|
4091
|
+
* A whole value: a leading `@` or `?` escaped, since either would make it a reference, and the value
|
|
4092
|
+
* quoted where its spaces would otherwise be collapsed or trimmed.
|
|
4093
|
+
*/
|
|
4094
|
+
function value$3(written) {
|
|
4095
|
+
const out = /^[@?]/.test(written) ? `\\${written}` : written;
|
|
4096
|
+
return LOOSE.test(out) ? `"${out}"` : out;
|
|
4097
|
+
}
|
|
4098
|
+
const PRINTF$1 = {
|
|
4099
|
+
platform: "Android",
|
|
4100
|
+
formatter: "String.format",
|
|
4101
|
+
holder: "Android resources",
|
|
4102
|
+
string: "s",
|
|
4103
|
+
escape: escape$1,
|
|
4104
|
+
plural: (name) => `{${name}} is a second plural in the message, and Android's <plurals> picks on one count: split it into a key per count, ${LEAVE_OUT}`
|
|
4105
|
+
};
|
|
4106
|
+
/**
|
|
4107
|
+
* A message as `<string>`, or `<plurals>` when it holds a plural. Android's picks between whole
|
|
4108
|
+
* messages, so the text around the plural goes into each arm: the pseudo-locale's brackets, or a
|
|
4109
|
+
* sentence the plural ends.
|
|
4110
|
+
*/
|
|
4111
|
+
function resource(name, ast, args) {
|
|
4112
|
+
const at = ast.findIndex((el) => el.type === TYPE$1.plural);
|
|
4113
|
+
const plural = ast[at];
|
|
4114
|
+
if (plural?.type !== TYPE$1.plural) {
|
|
4115
|
+
const written = formatString(ast, args, PRINTF$1);
|
|
4116
|
+
return ` <string name="${name}"${args.length === 0 && written.includes("%") ? " formatted=\"false\"" : ""}>${value$3(written)}</string>`;
|
|
4117
|
+
}
|
|
4118
|
+
const [before, after] = [ast.slice(0, at), ast.slice(at + 1)];
|
|
4119
|
+
const named = `{${plural.value}}`;
|
|
4120
|
+
if (plural.pluralType === "ordinal") throw new Error(`${named} is an ordinal, and Android's <plurals> are cardinal only: write a key per case and pick one in code, ${LEAVE_OUT}`);
|
|
4121
|
+
if (plural.offset !== 0) throw new Error(`${named} has an offset, which Android's <plurals> don't: count without it, and say the rest in each arm, ${LEAVE_OUT}`);
|
|
4122
|
+
const selection = selectionOf(plural);
|
|
4123
|
+
if (selection !== void 0 && (selection.fn.name !== "integer" || Object.keys(selection.fn.options).length > 0)) throw new Error(`${named} selects with :${selection.fn.name} and its options, and Android selects on the count as an integer: select with a bare :number or :integer, ${LEAVE_OUT}`);
|
|
4124
|
+
const exact = Object.keys(plural.options).filter((k) => k.startsWith("="));
|
|
4125
|
+
if (exact.length > 0) throw new Error(`${named} has the arm ${exact.join(", ")}, and Android's <plurals> have none for one number (its zero is a category, which most languages never pick): say it in a category's arm, or write a key for it and pick that in code, ${LEAVE_OUT}`);
|
|
4126
|
+
return ` <plurals name="${name}">\n${Object.entries(plural.options).map(([category, option]) => ` <item quantity="${category}">${value$3(formatString([
|
|
4127
|
+
...before,
|
|
4128
|
+
...option.value,
|
|
4129
|
+
...after
|
|
4130
|
+
], args, PRINTF$1, plural.value))}</item>`).join("\n")}\n </plurals>`;
|
|
4131
|
+
}
|
|
4132
|
+
/** The context and what each argument holds, as the comment above an entry. */
|
|
4133
|
+
function comment(m) {
|
|
4134
|
+
const said = noteOf(m);
|
|
4135
|
+
return said === "" ? "" : ` <!-- ${said.replace(/-{2,}/g, "-")} -->\n`;
|
|
4136
|
+
}
|
|
4137
|
+
/** The files `target` writes, by path, or a problem for every message it can't hold. */
|
|
4138
|
+
function androidResources(target, catalog, locales, entries, banner) {
|
|
4139
|
+
const source = locales[0];
|
|
4140
|
+
const asts = new Map([...entries].map(([locale, list]) => [locale, new Map(list)]));
|
|
4141
|
+
const taken = catalog.messages.flatMap((m) => {
|
|
4142
|
+
const words = wordsFor(target, m.key);
|
|
4143
|
+
return words === void 0 ? [] : [{
|
|
4144
|
+
m,
|
|
4145
|
+
name: leading(snake(words))
|
|
4146
|
+
}];
|
|
4147
|
+
});
|
|
4148
|
+
checkNames("android", taken.map(({ m, name }) => [m.key, name]), []);
|
|
4149
|
+
const problems = taken.filter((t) => KEYWORDS$3.has(t.name)).map(({ m, name }) => ({
|
|
4150
|
+
severity: "error",
|
|
4151
|
+
file: m.file,
|
|
4152
|
+
key: m.key,
|
|
4153
|
+
message: `android: ${m.key} is named ${name}, which Java keeps for itself: rename it`
|
|
4154
|
+
}));
|
|
4155
|
+
const bodies = new Map(locales.map((l) => [l, []]));
|
|
4156
|
+
const failed = /* @__PURE__ */ new Set();
|
|
4157
|
+
for (const { m, name } of taken) {
|
|
4158
|
+
const args = [...m.args.keys()];
|
|
4159
|
+
const written = /* @__PURE__ */ new Map();
|
|
4160
|
+
for (const locale of locales) {
|
|
4161
|
+
const found = foundIn(m.key, locale, locales, asts);
|
|
4162
|
+
if (failed.has(m.key)) break;
|
|
4163
|
+
if (found === void 0) continue;
|
|
4164
|
+
let entry = written.get(found);
|
|
4165
|
+
if (entry === void 0) {
|
|
4166
|
+
try {
|
|
4167
|
+
entry = `${comment(m)}${resource(name, asts.get(found).get(m.key), args)}`;
|
|
4168
|
+
} catch (e) {
|
|
4169
|
+
failed.add(m.key);
|
|
4170
|
+
problems.push({
|
|
4171
|
+
severity: "error",
|
|
4172
|
+
file: fileOf(m, found, source),
|
|
4173
|
+
key: m.key,
|
|
4174
|
+
message: `android: ${e.message}`
|
|
4175
|
+
});
|
|
4176
|
+
continue;
|
|
4177
|
+
}
|
|
4178
|
+
written.set(found, entry);
|
|
4179
|
+
}
|
|
4180
|
+
bodies.get(locale).push(entry);
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
const head = `<!-- ${banner.replace(/^\/\/ /, "")} -->`;
|
|
4184
|
+
return {
|
|
4185
|
+
files: new Map(locales.map((locale) => {
|
|
4186
|
+
const folder = locale === source ? "values" : `values-${qualifierOf(locale)}`;
|
|
4187
|
+
const body = bodies.get(locale).join("\n");
|
|
4188
|
+
const xml = `<?xml version="1.0" encoding="utf-8"?>\n${head}\n<resources>\n${body}${body === "" ? "" : "\n"}</resources>\n`;
|
|
4189
|
+
return [join(target.out, folder, `${target.name}.xml`), xml];
|
|
4190
|
+
})),
|
|
4191
|
+
problems
|
|
4192
|
+
};
|
|
4193
|
+
}
|
|
4194
|
+
// src/tools/native/apple.ts
|
|
4195
|
+
/**
|
|
4196
|
+
* The catalog as Apple `.strings` tables: `<locale>.lproj/<name>.strings` per locale, each holding
|
|
4197
|
+
* what that locale shows, every fallback resolved. Plain `.strings` and not a String Catalog, which
|
|
4198
|
+
* only Xcode compiles: an app bundled by other tools copies these into its `Resources` as they are.
|
|
4199
|
+
*
|
|
4200
|
+
* The Swift target covers what code shows; these are for what the system reads from a bundle. With
|
|
4201
|
+
* `plist`, the table is `InfoPlist`: the permission prompts and names in that Info.plist, an entry
|
|
4202
|
+
* per key it has, which its own text must match in the source locale.
|
|
4203
|
+
*/
|
|
4204
|
+
/** Text inside a `.strings` file's quotes; `%` doubled where the string has arguments. */
|
|
4205
|
+
function escape(text, formatted) {
|
|
4206
|
+
const out = text.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\n", "\\n").replaceAll("\r", "\\r").replaceAll(" ", "\\t");
|
|
4207
|
+
return formatted ? out.replaceAll("%", "%%") : out;
|
|
4208
|
+
}
|
|
4209
|
+
const PRINTF = {
|
|
4210
|
+
platform: "Apple",
|
|
4211
|
+
formatter: "String(format:)",
|
|
4212
|
+
holder: ".strings files",
|
|
4213
|
+
string: "@",
|
|
4214
|
+
escape,
|
|
4215
|
+
plural: (name) => `{${name}} is a plural, which a .strings file can't hold (it takes a .stringsdict, which this target doesn't write): show it from the Swift target, ${LEAVE_OUT}`
|
|
4216
|
+
};
|
|
4217
|
+
const ENTITIES = {
|
|
4218
|
+
amp: "&",
|
|
4219
|
+
lt: "<",
|
|
4220
|
+
gt: ">",
|
|
4221
|
+
quot: "\"",
|
|
4222
|
+
apos: "'"
|
|
4223
|
+
};
|
|
4224
|
+
/** An XML plist's string values by key, read as its text is: every `<key>` followed by a `<string>`. */
|
|
4225
|
+
function plistStrings(xml) {
|
|
4226
|
+
const strings = /* @__PURE__ */ new Map();
|
|
4227
|
+
for (const [, key, value] of xml.matchAll(/<key>([^<]*)<\/key>\s*(?:<string>([^<]*)<\/string>|<string\/>)/g)) strings.set(key, (value ?? "").replace(/&(amp|lt|gt|quot|apos|#x?[0-9a-f]+);/gi, (_, e) => e.startsWith("#") ? String.fromCodePoint(Number.parseInt(e.slice(/x/i.test(e[1]) ? 2 : 1), /x/i.test(e[1]) ? 16 : 10)) : ENTITIES[e.toLowerCase()]));
|
|
4228
|
+
return strings;
|
|
4229
|
+
}
|
|
4230
|
+
/** The files `target` writes, by path, or a problem for every message it can't hold. */
|
|
4231
|
+
function appleStrings(target, root, catalog, locales, entries, banner) {
|
|
4232
|
+
const source = locales[0];
|
|
4233
|
+
const asts = new Map([...entries].map(([locale, list]) => [locale, new Map(list)]));
|
|
4234
|
+
const problems = [];
|
|
4235
|
+
const error = (m, message, file = m.file) => problems.push({
|
|
4236
|
+
severity: "error",
|
|
4237
|
+
file,
|
|
4238
|
+
key: m.key,
|
|
4239
|
+
message: `apple: ${message}`
|
|
4240
|
+
});
|
|
4241
|
+
const plist = target.plist;
|
|
4242
|
+
const shown = plist === void 0 ? "" : relative(root, plist).replaceAll("\\", "/");
|
|
4243
|
+
if (plist !== void 0 && !existsSync(plist)) throw new Error(`apple: ${shown} is missing: name the Info.plist the table translates`);
|
|
4244
|
+
const strings = plist === void 0 ? void 0 : plistStrings(readFileSync(plist, "utf8"));
|
|
4245
|
+
const byLower = new Map([...strings?.keys() ?? []].map((k) => [k.toLowerCase(), k]));
|
|
4246
|
+
const taken = catalog.messages.flatMap((m) => {
|
|
4247
|
+
const rest = restOf(target, m.key);
|
|
4248
|
+
if (rest === void 0) return [];
|
|
4249
|
+
if (strings === void 0) return [{
|
|
4250
|
+
m,
|
|
4251
|
+
name: rest
|
|
4252
|
+
}];
|
|
4253
|
+
const name = byLower.get(wordsFor(target, m.key).join("").toLowerCase());
|
|
4254
|
+
if (name === void 0) {
|
|
4255
|
+
error(m, `${m.key} names no string in ${shown}: spell one of its keys, as ns-camera-usage-description is NSCameraUsageDescription, or add it there`);
|
|
4256
|
+
return [];
|
|
4257
|
+
}
|
|
4258
|
+
return [{
|
|
4259
|
+
m,
|
|
4260
|
+
name
|
|
4261
|
+
}];
|
|
4262
|
+
});
|
|
4263
|
+
checkNames("apple", taken.map(({ m, name }) => [m.key, name]), []);
|
|
4264
|
+
for (const { m, name } of taken) {
|
|
4265
|
+
if (strings === void 0) continue;
|
|
4266
|
+
if (m.args.size > 0) {
|
|
4267
|
+
error(m, `${name} is shown as it is written, with nothing filled in: say it without {${[...m.args.keys()].join("}, {")}}`);
|
|
4268
|
+
continue;
|
|
4269
|
+
}
|
|
4270
|
+
const text = staticLiteral(asts.get(source)?.get(m.key) ?? []);
|
|
4271
|
+
if (text !== void 0 && text !== strings.get(name)) error(m, `${shown} says ${JSON.stringify(strings.get(name))} for ${name}: make it ${JSON.stringify(text)}, as the catalog says`);
|
|
4272
|
+
}
|
|
4273
|
+
const lines = new Map(locales.map((l) => [l, []]));
|
|
4274
|
+
const failed = new Set(problems.map((p) => p.key));
|
|
4275
|
+
for (const { m, name } of taken) {
|
|
4276
|
+
if (failed.has(m.key)) continue;
|
|
4277
|
+
const args = [...m.args.keys()];
|
|
4278
|
+
const note = noteOf(m);
|
|
4279
|
+
const head = note === "" ? "" : `/* ${note.replaceAll("*/", "* /")} */\n`;
|
|
4280
|
+
const written = /* @__PURE__ */ new Map();
|
|
4281
|
+
for (const locale of locales) {
|
|
4282
|
+
const found = foundIn(m.key, locale, locales, asts);
|
|
4283
|
+
if (found === void 0) continue;
|
|
4284
|
+
let entry = written.get(found);
|
|
4285
|
+
if (entry === void 0) {
|
|
4286
|
+
try {
|
|
4287
|
+
entry = `${head}"${escape(name, false)}" = "${formatString(asts.get(found).get(m.key), args, PRINTF)}";\n`;
|
|
4288
|
+
} catch (e) {
|
|
4289
|
+
error(m, e.message, fileOf(m, found, source));
|
|
4290
|
+
break;
|
|
4291
|
+
}
|
|
4292
|
+
written.set(found, entry);
|
|
4293
|
+
}
|
|
4294
|
+
lines.get(locale).push(entry);
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4297
|
+
const top = `/* ${banner.replace(/^\/\/ /, "")} */\n`;
|
|
4298
|
+
return {
|
|
4299
|
+
files: new Map(locales.map((locale) => [join(target.out, `${locale}.lproj`, `${target.name}.strings`), [top, ...lines.get(locale)].join("\n")])),
|
|
4300
|
+
problems
|
|
4301
|
+
};
|
|
4302
|
+
}
|
|
4303
|
+
// src/tools/native/kotlin.ts
|
|
4304
|
+
const KEYWORDS$2 = new Set("as break class continue do else false for fun if in interface is null object package return super this throw true try typealias typeof val var when while".split(" "));
|
|
4305
|
+
const ident$2 = (name) => KEYWORDS$2.has(name) ? `\`${name}\`` : name;
|
|
4306
|
+
const lit$2 = (value) => stringLiteral(value, "kotlin");
|
|
4307
|
+
const entry = (tag) => leading(upper(wordsOf(tag)));
|
|
4308
|
+
const argName$2 = (name) => ident$2(leading(camel(wordsOf(name))));
|
|
4309
|
+
function doc$3(lines, indent) {
|
|
4310
|
+
return `${indent}/**\n${lines.map((l) => l.replaceAll("*/", "*\\/")).map((l) => l === "" ? `${indent} *` : `${indent} * ${l}`).join("\n")}\n${indent} */\n`;
|
|
4311
|
+
}
|
|
4312
|
+
/** The parts as an expression of type `String`, with `rt` the runtime object. */
|
|
4313
|
+
function expr$2(parts, indent, rt) {
|
|
4314
|
+
if (parts.length === 0) return "\"\"";
|
|
4315
|
+
return parts.map((p) => p.type === "text" ? lit$2(p.value) : value$2(p, indent, rt)).join(" + ");
|
|
4316
|
+
}
|
|
4317
|
+
function value$2(part, indent, rt) {
|
|
4318
|
+
const inner = `${indent} `;
|
|
4319
|
+
if (part.type === "string") return argName$2(part.arg);
|
|
4320
|
+
if (part.type === "number") return `${rt}.number(${part.offset === 0 ? argName$2(part.arg) : `${argName$2(part.arg)} - ${float(part.offset)}`}, ${rt}.F${part.format}, ${lit$2(part.fallback)})`;
|
|
4321
|
+
if (part.type === "select") {
|
|
4322
|
+
const arms = part.arms.map((a) => `${inner}${lit$2(a.key)} -> ${expr$2(a.parts, inner, rt)}\n`).join("");
|
|
4323
|
+
return `when (${argName$2(part.arg)}) {\n${arms}${inner}else -> ${expr$2(part.other, inner, rt)}\n${indent}}`;
|
|
4324
|
+
}
|
|
4325
|
+
if (part.arms.length + part.exact.length === 0) return expr$2(part.other, indent, rt);
|
|
4326
|
+
const arg = argName$2(part.arg);
|
|
4327
|
+
const on = part.on === "round" ? `${rt}.round(${arg})` : part.on === "percent" ? `${arg} * 100.0` : arg;
|
|
4328
|
+
const { minFrac, maxFrac } = part.rounding;
|
|
4329
|
+
const exact = part.exact.map((e) => `${float(e.value)} to ${lit$2(`=${e.value}`)}`).join(", ");
|
|
4330
|
+
const arms = [...part.exact.map((e) => ({
|
|
4331
|
+
key: `=${e.value}`,
|
|
4332
|
+
parts: e.parts
|
|
4333
|
+
})), ...part.arms].map((a) => `${inner}${lit$2(a.key)} -> ${expr$2(a.parts, inner, rt)}\n`).join("");
|
|
4334
|
+
return `when (${`${rt}.plural(${on}, ${float(part.offset)}, listOf(${exact}), ${rt}::r${part.rules}, ${minFrac}, ${maxFrac})`}) {\n${arms}${inner}else -> ${expr$2(part.other, inner, rt)}\n${indent}}`;
|
|
4335
|
+
}
|
|
4336
|
+
function member$1(m, name, rt) {
|
|
4337
|
+
const branches = m.bodies.map((b) => ` ${b.locales.map(entry).join(", ")} -> ${expr$2(b.parts, " ", rt)}\n`).join("");
|
|
4338
|
+
const argDocs = m.args.filter((a) => a.doc !== void 0).map((a) => `@param ${argName$2(a.name)} ${a.doc}`);
|
|
4339
|
+
const head = doc$3(argDocs.length > 0 ? [
|
|
4340
|
+
...m.doc,
|
|
4341
|
+
"",
|
|
4342
|
+
...argDocs
|
|
4343
|
+
] : m.doc, " ");
|
|
4344
|
+
const body = `when (this) {\n${branches} }`;
|
|
4345
|
+
if (m.args.length === 0) return `${head} val ${name}: String\n get() = ${body}\n`;
|
|
4346
|
+
return `${head} fun ${name}(${m.args.map((a) => `${argName$2(a.name)}: ${a.type === "number" ? "Number" : "String"}`).join(", ")}): String {\n${m.args.filter((a) => a.type === "number").map((a) => ` val ${argName$2(a.name)} = ${argName$2(a.name)}.toDouble()\n`).join("")} return ${body}\n }\n`;
|
|
4347
|
+
}
|
|
4348
|
+
function formatConst$2(spec, i) {
|
|
4349
|
+
const affix = ([prefix, suffix]) => `${lit$2(prefix)} to ${lit$2(suffix)}`;
|
|
4350
|
+
return ` val F${i} = Format(\n${[
|
|
4351
|
+
spec.minInt,
|
|
4352
|
+
spec.minFrac,
|
|
4353
|
+
spec.maxFrac,
|
|
4354
|
+
spec.shift,
|
|
4355
|
+
spec.grouping.primary,
|
|
4356
|
+
spec.grouping.secondary,
|
|
4357
|
+
spec.grouping.min,
|
|
4358
|
+
lit$2(spec.decimal),
|
|
4359
|
+
lit$2(spec.group),
|
|
4360
|
+
spec.digits === void 0 ? "null" : `listOf(${spec.digits.map(lit$2).join(", ")})`,
|
|
4361
|
+
affix(spec.positive),
|
|
4362
|
+
affix(spec.negative),
|
|
4363
|
+
affix(spec.zero),
|
|
4364
|
+
affix(spec.negativeZero),
|
|
4365
|
+
lit$2(spec.infinity),
|
|
4366
|
+
lit$2(spec.negativeInfinity)
|
|
4367
|
+
].map((f) => ` ${f}`).join(",\n")},\n )\n`;
|
|
4368
|
+
}
|
|
4369
|
+
const NUMBER$2 = `
|
|
4370
|
+
/** A number format, taken apart when the file was generated. */
|
|
4371
|
+
class Format(
|
|
4372
|
+
val minInt: Int,
|
|
4373
|
+
val minFrac: Int,
|
|
4374
|
+
val maxFrac: Int,
|
|
4375
|
+
val shift: Int,
|
|
4376
|
+
val primary: Int,
|
|
4377
|
+
val secondary: Int,
|
|
4378
|
+
val minGroup: Int,
|
|
4379
|
+
val decimal: String,
|
|
4380
|
+
val group: String,
|
|
4381
|
+
val digits: List<String>?,
|
|
4382
|
+
val positive: Pair<String, String>,
|
|
4383
|
+
val negative: Pair<String, String>,
|
|
4384
|
+
val zero: Pair<String, String>,
|
|
4385
|
+
val negativeZero: Pair<String, String>,
|
|
4386
|
+
val infinity: String,
|
|
4387
|
+
val negativeInfinity: String,
|
|
4388
|
+
)
|
|
4389
|
+
|
|
4390
|
+
/** \`value\` as \`format\` writes it, or \`fallback\` where it isn't a number. */
|
|
4391
|
+
fun number(value: Double, format: Format, fallback: String): String {
|
|
4392
|
+
if (value.isNaN()) return fallback
|
|
4393
|
+
if (value.isInfinite()) return if (value > 0) format.infinity else format.negativeInfinity
|
|
4394
|
+
val (written, frac) = digits(Math.abs(value), format.shift, format.minFrac, format.maxFrac)
|
|
4395
|
+
val zero = (written + frac).all { it == '0' }
|
|
4396
|
+
val negative = value.toRawBits() < 0
|
|
4397
|
+
val (prefix, suffix) = if (zero) (if (negative) format.negativeZero else format.zero) else (if (negative) format.negative else format.positive)
|
|
4398
|
+
val int = written.padStart(maxOf(format.minInt, 1), '0')
|
|
4399
|
+
val grouped = format.minGroup > 0 && int.length >= format.primary + format.minGroup
|
|
4400
|
+
val out = StringBuilder(prefix)
|
|
4401
|
+
for ((i, digit) in int.withIndex()) {
|
|
4402
|
+
val left = int.length - i
|
|
4403
|
+
if (grouped && i > 0 && (left == format.primary || (left > format.primary && (left - format.primary) % format.secondary == 0))) {
|
|
4404
|
+
out.append(format.group)
|
|
4405
|
+
}
|
|
4406
|
+
out.append(shown(digit, format))
|
|
4407
|
+
}
|
|
4408
|
+
if (frac.isNotEmpty()) {
|
|
4409
|
+
out.append(format.decimal)
|
|
4410
|
+
for (digit in frac) out.append(shown(digit, format))
|
|
4411
|
+
}
|
|
4412
|
+
return out.append(suffix).toString()
|
|
4413
|
+
}
|
|
4414
|
+
|
|
4415
|
+
private fun shown(digit: Char, format: Format): String = format.digits?.get(digit - '0') ?: digit.toString()
|
|
4416
|
+
|
|
4417
|
+
/**
|
|
4418
|
+
* The shortest digits that read back as \`value\`, as \`Intl\` rounds them: moved \`shift\` places
|
|
4419
|
+
* right, rounded half away from zero to \`max\` fraction digits, and trailing zeros cut to \`min\`.
|
|
4420
|
+
*/
|
|
4421
|
+
fun digits(value: Double, shift: Int, min: Int, max: Int): Pair<String, String> {
|
|
4422
|
+
// \`toString\` writes the shortest digits that read back, with an exponent past a size.
|
|
4423
|
+
val plain = java.math.BigDecimal(value.toString()).toPlainString()
|
|
4424
|
+
val int = plain.substringBefore('.')
|
|
4425
|
+
val all = (int + plain.substringAfter('.', "")).map { it - '0' }.toMutableList()
|
|
4426
|
+
var point = int.length + shift
|
|
4427
|
+
while (all.size <= point + max) all.add(0)
|
|
4428
|
+
val next = all[point + max]
|
|
4429
|
+
while (all.size > point + max) all.removeAt(all.size - 1)
|
|
4430
|
+
if (next >= 5) {
|
|
4431
|
+
var at = all.size - 1
|
|
4432
|
+
while (at >= 0 && all[at] == 9) all[at--] = 0
|
|
4433
|
+
if (at < 0) {
|
|
4434
|
+
all.add(0, 1)
|
|
4435
|
+
point += 1
|
|
4436
|
+
} else {
|
|
4437
|
+
all[at] += 1
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
4440
|
+
val frac = all.subList(point, all.size).toMutableList()
|
|
4441
|
+
while (frac.size > min && frac.last() == 0) frac.removeAt(frac.size - 1)
|
|
4442
|
+
return all.subList(0, point).dropWhile { it == 0 }.joinToString("") to frac.joinToString("")
|
|
4443
|
+
}
|
|
4444
|
+
`;
|
|
4445
|
+
const PLURAL$2 = `
|
|
4446
|
+
/** CLDR's plural operands of a number as written. */
|
|
4447
|
+
class Operands(val n: Double, val i: Double, val v: Double, val w: Double, val f: Double, val t: Double, val c: Double, val e: Double)
|
|
4448
|
+
|
|
4449
|
+
/** The arm a plural takes: the \`=N\` that \`value\` equals, else the category of \`value\` less \`offset\`. */
|
|
4450
|
+
fun plural(value: Double, offset: Double, exact: List<Pair<Double, String>>, rules: (Operands) -> String, min: Int, max: Int): String =
|
|
4451
|
+
exact.firstOrNull { it.first == value }?.second ?: category(rules, value - offset, min, max)
|
|
4452
|
+
|
|
4453
|
+
/** The category \`rules\` give \`value\`, rounded to between \`min\` and \`max\` fraction digits. */
|
|
4454
|
+
private fun category(rules: (Operands) -> String, value: Double, min: Int, max: Int): String {
|
|
4455
|
+
if (!value.isFinite()) return "other"
|
|
4456
|
+
val (written, frac) = digits(Math.abs(value), 0, min, max)
|
|
4457
|
+
val int = written.ifEmpty { "0" }
|
|
4458
|
+
val trimmed = frac.trimEnd('0')
|
|
4459
|
+
val read = { s: String -> s.toDoubleOrNull() ?: 0.0 }
|
|
4460
|
+
return rules(
|
|
4461
|
+
Operands(
|
|
4462
|
+
n = read(if (frac.isEmpty()) int else "$int.$frac"),
|
|
4463
|
+
i = read(int),
|
|
4464
|
+
v = frac.length.toDouble(),
|
|
4465
|
+
w = trimmed.length.toDouble(),
|
|
4466
|
+
f = read(frac),
|
|
4467
|
+
t = read(trimmed),
|
|
4468
|
+
c = 0.0,
|
|
4469
|
+
e = 0.0,
|
|
4470
|
+
),
|
|
4471
|
+
)
|
|
4472
|
+
}
|
|
4473
|
+
|
|
4474
|
+
/** An integer in the range, as CLDR's ranges hold. */
|
|
4475
|
+
private fun within(value: Double, from: Double, to: Double): Boolean = value % 1.0 == 0.0 && value >= from && value <= to
|
|
4476
|
+
|
|
4477
|
+
/** \`Math.round\`: half up, toward positive infinity. */
|
|
4478
|
+
fun round(value: Double): Double {
|
|
4479
|
+
val floor = Math.floor(value)
|
|
4480
|
+
return if (value - floor >= 0.5) floor + 1 else floor
|
|
4481
|
+
}
|
|
4482
|
+
`;
|
|
4483
|
+
function kotlinFile(model, target, banner) {
|
|
4484
|
+
const name = target.name;
|
|
4485
|
+
const rt = `${name}Runtime`;
|
|
4486
|
+
const names = model.messages.map((m) => [m.key, ident$2(leading(camel(m.words)))]);
|
|
4487
|
+
checkNames("kotlin", names, [
|
|
4488
|
+
"tag",
|
|
4489
|
+
"name",
|
|
4490
|
+
"ordinal",
|
|
4491
|
+
"entries",
|
|
4492
|
+
"negotiate",
|
|
4493
|
+
"SOURCE"
|
|
4494
|
+
]);
|
|
4495
|
+
const entries = model.locales.map((l) => ` /** \`${l}\`${l === model.source ? ", the source" : ""}. */\n ${entry(l)}(${lit$2(l)})`).join(",\n");
|
|
4496
|
+
const members = model.messages.map((m, i) => member$1(m, names[i][1], rt)).join("\n");
|
|
4497
|
+
const rules = model.rules.map(({ name: rule, rules }, i) => {
|
|
4498
|
+
return ` /** ${rule} */\n fun r${i}(o: Operands): String {\n${rules.map(([category, condition]) => ` if (${conditionCode(condition, {
|
|
4499
|
+
field: (op) => `o.${op}`,
|
|
4500
|
+
mod: (v, by) => `${v} % ${by}`,
|
|
4501
|
+
within: (v, from, to) => `within(${v}, ${from}, ${to})`
|
|
4502
|
+
})}) return ${lit$2(category)}\n`).join("")} return "other"\n }\n`;
|
|
4503
|
+
}).join("\n");
|
|
4504
|
+
const runtime = model.formats.length + model.rules.length === 0 ? "" : `
|
|
4505
|
+
/** What formats the messages, generated with them. */
|
|
4506
|
+
private object ${rt} {${NUMBER$2}${model.rules.length > 0 ? PLURAL$2 : ""}
|
|
4507
|
+
${model.formats.map(formatConst$2).join("\n")}${rules === "" ? "" : `\n${rules}`}}
|
|
4508
|
+
`;
|
|
4509
|
+
return `${banner}
|
|
4510
|
+
@file:Suppress("unused", "NAME_SHADOWING", "KotlinConstantConditions", "ConvertTwoComparisonsToRangeCheck")
|
|
4511
|
+
${target.package === void 0 ? "" : `\npackage ${target.package}\n`}
|
|
4512
|
+
/** A locale the catalog is written or translated in, and its messages. */
|
|
4513
|
+
enum class ${name}(val tag: String) {
|
|
4514
|
+
${entries};
|
|
4515
|
+
|
|
4516
|
+
${members}
|
|
4517
|
+
companion object {
|
|
4518
|
+
/** The locale the catalog is written in, which every message falls back to. */
|
|
4519
|
+
val SOURCE: ${name} = ${entry(model.source)}
|
|
4520
|
+
|
|
4521
|
+
/**
|
|
4522
|
+
* The best locale for what was asked, most preferred first: each tag tries an exact match, then
|
|
4523
|
+
* its parents (\`pt-BR\` finds \`pt\`), then any locale of its language. The source if none is.
|
|
4524
|
+
*/
|
|
4525
|
+
fun negotiate(requested: List<String>): ${name} {
|
|
4526
|
+
fun language(tag: String) = tag.substringBefore('-').lowercase()
|
|
4527
|
+
for (want in requested) {
|
|
4528
|
+
var tag = want
|
|
4529
|
+
while (true) {
|
|
4530
|
+
entries.firstOrNull { it.tag.equals(tag, ignoreCase = true) }?.let { return it }
|
|
4531
|
+
val cut = tag.lastIndexOf('-')
|
|
4532
|
+
if (cut < 0) break
|
|
4533
|
+
tag = tag.substring(0, cut)
|
|
4534
|
+
}
|
|
4535
|
+
entries.firstOrNull { language(it.tag) == language(want) }?.let { return it }
|
|
4536
|
+
}
|
|
4537
|
+
return SOURCE
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
${runtime}`;
|
|
4542
|
+
}
|
|
4543
|
+
// src/tools/native/rust.ts
|
|
4544
|
+
const KEYWORDS$1 = new Set("as async await break const continue crate dyn else enum extern false fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where while abstract become box do final macro override priv try typeof unsized virtual yield gen".split(" "));
|
|
4545
|
+
const ident$1 = (name) => KEYWORDS$1.has(name) ? `${name}_` : name;
|
|
4546
|
+
const lit$1 = (value) => stringLiteral(value, "rust");
|
|
4547
|
+
const variant = (tag) => pascal(wordsOf(tag));
|
|
4548
|
+
const argName$1 = (name) => ident$1(leading(snake(wordsOf(name))));
|
|
4549
|
+
function doc$2(lines, indent) {
|
|
4550
|
+
return lines.map((l) => l === "" ? `${indent}///\n` : `${indent}/// ${l}\n`).join("");
|
|
4551
|
+
}
|
|
4552
|
+
/** The parts as an expression of type `String`. */
|
|
4553
|
+
function expr$1(parts, indent) {
|
|
4554
|
+
if (parts.length === 0) return "String::new()";
|
|
4245
4555
|
const [only] = parts;
|
|
4246
4556
|
if (parts.length === 1 && only !== void 0) {
|
|
4247
4557
|
if (only.type === "text") return `String::from(${lit$1(only.value)})`;
|
|
@@ -4848,8 +5158,9 @@ ${runtime}`;
|
|
|
4848
5158
|
}
|
|
4849
5159
|
// src/tools/native/index.ts
|
|
4850
5160
|
/**
|
|
4851
|
-
* The native files `generate` writes beside the TypeScript, one per target in the config's `native
|
|
4852
|
-
*
|
|
5161
|
+
* The native files `generate` writes beside the TypeScript, one per target in the config's `native`
|
|
5162
|
+
* (Android's and Apple's string resources, a file per locale). A message a target can't render
|
|
5163
|
+
* fails the build with the reason, as a translation that drifted does.
|
|
4853
5164
|
*/
|
|
4854
5165
|
const WRITERS = {
|
|
4855
5166
|
rust: rustModule,
|
|
@@ -4859,18 +5170,22 @@ const WRITERS = {
|
|
|
4859
5170
|
function nativeFiles(config, catalog, locales, entries, banner) {
|
|
4860
5171
|
const files = /* @__PURE__ */ new Map();
|
|
4861
5172
|
const problems = [];
|
|
4862
|
-
for (const target of config.native) {
|
|
5173
|
+
for (const target of config.native) try {
|
|
5174
|
+
if (target.language === "android" || target.language === "apple") {
|
|
5175
|
+
const { files: written, problems: found } = target.language === "android" ? androidResources(target, catalog, locales, entries, banner) : appleStrings(target, config.root, catalog, locales, entries, banner);
|
|
5176
|
+
problems.push(...found);
|
|
5177
|
+
for (const [path, content] of written) files.set(path, content);
|
|
5178
|
+
continue;
|
|
5179
|
+
}
|
|
4863
5180
|
const { model, problems: found } = nativeModel(target, catalog, locales, entries);
|
|
4864
5181
|
problems.push(...found);
|
|
4865
5182
|
if (found.length > 0) continue;
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
});
|
|
4873
|
-
}
|
|
5183
|
+
files.set(target.out, WRITERS[target.language](model, target, banner));
|
|
5184
|
+
} catch (e) {
|
|
5185
|
+
problems.push({
|
|
5186
|
+
severity: "error",
|
|
5187
|
+
message: e.message
|
|
5188
|
+
});
|
|
4874
5189
|
}
|
|
4875
5190
|
if (problems.length > 0) throw new CatalogError(problems);
|
|
4876
5191
|
return files;
|
|
@@ -5170,4 +5485,4 @@ function generate(config, options = {}) {
|
|
|
5170
5485
|
}
|
|
5171
5486
|
export { writeMessage as A, entryLocation as C, whereOf as D, previewOf as E, toMf1 as F, intlNumber as M, parseMf2 as N, SYNTAX_NAME as O, toMf2 as P, KEY_PATTERN as S, loadCatalog as T, applyFixes as _, render as a, readEntry as b, pseudoText as c, hash as d, readLock as f, wordDiff as g, formatDiff as h, localesOf as i, intlDate as j, readMessage as k, changeOf as l, serializeLock as m, generate as n, writeFiles as o, reconcile as p, localeEntries as r, pseudoAst as s, catalogSchema as t, emptyLock as u, editEntry as v, entryLocator as w, CatalogError as x, importTranslations as y };
|
|
5172
5487
|
|
|
5173
|
-
//# sourceMappingURL=generate-
|
|
5488
|
+
//# sourceMappingURL=generate-BW3N8EGb.js.map
|