@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.
- package/README.md +2 -1
- package/dist/bundler-plugins/per-locale-build/vite-environments.test.js +2 -2
- package/dist/bundler-plugins/per-locale-build/vite-environments.test.js.map +1 -1
- package/dist/cli/commands/init/command.js +19 -1
- package/dist/cli/commands/init/command.js.map +1 -1
- package/dist/cli/defaults.js +2 -2
- package/dist/cli/defaults.js.map +1 -1
- package/dist/cli/steps/add-inlang-check.d.ts +19 -0
- package/dist/cli/steps/add-inlang-check.js +60 -0
- package/dist/cli/steps/add-inlang-check.js.map +1 -0
- package/dist/cli/steps/add-inlang-check.test.d.ts +1 -0
- package/dist/cli/steps/add-inlang-check.test.js +88 -0
- package/dist/cli/steps/add-inlang-check.test.js.map +1 -0
- package/dist/cli/steps/agents-md-suggestion.d.ts +11 -0
- package/dist/cli/steps/agents-md-suggestion.js +19 -0
- package/dist/cli/steps/agents-md-suggestion.js.map +1 -0
- package/dist/cli/steps/agents-md-suggestion.test.d.ts +1 -0
- package/dist/cli/steps/agents-md-suggestion.test.js +36 -0
- package/dist/cli/steps/agents-md-suggestion.test.js.map +1 -0
- package/dist/cli/steps/maybe-add-machine-translation.js +3 -2
- package/dist/cli/steps/maybe-add-machine-translation.js.map +1 -1
- package/dist/cli/steps/maybe-add-sherlock.js +1 -1
- package/dist/cli/steps/maybe-add-sherlock.js.map +1 -1
- package/dist/cli/utils.d.ts +9 -0
- package/dist/cli/utils.js +15 -0
- package/dist/cli/utils.js.map +1 -1
- package/dist/compiler/compile-annotation.d.ts +48 -1
- package/dist/compiler/compile-annotation.js +238 -10
- package/dist/compiler/compile-annotation.js.map +1 -1
- package/dist/compiler/compile-bundle.d.ts +3 -2
- package/dist/compiler/compile-bundle.js +30 -1
- package/dist/compiler/compile-bundle.js.map +1 -1
- package/dist/compiler/compile-bundle.test.js +18 -18
- package/dist/compiler/compile-bundle.test.js.map +1 -1
- package/dist/compiler/compile-local-variable.d.ts +3 -0
- package/dist/compiler/compile-local-variable.js +5 -2
- package/dist/compiler/compile-local-variable.js.map +1 -1
- package/dist/compiler/compile-local-variable.test.js +44 -4
- package/dist/compiler/compile-local-variable.test.js.map +1 -1
- package/dist/compiler/compile-message.d.ts +29 -1
- package/dist/compiler/compile-message.exact-number.test.d.ts +1 -0
- package/dist/compiler/compile-message.exact-number.test.js +786 -0
- package/dist/compiler/compile-message.exact-number.test.js.map +1 -0
- package/dist/compiler/compile-message.js +162 -66
- package/dist/compiler/compile-message.js.map +1 -1
- package/dist/compiler/compile-message.locals.test.d.ts +1 -0
- package/dist/compiler/compile-message.locals.test.js +498 -0
- package/dist/compiler/compile-message.locals.test.js.map +1 -0
- package/dist/compiler/compile-message.test.js +111 -20
- package/dist/compiler/compile-message.test.js.map +1 -1
- package/dist/compiler/compile-pattern.d.ts +5 -0
- package/dist/compiler/compile-pattern.js +6 -18
- package/dist/compiler/compile-pattern.js.map +1 -1
- package/dist/compiler/compile-pattern.test.js +165 -1
- package/dist/compiler/compile-pattern.test.js.map +1 -1
- package/dist/compiler/compile-project.js +4 -1
- package/dist/compiler/compile-project.js.map +1 -1
- package/dist/compiler/compile-project.test.js +1013 -31
- package/dist/compiler/compile-project.test.js.map +1 -1
- package/dist/compiler/compile.test.js +1 -1
- package/dist/compiler/compile.test.js.map +1 -1
- package/dist/compiler/create-readme.d.ts +4 -1
- package/dist/compiler/create-readme.js +33 -125
- package/dist/compiler/create-readme.js.map +1 -1
- package/dist/compiler/emit-ts-declarations.interop.test.js +2 -2
- package/dist/compiler/emit-ts-declarations.interop.test.js.map +1 -1
- package/dist/compiler/emit-ts-declarations.test.js +4 -4
- package/dist/compiler/emit-ts-declarations.test.js.map +1 -1
- package/dist/compiler/index.d.ts +1 -1
- package/dist/compiler/index.js +1 -1
- package/dist/compiler/index.js.map +1 -1
- package/dist/compiler/input-alias.d.ts +21 -0
- package/dist/compiler/input-alias.js +42 -0
- package/dist/compiler/input-alias.js.map +1 -0
- package/dist/compiler/input-alias.test.d.ts +1 -0
- package/dist/compiler/input-alias.test.js +58 -0
- package/dist/compiler/input-alias.test.js.map +1 -0
- package/dist/compiler/message-locals.d.ts +46 -0
- package/dist/compiler/message-locals.js +129 -0
- package/dist/compiler/message-locals.js.map +1 -0
- package/dist/compiler/output-structure/locale-modules.js +7 -1
- package/dist/compiler/output-structure/locale-modules.js.map +1 -1
- package/dist/compiler/output-structure/locale-modules.test.js +35 -0
- package/dist/compiler/output-structure/locale-modules.test.js.map +1 -1
- package/dist/compiler/output-structure/message-modules.js +5 -3
- package/dist/compiler/output-structure/message-modules.js.map +1 -1
- package/dist/compiler/output-structure/message-modules.test.js +35 -0
- package/dist/compiler/output-structure/message-modules.test.js.map +1 -1
- package/dist/compiler/registry.d.ts +12 -0
- package/dist/compiler/registry.js +97 -28
- package/dist/compiler/registry.js.map +1 -1
- package/dist/compiler/server/create-server-file.js +24 -0
- package/dist/compiler/server/create-server-file.js.map +1 -1
- package/dist/compiler/server/create-server-file.test.js +8 -2
- package/dist/compiler/server/create-server-file.test.js.map +1 -1
- package/dist/compiler/server/middleware.js +29 -4
- package/dist/compiler/server/middleware.js.map +1 -1
- package/dist/services/env-variables/index.js +1 -1
- package/dist/services/env-variables/index.js.map +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|