@inlang/paraglide-js 2.26.0 → 2.27.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.
Files changed (100) hide show
  1. package/README.md +2 -1
  2. package/dist/bundler-plugins/per-locale-build/vite-environments.test.js +2 -2
  3. package/dist/bundler-plugins/per-locale-build/vite-environments.test.js.map +1 -1
  4. package/dist/cli/commands/init/command.js +19 -1
  5. package/dist/cli/commands/init/command.js.map +1 -1
  6. package/dist/cli/defaults.js +2 -2
  7. package/dist/cli/defaults.js.map +1 -1
  8. package/dist/cli/steps/add-inlang-check.d.ts +19 -0
  9. package/dist/cli/steps/add-inlang-check.js +60 -0
  10. package/dist/cli/steps/add-inlang-check.js.map +1 -0
  11. package/dist/cli/steps/add-inlang-check.test.d.ts +1 -0
  12. package/dist/cli/steps/add-inlang-check.test.js +88 -0
  13. package/dist/cli/steps/add-inlang-check.test.js.map +1 -0
  14. package/dist/cli/steps/agents-md-suggestion.d.ts +11 -0
  15. package/dist/cli/steps/agents-md-suggestion.js +19 -0
  16. package/dist/cli/steps/agents-md-suggestion.js.map +1 -0
  17. package/dist/cli/steps/agents-md-suggestion.test.d.ts +1 -0
  18. package/dist/cli/steps/agents-md-suggestion.test.js +36 -0
  19. package/dist/cli/steps/agents-md-suggestion.test.js.map +1 -0
  20. package/dist/cli/steps/maybe-add-machine-translation.js +3 -2
  21. package/dist/cli/steps/maybe-add-machine-translation.js.map +1 -1
  22. package/dist/cli/steps/maybe-add-sherlock.js +1 -1
  23. package/dist/cli/steps/maybe-add-sherlock.js.map +1 -1
  24. package/dist/cli/utils.d.ts +9 -0
  25. package/dist/cli/utils.js +15 -0
  26. package/dist/cli/utils.js.map +1 -1
  27. package/dist/compiler/compile-annotation.d.ts +48 -1
  28. package/dist/compiler/compile-annotation.js +238 -10
  29. package/dist/compiler/compile-annotation.js.map +1 -1
  30. package/dist/compiler/compile-bundle.d.ts +3 -2
  31. package/dist/compiler/compile-bundle.js +30 -1
  32. package/dist/compiler/compile-bundle.js.map +1 -1
  33. package/dist/compiler/compile-bundle.test.js +18 -18
  34. package/dist/compiler/compile-bundle.test.js.map +1 -1
  35. package/dist/compiler/compile-local-variable.d.ts +3 -0
  36. package/dist/compiler/compile-local-variable.js +5 -2
  37. package/dist/compiler/compile-local-variable.js.map +1 -1
  38. package/dist/compiler/compile-local-variable.test.js +44 -4
  39. package/dist/compiler/compile-local-variable.test.js.map +1 -1
  40. package/dist/compiler/compile-message.d.ts +29 -1
  41. package/dist/compiler/compile-message.exact-number.test.d.ts +1 -0
  42. package/dist/compiler/compile-message.exact-number.test.js +786 -0
  43. package/dist/compiler/compile-message.exact-number.test.js.map +1 -0
  44. package/dist/compiler/compile-message.js +162 -66
  45. package/dist/compiler/compile-message.js.map +1 -1
  46. package/dist/compiler/compile-message.locals.test.d.ts +1 -0
  47. package/dist/compiler/compile-message.locals.test.js +498 -0
  48. package/dist/compiler/compile-message.locals.test.js.map +1 -0
  49. package/dist/compiler/compile-message.test.js +111 -20
  50. package/dist/compiler/compile-message.test.js.map +1 -1
  51. package/dist/compiler/compile-pattern.d.ts +5 -0
  52. package/dist/compiler/compile-pattern.js +6 -18
  53. package/dist/compiler/compile-pattern.js.map +1 -1
  54. package/dist/compiler/compile-pattern.test.js +165 -1
  55. package/dist/compiler/compile-pattern.test.js.map +1 -1
  56. package/dist/compiler/compile-project.js +4 -1
  57. package/dist/compiler/compile-project.js.map +1 -1
  58. package/dist/compiler/compile-project.test.js +1013 -31
  59. package/dist/compiler/compile-project.test.js.map +1 -1
  60. package/dist/compiler/compile.test.js +1 -1
  61. package/dist/compiler/compile.test.js.map +1 -1
  62. package/dist/compiler/create-readme.d.ts +4 -1
  63. package/dist/compiler/create-readme.js +33 -125
  64. package/dist/compiler/create-readme.js.map +1 -1
  65. package/dist/compiler/emit-ts-declarations.interop.test.js +2 -2
  66. package/dist/compiler/emit-ts-declarations.interop.test.js.map +1 -1
  67. package/dist/compiler/emit-ts-declarations.test.js +4 -4
  68. package/dist/compiler/emit-ts-declarations.test.js.map +1 -1
  69. package/dist/compiler/index.d.ts +1 -1
  70. package/dist/compiler/index.js +1 -1
  71. package/dist/compiler/index.js.map +1 -1
  72. package/dist/compiler/input-alias.d.ts +21 -0
  73. package/dist/compiler/input-alias.js +42 -0
  74. package/dist/compiler/input-alias.js.map +1 -0
  75. package/dist/compiler/input-alias.test.d.ts +1 -0
  76. package/dist/compiler/input-alias.test.js +58 -0
  77. package/dist/compiler/input-alias.test.js.map +1 -0
  78. package/dist/compiler/message-locals.d.ts +46 -0
  79. package/dist/compiler/message-locals.js +129 -0
  80. package/dist/compiler/message-locals.js.map +1 -0
  81. package/dist/compiler/output-structure/locale-modules.js +7 -1
  82. package/dist/compiler/output-structure/locale-modules.js.map +1 -1
  83. package/dist/compiler/output-structure/locale-modules.test.js +35 -0
  84. package/dist/compiler/output-structure/locale-modules.test.js.map +1 -1
  85. package/dist/compiler/output-structure/message-modules.js +5 -3
  86. package/dist/compiler/output-structure/message-modules.js.map +1 -1
  87. package/dist/compiler/output-structure/message-modules.test.js +35 -0
  88. package/dist/compiler/output-structure/message-modules.test.js.map +1 -1
  89. package/dist/compiler/registry.d.ts +12 -0
  90. package/dist/compiler/registry.js +97 -28
  91. package/dist/compiler/registry.js.map +1 -1
  92. package/dist/compiler/server/create-server-file.js +24 -0
  93. package/dist/compiler/server/create-server-file.js.map +1 -1
  94. package/dist/compiler/server/create-server-file.test.js +8 -2
  95. package/dist/compiler/server/create-server-file.test.js.map +1 -1
  96. package/dist/compiler/server/middleware.js +29 -4
  97. package/dist/compiler/server/middleware.js.map +1 -1
  98. package/dist/services/env-variables/index.js +1 -1
  99. package/dist/services/env-variables/index.js.map +1 -1
  100. package/package.json +3 -3
@@ -1,4 +1,5 @@
1
1
  import { expect, test, describe, vi, beforeEach } from "vitest";
2
+ import vm from "node:vm";
2
3
  import { createProject as typescriptProject, ts, } from "@ts-morph/bootstrap";
3
4
  import { Declaration, insertBundleNested, loadProjectInMemory, newProject, Pattern, VariableReference, } from "@inlang/sdk";
4
5
  import { compileProject, getFallbackMap } from "./compile-project.js";
@@ -163,6 +164,8 @@ test("emitReadme includes project path", async () => {
163
164
  expect(output["README.md"]).toContain(projectPath);
164
165
  expect(output["README.md"]).toContain("Compiled from:");
165
166
  expect(output["README.md"]).toContain("Paraglide JS");
167
+ expect(output["README.md"]).toContain(`npx @inlang/paraglide-js compile --project ${projectPath}`);
168
+ expect(output["README.md"]).toContain("npx @inlang/cli check");
166
169
  });
167
170
  test("throws during compile for invalid routeStrategies match patterns", async () => {
168
171
  const project = await loadProjectInMemory({
@@ -1250,6 +1253,970 @@ describe.each([
1250
1253
  }
1251
1254
  expect(diagnostics.length).toEqual(0);
1252
1255
  });
1256
+ test("ICU exact number aliases select numerically and pass checkJs", async () => {
1257
+ const icuProject = await loadProjectInMemory({
1258
+ blob: await newProject({
1259
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
1260
+ }),
1261
+ });
1262
+ const countPluralExact = (value) => value === undefined
1263
+ ? { type: "catchall-match", key: "countPluralExact" }
1264
+ : {
1265
+ type: "literal-match",
1266
+ key: "countPluralExact",
1267
+ value,
1268
+ };
1269
+ const countPlural = (value) => value === undefined
1270
+ ? { type: "catchall-match", key: "countPlural" }
1271
+ : { type: "literal-match", key: "countPlural", value };
1272
+ const withCount = (suffix) => [
1273
+ {
1274
+ type: "expression",
1275
+ arg: { type: "variable-reference", name: "count" },
1276
+ },
1277
+ { type: "text", value: suffix },
1278
+ ];
1279
+ // the shape the ICU1 plugin imports {count, plural, =0 {…} one {…} other {…}} as
1280
+ await insertBundleNested(icuProject.db, createBundleNested({
1281
+ id: "item_count",
1282
+ declarations: [
1283
+ { type: "input-variable", name: "count" },
1284
+ {
1285
+ type: "local-variable",
1286
+ name: "countPluralExact",
1287
+ value: {
1288
+ type: "expression",
1289
+ arg: { type: "variable-reference", name: "count" },
1290
+ },
1291
+ },
1292
+ {
1293
+ type: "local-variable",
1294
+ name: "countPlural",
1295
+ value: {
1296
+ type: "expression",
1297
+ arg: { type: "variable-reference", name: "count" },
1298
+ annotation: {
1299
+ type: "function-reference",
1300
+ name: "plural",
1301
+ options: [],
1302
+ },
1303
+ },
1304
+ },
1305
+ ],
1306
+ messages: [
1307
+ {
1308
+ locale: "en",
1309
+ selectors: [
1310
+ { type: "variable-reference", name: "countPluralExact" },
1311
+ { type: "variable-reference", name: "countPlural" },
1312
+ ],
1313
+ variants: [
1314
+ {
1315
+ matches: [countPluralExact("0"), countPlural()],
1316
+ pattern: [{ type: "text", value: "No items" }],
1317
+ },
1318
+ {
1319
+ matches: [countPluralExact(), countPlural("one")],
1320
+ pattern: withCount(" item"),
1321
+ },
1322
+ {
1323
+ matches: [countPluralExact(), countPlural()],
1324
+ pattern: withCount(" items"),
1325
+ },
1326
+ ],
1327
+ },
1328
+ {
1329
+ locale: "fr",
1330
+ selectors: [
1331
+ { type: "variable-reference", name: "countPluralExact" },
1332
+ { type: "variable-reference", name: "countPlural" },
1333
+ ],
1334
+ // an editor appends the exact variant last
1335
+ variants: [
1336
+ {
1337
+ matches: [countPluralExact(), countPlural("one")],
1338
+ pattern: withCount(" élément"),
1339
+ },
1340
+ {
1341
+ matches: [countPluralExact(), countPlural()],
1342
+ pattern: withCount(" éléments"),
1343
+ },
1344
+ {
1345
+ matches: [countPluralExact("0"), countPlural()],
1346
+ pattern: [{ type: "text", value: "Aucun élément" }],
1347
+ },
1348
+ ],
1349
+ },
1350
+ ],
1351
+ }));
1352
+ // en narrows `status` by a direct match, fr matches through an alias
1353
+ // with a catchall, so any status is accepted
1354
+ await insertBundleNested(icuProject.db, createBundleNested({
1355
+ id: "status_message",
1356
+ declarations: [
1357
+ { type: "input-variable", name: "status" },
1358
+ {
1359
+ type: "local-variable",
1360
+ name: "statusExact",
1361
+ value: {
1362
+ type: "expression",
1363
+ arg: { type: "variable-reference", name: "status" },
1364
+ },
1365
+ },
1366
+ ],
1367
+ messages: [
1368
+ {
1369
+ locale: "en",
1370
+ selectors: [{ type: "variable-reference", name: "status" }],
1371
+ variants: [
1372
+ {
1373
+ matches: [
1374
+ { type: "literal-match", key: "status", value: "active" },
1375
+ ],
1376
+ // reads statusExact: unused bundle locals fail noUnusedLocals
1377
+ pattern: [
1378
+ { type: "text", value: "Active: " },
1379
+ {
1380
+ type: "expression",
1381
+ arg: { type: "variable-reference", name: "statusExact" },
1382
+ },
1383
+ ],
1384
+ },
1385
+ ],
1386
+ },
1387
+ {
1388
+ locale: "fr",
1389
+ selectors: [{ type: "variable-reference", name: "statusExact" }],
1390
+ variants: [
1391
+ {
1392
+ matches: [
1393
+ { type: "literal-match", key: "statusExact", value: "0" },
1394
+ ],
1395
+ pattern: [{ type: "text", value: "Zéro" }],
1396
+ },
1397
+ {
1398
+ matches: [{ type: "catchall-match", key: "statusExact" }],
1399
+ pattern: [{ type: "text", value: "Autre" }],
1400
+ },
1401
+ ],
1402
+ },
1403
+ ],
1404
+ }));
1405
+ const output = await compileProject({
1406
+ project: icuProject,
1407
+ compilerOptions,
1408
+ });
1409
+ const tsProject = await typescriptProject({
1410
+ useInMemoryFileSystem: true,
1411
+ compilerOptions: superStrictRuleOutAnyErrorTsSettings,
1412
+ });
1413
+ for (const [fileName, code] of Object.entries(output)) {
1414
+ if (fileName.endsWith(".js") || fileName.endsWith(".ts")) {
1415
+ tsProject.createSourceFile(fileName, code);
1416
+ }
1417
+ }
1418
+ tsProject.createSourceFile("test.ts", `
1419
+ import * as m from "./messages.js"
1420
+
1421
+ m.item_count({ count: 0 }) satisfies string
1422
+ m.item_count({ count: "0" }) satisfies string
1423
+ m.status_message({ status: "active" }) satisfies string
1424
+ m.status_message({ status: "inactive" }) satisfies string
1425
+ `);
1426
+ const program = tsProject.createProgram();
1427
+ const diagnostics = ts.getPreEmitDiagnostics(program).filter((d) => {
1428
+ return !d.messageText
1429
+ .toString()
1430
+ .includes("Cannot find module 'async_hooks'");
1431
+ });
1432
+ for (const diagnostic of diagnostics) {
1433
+ console.error(diagnostic.messageText, diagnostic.file?.fileName);
1434
+ }
1435
+ expect(diagnostics.length).toEqual(0);
1436
+ const { m } = await importCode(await bundleCode(output, `export * as m from "./paraglide/messages.js"`));
1437
+ const render = (locale) => [0, 1, 2, 5, 21, "0"].map((count) => m.item_count({ count }, { locale }));
1438
+ expect(render("en")).toEqual([
1439
+ "No items",
1440
+ "1 item",
1441
+ "2 items",
1442
+ "5 items",
1443
+ "21 items",
1444
+ "No items",
1445
+ ]);
1446
+ expect(render("fr")).toEqual([
1447
+ "Aucun élément",
1448
+ "1 élément",
1449
+ "2 éléments",
1450
+ "5 éléments",
1451
+ "21 éléments",
1452
+ "Aucun élément",
1453
+ ]);
1454
+ expect(m.status_message({ status: "inactive" }, { locale: "fr" })).toBe("Autre");
1455
+ });
1456
+ test("emits only the locals a locale reads, in dependency order, and supports plural offsets under checkJs", async () => {
1457
+ const localsProject = await loadProjectInMemory({
1458
+ blob: await newProject({
1459
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
1460
+ }),
1461
+ });
1462
+ const ref = (name) => ({ type: "variable-reference", name });
1463
+ const literal = (key, value) => ({ type: "literal-match", key, value });
1464
+ const catchall = (key) => ({ type: "catchall-match", key });
1465
+ const text = (value) => [{ type: "text", value }];
1466
+ // en never reads statusExact, fr only reads it
1467
+ await insertBundleNested(localsProject.db, createBundleNested({
1468
+ id: "status_label",
1469
+ declarations: [
1470
+ { type: "input-variable", name: "status" },
1471
+ {
1472
+ type: "local-variable",
1473
+ name: "statusExact",
1474
+ value: { type: "expression", arg: ref("status") },
1475
+ },
1476
+ ],
1477
+ messages: [
1478
+ {
1479
+ locale: "en",
1480
+ selectors: [ref("status")],
1481
+ variants: [
1482
+ {
1483
+ matches: [literal("status", "active")],
1484
+ pattern: text("Active"),
1485
+ },
1486
+ { matches: [catchall("status")], pattern: text("Other") },
1487
+ ],
1488
+ },
1489
+ {
1490
+ locale: "fr",
1491
+ selectors: [ref("statusExact")],
1492
+ variants: [
1493
+ {
1494
+ matches: [literal("statusExact", "0")],
1495
+ pattern: text("Zéro"),
1496
+ },
1497
+ { matches: [catchall("statusExact")], pattern: text("Autre") },
1498
+ ],
1499
+ },
1500
+ ],
1501
+ }));
1502
+ // the shape the ICU1 plugin imports
1503
+ // {count, plural, offset:1 =0 {…} =1 {…} one {…} other {…}} as
1504
+ await insertBundleNested(localsProject.db, createBundleNested({
1505
+ id: "guest_list",
1506
+ declarations: [
1507
+ { type: "input-variable", name: "count" },
1508
+ {
1509
+ type: "local-variable",
1510
+ name: "countPluralOffset1",
1511
+ value: {
1512
+ type: "expression",
1513
+ arg: ref("count"),
1514
+ annotation: {
1515
+ type: "function-reference",
1516
+ name: "plural",
1517
+ options: [
1518
+ { name: "offset", value: { type: "literal", value: "1" } },
1519
+ ],
1520
+ },
1521
+ },
1522
+ },
1523
+ {
1524
+ type: "local-variable",
1525
+ name: "countPluralOffset1Exact",
1526
+ value: { type: "expression", arg: ref("count") },
1527
+ },
1528
+ ],
1529
+ messages: [
1530
+ {
1531
+ locale: "en",
1532
+ selectors: [
1533
+ ref("countPluralOffset1Exact"),
1534
+ ref("countPluralOffset1"),
1535
+ ],
1536
+ variants: [
1537
+ {
1538
+ matches: [
1539
+ literal("countPluralOffset1Exact", "0"),
1540
+ catchall("countPluralOffset1"),
1541
+ ],
1542
+ pattern: text("Nobody"),
1543
+ },
1544
+ {
1545
+ matches: [
1546
+ literal("countPluralOffset1Exact", "1"),
1547
+ catchall("countPluralOffset1"),
1548
+ ],
1549
+ pattern: text("Only you"),
1550
+ },
1551
+ {
1552
+ matches: [
1553
+ catchall("countPluralOffset1Exact"),
1554
+ literal("countPluralOffset1", "one"),
1555
+ ],
1556
+ pattern: text("You and one other"),
1557
+ },
1558
+ {
1559
+ matches: [
1560
+ catchall("countPluralOffset1Exact"),
1561
+ catchall("countPluralOffset1"),
1562
+ ],
1563
+ pattern: text("You and others"),
1564
+ },
1565
+ ],
1566
+ },
1567
+ {
1568
+ // fr does not translate the exact cases: countPluralOffset1Exact
1569
+ // is not read
1570
+ locale: "fr",
1571
+ selectors: [ref("countPluralOffset1")],
1572
+ variants: [
1573
+ {
1574
+ matches: [literal("countPluralOffset1", "one")],
1575
+ pattern: text("Vous et un autre"),
1576
+ },
1577
+ {
1578
+ matches: [catchall("countPluralOffset1")],
1579
+ pattern: text("Vous et d'autres"),
1580
+ },
1581
+ ],
1582
+ },
1583
+ ],
1584
+ }));
1585
+ // fr reads no variable at all
1586
+ await insertBundleNested(localsProject.db, createBundleNested({
1587
+ id: "greeting_label",
1588
+ declarations: [
1589
+ { type: "input-variable", name: "name" },
1590
+ {
1591
+ type: "local-variable",
1592
+ name: "nameValue",
1593
+ value: { type: "expression", arg: ref("name") },
1594
+ },
1595
+ ],
1596
+ messages: [
1597
+ {
1598
+ locale: "en",
1599
+ selectors: [],
1600
+ variants: [
1601
+ {
1602
+ matches: [],
1603
+ pattern: [
1604
+ { type: "text", value: "Hello " },
1605
+ { type: "expression", arg: ref("nameValue") },
1606
+ ],
1607
+ },
1608
+ ],
1609
+ },
1610
+ {
1611
+ locale: "fr",
1612
+ selectors: [],
1613
+ variants: [{ matches: [], pattern: text("Bonjour") }],
1614
+ },
1615
+ ],
1616
+ }));
1617
+ // a local declared before the local it reads
1618
+ await insertBundleNested(localsProject.db, createBundleNested({
1619
+ id: "reordered_locals",
1620
+ declarations: [
1621
+ { type: "input-variable", name: "count" },
1622
+ {
1623
+ type: "local-variable",
1624
+ name: "countPlural",
1625
+ value: {
1626
+ type: "expression",
1627
+ arg: ref("countValue"),
1628
+ annotation: {
1629
+ type: "function-reference",
1630
+ name: "plural",
1631
+ options: [],
1632
+ },
1633
+ },
1634
+ },
1635
+ {
1636
+ type: "local-variable",
1637
+ name: "countValue",
1638
+ value: { type: "expression", arg: ref("count") },
1639
+ },
1640
+ ],
1641
+ messages: [
1642
+ {
1643
+ locale: "en",
1644
+ selectors: [ref("countPlural")],
1645
+ variants: [
1646
+ {
1647
+ matches: [literal("countPlural", "one")],
1648
+ pattern: text("one"),
1649
+ },
1650
+ { matches: [catchall("countPlural")], pattern: text("other") },
1651
+ ],
1652
+ },
1653
+ ],
1654
+ }));
1655
+ const output = await compileProject({
1656
+ project: localsProject,
1657
+ compilerOptions,
1658
+ });
1659
+ const tsProject = await typescriptProject({
1660
+ useInMemoryFileSystem: true,
1661
+ compilerOptions: superStrictRuleOutAnyErrorTsSettings,
1662
+ });
1663
+ for (const [fileName, code] of Object.entries(output)) {
1664
+ if (fileName.endsWith(".js") || fileName.endsWith(".ts")) {
1665
+ tsProject.createSourceFile(fileName, code);
1666
+ }
1667
+ }
1668
+ tsProject.createSourceFile("test.ts", `
1669
+ import * as m from "./messages.js"
1670
+
1671
+ m.status_label({ status: "active" }) satisfies string
1672
+ m.guest_list({ count: 0 }) satisfies string
1673
+ m.reordered_locals({ count: 1 }) satisfies string
1674
+ m.greeting_label({ name: "Ada" }) satisfies string
1675
+ `);
1676
+ const program = tsProject.createProgram();
1677
+ const diagnostics = ts.getPreEmitDiagnostics(program).filter((d) => {
1678
+ return !d.messageText
1679
+ .toString()
1680
+ .includes("Cannot find module 'async_hooks'");
1681
+ });
1682
+ for (const diagnostic of diagnostics) {
1683
+ console.error(diagnostic.messageText, diagnostic.file?.fileName);
1684
+ }
1685
+ expect(diagnostics.length).toEqual(0);
1686
+ const { m } = await importCode(await bundleCode(output, `export * as m from "./paraglide/messages.js"`));
1687
+ expect(["active", "inactive"].map((status) => m.status_label({ status }, { locale: "en" }))).toEqual(["Active", "Other"]);
1688
+ expect([0, "0", 1].map((status) => m.status_label({ status }, { locale: "fr" }))).toEqual(["Zéro", "Zéro", "Autre"]);
1689
+ const guests = (locale) => [0, 1, 2, 3, 22, "1", "2"].map((count) => m.guest_list({ count }, { locale }));
1690
+ expect(guests("en")).toEqual([
1691
+ "Nobody",
1692
+ "Only you",
1693
+ "You and one other",
1694
+ "You and others",
1695
+ "You and others",
1696
+ "Only you",
1697
+ "You and one other",
1698
+ ]);
1699
+ // French "one" covers 0 and 1 (and -1 for 0 guests), so 1 and 2 guests
1700
+ // (0 and 1 others) select "one"
1701
+ expect(guests("fr")).toEqual([
1702
+ "Vous et un autre",
1703
+ "Vous et un autre",
1704
+ "Vous et un autre",
1705
+ "Vous et d'autres",
1706
+ "Vous et d'autres",
1707
+ "Vous et un autre",
1708
+ "Vous et un autre",
1709
+ ]);
1710
+ expect([1, 2].map((count) => m.reordered_locals({ count }, { locale: "en" }))).toEqual(["one", "other"]);
1711
+ expect(m.greeting_label({ name: "Ada" }, { locale: "en" })).toBe("Hello Ada");
1712
+ expect(m.greeting_label({ name: "Ada" }, { locale: "fr" })).toBe("Bonjour");
1713
+ });
1714
+ test("imports the registry only where a message calls it, and formats ICU # with offset under checkJs", async () => {
1715
+ const compileChecked = async (bundles) => {
1716
+ const icuProject = await loadProjectInMemory({
1717
+ blob: await newProject({
1718
+ settings: { baseLocale: "en", locales: ["en", "de"] },
1719
+ }),
1720
+ });
1721
+ for (const bundle of bundles) {
1722
+ await insertBundleNested(icuProject.db, createBundleNested(bundle));
1723
+ }
1724
+ const output = await compileProject({
1725
+ project: icuProject,
1726
+ compilerOptions,
1727
+ });
1728
+ const tsProject = await typescriptProject({
1729
+ useInMemoryFileSystem: true,
1730
+ compilerOptions: superStrictRuleOutAnyErrorTsSettings,
1731
+ });
1732
+ for (const [fileName, code] of Object.entries(output)) {
1733
+ if (fileName.endsWith(".js") || fileName.endsWith(".ts")) {
1734
+ tsProject.createSourceFile(fileName, code);
1735
+ }
1736
+ }
1737
+ const diagnostics = ts
1738
+ .getPreEmitDiagnostics(tsProject.createProgram())
1739
+ .filter((d) => !d.messageText
1740
+ .toString()
1741
+ .includes("Cannot find module 'async_hooks'"));
1742
+ for (const diagnostic of diagnostics) {
1743
+ console.error(diagnostic.messageText, diagnostic.file?.fileName);
1744
+ }
1745
+ expect(diagnostics.length).toEqual(0);
1746
+ return output;
1747
+ };
1748
+ const ref = (name) => ({ type: "variable-reference", name });
1749
+ const literal = (key, value) => ({ type: "literal-match", key, value });
1750
+ const catchall = (key) => ({ type: "catchall-match", key });
1751
+ const text = (value) => ({ type: "text", value });
1752
+ const pound = (offset) => ({
1753
+ type: "expression",
1754
+ arg: ref("count"),
1755
+ annotation: {
1756
+ type: "function-reference",
1757
+ name: "icu:pound",
1758
+ options: offset === undefined
1759
+ ? []
1760
+ : [
1761
+ {
1762
+ name: "offset",
1763
+ value: { type: "literal", value: offset },
1764
+ },
1765
+ ],
1766
+ },
1767
+ });
1768
+ // message text that contains "registry." must not import the registry
1769
+ const textOnly = await compileChecked([
1770
+ {
1771
+ id: "registry_text",
1772
+ declarations: [],
1773
+ messages: [
1774
+ {
1775
+ locale: "en",
1776
+ selectors: [],
1777
+ variants: [
1778
+ { matches: [], pattern: [text("Open the registry. Now.")] },
1779
+ ],
1780
+ },
1781
+ {
1782
+ locale: "de",
1783
+ selectors: [],
1784
+ variants: [
1785
+ {
1786
+ matches: [],
1787
+ pattern: [text("Öffne die registry. Jetzt.")],
1788
+ },
1789
+ ],
1790
+ },
1791
+ ],
1792
+ },
1793
+ // an unknown formatter on a local interpolates the raw value
1794
+ {
1795
+ id: "custom_local",
1796
+ declarations: [
1797
+ { type: "input-variable", name: "amount" },
1798
+ {
1799
+ type: "local-variable",
1800
+ name: "formatted",
1801
+ value: {
1802
+ type: "expression",
1803
+ arg: ref("amount"),
1804
+ annotation: {
1805
+ type: "function-reference",
1806
+ name: "customLocalFormatter",
1807
+ options: [],
1808
+ },
1809
+ },
1810
+ },
1811
+ ],
1812
+ messages: [
1813
+ {
1814
+ locale: "en",
1815
+ selectors: [],
1816
+ variants: [
1817
+ {
1818
+ matches: [],
1819
+ pattern: [
1820
+ text("Total: "),
1821
+ { type: "expression", arg: ref("formatted") },
1822
+ ],
1823
+ },
1824
+ ],
1825
+ },
1826
+ ],
1827
+ },
1828
+ ]);
1829
+ for (const [fileName, code] of Object.entries(textOnly)) {
1830
+ if (fileName.startsWith("messages/")) {
1831
+ expect(code).not.toContain("import * as registry");
1832
+ }
1833
+ }
1834
+ const { m: textMessages } = await importCode(await bundleCode(textOnly, `export * as m from "./paraglide/messages.js"`));
1835
+ expect(textMessages.registry_text({}, { locale: "en" })).toBe("Open the registry. Now.");
1836
+ expect(textMessages.custom_local({ amount: 1234 }, { locale: "en" })).toBe("Total: 1234");
1837
+ // {count, plural, offset:1 =0 {Nobody} =1 {You} one {You and # other} other {You and # others}}
1838
+ // as the ICU1 plugin imports it, and `#` without an offset
1839
+ const output = await compileChecked([
1840
+ {
1841
+ id: "guest_count",
1842
+ declarations: [
1843
+ { type: "input-variable", name: "count" },
1844
+ {
1845
+ type: "local-variable",
1846
+ name: "countPluralOffset1",
1847
+ value: {
1848
+ type: "expression",
1849
+ arg: ref("count"),
1850
+ annotation: {
1851
+ type: "function-reference",
1852
+ name: "plural",
1853
+ options: [
1854
+ { name: "offset", value: { type: "literal", value: "1" } },
1855
+ ],
1856
+ },
1857
+ },
1858
+ },
1859
+ {
1860
+ type: "local-variable",
1861
+ name: "countPluralOffset1Exact",
1862
+ value: { type: "expression", arg: ref("count") },
1863
+ },
1864
+ ],
1865
+ messages: [
1866
+ {
1867
+ locale: "en",
1868
+ selectors: [
1869
+ ref("countPluralOffset1Exact"),
1870
+ ref("countPluralOffset1"),
1871
+ ],
1872
+ variants: [
1873
+ {
1874
+ matches: [
1875
+ literal("countPluralOffset1Exact", "0"),
1876
+ catchall("countPluralOffset1"),
1877
+ ],
1878
+ pattern: [text("Nobody")],
1879
+ },
1880
+ {
1881
+ matches: [
1882
+ literal("countPluralOffset1Exact", "1"),
1883
+ catchall("countPluralOffset1"),
1884
+ ],
1885
+ pattern: [text("You")],
1886
+ },
1887
+ {
1888
+ matches: [
1889
+ catchall("countPluralOffset1Exact"),
1890
+ literal("countPluralOffset1", "one"),
1891
+ ],
1892
+ pattern: [text("You and "), pound("1"), text(" other")],
1893
+ },
1894
+ {
1895
+ matches: [
1896
+ catchall("countPluralOffset1Exact"),
1897
+ catchall("countPluralOffset1"),
1898
+ ],
1899
+ pattern: [text("You and "), pound("1"), text(" others")],
1900
+ },
1901
+ ],
1902
+ },
1903
+ ],
1904
+ },
1905
+ {
1906
+ id: "item_total",
1907
+ declarations: [
1908
+ { type: "input-variable", name: "count" },
1909
+ {
1910
+ type: "local-variable",
1911
+ name: "countPlural",
1912
+ value: {
1913
+ type: "expression",
1914
+ arg: ref("count"),
1915
+ annotation: {
1916
+ type: "function-reference",
1917
+ name: "plural",
1918
+ options: [],
1919
+ },
1920
+ },
1921
+ },
1922
+ ],
1923
+ messages: [
1924
+ {
1925
+ locale: "en",
1926
+ selectors: [ref("countPlural")],
1927
+ variants: [
1928
+ {
1929
+ matches: [literal("countPlural", "one")],
1930
+ pattern: [pound(), text(" item")],
1931
+ },
1932
+ {
1933
+ matches: [catchall("countPlural")],
1934
+ pattern: [pound(), text(" items")],
1935
+ },
1936
+ ],
1937
+ },
1938
+ {
1939
+ locale: "de",
1940
+ selectors: [ref("countPlural")],
1941
+ variants: [
1942
+ {
1943
+ matches: [literal("countPlural", "one")],
1944
+ pattern: [pound(), text(" Artikel")],
1945
+ },
1946
+ {
1947
+ matches: [catchall("countPlural")],
1948
+ pattern: [pound(), text(" Artikel")],
1949
+ },
1950
+ ],
1951
+ },
1952
+ ],
1953
+ },
1954
+ {
1955
+ id: "variable_offset",
1956
+ declarations: [
1957
+ { type: "input-variable", name: "count" },
1958
+ { type: "input-variable", name: "off" },
1959
+ {
1960
+ type: "local-variable",
1961
+ name: "countPlural",
1962
+ value: {
1963
+ type: "expression",
1964
+ arg: ref("count"),
1965
+ annotation: {
1966
+ type: "function-reference",
1967
+ name: "plural",
1968
+ options: [{ name: "offset", value: ref("off") }],
1969
+ },
1970
+ },
1971
+ },
1972
+ ],
1973
+ messages: [
1974
+ {
1975
+ locale: "en",
1976
+ selectors: [ref("countPlural")],
1977
+ variants: [
1978
+ {
1979
+ matches: [literal("countPlural", "one")],
1980
+ pattern: [
1981
+ text("one: "),
1982
+ {
1983
+ type: "expression",
1984
+ arg: ref("count"),
1985
+ annotation: {
1986
+ type: "function-reference",
1987
+ name: "icu:pound",
1988
+ options: [{ name: "offset", value: ref("off") }],
1989
+ },
1990
+ },
1991
+ ],
1992
+ },
1993
+ {
1994
+ matches: [catchall("countPlural")],
1995
+ pattern: [text("other")],
1996
+ },
1997
+ ],
1998
+ },
1999
+ ],
2000
+ },
2001
+ ]);
2002
+ const { m } = await importCode(await bundleCode(output, `export * as m from "./paraglide/messages.js"`));
2003
+ // offsets from variables type-check and are converted with Number()
2004
+ expect(m.variable_offset({ count: 3, off: 2 }, { locale: "en" })).toBe("one: 1");
2005
+ expect(m.variable_offset({ count: 3, off: "2" }, { locale: "en" })).toBe("one: 1");
2006
+ expect([0, 1, 2, 3, 1234].map((count) => m.guest_count({ count }, { locale: "en" }))).toEqual([
2007
+ "Nobody",
2008
+ "You",
2009
+ "You and 1 other",
2010
+ "You and 2 others",
2011
+ "You and 1,233 others",
2012
+ ]);
2013
+ expect([1, 2, 1234].map((count) => m.item_total({ count }, { locale: "en" }))).toEqual(["1 item", "2 items", "1,234 items"]);
2014
+ expect(m.item_total({ count: 1234 }, { locale: "de" })).toBe("1.234 Artikel");
2015
+ });
2016
+ });
2017
+ describe.each([
2018
+ { outputStructure: "locale-modules" },
2019
+ { outputStructure: "message-modules" },
2020
+ ])("markup messages with fallbacks ($outputStructure)", ({ outputStructure }) => {
2021
+ test(".parts() type-checks under strict checkJs and renders", async () => {
2022
+ const markupProject = await loadProjectInMemory({
2023
+ blob: await newProject({
2024
+ settings: { baseLocale: "en", locales: ["en", "de"] },
2025
+ }),
2026
+ });
2027
+ const bold = (value) => [
2028
+ { type: "markup-start", name: "b" },
2029
+ { type: "text", value },
2030
+ { type: "markup-end", name: "b" },
2031
+ ];
2032
+ const bundles = [
2033
+ // de falls back to en
2034
+ {
2035
+ id: "markup_fallback",
2036
+ messages: [{ locale: "en", variants: [{ pattern: bold("Hello") }] }],
2037
+ },
2038
+ // en has no markup, de has
2039
+ {
2040
+ id: "markup_mixed",
2041
+ messages: [
2042
+ {
2043
+ locale: "en",
2044
+ variants: [{ pattern: [{ type: "text", value: "Hello" }] }],
2045
+ },
2046
+ { locale: "de", variants: [{ pattern: bold("Hallo") }] },
2047
+ ],
2048
+ },
2049
+ // en (the base locale) has no message and no fallback
2050
+ {
2051
+ id: "markup_missing_base",
2052
+ messages: [{ locale: "de", variants: [{ pattern: bold("Hallo") }] }],
2053
+ },
2054
+ ];
2055
+ for (const bundle of bundles) {
2056
+ await insertBundleNested(markupProject.db, createBundleNested(bundle));
2057
+ }
2058
+ const output = await compileProject({
2059
+ project: markupProject,
2060
+ compilerOptions: {
2061
+ outputStructure,
2062
+ strategy: ["globalVariable", "baseLocale"],
2063
+ },
2064
+ });
2065
+ const tsProject = await typescriptProject({
2066
+ useInMemoryFileSystem: true,
2067
+ compilerOptions: {
2068
+ outDir: "dist",
2069
+ declaration: true,
2070
+ allowJs: true,
2071
+ checkJs: true,
2072
+ noImplicitAny: true,
2073
+ noUnusedLocals: true,
2074
+ noUnusedParameters: true,
2075
+ noImplicitReturns: true,
2076
+ noImplicitThis: true,
2077
+ noUncheckedIndexedAccess: true,
2078
+ noPropertyAccessFromIndexSignature: true,
2079
+ module: ts.ModuleKind.Node16,
2080
+ strict: true,
2081
+ },
2082
+ });
2083
+ for (const [fileName, code] of Object.entries(output)) {
2084
+ if (fileName.endsWith(".js") || fileName.endsWith(".ts")) {
2085
+ tsProject.createSourceFile(fileName, code);
2086
+ }
2087
+ }
2088
+ const diagnostics = ts
2089
+ .getPreEmitDiagnostics(tsProject.createProgram())
2090
+ .filter((d) => !d.messageText
2091
+ .toString()
2092
+ .includes("Cannot find module 'async_hooks'"));
2093
+ for (const diagnostic of diagnostics) {
2094
+ console.error(diagnostic.messageText, diagnostic.file?.fileName);
2095
+ }
2096
+ expect(diagnostics.length).toEqual(0);
2097
+ const { m } = await importCode(await bundleCode(output, `export * as m from "./paraglide/messages.js"`));
2098
+ const boldParts = (value) => [
2099
+ { type: "markup-start", name: "b", options: {}, attributes: {} },
2100
+ { type: "text", value },
2101
+ { type: "markup-end", name: "b", options: {}, attributes: {} },
2102
+ ];
2103
+ expect(m.markup_fallback.parts({}, { locale: "de" })).toEqual(boldParts("Hello"));
2104
+ expect(m.markup_mixed.parts({}, { locale: "en" })).toEqual([
2105
+ { type: "text", value: "Hello" },
2106
+ ]);
2107
+ expect(m.markup_mixed.parts({}, { locale: "de" })).toEqual(boldParts("Hallo"));
2108
+ expect(m.markup_missing_base.parts({}, { locale: "en" })).toEqual([
2109
+ { type: "text", value: "markup_missing_base" },
2110
+ ]);
2111
+ });
2112
+ });
2113
+ test("messages injected by the middleware in experimentalMiddlewareLocaleSplitting mode can call the registry", async () => {
2114
+ const splitProject = await loadProjectInMemory({
2115
+ blob: await newProject({
2116
+ settings: { baseLocale: "en", locales: ["en", "de"] },
2117
+ }),
2118
+ });
2119
+ const ref = (name) => ({ type: "variable-reference", name });
2120
+ // {count, plural, =0 {No items} other {# items}} as the ICU1 plugin imports it
2121
+ await insertBundleNested(splitProject.db, createBundleNested({
2122
+ id: "item_count",
2123
+ declarations: [
2124
+ { type: "input-variable", name: "count" },
2125
+ {
2126
+ type: "local-variable",
2127
+ name: "countPluralExact",
2128
+ value: { type: "expression", arg: ref("count") },
2129
+ },
2130
+ {
2131
+ type: "local-variable",
2132
+ name: "countPlural",
2133
+ value: {
2134
+ type: "expression",
2135
+ arg: ref("count"),
2136
+ annotation: {
2137
+ type: "function-reference",
2138
+ name: "plural",
2139
+ options: [],
2140
+ },
2141
+ },
2142
+ },
2143
+ ],
2144
+ messages: [
2145
+ {
2146
+ locale: "en",
2147
+ selectors: [ref("countPluralExact"), ref("countPlural")],
2148
+ variants: [
2149
+ {
2150
+ matches: [
2151
+ { type: "literal-match", key: "countPluralExact", value: "0" },
2152
+ { type: "catchall-match", key: "countPlural" },
2153
+ ],
2154
+ pattern: [{ type: "text", value: "No items" }],
2155
+ },
2156
+ {
2157
+ matches: [
2158
+ { type: "catchall-match", key: "countPluralExact" },
2159
+ { type: "catchall-match", key: "countPlural" },
2160
+ ],
2161
+ pattern: [
2162
+ {
2163
+ type: "expression",
2164
+ arg: ref("count"),
2165
+ annotation: {
2166
+ type: "function-reference",
2167
+ name: "icu:pound",
2168
+ options: [],
2169
+ },
2170
+ },
2171
+ { type: "text", value: " items" },
2172
+ ],
2173
+ },
2174
+ ],
2175
+ },
2176
+ ],
2177
+ }));
2178
+ await insertBundleNested(splitProject.db, createBundleNested({
2179
+ id: "plain_text",
2180
+ messages: [
2181
+ {
2182
+ locale: "en",
2183
+ variants: [{ pattern: [{ type: "text", value: "Hello" }] }],
2184
+ },
2185
+ ],
2186
+ }));
2187
+ const output = await compileProject({
2188
+ project: splitProject,
2189
+ compilerOptions: {
2190
+ strategy: ["baseLocale"],
2191
+ experimentalMiddlewareLocaleSplitting: true,
2192
+ disableAsyncLocalStorage: true,
2193
+ },
2194
+ });
2195
+ const { m, paraglideMiddleware } = await importCode(await bundleCode(output, `export * as m from "./paraglide/messages.js"
2196
+ export { paraglideMiddleware } from "./paraglide/server.js"`));
2197
+ const render = async (messages) => {
2198
+ const response = await paraglideMiddleware(new Request("https://example.com/", {
2199
+ headers: { "Sec-Fetch-Dest": "document" },
2200
+ }), () => new Response(`<html><head></head><body>${messages(m)}</body></html>`, {
2201
+ headers: { "Content-Type": "text/html" },
2202
+ }));
2203
+ const html = await response.text();
2204
+ const script = html.match(/<script[^>]*>([\s\S]*?)<\/script>/)?.[1];
2205
+ expect(script).toBeDefined();
2206
+ // the client evaluates the script without registry.js
2207
+ const context = {};
2208
+ vm.runInNewContext(script, context);
2209
+ return { html, ssr: context.__paraglide.ssr };
2210
+ };
2211
+ const withRegistry = await render((m) => m.item_count({ count: 5 }));
2212
+ expect(withRegistry.html).toContain("<body>5 items</body>");
2213
+ expect(withRegistry.ssr.item_count({ count: 5 })).toBe("5 items");
2214
+ expect(withRegistry.ssr.item_count({ count: 1234 })).toBe("1,234 items");
2215
+ expect(withRegistry.ssr.item_count({ count: 0 })).toBe("No items");
2216
+ // messages that don't call the registry get no registry code
2217
+ const withoutRegistry = await render((m) => m.plain_text());
2218
+ expect(withoutRegistry.ssr.plain_text()).toBe("Hello");
2219
+ expect(withoutRegistry.html).not.toContain("const registry");
1253
2220
  });
1254
2221
  async function bundleCode(output, file) {
1255
2222
  output["runtime.js"] = output["runtime.js"].replace('import "@inlang/paraglide-js/urlpattern-polyfill";', "/** @type {any} */const URLPattern = {};").replace('const { AsyncLocalStorage } = await import("async_hooks");', "const AsyncLocalStorage = class {};");
@@ -1333,13 +2300,13 @@ const mockBundles = [
1333
2300
  messages: [
1334
2301
  {
1335
2302
  id: "depressed_dog_en",
1336
- bundleId: "depressed_dog",
2303
+ bundle_id: "depressed_dog",
1337
2304
  locale: "en",
1338
2305
  selectors: [],
1339
2306
  variants: [
1340
2307
  {
1341
2308
  id: "depressed_dog_en_variant_one",
1342
- messageId: "depressed_dog_en",
2309
+ message_id: "depressed_dog_en",
1343
2310
  matches: [],
1344
2311
  pattern: [
1345
2312
  { type: "text", value: "Good morning " },
@@ -1354,13 +2321,13 @@ const mockBundles = [
1354
2321
  },
1355
2322
  {
1356
2323
  id: "depressed_dog_de",
1357
- bundleId: "depressed_dog",
2324
+ bundle_id: "depressed_dog",
1358
2325
  locale: "de",
1359
2326
  selectors: [],
1360
2327
  variants: [
1361
2328
  {
1362
2329
  id: "depressed_dog_de_variant_one",
1363
- messageId: "depressed_dog_de",
2330
+ message_id: "depressed_dog_de",
1364
2331
  matches: [],
1365
2332
  pattern: [
1366
2333
  { type: "text", value: "Guten Morgen " },
@@ -1390,13 +2357,13 @@ const mockBundles = [
1390
2357
  messages: [
1391
2358
  {
1392
2359
  id: "insane_cats_en",
1393
- bundleId: "insane_cats",
2360
+ bundle_id: "insane_cats",
1394
2361
  locale: "en",
1395
2362
  selectors: [],
1396
2363
  variants: [
1397
2364
  {
1398
2365
  id: "insane_cats_en_variant_one",
1399
- messageId: "insane_cats_en",
2366
+ message_id: "insane_cats_en",
1400
2367
  matches: [],
1401
2368
  pattern: [
1402
2369
  { type: "text", value: "Hello " },
@@ -1416,13 +2383,13 @@ const mockBundles = [
1416
2383
  },
1417
2384
  {
1418
2385
  id: "insane_cats_de",
1419
- bundleId: "insane_cats",
2386
+ bundle_id: "insane_cats",
1420
2387
  locale: "de",
1421
2388
  selectors: [],
1422
2389
  variants: [
1423
2390
  {
1424
2391
  id: "insane_cats_de_variant_one",
1425
- messageId: "insane_cats_de",
2392
+ message_id: "insane_cats_de",
1426
2393
  matches: [],
1427
2394
  pattern: [
1428
2395
  { type: "text", value: "Hallo " },
@@ -1586,13 +2553,13 @@ const mockBundles = [
1586
2553
  messages: [
1587
2554
  {
1588
2555
  id: "auth_password_error_en",
1589
- bundleId: "auth_password_error",
2556
+ bundle_id: "auth_password_error",
1590
2557
  locale: "en",
1591
2558
  selectors: [],
1592
2559
  variants: [
1593
2560
  {
1594
2561
  id: "auth_password_error_en_variant_invalid",
1595
- messageId: "auth_password_error_en",
2562
+ message_id: "auth_password_error_en",
1596
2563
  matches: [{ type: "literal-match", key: "type", value: "invalid" }],
1597
2564
  pattern: [
1598
2565
  {
@@ -1603,7 +2570,7 @@ const mockBundles = [
1603
2570
  },
1604
2571
  {
1605
2572
  id: "auth_password_error_en_variant_empty",
1606
- messageId: "auth_password_error_en",
2573
+ message_id: "auth_password_error_en",
1607
2574
  matches: [{ type: "literal-match", key: "type", value: "empty" }],
1608
2575
  pattern: [
1609
2576
  {
@@ -1614,7 +2581,7 @@ const mockBundles = [
1614
2581
  },
1615
2582
  {
1616
2583
  id: "auth_password_error_en_variant_min",
1617
- messageId: "auth_password_error_en",
2584
+ message_id: "auth_password_error_en",
1618
2585
  matches: [
1619
2586
  {
1620
2587
  type: "literal-match",
@@ -1631,7 +2598,7 @@ const mockBundles = [
1631
2598
  },
1632
2599
  {
1633
2600
  id: "auth_password_error_en_variant_secure",
1634
- messageId: "auth_password_error_en",
2601
+ message_id: "auth_password_error_en",
1635
2602
  matches: [{ type: "literal-match", key: "type", value: "secure" }],
1636
2603
  pattern: [
1637
2604
  {
@@ -1655,13 +2622,13 @@ const mockBundles = [
1655
2622
  messages: [
1656
2623
  {
1657
2624
  id: "error_with_dash_en",
1658
- bundleId: "error_with_dash",
2625
+ bundle_id: "error_with_dash",
1659
2626
  locale: "en",
1660
2627
  selectors: [],
1661
2628
  variants: [
1662
2629
  {
1663
2630
  id: "error_with_dash_en_variant_network",
1664
- messageId: "error_with_dash_en",
2631
+ message_id: "error_with_dash_en",
1665
2632
  matches: [
1666
2633
  { type: "literal-match", key: "error-type", value: "network" },
1667
2634
  ],
@@ -1669,7 +2636,7 @@ const mockBundles = [
1669
2636
  },
1670
2637
  {
1671
2638
  id: "error_with_dash_en_variant_timeout",
1672
- messageId: "error_with_dash_en",
2639
+ message_id: "error_with_dash_en",
1673
2640
  matches: [
1674
2641
  {
1675
2642
  type: "literal-match",
@@ -1694,13 +2661,13 @@ const mockBundles = [
1694
2661
  messages: [
1695
2662
  {
1696
2663
  id: "auth_password_error_catchall_en",
1697
- bundleId: "auth_password_error_catchall",
2664
+ bundle_id: "auth_password_error_catchall",
1698
2665
  locale: "en",
1699
2666
  selectors: [],
1700
2667
  variants: [
1701
2668
  {
1702
2669
  id: "auth_password_error_catchall_en_variant_invalid",
1703
- messageId: "auth_password_error_catchall_en",
2670
+ message_id: "auth_password_error_catchall_en",
1704
2671
  matches: [{ type: "literal-match", key: "type", value: "invalid" }],
1705
2672
  pattern: [
1706
2673
  {
@@ -1711,7 +2678,7 @@ const mockBundles = [
1711
2678
  },
1712
2679
  {
1713
2680
  id: "auth_password_error_catchall_en_variant_catchall",
1714
- messageId: "auth_password_error_catchall_en",
2681
+ message_id: "auth_password_error_catchall_en",
1715
2682
  matches: [{ type: "catchall-match", key: "type" }],
1716
2683
  pattern: [
1717
2684
  {
@@ -1739,13 +2706,13 @@ const mockBundles = [
1739
2706
  messages: [
1740
2707
  {
1741
2708
  id: "multi_key_status_type_en",
1742
- bundleId: "multi_key_status_type",
2709
+ bundle_id: "multi_key_status_type",
1743
2710
  locale: "en",
1744
2711
  selectors: [],
1745
2712
  variants: [
1746
2713
  {
1747
2714
  id: "multi_key_status_type_en_variant_ready_invalid",
1748
- messageId: "multi_key_status_type_en",
2715
+ message_id: "multi_key_status_type_en",
1749
2716
  matches: [
1750
2717
  { type: "literal-match", key: "type", value: "invalid" },
1751
2718
  { type: "literal-match", key: "status", value: "ready" },
@@ -1754,7 +2721,7 @@ const mockBundles = [
1754
2721
  },
1755
2722
  {
1756
2723
  id: "multi_key_status_type_en_variant_done_empty",
1757
- messageId: "multi_key_status_type_en",
2724
+ message_id: "multi_key_status_type_en",
1758
2725
  matches: [
1759
2726
  { type: "literal-match", key: "type", value: "empty" },
1760
2727
  { type: "literal-match", key: "status", value: "done" },
@@ -1763,7 +2730,7 @@ const mockBundles = [
1763
2730
  },
1764
2731
  {
1765
2732
  id: "multi_key_status_type_en_variant_failed_secure",
1766
- messageId: "multi_key_status_type_en",
2733
+ message_id: "multi_key_status_type_en",
1767
2734
  matches: [
1768
2735
  { type: "literal-match", key: "type", value: "secure" },
1769
2736
  { type: "literal-match", key: "status", value: "failed" },
@@ -1785,19 +2752,19 @@ const mockBundles = [
1785
2752
  messages: [
1786
2753
  {
1787
2754
  id: "empty_matches_catchall_en",
1788
- bundleId: "empty_matches_catchall",
2755
+ bundle_id: "empty_matches_catchall",
1789
2756
  locale: "en",
1790
2757
  selectors: [],
1791
2758
  variants: [
1792
2759
  {
1793
2760
  id: "empty_matches_catchall_en_variant_invalid",
1794
- messageId: "empty_matches_catchall_en",
2761
+ message_id: "empty_matches_catchall_en",
1795
2762
  matches: [{ type: "literal-match", key: "type", value: "invalid" }],
1796
2763
  pattern: [{ type: "text", value: "Invalid" }],
1797
2764
  },
1798
2765
  {
1799
2766
  id: "empty_matches_catchall_en_variant_any",
1800
- messageId: "empty_matches_catchall_en",
2767
+ message_id: "empty_matches_catchall_en",
1801
2768
  matches: [],
1802
2769
  pattern: [{ type: "text", value: "Any" }],
1803
2770
  },
@@ -1816,19 +2783,19 @@ const mockBundles = [
1816
2783
  messages: [
1817
2784
  {
1818
2785
  id: "numeric_input_match_en",
1819
- bundleId: "numeric_input_match",
2786
+ bundle_id: "numeric_input_match",
1820
2787
  locale: "en",
1821
2788
  selectors: [],
1822
2789
  variants: [
1823
2790
  {
1824
2791
  id: "numeric_input_match_en_variant_1",
1825
- messageId: "numeric_input_match_en",
2792
+ message_id: "numeric_input_match_en",
1826
2793
  matches: [{ type: "literal-match", key: "input", value: "1" }],
1827
2794
  pattern: [{ type: "text", value: "Thing 1" }],
1828
2795
  },
1829
2796
  {
1830
2797
  id: "numeric_input_match_en_variant_2",
1831
- messageId: "numeric_input_match_en",
2798
+ message_id: "numeric_input_match_en",
1832
2799
  matches: [{ type: "literal-match", key: "input", value: "2" }],
1833
2800
  pattern: [{ type: "text", value: "Thing 2" }],
1834
2801
  },
@@ -1846,7 +2813,7 @@ function createBundleNested(args) {
1846
2813
  declarations: args.declarations ?? [],
1847
2814
  messages: args.messages.map((message) => ({
1848
2815
  id: args.id + "_" + message.locale,
1849
- bundleId: args.id,
2816
+ bundle_id: args.id,
1850
2817
  locale: message.locale,
1851
2818
  selectors: message.selectors ?? [],
1852
2819
  variants: message.variants.map((variant) => ({
@@ -1855,11 +2822,26 @@ function createBundleNested(args) {
1855
2822
  message.locale +
1856
2823
  "_" +
1857
2824
  Math.random().toString(36).slice(2),
1858
- messageId: args.id,
2825
+ message_id: args.id,
1859
2826
  matches: variant.matches ?? [],
1860
2827
  pattern: variant.pattern,
1861
2828
  })),
1862
2829
  })),
1863
2830
  };
1864
2831
  }
2832
+ test("emitReadme only claims the output is git-ignored when a .gitignore is emitted", async () => {
2833
+ const project = await loadProjectInMemory({
2834
+ blob: await newProject({
2835
+ settings: { locales: ["en"], baseLocale: "en" },
2836
+ }),
2837
+ });
2838
+ const withGitignore = await compileProject({ project });
2839
+ const withoutGitignore = await compileProject({
2840
+ project,
2841
+ compilerOptions: { emitGitIgnore: false },
2842
+ });
2843
+ expect(withGitignore["README.md"]).toContain("git-ignored");
2844
+ expect(withoutGitignore).not.toHaveProperty(".gitignore");
2845
+ expect(withoutGitignore["README.md"]).not.toContain("git-ignored");
2846
+ });
1865
2847
  //# sourceMappingURL=compile-project.test.js.map