@inlang/paraglide-js 2.26.0 → 2.28.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 +11 -1
- package/dist/bundler-plugins/per-locale-build/index.js +14 -7
- package/dist/bundler-plugins/per-locale-build/index.js.map +1 -1
- package/dist/bundler-plugins/per-locale-build/vite-environments.test.js +237 -4
- package/dist/bundler-plugins/per-locale-build/vite-environments.test.js.map +1 -1
- package/dist/bundler-plugins/unplugin.d.ts +1 -1
- package/dist/bundler-plugins/unplugin.js +79 -10
- package/dist/bundler-plugins/unplugin.js.map +1 -1
- package/dist/bundler-plugins/unplugin.test.js +334 -1
- package/dist/bundler-plugins/unplugin.test.js.map +1 -1
- package/dist/bundler-plugins/vite.js +2 -1
- package/dist/bundler-plugins/vite.js.map +1 -1
- package/dist/bundler-plugins/vite.test.js +1 -1
- package/dist/bundler-plugins/vite.test.js.map +1 -1
- package/dist/cli/commands/compile/command.js +13 -0
- package/dist/cli/commands/compile/command.js.map +1 -1
- package/dist/cli/commands/compile/command.test.js +87 -3
- package/dist/cli/commands/compile/command.test.js.map +1 -1
- package/dist/cli/commands/init/command.d.ts +5 -0
- package/dist/cli/commands/init/command.js +216 -44
- package/dist/cli/commands/init/command.js.map +1 -1
- package/dist/cli/commands/init/command.test.d.ts +1 -0
- package/dist/cli/commands/init/command.test.js +123 -0
- package/dist/cli/commands/init/command.test.js.map +1 -0
- package/dist/cli/defaults.js +2 -2
- package/dist/cli/defaults.js.map +1 -1
- package/dist/cli/init-options.d.ts +11 -0
- package/dist/cli/init-options.js +2 -0
- package/dist/cli/init-options.js.map +1 -0
- 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/add-message-lint.d.ts +34 -0
- package/dist/cli/steps/add-message-lint.js +105 -0
- package/dist/cli/steps/add-message-lint.js.map +1 -0
- package/dist/cli/steps/add-message-lint.test.d.ts +1 -0
- package/dist/cli/steps/add-message-lint.test.js +74 -0
- package/dist/cli/steps/add-message-lint.test.js.map +1 -0
- package/dist/cli/steps/add-vite-plugin.d.ts +18 -0
- package/dist/cli/steps/add-vite-plugin.js +34 -6
- package/dist/cli/steps/add-vite-plugin.js.map +1 -1
- package/dist/cli/steps/add-vite-plugin.test.js +25 -9
- package/dist/cli/steps/add-vite-plugin.test.js.map +1 -1
- 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/check-for-uncomitted-changes.d.ts +3 -0
- package/dist/cli/steps/check-for-uncomitted-changes.js +15 -3
- package/dist/cli/steps/check-for-uncomitted-changes.js.map +1 -1
- package/dist/cli/steps/detect-bundler.d.ts +1 -0
- package/dist/cli/steps/detect-bundler.js +7 -1
- package/dist/cli/steps/detect-bundler.js.map +1 -1
- package/dist/cli/steps/initialize-inlang-project.d.ts +4 -3
- package/dist/cli/steps/initialize-inlang-project.js +60 -157
- package/dist/cli/steps/initialize-inlang-project.js.map +1 -1
- package/dist/cli/steps/maybe-add-machine-translation.d.ts +4 -0
- package/dist/cli/steps/maybe-add-machine-translation.js +14 -8
- package/dist/cli/steps/maybe-add-machine-translation.js.map +1 -1
- package/dist/cli/steps/maybe-add-sherlock.d.ts +2 -0
- package/dist/cli/steps/maybe-add-sherlock.js +13 -7
- package/dist/cli/steps/maybe-add-sherlock.js.map +1 -1
- package/dist/cli/steps/maybe-add-sherlock.test.d.ts +1 -0
- package/dist/cli/steps/maybe-add-sherlock.test.js +44 -0
- package/dist/cli/steps/maybe-add-sherlock.test.js.map +1 -0
- package/dist/cli/steps/prompt-for-outdir.d.ts +2 -0
- package/dist/cli/steps/prompt-for-outdir.js +4 -0
- package/dist/cli/steps/prompt-for-outdir.js.map +1 -1
- package/dist/cli/steps/update-package-json.js +8 -13
- package/dist/cli/steps/update-package-json.js.map +1 -1
- package/dist/cli/steps/update-ts-config.d.ts +6 -0
- package/dist/cli/steps/update-ts-config.js +27 -4
- package/dist/cli/steps/update-ts-config.js.map +1 -1
- package/dist/cli/utils.d.ts +11 -0
- package/dist/cli/utils.js +21 -0
- package/dist/cli/utils.js.map +1 -1
- package/dist/cli/utils.test.d.ts +1 -0
- package/dist/cli/utils.test.js +16 -0
- package/dist/cli/utils.test.js.map +1 -0
- package/dist/compiler/compile-annotation.d.ts +48 -1
- package/dist/compiler/compile-annotation.js +243 -10
- package/dist/compiler/compile-annotation.js.map +1 -1
- package/dist/compiler/compile-bundle.d.ts +7 -2
- package/dist/compiler/compile-bundle.js +97 -3
- package/dist/compiler/compile-bundle.js.map +1 -1
- package/dist/compiler/compile-bundle.test.js +232 -19
- package/dist/compiler/compile-bundle.test.js.map +1 -1
- package/dist/compiler/compile-lifecycle.test.d.ts +1 -0
- package/dist/compiler/compile-lifecycle.test.js +69 -0
- package/dist/compiler/compile-lifecycle.test.js.map +1 -0
- 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 +55 -15
- 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 +113 -22
- 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 +169 -5
- package/dist/compiler/compile-pattern.test.js.map +1 -1
- package/dist/compiler/compile-project.js +5 -1
- package/dist/compiler/compile-project.js.map +1 -1
- package/dist/compiler/compile-project.test.js +1260 -185
- package/dist/compiler/compile-project.test.js.map +1 -1
- package/dist/compiler/compile.js +36 -17
- package/dist/compiler/compile.js.map +1 -1
- package/dist/compiler/compile.test.js +71 -1
- package/dist/compiler/compile.test.js.map +1 -1
- package/dist/compiler/compiler-options.d.ts +41 -0
- package/dist/compiler/compiler-options.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 +6 -4
- 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 +158 -31
- package/dist/compiler/registry.js.map +1 -1
- package/dist/compiler/registry.test.d.ts +1 -0
- package/dist/compiler/registry.test.js +291 -0
- package/dist/compiler/registry.test.js.map +1 -0
- package/dist/compiler/runtime/create-runtime.d.ts +2 -0
- package/dist/compiler/runtime/create-runtime.js +9 -0
- package/dist/compiler/runtime/create-runtime.js.map +1 -1
- package/dist/compiler/runtime/extract-locale-from-cookie.js +8 -1
- package/dist/compiler/runtime/extract-locale-from-cookie.js.map +1 -1
- package/dist/compiler/runtime/extract-locale-from-cookie.test.js +26 -0
- package/dist/compiler/runtime/extract-locale-from-cookie.test.js.map +1 -1
- package/dist/compiler/runtime/extract-locale-from-header.js +47 -5
- package/dist/compiler/runtime/extract-locale-from-header.js.map +1 -1
- package/dist/compiler/runtime/extract-locale-from-header.test.js +58 -0
- package/dist/compiler/runtime/extract-locale-from-header.test.js.map +1 -1
- package/dist/compiler/runtime/get-locale.js +7 -1
- package/dist/compiler/runtime/get-locale.js.map +1 -1
- package/dist/compiler/runtime/get-locale.test.js +95 -0
- package/dist/compiler/runtime/get-locale.test.js.map +1 -1
- package/dist/compiler/runtime/localize-href.d.ts +8 -0
- package/dist/compiler/runtime/localize-href.js +60 -0
- package/dist/compiler/runtime/localize-href.js.map +1 -1
- package/dist/compiler/runtime/localize-href.test.js +227 -1
- package/dist/compiler/runtime/localize-href.test.js.map +1 -1
- package/dist/compiler/runtime/set-locale.js +24 -6
- package/dist/compiler/runtime/set-locale.js.map +1 -1
- package/dist/compiler/runtime/set-locale.test.js +182 -0
- package/dist/compiler/runtime/set-locale.test.js.map +1 -1
- package/dist/compiler/runtime/type.d.ts +2 -0
- package/dist/compiler/runtime/type.js.map +1 -1
- package/dist/compiler/runtime/type.test.js +34 -0
- package/dist/compiler/runtime/type.test.js.map +1 -1
- package/dist/compiler/runtime/variables.d.ts +2 -0
- package/dist/compiler/runtime/variables.js +7 -0
- package/dist/compiler/runtime/variables.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.d.ts +8 -2
- package/dist/compiler/server/middleware.js +204 -11
- package/dist/compiler/server/middleware.js.map +1 -1
- package/dist/compiler/server/middleware.test.js +750 -3
- package/dist/compiler/server/middleware.test.js.map +1 -1
- package/dist/eslint/index.d.ts +4 -0
- package/dist/eslint/index.js +8 -0
- package/dist/eslint/index.js.map +1 -0
- package/dist/eslint/no-dynamic-message-access.d.ts +3 -0
- package/dist/eslint/no-dynamic-message-access.js +150 -0
- package/dist/eslint/no-dynamic-message-access.js.map +1 -0
- package/dist/eslint/no-dynamic-message-access.test.d.ts +1 -0
- package/dist/eslint/no-dynamic-message-access.test.js +126 -0
- package/dist/eslint/no-dynamic-message-access.test.js.map +1 -0
- package/dist/eslint/oxlint.test.d.ts +1 -0
- package/dist/eslint/oxlint.test.js +64 -0
- package/dist/eslint/oxlint.test.js.map +1 -0
- package/dist/services/config/config-schema.d.ts +2 -0
- package/dist/services/config/config-schema.js +2 -0
- package/dist/services/config/config-schema.js.map +1 -1
- package/dist/services/config/load-paraglide-config.test.js +12 -0
- package/dist/services/config/load-paraglide-config.test.js.map +1 -1
- package/dist/services/env-variables/index.js +1 -1
- package/dist/services/env-variables/index.js.map +1 -1
- package/dist/services/file-handling/write-output.bench.js +51 -11
- package/dist/services/file-handling/write-output.bench.js.map +1 -1
- package/dist/services/file-handling/write-output.js +60 -20
- package/dist/services/file-handling/write-output.js.map +1 -1
- package/dist/services/file-handling/write-output.test.js +175 -0
- package/dist/services/file-handling/write-output.test.js.map +1 -1
- package/package.json +15 -5
|
@@ -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";
|
|
@@ -24,6 +25,35 @@ function hasFallbackCycle(fallbackMap, start) {
|
|
|
24
25
|
}
|
|
25
26
|
return false;
|
|
26
27
|
}
|
|
28
|
+
test("locale-modules retain language-grouped static text helpers", async () => {
|
|
29
|
+
const project = await loadProjectInMemory({
|
|
30
|
+
blob: await newProject({
|
|
31
|
+
settings: { baseLocale: "en", locales: ["en", "de"] },
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
await insertBundleNested(project.db, createBundleNested({
|
|
35
|
+
id: "literal_layout",
|
|
36
|
+
declarations: [],
|
|
37
|
+
messages: ["en", "de"].map((locale) => ({
|
|
38
|
+
locale,
|
|
39
|
+
variants: [
|
|
40
|
+
{
|
|
41
|
+
pattern: [
|
|
42
|
+
{ type: "text", value: `layout text ${locale}` },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
})),
|
|
47
|
+
}));
|
|
48
|
+
const output = await compileProject({
|
|
49
|
+
project,
|
|
50
|
+
compilerOptions: { outputStructure: "locale-modules" },
|
|
51
|
+
});
|
|
52
|
+
expect(output["messages/_index.js"]).toContain("return __de.literal_layout(inputs)");
|
|
53
|
+
expect(output["messages/_index.js"]).not.toContain('return /** @type {LocalizedString} */ ("layout text de")');
|
|
54
|
+
expect(output["messages/de.js"]).toContain("export const literal_layout =");
|
|
55
|
+
await project.close();
|
|
56
|
+
});
|
|
27
57
|
test("emitGitignore", async () => {
|
|
28
58
|
const project = await loadProjectInMemory({
|
|
29
59
|
blob: await newProject({
|
|
@@ -87,6 +117,70 @@ test("marks tree-shakeable generated message layouts as side-effect-free", async
|
|
|
87
117
|
sideEffects: false,
|
|
88
118
|
});
|
|
89
119
|
});
|
|
120
|
+
test("inlines static text and retains only fallback helpers under strict checkJs", async () => {
|
|
121
|
+
const project = await loadProjectInMemory({
|
|
122
|
+
blob: await newProject({
|
|
123
|
+
settings: {
|
|
124
|
+
baseLocale: "en",
|
|
125
|
+
locales: ["en", "de", "de-AT"],
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
});
|
|
129
|
+
await insertBundleNested(project.db, createBundleNested({
|
|
130
|
+
id: "plain_greeting",
|
|
131
|
+
messages: [
|
|
132
|
+
{
|
|
133
|
+
locale: "en",
|
|
134
|
+
variants: [{ pattern: [{ type: "text", value: "Hello" }] }],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
locale: "de",
|
|
138
|
+
variants: [{ pattern: [{ type: "text", value: "Hallo" }] }],
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
}));
|
|
142
|
+
const output = await compileProject({
|
|
143
|
+
project,
|
|
144
|
+
compilerOptions: { outputStructure: "message-modules" },
|
|
145
|
+
});
|
|
146
|
+
const messageModule = output["messages/plain_greeting.js"];
|
|
147
|
+
// The base locale is returned inline, while de-AT's missing-locale alias
|
|
148
|
+
// still needs the de message function.
|
|
149
|
+
expect(messageModule).not.toContain("const en_plain_greeting =");
|
|
150
|
+
expect(messageModule).toContain("const de_plain_greeting =");
|
|
151
|
+
expect(messageModule).toMatch(/const de_at\d*_plain_greeting = de_plain_greeting;/);
|
|
152
|
+
const tsProject = await typescriptProject({
|
|
153
|
+
useInMemoryFileSystem: true,
|
|
154
|
+
compilerOptions: {
|
|
155
|
+
outDir: "dist",
|
|
156
|
+
declaration: true,
|
|
157
|
+
allowJs: true,
|
|
158
|
+
checkJs: true,
|
|
159
|
+
noImplicitAny: true,
|
|
160
|
+
noUnusedLocals: true,
|
|
161
|
+
noUnusedParameters: true,
|
|
162
|
+
noImplicitReturns: true,
|
|
163
|
+
noImplicitThis: true,
|
|
164
|
+
noUncheckedIndexedAccess: true,
|
|
165
|
+
noPropertyAccessFromIndexSignature: true,
|
|
166
|
+
module: ts.ModuleKind.Node16,
|
|
167
|
+
strict: true,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
for (const [fileName, code] of Object.entries(output)) {
|
|
171
|
+
if (fileName.endsWith(".js") || fileName.endsWith(".ts")) {
|
|
172
|
+
tsProject.createSourceFile(fileName, code);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const diagnostics = ts
|
|
176
|
+
.getPreEmitDiagnostics(tsProject.createProgram())
|
|
177
|
+
.filter((d) => !d.messageText.toString().includes("Cannot find module 'async_hooks'"));
|
|
178
|
+
expect(diagnostics).toEqual([]);
|
|
179
|
+
const { m } = await importCode(await bundleCode(output, `export * as m from "./paraglide/messages.js"`));
|
|
180
|
+
expect(m.plain_greeting({}, { locale: "en" })).toBe("Hello");
|
|
181
|
+
expect(m.plain_greeting({}, { locale: "de" })).toBe("Hallo");
|
|
182
|
+
expect(m.plain_greeting({}, { locale: "de-AT" })).toBe("Hallo");
|
|
183
|
+
});
|
|
90
184
|
test("emitPrettierIgnore", async () => {
|
|
91
185
|
const project = await loadProjectInMemory({
|
|
92
186
|
blob: await newProject({
|
|
@@ -163,6 +257,8 @@ test("emitReadme includes project path", async () => {
|
|
|
163
257
|
expect(output["README.md"]).toContain(projectPath);
|
|
164
258
|
expect(output["README.md"]).toContain("Compiled from:");
|
|
165
259
|
expect(output["README.md"]).toContain("Paraglide JS");
|
|
260
|
+
expect(output["README.md"]).toContain(`npx @inlang/paraglide-js compile --project ${projectPath}`);
|
|
261
|
+
expect(output["README.md"]).toContain("npx @inlang/cli check");
|
|
166
262
|
});
|
|
167
263
|
test("throws during compile for invalid routeStrategies match patterns", async () => {
|
|
168
264
|
const project = await loadProjectInMemory({
|
|
@@ -1143,7 +1239,7 @@ describe.each([
|
|
|
1143
1239
|
project: projectWithPatternAnnotation,
|
|
1144
1240
|
compilerOptions,
|
|
1145
1241
|
});
|
|
1146
|
-
expect(Object.values(output).join("\n")).toContain('registry.number("en", i?.count, { maximumFractionDigits: 1 })');
|
|
1242
|
+
expect(Object.values(output).join("\n")).toContain('registry.number("en", i?.count, { maximumFractionDigits: 1 }, true)');
|
|
1147
1243
|
const tsProject = await typescriptProject({
|
|
1148
1244
|
useInMemoryFileSystem: true,
|
|
1149
1245
|
compilerOptions: superStrictRuleOutAnyErrorTsSettings,
|
|
@@ -1250,179 +1346,1143 @@ describe.each([
|
|
|
1250
1346
|
}
|
|
1251
1347
|
expect(diagnostics.length).toEqual(0);
|
|
1252
1348
|
});
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
input: ["main.js"],
|
|
1258
|
-
plugins: [
|
|
1259
|
-
// @ts-expect-error - rollup types are not up to date
|
|
1260
|
-
virtual({
|
|
1261
|
-
...Object.fromEntries(Object.entries(output).map(([fileName, code]) => [
|
|
1262
|
-
"paraglide/" + fileName,
|
|
1263
|
-
code,
|
|
1264
|
-
])),
|
|
1265
|
-
"main.js": file,
|
|
1349
|
+
test("ICU exact number aliases select numerically and pass checkJs", async () => {
|
|
1350
|
+
const icuProject = await loadProjectInMemory({
|
|
1351
|
+
blob: await newProject({
|
|
1352
|
+
settings: { baseLocale: "en", locales: ["en", "fr"] },
|
|
1266
1353
|
}),
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
}
|
|
1280
|
-
const project = await loadProjectInMemory({
|
|
1281
|
-
blob: await newProject({
|
|
1282
|
-
settings: {
|
|
1283
|
-
baseLocale: "en",
|
|
1284
|
-
locales: ["en", "de", "en-US"],
|
|
1285
|
-
},
|
|
1286
|
-
}),
|
|
1287
|
-
});
|
|
1288
|
-
const mockBundles = [
|
|
1289
|
-
createBundleNested({
|
|
1290
|
-
id: "sad_penguin_bundle",
|
|
1291
|
-
messages: [
|
|
1292
|
-
{
|
|
1293
|
-
locale: "en",
|
|
1294
|
-
variants: [{ pattern: [{ type: "text", value: "A simple message." }] }],
|
|
1295
|
-
},
|
|
1296
|
-
{
|
|
1297
|
-
locale: "de",
|
|
1298
|
-
variants: [
|
|
1299
|
-
{ pattern: [{ type: "text", value: "Eine einfache Nachricht." }] },
|
|
1300
|
-
],
|
|
1301
|
-
},
|
|
1302
|
-
],
|
|
1303
|
-
}),
|
|
1304
|
-
createBundleNested({
|
|
1305
|
-
id: "Sad_penguin_bundle",
|
|
1306
|
-
messages: [
|
|
1307
|
-
{
|
|
1308
|
-
locale: "en",
|
|
1309
|
-
variants: [
|
|
1310
|
-
{ pattern: [{ type: "text", value: "Capital Sad penguin" }] },
|
|
1311
|
-
],
|
|
1312
|
-
},
|
|
1313
|
-
{
|
|
1314
|
-
locale: "de",
|
|
1315
|
-
variants: [
|
|
1316
|
-
{
|
|
1317
|
-
pattern: [
|
|
1318
|
-
{ type: "text", value: "Grossgeschriebenes Sad penguin" },
|
|
1319
|
-
],
|
|
1320
|
-
},
|
|
1321
|
-
],
|
|
1322
|
-
},
|
|
1323
|
-
],
|
|
1324
|
-
}),
|
|
1325
|
-
{
|
|
1326
|
-
id: "depressed_dog",
|
|
1327
|
-
declarations: [
|
|
1354
|
+
});
|
|
1355
|
+
const countPluralExact = (value) => value === undefined
|
|
1356
|
+
? { type: "catchall-match", key: "countPluralExact" }
|
|
1357
|
+
: {
|
|
1358
|
+
type: "literal-match",
|
|
1359
|
+
key: "countPluralExact",
|
|
1360
|
+
value,
|
|
1361
|
+
};
|
|
1362
|
+
const countPlural = (value) => value === undefined
|
|
1363
|
+
? { type: "catchall-match", key: "countPlural" }
|
|
1364
|
+
: { type: "literal-match", key: "countPlural", value };
|
|
1365
|
+
const withCount = (suffix) => [
|
|
1328
1366
|
{
|
|
1329
|
-
type: "
|
|
1330
|
-
|
|
1367
|
+
type: "expression",
|
|
1368
|
+
arg: { type: "variable-reference", name: "count" },
|
|
1331
1369
|
},
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
{ type: "text", value: "Good morning " },
|
|
1346
|
-
{
|
|
1347
|
-
type: "expression",
|
|
1348
|
-
arg: { type: "variable-reference", name: "name" },
|
|
1349
|
-
},
|
|
1350
|
-
{ type: "text", value: "!" },
|
|
1351
|
-
],
|
|
1370
|
+
{ type: "text", value: suffix },
|
|
1371
|
+
];
|
|
1372
|
+
// the shape the ICU1 plugin imports {count, plural, =0 {…} one {…} other {…}} as
|
|
1373
|
+
await insertBundleNested(icuProject.db, createBundleNested({
|
|
1374
|
+
id: "item_count",
|
|
1375
|
+
declarations: [
|
|
1376
|
+
{ type: "input-variable", name: "count" },
|
|
1377
|
+
{
|
|
1378
|
+
type: "local-variable",
|
|
1379
|
+
name: "countPluralExact",
|
|
1380
|
+
value: {
|
|
1381
|
+
type: "expression",
|
|
1382
|
+
arg: { type: "variable-reference", name: "count" },
|
|
1352
1383
|
},
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
pattern: [
|
|
1366
|
-
{ type: "text", value: "Guten Morgen " },
|
|
1367
|
-
{
|
|
1368
|
-
type: "expression",
|
|
1369
|
-
arg: { type: "variable-reference", name: "name" },
|
|
1370
|
-
},
|
|
1371
|
-
{ type: "text", value: "!" },
|
|
1372
|
-
],
|
|
1384
|
+
},
|
|
1385
|
+
{
|
|
1386
|
+
type: "local-variable",
|
|
1387
|
+
name: "countPlural",
|
|
1388
|
+
value: {
|
|
1389
|
+
type: "expression",
|
|
1390
|
+
arg: { type: "variable-reference", name: "count" },
|
|
1391
|
+
annotation: {
|
|
1392
|
+
type: "function-reference",
|
|
1393
|
+
name: "plural",
|
|
1394
|
+
options: [],
|
|
1395
|
+
},
|
|
1373
1396
|
},
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1397
|
+
},
|
|
1398
|
+
],
|
|
1399
|
+
messages: [
|
|
1400
|
+
{
|
|
1401
|
+
locale: "en",
|
|
1402
|
+
selectors: [
|
|
1403
|
+
{ type: "variable-reference", name: "countPluralExact" },
|
|
1404
|
+
{ type: "variable-reference", name: "countPlural" },
|
|
1405
|
+
],
|
|
1406
|
+
variants: [
|
|
1407
|
+
{
|
|
1408
|
+
matches: [countPluralExact("0"), countPlural()],
|
|
1409
|
+
pattern: [{ type: "text", value: "No items" }],
|
|
1410
|
+
},
|
|
1411
|
+
{
|
|
1412
|
+
matches: [countPluralExact(), countPlural("one")],
|
|
1413
|
+
pattern: withCount(" item"),
|
|
1414
|
+
},
|
|
1415
|
+
{
|
|
1416
|
+
matches: [countPluralExact(), countPlural()],
|
|
1417
|
+
pattern: withCount(" items"),
|
|
1418
|
+
},
|
|
1419
|
+
],
|
|
1420
|
+
},
|
|
1421
|
+
{
|
|
1422
|
+
locale: "fr",
|
|
1423
|
+
selectors: [
|
|
1424
|
+
{ type: "variable-reference", name: "countPluralExact" },
|
|
1425
|
+
{ type: "variable-reference", name: "countPlural" },
|
|
1426
|
+
],
|
|
1427
|
+
// an editor appends the exact variant last
|
|
1428
|
+
variants: [
|
|
1429
|
+
{
|
|
1430
|
+
matches: [countPluralExact(), countPlural("one")],
|
|
1431
|
+
pattern: withCount(" élément"),
|
|
1432
|
+
},
|
|
1433
|
+
{
|
|
1434
|
+
matches: [countPluralExact(), countPlural()],
|
|
1435
|
+
pattern: withCount(" éléments"),
|
|
1436
|
+
},
|
|
1437
|
+
{
|
|
1438
|
+
matches: [countPluralExact("0"), countPlural()],
|
|
1439
|
+
pattern: [{ type: "text", value: "Aucun élément" }],
|
|
1440
|
+
},
|
|
1441
|
+
],
|
|
1442
|
+
},
|
|
1443
|
+
],
|
|
1444
|
+
}));
|
|
1445
|
+
// en narrows `status` by a direct match, fr matches through an alias
|
|
1446
|
+
// with a catchall, so any status is accepted
|
|
1447
|
+
await insertBundleNested(icuProject.db, createBundleNested({
|
|
1448
|
+
id: "status_message",
|
|
1449
|
+
declarations: [
|
|
1450
|
+
{ type: "input-variable", name: "status" },
|
|
1451
|
+
{
|
|
1452
|
+
type: "local-variable",
|
|
1453
|
+
name: "statusExact",
|
|
1454
|
+
value: {
|
|
1455
|
+
type: "expression",
|
|
1456
|
+
arg: { type: "variable-reference", name: "status" },
|
|
1414
1457
|
},
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1458
|
+
},
|
|
1459
|
+
],
|
|
1460
|
+
messages: [
|
|
1461
|
+
{
|
|
1462
|
+
locale: "en",
|
|
1463
|
+
selectors: [{ type: "variable-reference", name: "status" }],
|
|
1464
|
+
variants: [
|
|
1465
|
+
{
|
|
1466
|
+
matches: [
|
|
1467
|
+
{ type: "literal-match", key: "status", value: "active" },
|
|
1468
|
+
],
|
|
1469
|
+
// reads statusExact: unused bundle locals fail noUnusedLocals
|
|
1470
|
+
pattern: [
|
|
1471
|
+
{ type: "text", value: "Active: " },
|
|
1472
|
+
{
|
|
1473
|
+
type: "expression",
|
|
1474
|
+
arg: { type: "variable-reference", name: "statusExact" },
|
|
1475
|
+
},
|
|
1476
|
+
],
|
|
1477
|
+
},
|
|
1478
|
+
],
|
|
1479
|
+
},
|
|
1480
|
+
{
|
|
1481
|
+
locale: "fr",
|
|
1482
|
+
selectors: [{ type: "variable-reference", name: "statusExact" }],
|
|
1483
|
+
variants: [
|
|
1484
|
+
{
|
|
1485
|
+
matches: [
|
|
1486
|
+
{ type: "literal-match", key: "statusExact", value: "0" },
|
|
1487
|
+
],
|
|
1488
|
+
pattern: [{ type: "text", value: "Zéro" }],
|
|
1489
|
+
},
|
|
1490
|
+
{
|
|
1491
|
+
matches: [{ type: "catchall-match", key: "statusExact" }],
|
|
1492
|
+
pattern: [{ type: "text", value: "Autre" }],
|
|
1493
|
+
},
|
|
1494
|
+
],
|
|
1495
|
+
},
|
|
1496
|
+
],
|
|
1497
|
+
}));
|
|
1498
|
+
const output = await compileProject({
|
|
1499
|
+
project: icuProject,
|
|
1500
|
+
compilerOptions,
|
|
1501
|
+
});
|
|
1502
|
+
const tsProject = await typescriptProject({
|
|
1503
|
+
useInMemoryFileSystem: true,
|
|
1504
|
+
compilerOptions: superStrictRuleOutAnyErrorTsSettings,
|
|
1505
|
+
});
|
|
1506
|
+
for (const [fileName, code] of Object.entries(output)) {
|
|
1507
|
+
if (fileName.endsWith(".js") || fileName.endsWith(".ts")) {
|
|
1508
|
+
tsProject.createSourceFile(fileName, code);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
tsProject.createSourceFile("test.ts", `
|
|
1512
|
+
import * as m from "./messages.js"
|
|
1513
|
+
|
|
1514
|
+
m.item_count({ count: 0 }) satisfies string
|
|
1515
|
+
m.item_count({ count: "0" }) satisfies string
|
|
1516
|
+
m.status_message({ status: "active" }) satisfies string
|
|
1517
|
+
m.status_message({ status: "inactive" }) satisfies string
|
|
1518
|
+
`);
|
|
1519
|
+
const program = tsProject.createProgram();
|
|
1520
|
+
const diagnostics = ts.getPreEmitDiagnostics(program).filter((d) => {
|
|
1521
|
+
return !d.messageText
|
|
1522
|
+
.toString()
|
|
1523
|
+
.includes("Cannot find module 'async_hooks'");
|
|
1524
|
+
});
|
|
1525
|
+
for (const diagnostic of diagnostics) {
|
|
1526
|
+
console.error(diagnostic.messageText, diagnostic.file?.fileName);
|
|
1527
|
+
}
|
|
1528
|
+
expect(diagnostics.length).toEqual(0);
|
|
1529
|
+
const { m } = await importCode(await bundleCode(output, `export * as m from "./paraglide/messages.js"`));
|
|
1530
|
+
const render = (locale) => [0, 1, 2, 5, 21, "0"].map((count) => m.item_count({ count }, { locale }));
|
|
1531
|
+
expect(render("en")).toEqual([
|
|
1532
|
+
"No items",
|
|
1533
|
+
"1 item",
|
|
1534
|
+
"2 items",
|
|
1535
|
+
"5 items",
|
|
1536
|
+
"21 items",
|
|
1537
|
+
"No items",
|
|
1538
|
+
]);
|
|
1539
|
+
expect(render("fr")).toEqual([
|
|
1540
|
+
"Aucun élément",
|
|
1541
|
+
"1 élément",
|
|
1542
|
+
"2 éléments",
|
|
1543
|
+
"5 éléments",
|
|
1544
|
+
"21 éléments",
|
|
1545
|
+
"Aucun élément",
|
|
1546
|
+
]);
|
|
1547
|
+
expect(m.status_message({ status: "inactive" }, { locale: "fr" })).toBe("Autre");
|
|
1548
|
+
});
|
|
1549
|
+
test("emits only the locals a locale reads, in dependency order, and supports plural offsets under checkJs", async () => {
|
|
1550
|
+
const localsProject = await loadProjectInMemory({
|
|
1551
|
+
blob: await newProject({
|
|
1552
|
+
settings: { baseLocale: "en", locales: ["en", "fr"] },
|
|
1553
|
+
}),
|
|
1554
|
+
});
|
|
1555
|
+
const ref = (name) => ({ type: "variable-reference", name });
|
|
1556
|
+
const literal = (key, value) => ({ type: "literal-match", key, value });
|
|
1557
|
+
const catchall = (key) => ({ type: "catchall-match", key });
|
|
1558
|
+
const text = (value) => [{ type: "text", value }];
|
|
1559
|
+
// en never reads statusExact, fr only reads it
|
|
1560
|
+
await insertBundleNested(localsProject.db, createBundleNested({
|
|
1561
|
+
id: "status_label",
|
|
1562
|
+
declarations: [
|
|
1563
|
+
{ type: "input-variable", name: "status" },
|
|
1564
|
+
{
|
|
1565
|
+
type: "local-variable",
|
|
1566
|
+
name: "statusExact",
|
|
1567
|
+
value: { type: "expression", arg: ref("status") },
|
|
1568
|
+
},
|
|
1569
|
+
],
|
|
1570
|
+
messages: [
|
|
1571
|
+
{
|
|
1572
|
+
locale: "en",
|
|
1573
|
+
selectors: [ref("status")],
|
|
1574
|
+
variants: [
|
|
1575
|
+
{
|
|
1576
|
+
matches: [literal("status", "active")],
|
|
1577
|
+
pattern: text("Active"),
|
|
1578
|
+
},
|
|
1579
|
+
{ matches: [catchall("status")], pattern: text("Other") },
|
|
1580
|
+
],
|
|
1581
|
+
},
|
|
1582
|
+
{
|
|
1583
|
+
locale: "fr",
|
|
1584
|
+
selectors: [ref("statusExact")],
|
|
1585
|
+
variants: [
|
|
1586
|
+
{
|
|
1587
|
+
matches: [literal("statusExact", "0")],
|
|
1588
|
+
pattern: text("Zéro"),
|
|
1589
|
+
},
|
|
1590
|
+
{ matches: [catchall("statusExact")], pattern: text("Autre") },
|
|
1591
|
+
],
|
|
1592
|
+
},
|
|
1593
|
+
],
|
|
1594
|
+
}));
|
|
1595
|
+
// the shape the ICU1 plugin imports
|
|
1596
|
+
// {count, plural, offset:1 =0 {…} =1 {…} one {…} other {…}} as
|
|
1597
|
+
await insertBundleNested(localsProject.db, createBundleNested({
|
|
1598
|
+
id: "guest_list",
|
|
1599
|
+
declarations: [
|
|
1600
|
+
{ type: "input-variable", name: "count" },
|
|
1601
|
+
{
|
|
1602
|
+
type: "local-variable",
|
|
1603
|
+
name: "countPluralOffset1",
|
|
1604
|
+
value: {
|
|
1605
|
+
type: "expression",
|
|
1606
|
+
arg: ref("count"),
|
|
1607
|
+
annotation: {
|
|
1608
|
+
type: "function-reference",
|
|
1609
|
+
name: "plural",
|
|
1610
|
+
options: [
|
|
1611
|
+
{ name: "offset", value: { type: "literal", value: "1" } },
|
|
1612
|
+
],
|
|
1613
|
+
},
|
|
1614
|
+
},
|
|
1615
|
+
},
|
|
1616
|
+
{
|
|
1617
|
+
type: "local-variable",
|
|
1618
|
+
name: "countPluralOffset1Exact",
|
|
1619
|
+
value: { type: "expression", arg: ref("count") },
|
|
1620
|
+
},
|
|
1621
|
+
],
|
|
1622
|
+
messages: [
|
|
1623
|
+
{
|
|
1624
|
+
locale: "en",
|
|
1625
|
+
selectors: [
|
|
1626
|
+
ref("countPluralOffset1Exact"),
|
|
1627
|
+
ref("countPluralOffset1"),
|
|
1628
|
+
],
|
|
1629
|
+
variants: [
|
|
1630
|
+
{
|
|
1631
|
+
matches: [
|
|
1632
|
+
literal("countPluralOffset1Exact", "0"),
|
|
1633
|
+
catchall("countPluralOffset1"),
|
|
1634
|
+
],
|
|
1635
|
+
pattern: text("Nobody"),
|
|
1636
|
+
},
|
|
1637
|
+
{
|
|
1638
|
+
matches: [
|
|
1639
|
+
literal("countPluralOffset1Exact", "1"),
|
|
1640
|
+
catchall("countPluralOffset1"),
|
|
1641
|
+
],
|
|
1642
|
+
pattern: text("Only you"),
|
|
1643
|
+
},
|
|
1644
|
+
{
|
|
1645
|
+
matches: [
|
|
1646
|
+
catchall("countPluralOffset1Exact"),
|
|
1647
|
+
literal("countPluralOffset1", "one"),
|
|
1648
|
+
],
|
|
1649
|
+
pattern: text("You and one other"),
|
|
1650
|
+
},
|
|
1651
|
+
{
|
|
1652
|
+
matches: [
|
|
1653
|
+
catchall("countPluralOffset1Exact"),
|
|
1654
|
+
catchall("countPluralOffset1"),
|
|
1655
|
+
],
|
|
1656
|
+
pattern: text("You and others"),
|
|
1657
|
+
},
|
|
1658
|
+
],
|
|
1659
|
+
},
|
|
1660
|
+
{
|
|
1661
|
+
// fr does not translate the exact cases: countPluralOffset1Exact
|
|
1662
|
+
// is not read
|
|
1663
|
+
locale: "fr",
|
|
1664
|
+
selectors: [ref("countPluralOffset1")],
|
|
1665
|
+
variants: [
|
|
1666
|
+
{
|
|
1667
|
+
matches: [literal("countPluralOffset1", "one")],
|
|
1668
|
+
pattern: text("Vous et un autre"),
|
|
1669
|
+
},
|
|
1670
|
+
{
|
|
1671
|
+
matches: [catchall("countPluralOffset1")],
|
|
1672
|
+
pattern: text("Vous et d'autres"),
|
|
1673
|
+
},
|
|
1674
|
+
],
|
|
1675
|
+
},
|
|
1676
|
+
],
|
|
1677
|
+
}));
|
|
1678
|
+
// fr reads no variable at all
|
|
1679
|
+
await insertBundleNested(localsProject.db, createBundleNested({
|
|
1680
|
+
id: "greeting_label",
|
|
1681
|
+
declarations: [
|
|
1682
|
+
{ type: "input-variable", name: "name" },
|
|
1683
|
+
{
|
|
1684
|
+
type: "local-variable",
|
|
1685
|
+
name: "nameValue",
|
|
1686
|
+
value: { type: "expression", arg: ref("name") },
|
|
1687
|
+
},
|
|
1688
|
+
],
|
|
1689
|
+
messages: [
|
|
1690
|
+
{
|
|
1691
|
+
locale: "en",
|
|
1692
|
+
selectors: [],
|
|
1693
|
+
variants: [
|
|
1694
|
+
{
|
|
1695
|
+
matches: [],
|
|
1696
|
+
pattern: [
|
|
1697
|
+
{ type: "text", value: "Hello " },
|
|
1698
|
+
{ type: "expression", arg: ref("nameValue") },
|
|
1699
|
+
],
|
|
1700
|
+
},
|
|
1701
|
+
],
|
|
1702
|
+
},
|
|
1703
|
+
{
|
|
1704
|
+
locale: "fr",
|
|
1705
|
+
selectors: [],
|
|
1706
|
+
variants: [{ matches: [], pattern: text("Bonjour") }],
|
|
1707
|
+
},
|
|
1708
|
+
],
|
|
1709
|
+
}));
|
|
1710
|
+
// a local declared before the local it reads
|
|
1711
|
+
await insertBundleNested(localsProject.db, createBundleNested({
|
|
1712
|
+
id: "reordered_locals",
|
|
1713
|
+
declarations: [
|
|
1714
|
+
{ type: "input-variable", name: "count" },
|
|
1715
|
+
{
|
|
1716
|
+
type: "local-variable",
|
|
1717
|
+
name: "countPlural",
|
|
1718
|
+
value: {
|
|
1719
|
+
type: "expression",
|
|
1720
|
+
arg: ref("countValue"),
|
|
1721
|
+
annotation: {
|
|
1722
|
+
type: "function-reference",
|
|
1723
|
+
name: "plural",
|
|
1724
|
+
options: [],
|
|
1725
|
+
},
|
|
1726
|
+
},
|
|
1727
|
+
},
|
|
1728
|
+
{
|
|
1729
|
+
type: "local-variable",
|
|
1730
|
+
name: "countValue",
|
|
1731
|
+
value: { type: "expression", arg: ref("count") },
|
|
1732
|
+
},
|
|
1733
|
+
],
|
|
1734
|
+
messages: [
|
|
1735
|
+
{
|
|
1736
|
+
locale: "en",
|
|
1737
|
+
selectors: [ref("countPlural")],
|
|
1738
|
+
variants: [
|
|
1739
|
+
{
|
|
1740
|
+
matches: [literal("countPlural", "one")],
|
|
1741
|
+
pattern: text("one"),
|
|
1742
|
+
},
|
|
1743
|
+
{ matches: [catchall("countPlural")], pattern: text("other") },
|
|
1744
|
+
],
|
|
1745
|
+
},
|
|
1746
|
+
],
|
|
1747
|
+
}));
|
|
1748
|
+
const output = await compileProject({
|
|
1749
|
+
project: localsProject,
|
|
1750
|
+
compilerOptions,
|
|
1751
|
+
});
|
|
1752
|
+
const tsProject = await typescriptProject({
|
|
1753
|
+
useInMemoryFileSystem: true,
|
|
1754
|
+
compilerOptions: superStrictRuleOutAnyErrorTsSettings,
|
|
1755
|
+
});
|
|
1756
|
+
for (const [fileName, code] of Object.entries(output)) {
|
|
1757
|
+
if (fileName.endsWith(".js") || fileName.endsWith(".ts")) {
|
|
1758
|
+
tsProject.createSourceFile(fileName, code);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
tsProject.createSourceFile("test.ts", `
|
|
1762
|
+
import * as m from "./messages.js"
|
|
1763
|
+
|
|
1764
|
+
m.status_label({ status: "active" }) satisfies string
|
|
1765
|
+
m.guest_list({ count: 0 }) satisfies string
|
|
1766
|
+
m.reordered_locals({ count: 1 }) satisfies string
|
|
1767
|
+
m.greeting_label({ name: "Ada" }) satisfies string
|
|
1768
|
+
`);
|
|
1769
|
+
const program = tsProject.createProgram();
|
|
1770
|
+
const diagnostics = ts.getPreEmitDiagnostics(program).filter((d) => {
|
|
1771
|
+
return !d.messageText
|
|
1772
|
+
.toString()
|
|
1773
|
+
.includes("Cannot find module 'async_hooks'");
|
|
1774
|
+
});
|
|
1775
|
+
for (const diagnostic of diagnostics) {
|
|
1776
|
+
console.error(diagnostic.messageText, diagnostic.file?.fileName);
|
|
1777
|
+
}
|
|
1778
|
+
expect(diagnostics.length).toEqual(0);
|
|
1779
|
+
const { m } = await importCode(await bundleCode(output, `export * as m from "./paraglide/messages.js"`));
|
|
1780
|
+
expect(["active", "inactive"].map((status) => m.status_label({ status }, { locale: "en" }))).toEqual(["Active", "Other"]);
|
|
1781
|
+
expect([0, "0", 1].map((status) => m.status_label({ status }, { locale: "fr" }))).toEqual(["Zéro", "Zéro", "Autre"]);
|
|
1782
|
+
const guests = (locale) => [0, 1, 2, 3, 22, "1", "2"].map((count) => m.guest_list({ count }, { locale }));
|
|
1783
|
+
expect(guests("en")).toEqual([
|
|
1784
|
+
"Nobody",
|
|
1785
|
+
"Only you",
|
|
1786
|
+
"You and one other",
|
|
1787
|
+
"You and others",
|
|
1788
|
+
"You and others",
|
|
1789
|
+
"Only you",
|
|
1790
|
+
"You and one other",
|
|
1791
|
+
]);
|
|
1792
|
+
// French "one" covers 0 and 1 (and -1 for 0 guests), so 1 and 2 guests
|
|
1793
|
+
// (0 and 1 others) select "one"
|
|
1794
|
+
expect(guests("fr")).toEqual([
|
|
1795
|
+
"Vous et un autre",
|
|
1796
|
+
"Vous et un autre",
|
|
1797
|
+
"Vous et un autre",
|
|
1798
|
+
"Vous et d'autres",
|
|
1799
|
+
"Vous et d'autres",
|
|
1800
|
+
"Vous et un autre",
|
|
1801
|
+
"Vous et un autre",
|
|
1802
|
+
]);
|
|
1803
|
+
expect([1, 2].map((count) => m.reordered_locals({ count }, { locale: "en" }))).toEqual(["one", "other"]);
|
|
1804
|
+
expect(m.greeting_label({ name: "Ada" }, { locale: "en" })).toBe("Hello Ada");
|
|
1805
|
+
expect(m.greeting_label({ name: "Ada" }, { locale: "fr" })).toBe("Bonjour");
|
|
1806
|
+
});
|
|
1807
|
+
test("imports the registry only where a message calls it, and formats ICU # with offset under checkJs", async () => {
|
|
1808
|
+
const compileChecked = async (bundles) => {
|
|
1809
|
+
const icuProject = await loadProjectInMemory({
|
|
1810
|
+
blob: await newProject({
|
|
1811
|
+
settings: { baseLocale: "en", locales: ["en", "de"] },
|
|
1812
|
+
}),
|
|
1813
|
+
});
|
|
1814
|
+
for (const bundle of bundles) {
|
|
1815
|
+
await insertBundleNested(icuProject.db, createBundleNested(bundle));
|
|
1816
|
+
}
|
|
1817
|
+
const output = await compileProject({
|
|
1818
|
+
project: icuProject,
|
|
1819
|
+
compilerOptions,
|
|
1820
|
+
});
|
|
1821
|
+
const tsProject = await typescriptProject({
|
|
1822
|
+
useInMemoryFileSystem: true,
|
|
1823
|
+
compilerOptions: superStrictRuleOutAnyErrorTsSettings,
|
|
1824
|
+
});
|
|
1825
|
+
for (const [fileName, code] of Object.entries(output)) {
|
|
1826
|
+
if (fileName.endsWith(".js") || fileName.endsWith(".ts")) {
|
|
1827
|
+
tsProject.createSourceFile(fileName, code);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
const diagnostics = ts
|
|
1831
|
+
.getPreEmitDiagnostics(tsProject.createProgram())
|
|
1832
|
+
.filter((d) => !d.messageText
|
|
1833
|
+
.toString()
|
|
1834
|
+
.includes("Cannot find module 'async_hooks'"));
|
|
1835
|
+
for (const diagnostic of diagnostics) {
|
|
1836
|
+
console.error(diagnostic.messageText, diagnostic.file?.fileName);
|
|
1837
|
+
}
|
|
1838
|
+
expect(diagnostics.length).toEqual(0);
|
|
1839
|
+
return output;
|
|
1840
|
+
};
|
|
1841
|
+
const ref = (name) => ({ type: "variable-reference", name });
|
|
1842
|
+
const literal = (key, value) => ({ type: "literal-match", key, value });
|
|
1843
|
+
const catchall = (key) => ({ type: "catchall-match", key });
|
|
1844
|
+
const text = (value) => ({ type: "text", value });
|
|
1845
|
+
const pound = (offset) => ({
|
|
1846
|
+
type: "expression",
|
|
1847
|
+
arg: ref("count"),
|
|
1848
|
+
annotation: {
|
|
1849
|
+
type: "function-reference",
|
|
1850
|
+
name: "icu:pound",
|
|
1851
|
+
options: offset === undefined
|
|
1852
|
+
? []
|
|
1853
|
+
: [
|
|
1854
|
+
{
|
|
1855
|
+
name: "offset",
|
|
1856
|
+
value: { type: "literal", value: offset },
|
|
1857
|
+
},
|
|
1858
|
+
],
|
|
1859
|
+
},
|
|
1860
|
+
});
|
|
1861
|
+
// message text that contains "registry." must not import the registry
|
|
1862
|
+
const textOnly = await compileChecked([
|
|
1863
|
+
{
|
|
1864
|
+
id: "registry_text",
|
|
1865
|
+
declarations: [],
|
|
1866
|
+
messages: [
|
|
1867
|
+
{
|
|
1868
|
+
locale: "en",
|
|
1869
|
+
selectors: [],
|
|
1870
|
+
variants: [
|
|
1871
|
+
{ matches: [], pattern: [text("Open the registry. Now.")] },
|
|
1872
|
+
],
|
|
1873
|
+
},
|
|
1874
|
+
{
|
|
1875
|
+
locale: "de",
|
|
1876
|
+
selectors: [],
|
|
1877
|
+
variants: [
|
|
1878
|
+
{
|
|
1879
|
+
matches: [],
|
|
1880
|
+
pattern: [text("Öffne die registry. Jetzt.")],
|
|
1881
|
+
},
|
|
1882
|
+
],
|
|
1883
|
+
},
|
|
1884
|
+
],
|
|
1885
|
+
},
|
|
1886
|
+
// an unknown formatter on a local interpolates the raw value
|
|
1887
|
+
{
|
|
1888
|
+
id: "custom_local",
|
|
1889
|
+
declarations: [
|
|
1890
|
+
{ type: "input-variable", name: "amount" },
|
|
1891
|
+
{
|
|
1892
|
+
type: "local-variable",
|
|
1893
|
+
name: "formatted",
|
|
1894
|
+
value: {
|
|
1895
|
+
type: "expression",
|
|
1896
|
+
arg: ref("amount"),
|
|
1897
|
+
annotation: {
|
|
1898
|
+
type: "function-reference",
|
|
1899
|
+
name: "customLocalFormatter",
|
|
1900
|
+
options: [],
|
|
1901
|
+
},
|
|
1902
|
+
},
|
|
1903
|
+
},
|
|
1904
|
+
],
|
|
1905
|
+
messages: [
|
|
1906
|
+
{
|
|
1907
|
+
locale: "en",
|
|
1908
|
+
selectors: [],
|
|
1909
|
+
variants: [
|
|
1910
|
+
{
|
|
1911
|
+
matches: [],
|
|
1912
|
+
pattern: [
|
|
1913
|
+
text("Total: "),
|
|
1914
|
+
{ type: "expression", arg: ref("formatted") },
|
|
1915
|
+
],
|
|
1916
|
+
},
|
|
1917
|
+
],
|
|
1918
|
+
},
|
|
1919
|
+
],
|
|
1920
|
+
},
|
|
1921
|
+
]);
|
|
1922
|
+
for (const [fileName, code] of Object.entries(textOnly)) {
|
|
1923
|
+
if (fileName.startsWith("messages/")) {
|
|
1924
|
+
expect(code).not.toContain("import * as registry");
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
const { m: textMessages } = await importCode(await bundleCode(textOnly, `export * as m from "./paraglide/messages.js"`));
|
|
1928
|
+
expect(textMessages.registry_text({}, { locale: "en" })).toBe("Open the registry. Now.");
|
|
1929
|
+
expect(textMessages.custom_local({ amount: 1234 }, { locale: "en" })).toBe("Total: 1234");
|
|
1930
|
+
// {count, plural, offset:1 =0 {Nobody} =1 {You} one {You and # other} other {You and # others}}
|
|
1931
|
+
// as the ICU1 plugin imports it, and `#` without an offset
|
|
1932
|
+
const output = await compileChecked([
|
|
1933
|
+
{
|
|
1934
|
+
id: "guest_count",
|
|
1935
|
+
declarations: [
|
|
1936
|
+
{ type: "input-variable", name: "count" },
|
|
1937
|
+
{
|
|
1938
|
+
type: "local-variable",
|
|
1939
|
+
name: "countPluralOffset1",
|
|
1940
|
+
value: {
|
|
1941
|
+
type: "expression",
|
|
1942
|
+
arg: ref("count"),
|
|
1943
|
+
annotation: {
|
|
1944
|
+
type: "function-reference",
|
|
1945
|
+
name: "plural",
|
|
1946
|
+
options: [
|
|
1947
|
+
{ name: "offset", value: { type: "literal", value: "1" } },
|
|
1948
|
+
],
|
|
1949
|
+
},
|
|
1950
|
+
},
|
|
1951
|
+
},
|
|
1952
|
+
{
|
|
1953
|
+
type: "local-variable",
|
|
1954
|
+
name: "countPluralOffset1Exact",
|
|
1955
|
+
value: { type: "expression", arg: ref("count") },
|
|
1956
|
+
},
|
|
1957
|
+
],
|
|
1958
|
+
messages: [
|
|
1959
|
+
{
|
|
1960
|
+
locale: "en",
|
|
1961
|
+
selectors: [
|
|
1962
|
+
ref("countPluralOffset1Exact"),
|
|
1963
|
+
ref("countPluralOffset1"),
|
|
1964
|
+
],
|
|
1965
|
+
variants: [
|
|
1966
|
+
{
|
|
1967
|
+
matches: [
|
|
1968
|
+
literal("countPluralOffset1Exact", "0"),
|
|
1969
|
+
catchall("countPluralOffset1"),
|
|
1970
|
+
],
|
|
1971
|
+
pattern: [text("Nobody")],
|
|
1972
|
+
},
|
|
1973
|
+
{
|
|
1974
|
+
matches: [
|
|
1975
|
+
literal("countPluralOffset1Exact", "1"),
|
|
1976
|
+
catchall("countPluralOffset1"),
|
|
1977
|
+
],
|
|
1978
|
+
pattern: [text("You")],
|
|
1979
|
+
},
|
|
1980
|
+
{
|
|
1981
|
+
matches: [
|
|
1982
|
+
catchall("countPluralOffset1Exact"),
|
|
1983
|
+
literal("countPluralOffset1", "one"),
|
|
1984
|
+
],
|
|
1985
|
+
pattern: [text("You and "), pound("1"), text(" other")],
|
|
1986
|
+
},
|
|
1987
|
+
{
|
|
1988
|
+
matches: [
|
|
1989
|
+
catchall("countPluralOffset1Exact"),
|
|
1990
|
+
catchall("countPluralOffset1"),
|
|
1991
|
+
],
|
|
1992
|
+
pattern: [text("You and "), pound("1"), text(" others")],
|
|
1993
|
+
},
|
|
1994
|
+
],
|
|
1995
|
+
},
|
|
1996
|
+
],
|
|
1997
|
+
},
|
|
1998
|
+
{
|
|
1999
|
+
id: "item_total",
|
|
2000
|
+
declarations: [
|
|
2001
|
+
{ type: "input-variable", name: "count" },
|
|
2002
|
+
{
|
|
2003
|
+
type: "local-variable",
|
|
2004
|
+
name: "countPlural",
|
|
2005
|
+
value: {
|
|
2006
|
+
type: "expression",
|
|
2007
|
+
arg: ref("count"),
|
|
2008
|
+
annotation: {
|
|
2009
|
+
type: "function-reference",
|
|
2010
|
+
name: "plural",
|
|
2011
|
+
options: [],
|
|
2012
|
+
},
|
|
2013
|
+
},
|
|
2014
|
+
},
|
|
2015
|
+
],
|
|
2016
|
+
messages: [
|
|
2017
|
+
{
|
|
2018
|
+
locale: "en",
|
|
2019
|
+
selectors: [ref("countPlural")],
|
|
2020
|
+
variants: [
|
|
2021
|
+
{
|
|
2022
|
+
matches: [literal("countPlural", "one")],
|
|
2023
|
+
pattern: [pound(), text(" item")],
|
|
2024
|
+
},
|
|
2025
|
+
{
|
|
2026
|
+
matches: [catchall("countPlural")],
|
|
2027
|
+
pattern: [pound(), text(" items")],
|
|
2028
|
+
},
|
|
2029
|
+
],
|
|
2030
|
+
},
|
|
2031
|
+
{
|
|
2032
|
+
locale: "de",
|
|
2033
|
+
selectors: [ref("countPlural")],
|
|
2034
|
+
variants: [
|
|
2035
|
+
{
|
|
2036
|
+
matches: [literal("countPlural", "one")],
|
|
2037
|
+
pattern: [pound(), text(" Artikel")],
|
|
2038
|
+
},
|
|
2039
|
+
{
|
|
2040
|
+
matches: [catchall("countPlural")],
|
|
2041
|
+
pattern: [pound(), text(" Artikel")],
|
|
2042
|
+
},
|
|
2043
|
+
],
|
|
2044
|
+
},
|
|
2045
|
+
],
|
|
2046
|
+
},
|
|
2047
|
+
{
|
|
2048
|
+
id: "variable_offset",
|
|
2049
|
+
declarations: [
|
|
2050
|
+
{ type: "input-variable", name: "count" },
|
|
2051
|
+
{ type: "input-variable", name: "off" },
|
|
2052
|
+
{
|
|
2053
|
+
type: "local-variable",
|
|
2054
|
+
name: "countPlural",
|
|
2055
|
+
value: {
|
|
2056
|
+
type: "expression",
|
|
2057
|
+
arg: ref("count"),
|
|
2058
|
+
annotation: {
|
|
2059
|
+
type: "function-reference",
|
|
2060
|
+
name: "plural",
|
|
2061
|
+
options: [{ name: "offset", value: ref("off") }],
|
|
2062
|
+
},
|
|
2063
|
+
},
|
|
2064
|
+
},
|
|
2065
|
+
],
|
|
2066
|
+
messages: [
|
|
2067
|
+
{
|
|
2068
|
+
locale: "en",
|
|
2069
|
+
selectors: [ref("countPlural")],
|
|
2070
|
+
variants: [
|
|
2071
|
+
{
|
|
2072
|
+
matches: [literal("countPlural", "one")],
|
|
2073
|
+
pattern: [
|
|
2074
|
+
text("one: "),
|
|
2075
|
+
{
|
|
2076
|
+
type: "expression",
|
|
2077
|
+
arg: ref("count"),
|
|
2078
|
+
annotation: {
|
|
2079
|
+
type: "function-reference",
|
|
2080
|
+
name: "icu:pound",
|
|
2081
|
+
options: [{ name: "offset", value: ref("off") }],
|
|
2082
|
+
},
|
|
2083
|
+
},
|
|
2084
|
+
],
|
|
2085
|
+
},
|
|
2086
|
+
{
|
|
2087
|
+
matches: [catchall("countPlural")],
|
|
2088
|
+
pattern: [text("other")],
|
|
2089
|
+
},
|
|
2090
|
+
],
|
|
2091
|
+
},
|
|
2092
|
+
],
|
|
2093
|
+
},
|
|
2094
|
+
]);
|
|
2095
|
+
const { m } = await importCode(await bundleCode(output, `export * as m from "./paraglide/messages.js"`));
|
|
2096
|
+
// offsets from variables type-check and are converted with Number()
|
|
2097
|
+
expect(m.variable_offset({ count: 3, off: 2 }, { locale: "en" })).toBe("one: 1");
|
|
2098
|
+
expect(m.variable_offset({ count: 3, off: "2" }, { locale: "en" })).toBe("one: 1");
|
|
2099
|
+
expect([0, 1, 2, 3, 1234].map((count) => m.guest_count({ count }, { locale: "en" }))).toEqual([
|
|
2100
|
+
"Nobody",
|
|
2101
|
+
"You",
|
|
2102
|
+
"You and 1 other",
|
|
2103
|
+
"You and 2 others",
|
|
2104
|
+
"You and 1,233 others",
|
|
2105
|
+
]);
|
|
2106
|
+
expect([1, 2, 1234].map((count) => m.item_total({ count }, { locale: "en" }))).toEqual(["1 item", "2 items", "1,234 items"]);
|
|
2107
|
+
expect(m.item_total({ count: 1234 }, { locale: "de" })).toBe("1.234 Artikel");
|
|
2108
|
+
});
|
|
2109
|
+
});
|
|
2110
|
+
describe.each([
|
|
2111
|
+
{ outputStructure: "locale-modules" },
|
|
2112
|
+
{ outputStructure: "message-modules" },
|
|
2113
|
+
])("markup messages with fallbacks ($outputStructure)", ({ outputStructure }) => {
|
|
2114
|
+
test(".parts() type-checks under strict checkJs and renders", async () => {
|
|
2115
|
+
const markupProject = await loadProjectInMemory({
|
|
2116
|
+
blob: await newProject({
|
|
2117
|
+
settings: { baseLocale: "en", locales: ["en", "de"] },
|
|
2118
|
+
}),
|
|
2119
|
+
});
|
|
2120
|
+
const bold = (value) => [
|
|
2121
|
+
{ type: "markup-start", name: "b" },
|
|
2122
|
+
{ type: "text", value },
|
|
2123
|
+
{ type: "markup-end", name: "b" },
|
|
2124
|
+
];
|
|
2125
|
+
const bundles = [
|
|
2126
|
+
// de falls back to en
|
|
2127
|
+
{
|
|
2128
|
+
id: "markup_fallback",
|
|
2129
|
+
messages: [{ locale: "en", variants: [{ pattern: bold("Hello") }] }],
|
|
2130
|
+
},
|
|
2131
|
+
// en has no markup, de has
|
|
2132
|
+
{
|
|
2133
|
+
id: "markup_mixed",
|
|
2134
|
+
messages: [
|
|
2135
|
+
{
|
|
2136
|
+
locale: "en",
|
|
2137
|
+
variants: [{ pattern: [{ type: "text", value: "Hello" }] }],
|
|
2138
|
+
},
|
|
2139
|
+
{ locale: "de", variants: [{ pattern: bold("Hallo") }] },
|
|
2140
|
+
],
|
|
2141
|
+
},
|
|
2142
|
+
// en (the base locale) has no message and no fallback
|
|
2143
|
+
{
|
|
2144
|
+
id: "markup_missing_base",
|
|
2145
|
+
messages: [{ locale: "de", variants: [{ pattern: bold("Hallo") }] }],
|
|
2146
|
+
},
|
|
2147
|
+
];
|
|
2148
|
+
for (const bundle of bundles) {
|
|
2149
|
+
await insertBundleNested(markupProject.db, createBundleNested(bundle));
|
|
2150
|
+
}
|
|
2151
|
+
const output = await compileProject({
|
|
2152
|
+
project: markupProject,
|
|
2153
|
+
compilerOptions: {
|
|
2154
|
+
outputStructure,
|
|
2155
|
+
strategy: ["globalVariable", "baseLocale"],
|
|
2156
|
+
},
|
|
2157
|
+
});
|
|
2158
|
+
const tsProject = await typescriptProject({
|
|
2159
|
+
useInMemoryFileSystem: true,
|
|
2160
|
+
compilerOptions: {
|
|
2161
|
+
outDir: "dist",
|
|
2162
|
+
declaration: true,
|
|
2163
|
+
allowJs: true,
|
|
2164
|
+
checkJs: true,
|
|
2165
|
+
noImplicitAny: true,
|
|
2166
|
+
noUnusedLocals: true,
|
|
2167
|
+
noUnusedParameters: true,
|
|
2168
|
+
noImplicitReturns: true,
|
|
2169
|
+
noImplicitThis: true,
|
|
2170
|
+
noUncheckedIndexedAccess: true,
|
|
2171
|
+
noPropertyAccessFromIndexSignature: true,
|
|
2172
|
+
module: ts.ModuleKind.Node16,
|
|
2173
|
+
strict: true,
|
|
2174
|
+
},
|
|
2175
|
+
});
|
|
2176
|
+
for (const [fileName, code] of Object.entries(output)) {
|
|
2177
|
+
if (fileName.endsWith(".js") || fileName.endsWith(".ts")) {
|
|
2178
|
+
tsProject.createSourceFile(fileName, code);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
const diagnostics = ts
|
|
2182
|
+
.getPreEmitDiagnostics(tsProject.createProgram())
|
|
2183
|
+
.filter((d) => !d.messageText
|
|
2184
|
+
.toString()
|
|
2185
|
+
.includes("Cannot find module 'async_hooks'"));
|
|
2186
|
+
for (const diagnostic of diagnostics) {
|
|
2187
|
+
console.error(diagnostic.messageText, diagnostic.file?.fileName);
|
|
2188
|
+
}
|
|
2189
|
+
expect(diagnostics.length).toEqual(0);
|
|
2190
|
+
const { m } = await importCode(await bundleCode(output, `export * as m from "./paraglide/messages.js"`));
|
|
2191
|
+
const boldParts = (value) => [
|
|
2192
|
+
{ type: "markup-start", name: "b", options: {}, attributes: {} },
|
|
2193
|
+
{ type: "text", value },
|
|
2194
|
+
{ type: "markup-end", name: "b", options: {}, attributes: {} },
|
|
2195
|
+
];
|
|
2196
|
+
expect(m.markup_fallback.parts({}, { locale: "de" })).toEqual(boldParts("Hello"));
|
|
2197
|
+
expect(m.markup_mixed.parts({}, { locale: "en" })).toEqual([
|
|
2198
|
+
{ type: "text", value: "Hello" },
|
|
2199
|
+
]);
|
|
2200
|
+
expect(m.markup_mixed.parts({}, { locale: "de" })).toEqual(boldParts("Hallo"));
|
|
2201
|
+
expect(m.markup_missing_base.parts({}, { locale: "en" })).toEqual([
|
|
2202
|
+
{ type: "text", value: "markup_missing_base" },
|
|
2203
|
+
]);
|
|
2204
|
+
});
|
|
2205
|
+
});
|
|
2206
|
+
test("messages injected by the middleware in experimentalMiddlewareLocaleSplitting mode can call the registry", async () => {
|
|
2207
|
+
const splitProject = await loadProjectInMemory({
|
|
2208
|
+
blob: await newProject({
|
|
2209
|
+
settings: { baseLocale: "en", locales: ["en", "de"] },
|
|
2210
|
+
}),
|
|
2211
|
+
});
|
|
2212
|
+
const ref = (name) => ({ type: "variable-reference", name });
|
|
2213
|
+
// {count, plural, =0 {No items} other {# items}} as the ICU1 plugin imports it
|
|
2214
|
+
await insertBundleNested(splitProject.db, createBundleNested({
|
|
2215
|
+
id: "item_count",
|
|
2216
|
+
declarations: [
|
|
2217
|
+
{ type: "input-variable", name: "count" },
|
|
2218
|
+
{
|
|
2219
|
+
type: "local-variable",
|
|
2220
|
+
name: "countPluralExact",
|
|
2221
|
+
value: { type: "expression", arg: ref("count") },
|
|
2222
|
+
},
|
|
2223
|
+
{
|
|
2224
|
+
type: "local-variable",
|
|
2225
|
+
name: "countPlural",
|
|
2226
|
+
value: {
|
|
2227
|
+
type: "expression",
|
|
2228
|
+
arg: ref("count"),
|
|
2229
|
+
annotation: {
|
|
2230
|
+
type: "function-reference",
|
|
2231
|
+
name: "plural",
|
|
2232
|
+
options: [],
|
|
2233
|
+
},
|
|
2234
|
+
},
|
|
2235
|
+
},
|
|
2236
|
+
],
|
|
2237
|
+
messages: [
|
|
2238
|
+
{
|
|
2239
|
+
locale: "en",
|
|
2240
|
+
selectors: [ref("countPluralExact"), ref("countPlural")],
|
|
2241
|
+
variants: [
|
|
2242
|
+
{
|
|
2243
|
+
matches: [
|
|
2244
|
+
{ type: "literal-match", key: "countPluralExact", value: "0" },
|
|
2245
|
+
{ type: "catchall-match", key: "countPlural" },
|
|
2246
|
+
],
|
|
2247
|
+
pattern: [{ type: "text", value: "No items" }],
|
|
2248
|
+
},
|
|
2249
|
+
{
|
|
2250
|
+
matches: [
|
|
2251
|
+
{ type: "catchall-match", key: "countPluralExact" },
|
|
2252
|
+
{ type: "catchall-match", key: "countPlural" },
|
|
2253
|
+
],
|
|
2254
|
+
pattern: [
|
|
2255
|
+
{
|
|
2256
|
+
type: "expression",
|
|
2257
|
+
arg: ref("count"),
|
|
2258
|
+
annotation: {
|
|
2259
|
+
type: "function-reference",
|
|
2260
|
+
name: "icu:pound",
|
|
2261
|
+
options: [],
|
|
2262
|
+
},
|
|
2263
|
+
},
|
|
2264
|
+
{ type: "text", value: " items" },
|
|
2265
|
+
],
|
|
2266
|
+
},
|
|
2267
|
+
],
|
|
2268
|
+
},
|
|
2269
|
+
],
|
|
2270
|
+
}));
|
|
2271
|
+
await insertBundleNested(splitProject.db, createBundleNested({
|
|
2272
|
+
id: "plain_text",
|
|
2273
|
+
messages: [
|
|
2274
|
+
{
|
|
2275
|
+
locale: "en",
|
|
2276
|
+
variants: [{ pattern: [{ type: "text", value: "Hello" }] }],
|
|
2277
|
+
},
|
|
2278
|
+
],
|
|
2279
|
+
}));
|
|
2280
|
+
const output = await compileProject({
|
|
2281
|
+
project: splitProject,
|
|
2282
|
+
compilerOptions: {
|
|
2283
|
+
strategy: ["baseLocale"],
|
|
2284
|
+
experimentalMiddlewareLocaleSplitting: true,
|
|
2285
|
+
disableAsyncLocalStorage: true,
|
|
2286
|
+
},
|
|
2287
|
+
});
|
|
2288
|
+
const { m, paraglideMiddleware } = await importCode(await bundleCode(output, `export * as m from "./paraglide/messages.js"
|
|
2289
|
+
export { paraglideMiddleware } from "./paraglide/server.js"`));
|
|
2290
|
+
const render = async (messages) => {
|
|
2291
|
+
const response = await paraglideMiddleware(new Request("https://example.com/", {
|
|
2292
|
+
headers: { "Sec-Fetch-Dest": "document" },
|
|
2293
|
+
}), () => new Response(`<html><head></head><body>${messages(m)}</body></html>`, {
|
|
2294
|
+
headers: { "Content-Type": "text/html" },
|
|
2295
|
+
}));
|
|
2296
|
+
const html = await response.text();
|
|
2297
|
+
const script = html.match(/<script[^>]*>([\s\S]*?)<\/script>/)?.[1];
|
|
2298
|
+
expect(script).toBeDefined();
|
|
2299
|
+
// the client evaluates the script without registry.js
|
|
2300
|
+
const context = {};
|
|
2301
|
+
vm.runInNewContext(script, context);
|
|
2302
|
+
return { html, ssr: context.__paraglide.ssr };
|
|
2303
|
+
};
|
|
2304
|
+
const withRegistry = await render((m) => m.item_count({ count: 5 }));
|
|
2305
|
+
expect(withRegistry.html).toContain("<body>5 items</body>");
|
|
2306
|
+
expect(withRegistry.ssr.item_count({ count: 5 })).toBe("5 items");
|
|
2307
|
+
expect(withRegistry.ssr.item_count({ count: 1234 })).toBe("1,234 items");
|
|
2308
|
+
expect(withRegistry.ssr.item_count({ count: 0 })).toBe("No items");
|
|
2309
|
+
// messages that don't call the registry get no registry code
|
|
2310
|
+
const withoutRegistry = await render((m) => m.plain_text());
|
|
2311
|
+
expect(withoutRegistry.ssr.plain_text()).toBe("Hello");
|
|
2312
|
+
expect(withoutRegistry.html).not.toContain("const registry");
|
|
2313
|
+
});
|
|
2314
|
+
async function bundleCode(output, file) {
|
|
2315
|
+
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 {};");
|
|
2316
|
+
const bundle = await rolldown({
|
|
2317
|
+
input: ["main.js"],
|
|
2318
|
+
plugins: [
|
|
2319
|
+
// @ts-expect-error - rollup types are not up to date
|
|
2320
|
+
virtual({
|
|
2321
|
+
...Object.fromEntries(Object.entries(output).map(([fileName, code]) => [
|
|
2322
|
+
"paraglide/" + fileName,
|
|
2323
|
+
code,
|
|
2324
|
+
])),
|
|
2325
|
+
"main.js": file,
|
|
2326
|
+
}),
|
|
2327
|
+
],
|
|
2328
|
+
});
|
|
2329
|
+
const compiled = await bundle.generate({ format: "esm" });
|
|
2330
|
+
const code = compiled.output[0].code;
|
|
2331
|
+
return code;
|
|
2332
|
+
}
|
|
2333
|
+
async function importCode(code) {
|
|
2334
|
+
// The random comment ensures that each code is a unique module
|
|
2335
|
+
// and state is not shared between imports
|
|
2336
|
+
const randomComment = `// ${Math.random()}`;
|
|
2337
|
+
const codeWithComment = `${randomComment}\n${code}`;
|
|
2338
|
+
return await import(`data:application/javascript;base64,${Buffer.from(codeWithComment, "utf8").toString("base64")}`);
|
|
2339
|
+
}
|
|
2340
|
+
const project = await loadProjectInMemory({
|
|
2341
|
+
blob: await newProject({
|
|
2342
|
+
settings: {
|
|
2343
|
+
baseLocale: "en",
|
|
2344
|
+
locales: ["en", "de", "en-US"],
|
|
2345
|
+
},
|
|
2346
|
+
}),
|
|
2347
|
+
});
|
|
2348
|
+
const mockBundles = [
|
|
2349
|
+
createBundleNested({
|
|
2350
|
+
id: "sad_penguin_bundle",
|
|
2351
|
+
messages: [
|
|
2352
|
+
{
|
|
2353
|
+
locale: "en",
|
|
2354
|
+
variants: [{ pattern: [{ type: "text", value: "A simple message." }] }],
|
|
2355
|
+
},
|
|
2356
|
+
{
|
|
2357
|
+
locale: "de",
|
|
2358
|
+
variants: [
|
|
2359
|
+
{ pattern: [{ type: "text", value: "Eine einfache Nachricht." }] },
|
|
2360
|
+
],
|
|
2361
|
+
},
|
|
2362
|
+
],
|
|
2363
|
+
}),
|
|
2364
|
+
createBundleNested({
|
|
2365
|
+
id: "Sad_penguin_bundle",
|
|
2366
|
+
messages: [
|
|
2367
|
+
{
|
|
2368
|
+
locale: "en",
|
|
2369
|
+
variants: [
|
|
2370
|
+
{ pattern: [{ type: "text", value: "Capital Sad penguin" }] },
|
|
2371
|
+
],
|
|
2372
|
+
},
|
|
2373
|
+
{
|
|
2374
|
+
locale: "de",
|
|
2375
|
+
variants: [
|
|
2376
|
+
{
|
|
2377
|
+
pattern: [
|
|
2378
|
+
{ type: "text", value: "Grossgeschriebenes Sad penguin" },
|
|
2379
|
+
],
|
|
2380
|
+
},
|
|
2381
|
+
],
|
|
2382
|
+
},
|
|
2383
|
+
],
|
|
2384
|
+
}),
|
|
2385
|
+
{
|
|
2386
|
+
id: "depressed_dog",
|
|
2387
|
+
declarations: [
|
|
2388
|
+
{
|
|
2389
|
+
type: "input-variable",
|
|
2390
|
+
name: "name",
|
|
2391
|
+
},
|
|
2392
|
+
],
|
|
2393
|
+
messages: [
|
|
2394
|
+
{
|
|
2395
|
+
id: "depressed_dog_en",
|
|
2396
|
+
bundle_id: "depressed_dog",
|
|
2397
|
+
locale: "en",
|
|
2398
|
+
selectors: [],
|
|
2399
|
+
variants: [
|
|
2400
|
+
{
|
|
2401
|
+
id: "depressed_dog_en_variant_one",
|
|
2402
|
+
message_id: "depressed_dog_en",
|
|
2403
|
+
matches: [],
|
|
2404
|
+
pattern: [
|
|
2405
|
+
{ type: "text", value: "Good morning " },
|
|
2406
|
+
{
|
|
2407
|
+
type: "expression",
|
|
2408
|
+
arg: { type: "variable-reference", name: "name" },
|
|
2409
|
+
},
|
|
2410
|
+
{ type: "text", value: "!" },
|
|
2411
|
+
],
|
|
2412
|
+
},
|
|
2413
|
+
],
|
|
2414
|
+
},
|
|
2415
|
+
{
|
|
2416
|
+
id: "depressed_dog_de",
|
|
2417
|
+
bundle_id: "depressed_dog",
|
|
2418
|
+
locale: "de",
|
|
2419
|
+
selectors: [],
|
|
2420
|
+
variants: [
|
|
2421
|
+
{
|
|
2422
|
+
id: "depressed_dog_de_variant_one",
|
|
2423
|
+
message_id: "depressed_dog_de",
|
|
2424
|
+
matches: [],
|
|
2425
|
+
pattern: [
|
|
2426
|
+
{ type: "text", value: "Guten Morgen " },
|
|
2427
|
+
{
|
|
2428
|
+
type: "expression",
|
|
2429
|
+
arg: { type: "variable-reference", name: "name" },
|
|
2430
|
+
},
|
|
2431
|
+
{ type: "text", value: "!" },
|
|
2432
|
+
],
|
|
2433
|
+
},
|
|
2434
|
+
],
|
|
2435
|
+
},
|
|
2436
|
+
],
|
|
2437
|
+
},
|
|
2438
|
+
{
|
|
2439
|
+
id: "insane_cats",
|
|
2440
|
+
declarations: [
|
|
2441
|
+
{
|
|
2442
|
+
type: "input-variable",
|
|
2443
|
+
name: "name",
|
|
2444
|
+
},
|
|
2445
|
+
{
|
|
2446
|
+
type: "input-variable",
|
|
2447
|
+
name: "count",
|
|
2448
|
+
},
|
|
2449
|
+
],
|
|
2450
|
+
messages: [
|
|
2451
|
+
{
|
|
2452
|
+
id: "insane_cats_en",
|
|
2453
|
+
bundle_id: "insane_cats",
|
|
2454
|
+
locale: "en",
|
|
2455
|
+
selectors: [],
|
|
2456
|
+
variants: [
|
|
2457
|
+
{
|
|
2458
|
+
id: "insane_cats_en_variant_one",
|
|
2459
|
+
message_id: "insane_cats_en",
|
|
2460
|
+
matches: [],
|
|
2461
|
+
pattern: [
|
|
2462
|
+
{ type: "text", value: "Hello " },
|
|
2463
|
+
{
|
|
2464
|
+
type: "expression",
|
|
2465
|
+
arg: { type: "variable-reference", name: "name" },
|
|
2466
|
+
},
|
|
2467
|
+
{ type: "text", value: "! You have " },
|
|
2468
|
+
{
|
|
2469
|
+
type: "expression",
|
|
2470
|
+
arg: { type: "variable-reference", name: "count" },
|
|
2471
|
+
},
|
|
2472
|
+
{ type: "text", value: " messages." },
|
|
2473
|
+
],
|
|
2474
|
+
},
|
|
2475
|
+
],
|
|
2476
|
+
},
|
|
2477
|
+
{
|
|
1418
2478
|
id: "insane_cats_de",
|
|
1419
|
-
|
|
2479
|
+
bundle_id: "insane_cats",
|
|
1420
2480
|
locale: "de",
|
|
1421
2481
|
selectors: [],
|
|
1422
2482
|
variants: [
|
|
1423
2483
|
{
|
|
1424
2484
|
id: "insane_cats_de_variant_one",
|
|
1425
|
-
|
|
2485
|
+
message_id: "insane_cats_de",
|
|
1426
2486
|
matches: [],
|
|
1427
2487
|
pattern: [
|
|
1428
2488
|
{ type: "text", value: "Hallo " },
|
|
@@ -1586,13 +2646,13 @@ const mockBundles = [
|
|
|
1586
2646
|
messages: [
|
|
1587
2647
|
{
|
|
1588
2648
|
id: "auth_password_error_en",
|
|
1589
|
-
|
|
2649
|
+
bundle_id: "auth_password_error",
|
|
1590
2650
|
locale: "en",
|
|
1591
2651
|
selectors: [],
|
|
1592
2652
|
variants: [
|
|
1593
2653
|
{
|
|
1594
2654
|
id: "auth_password_error_en_variant_invalid",
|
|
1595
|
-
|
|
2655
|
+
message_id: "auth_password_error_en",
|
|
1596
2656
|
matches: [{ type: "literal-match", key: "type", value: "invalid" }],
|
|
1597
2657
|
pattern: [
|
|
1598
2658
|
{
|
|
@@ -1603,7 +2663,7 @@ const mockBundles = [
|
|
|
1603
2663
|
},
|
|
1604
2664
|
{
|
|
1605
2665
|
id: "auth_password_error_en_variant_empty",
|
|
1606
|
-
|
|
2666
|
+
message_id: "auth_password_error_en",
|
|
1607
2667
|
matches: [{ type: "literal-match", key: "type", value: "empty" }],
|
|
1608
2668
|
pattern: [
|
|
1609
2669
|
{
|
|
@@ -1614,7 +2674,7 @@ const mockBundles = [
|
|
|
1614
2674
|
},
|
|
1615
2675
|
{
|
|
1616
2676
|
id: "auth_password_error_en_variant_min",
|
|
1617
|
-
|
|
2677
|
+
message_id: "auth_password_error_en",
|
|
1618
2678
|
matches: [
|
|
1619
2679
|
{
|
|
1620
2680
|
type: "literal-match",
|
|
@@ -1631,7 +2691,7 @@ const mockBundles = [
|
|
|
1631
2691
|
},
|
|
1632
2692
|
{
|
|
1633
2693
|
id: "auth_password_error_en_variant_secure",
|
|
1634
|
-
|
|
2694
|
+
message_id: "auth_password_error_en",
|
|
1635
2695
|
matches: [{ type: "literal-match", key: "type", value: "secure" }],
|
|
1636
2696
|
pattern: [
|
|
1637
2697
|
{
|
|
@@ -1655,13 +2715,13 @@ const mockBundles = [
|
|
|
1655
2715
|
messages: [
|
|
1656
2716
|
{
|
|
1657
2717
|
id: "error_with_dash_en",
|
|
1658
|
-
|
|
2718
|
+
bundle_id: "error_with_dash",
|
|
1659
2719
|
locale: "en",
|
|
1660
2720
|
selectors: [],
|
|
1661
2721
|
variants: [
|
|
1662
2722
|
{
|
|
1663
2723
|
id: "error_with_dash_en_variant_network",
|
|
1664
|
-
|
|
2724
|
+
message_id: "error_with_dash_en",
|
|
1665
2725
|
matches: [
|
|
1666
2726
|
{ type: "literal-match", key: "error-type", value: "network" },
|
|
1667
2727
|
],
|
|
@@ -1669,7 +2729,7 @@ const mockBundles = [
|
|
|
1669
2729
|
},
|
|
1670
2730
|
{
|
|
1671
2731
|
id: "error_with_dash_en_variant_timeout",
|
|
1672
|
-
|
|
2732
|
+
message_id: "error_with_dash_en",
|
|
1673
2733
|
matches: [
|
|
1674
2734
|
{
|
|
1675
2735
|
type: "literal-match",
|
|
@@ -1694,13 +2754,13 @@ const mockBundles = [
|
|
|
1694
2754
|
messages: [
|
|
1695
2755
|
{
|
|
1696
2756
|
id: "auth_password_error_catchall_en",
|
|
1697
|
-
|
|
2757
|
+
bundle_id: "auth_password_error_catchall",
|
|
1698
2758
|
locale: "en",
|
|
1699
2759
|
selectors: [],
|
|
1700
2760
|
variants: [
|
|
1701
2761
|
{
|
|
1702
2762
|
id: "auth_password_error_catchall_en_variant_invalid",
|
|
1703
|
-
|
|
2763
|
+
message_id: "auth_password_error_catchall_en",
|
|
1704
2764
|
matches: [{ type: "literal-match", key: "type", value: "invalid" }],
|
|
1705
2765
|
pattern: [
|
|
1706
2766
|
{
|
|
@@ -1711,7 +2771,7 @@ const mockBundles = [
|
|
|
1711
2771
|
},
|
|
1712
2772
|
{
|
|
1713
2773
|
id: "auth_password_error_catchall_en_variant_catchall",
|
|
1714
|
-
|
|
2774
|
+
message_id: "auth_password_error_catchall_en",
|
|
1715
2775
|
matches: [{ type: "catchall-match", key: "type" }],
|
|
1716
2776
|
pattern: [
|
|
1717
2777
|
{
|
|
@@ -1739,13 +2799,13 @@ const mockBundles = [
|
|
|
1739
2799
|
messages: [
|
|
1740
2800
|
{
|
|
1741
2801
|
id: "multi_key_status_type_en",
|
|
1742
|
-
|
|
2802
|
+
bundle_id: "multi_key_status_type",
|
|
1743
2803
|
locale: "en",
|
|
1744
2804
|
selectors: [],
|
|
1745
2805
|
variants: [
|
|
1746
2806
|
{
|
|
1747
2807
|
id: "multi_key_status_type_en_variant_ready_invalid",
|
|
1748
|
-
|
|
2808
|
+
message_id: "multi_key_status_type_en",
|
|
1749
2809
|
matches: [
|
|
1750
2810
|
{ type: "literal-match", key: "type", value: "invalid" },
|
|
1751
2811
|
{ type: "literal-match", key: "status", value: "ready" },
|
|
@@ -1754,7 +2814,7 @@ const mockBundles = [
|
|
|
1754
2814
|
},
|
|
1755
2815
|
{
|
|
1756
2816
|
id: "multi_key_status_type_en_variant_done_empty",
|
|
1757
|
-
|
|
2817
|
+
message_id: "multi_key_status_type_en",
|
|
1758
2818
|
matches: [
|
|
1759
2819
|
{ type: "literal-match", key: "type", value: "empty" },
|
|
1760
2820
|
{ type: "literal-match", key: "status", value: "done" },
|
|
@@ -1763,7 +2823,7 @@ const mockBundles = [
|
|
|
1763
2823
|
},
|
|
1764
2824
|
{
|
|
1765
2825
|
id: "multi_key_status_type_en_variant_failed_secure",
|
|
1766
|
-
|
|
2826
|
+
message_id: "multi_key_status_type_en",
|
|
1767
2827
|
matches: [
|
|
1768
2828
|
{ type: "literal-match", key: "type", value: "secure" },
|
|
1769
2829
|
{ type: "literal-match", key: "status", value: "failed" },
|
|
@@ -1785,19 +2845,19 @@ const mockBundles = [
|
|
|
1785
2845
|
messages: [
|
|
1786
2846
|
{
|
|
1787
2847
|
id: "empty_matches_catchall_en",
|
|
1788
|
-
|
|
2848
|
+
bundle_id: "empty_matches_catchall",
|
|
1789
2849
|
locale: "en",
|
|
1790
2850
|
selectors: [],
|
|
1791
2851
|
variants: [
|
|
1792
2852
|
{
|
|
1793
2853
|
id: "empty_matches_catchall_en_variant_invalid",
|
|
1794
|
-
|
|
2854
|
+
message_id: "empty_matches_catchall_en",
|
|
1795
2855
|
matches: [{ type: "literal-match", key: "type", value: "invalid" }],
|
|
1796
2856
|
pattern: [{ type: "text", value: "Invalid" }],
|
|
1797
2857
|
},
|
|
1798
2858
|
{
|
|
1799
2859
|
id: "empty_matches_catchall_en_variant_any",
|
|
1800
|
-
|
|
2860
|
+
message_id: "empty_matches_catchall_en",
|
|
1801
2861
|
matches: [],
|
|
1802
2862
|
pattern: [{ type: "text", value: "Any" }],
|
|
1803
2863
|
},
|
|
@@ -1816,19 +2876,19 @@ const mockBundles = [
|
|
|
1816
2876
|
messages: [
|
|
1817
2877
|
{
|
|
1818
2878
|
id: "numeric_input_match_en",
|
|
1819
|
-
|
|
2879
|
+
bundle_id: "numeric_input_match",
|
|
1820
2880
|
locale: "en",
|
|
1821
2881
|
selectors: [],
|
|
1822
2882
|
variants: [
|
|
1823
2883
|
{
|
|
1824
2884
|
id: "numeric_input_match_en_variant_1",
|
|
1825
|
-
|
|
2885
|
+
message_id: "numeric_input_match_en",
|
|
1826
2886
|
matches: [{ type: "literal-match", key: "input", value: "1" }],
|
|
1827
2887
|
pattern: [{ type: "text", value: "Thing 1" }],
|
|
1828
2888
|
},
|
|
1829
2889
|
{
|
|
1830
2890
|
id: "numeric_input_match_en_variant_2",
|
|
1831
|
-
|
|
2891
|
+
message_id: "numeric_input_match_en",
|
|
1832
2892
|
matches: [{ type: "literal-match", key: "input", value: "2" }],
|
|
1833
2893
|
pattern: [{ type: "text", value: "Thing 2" }],
|
|
1834
2894
|
},
|
|
@@ -1846,7 +2906,7 @@ function createBundleNested(args) {
|
|
|
1846
2906
|
declarations: args.declarations ?? [],
|
|
1847
2907
|
messages: args.messages.map((message) => ({
|
|
1848
2908
|
id: args.id + "_" + message.locale,
|
|
1849
|
-
|
|
2909
|
+
bundle_id: args.id,
|
|
1850
2910
|
locale: message.locale,
|
|
1851
2911
|
selectors: message.selectors ?? [],
|
|
1852
2912
|
variants: message.variants.map((variant) => ({
|
|
@@ -1855,11 +2915,26 @@ function createBundleNested(args) {
|
|
|
1855
2915
|
message.locale +
|
|
1856
2916
|
"_" +
|
|
1857
2917
|
Math.random().toString(36).slice(2),
|
|
1858
|
-
|
|
2918
|
+
message_id: args.id,
|
|
1859
2919
|
matches: variant.matches ?? [],
|
|
1860
2920
|
pattern: variant.pattern,
|
|
1861
2921
|
})),
|
|
1862
2922
|
})),
|
|
1863
2923
|
};
|
|
1864
2924
|
}
|
|
2925
|
+
test("emitReadme only claims the output is git-ignored when a .gitignore is emitted", async () => {
|
|
2926
|
+
const project = await loadProjectInMemory({
|
|
2927
|
+
blob: await newProject({
|
|
2928
|
+
settings: { locales: ["en"], baseLocale: "en" },
|
|
2929
|
+
}),
|
|
2930
|
+
});
|
|
2931
|
+
const withGitignore = await compileProject({ project });
|
|
2932
|
+
const withoutGitignore = await compileProject({
|
|
2933
|
+
project,
|
|
2934
|
+
compilerOptions: { emitGitIgnore: false },
|
|
2935
|
+
});
|
|
2936
|
+
expect(withGitignore["README.md"]).toContain("git-ignored");
|
|
2937
|
+
expect(withoutGitignore).not.toHaveProperty(".gitignore");
|
|
2938
|
+
expect(withoutGitignore["README.md"]).not.toContain("git-ignored");
|
|
2939
|
+
});
|
|
1865
2940
|
//# sourceMappingURL=compile-project.test.js.map
|