wizards 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/apps/runtime/dist/agents/architect.js +32 -13
  2. package/apps/runtime/dist/agents/subscription.js +12 -12
  3. package/apps/runtime/dist/app.js +9 -5
  4. package/apps/runtime/dist/assistant/knows.js +2 -2
  5. package/apps/runtime/dist/authoring/example.js +21 -20
  6. package/apps/runtime/dist/authoring/guide.js +4 -2
  7. package/apps/runtime/dist/connectors/imap.js +4 -4
  8. package/apps/runtime/dist/connectors/index.js +40 -15
  9. package/apps/runtime/dist/connectors/plugin-connections.js +3 -2
  10. package/apps/runtime/dist/connectors/via-account.js +8 -6
  11. package/apps/runtime/dist/docs/dev/plugins/connections.md +1 -1
  12. package/apps/runtime/dist/docs/dev/plugins/reference.md +3 -2
  13. package/apps/runtime/dist/docs/dev/plugins/runners.md +38 -2
  14. package/apps/runtime/dist/docs/dev/plugins/server.md +3 -2
  15. package/apps/runtime/dist/docs/dev/plugins/starters.md +1 -1
  16. package/apps/runtime/dist/docs/dev/plugins/studio.md +2 -1
  17. package/apps/runtime/dist/documents/parse.js +5 -3
  18. package/apps/runtime/dist/engine/events.js +1 -1
  19. package/apps/runtime/dist/engine/input.js +28 -8
  20. package/apps/runtime/dist/engine/prepare.js +2 -2
  21. package/apps/runtime/dist/engine/requirements.js +12 -3
  22. package/apps/runtime/dist/engine/runner.js +34 -23
  23. package/apps/runtime/dist/engine/steps.js +6 -5
  24. package/apps/runtime/dist/engine/surface.js +2 -1
  25. package/apps/runtime/dist/engine/template.js +1 -1
  26. package/apps/runtime/dist/engine/types.js +2 -1
  27. package/apps/runtime/dist/film/project.js +1 -1
  28. package/apps/runtime/dist/film/skills.js +2 -2
  29. package/apps/runtime/dist/film/sounds.js +1 -1
  30. package/apps/runtime/dist/film/step.js +7 -4
  31. package/apps/runtime/dist/film/tools.js +1 -1
  32. package/apps/runtime/dist/geocode.js +1 -1
  33. package/apps/runtime/dist/harness/apple.js +0 -5
  34. package/apps/runtime/dist/harness/claude.js +2 -1
  35. package/apps/runtime/dist/harness/codex.js +2 -1
  36. package/apps/runtime/dist/harness/model.js +4 -3
  37. package/apps/runtime/dist/harness/terminal.js +4 -3
  38. package/apps/runtime/dist/model-errors.js +8 -3
  39. package/apps/runtime/dist/models.js +58 -38
  40. package/apps/runtime/dist/plugins/channels.js +1 -0
  41. package/apps/runtime/dist/plugins/runs.js +2 -1
  42. package/apps/runtime/dist/routes/api.js +16 -5
  43. package/apps/runtime/dist/routes/lang.js +13 -0
  44. package/apps/runtime/dist/routes/runs.js +12 -19
  45. package/apps/runtime/dist/routes/studio.js +40 -28
  46. package/apps/runtime/dist/services/access.js +4 -3
  47. package/apps/runtime/dist/services/architect.js +15 -4
  48. package/apps/runtime/dist/services/assistant-chats.js +2 -1
  49. package/apps/runtime/dist/services/brand.js +3 -2
  50. package/apps/runtime/dist/services/errors.js +7 -5
  51. package/apps/runtime/dist/services/knowledge-model.js +3 -2
  52. package/apps/runtime/dist/services/marketplace.js +6 -5
  53. package/apps/runtime/dist/services/package.js +10 -3
  54. package/apps/runtime/dist/services/profile.js +12 -0
  55. package/apps/runtime/dist/services/project-files.js +13 -12
  56. package/apps/runtime/dist/services/projects.js +11 -6
  57. package/apps/runtime/dist/services/runners.js +17 -2
  58. package/apps/runtime/dist/services/runs.js +18 -11
  59. package/apps/runtime/dist/services/shares.js +2 -1
  60. package/apps/runtime/dist/services/site.js +21 -10
  61. package/apps/runtime/dist/services/space-categories.js +7 -6
  62. package/apps/runtime/dist/services/space-data.js +19 -16
  63. package/apps/runtime/dist/services/spaces.js +8 -7
  64. package/apps/runtime/dist/services/wizards.js +41 -20
  65. package/apps/runtime/dist/store/errors.js +2 -1
  66. package/apps/runtime/dist/store/index.js +21 -4
  67. package/apps/runtime/dist/store/shared-lists.js +2 -3
  68. package/apps/runtime/dist/tools/index.js +17 -2
  69. package/apps/runtime/dist/tools/plugin.js +3 -2
  70. package/apps/runtime/dist/widgets/render.js +1 -1
  71. package/apps/runtime/dist/widgets/step.js +2 -1
  72. package/apps/web/dist/assets/{AboutDialog-nLn3vZAN.js → AboutDialog-i-qD_Nea.js} +1 -1
  73. package/apps/web/dist/assets/{ChangelogDialog-Cbx3mN2p.js → ChangelogDialog-BYuVcmKR.js} +1 -1
  74. package/apps/web/dist/assets/ChatView-DMEYEsA7.js +10 -0
  75. package/apps/web/dist/assets/EditorPage-Bry35WHx.js +3 -0
  76. package/apps/web/dist/assets/{FlowPreview-Bvj3R8g4.js → FlowPreview-BsfjDVv8.js} +1 -1
  77. package/apps/web/dist/assets/{Harness-D-Z3MbYq.js → Harness-BxQDTGJk.js} +2 -2
  78. package/apps/web/dist/assets/NewWizardChannels-DPnLT-md.js +1 -0
  79. package/apps/web/dist/assets/NewWizardPage-BSh_bUIJ.js +7 -0
  80. package/apps/web/dist/assets/{OnboardingPage-CdoAfnI2.js → OnboardingPage-BZrRdVwr.js} +1 -1
  81. package/apps/web/dist/assets/ProjectPage-_3L9R3SZ.js +1 -0
  82. package/apps/web/dist/assets/PublicRunner-DXbh8uhe.js +1 -0
  83. package/apps/web/dist/assets/{ReadOnly-DQPHmvS0.js → ReadOnly-DFysMEMF.js} +1 -1
  84. package/apps/web/dist/assets/RunDrawer-DpUakYb9.js +4 -0
  85. package/apps/web/dist/assets/RunnerView-DQ-NgcCa.js +1 -0
  86. package/apps/web/dist/assets/SettingsPage-DyUCIRjo.js +1 -0
  87. package/apps/web/dist/assets/SetupPage-BgVHvN6j.js +1 -0
  88. package/apps/web/dist/assets/SharePage-BI6vkc9c.js +1 -0
  89. package/apps/web/dist/assets/changelog-Cnv7ZZEL.js +1 -0
  90. package/apps/web/dist/assets/{circle-alert-DCBV7n5k.js → circle-alert-BVj6idzu.js} +1 -1
  91. package/apps/web/dist/assets/esm-hSq-jeOG.js +7 -0
  92. package/apps/web/dist/assets/index-Ca1841qE.js +14 -0
  93. package/apps/web/dist/assets/index-GSl2Rcjy.css +1 -0
  94. package/apps/web/dist/assets/{lock-DlzTfwxP.js → lock-BoWS-JhN.js} +1 -1
  95. package/apps/web/dist/assets/{phone-BFD815nc.js → phone-CeqyvwK-.js} +1 -1
  96. package/apps/web/dist/assets/settings-menu-BK8WpcGv.js +1 -0
  97. package/apps/web/dist/assets/ui-BbIh6SOJ.js +134 -0
  98. package/apps/web/dist/assets/{upload-BEf5-d6X.js → upload-BQrAlHIG.js} +1 -1
  99. package/apps/web/dist/index.html +4 -4
  100. package/apps/web/dist/mcp/flow.html +76 -76
  101. package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +28 -4
  102. package/node_modules/@engenty-wizards/plugin-sdk/dist/studio.d.ts +52 -1
  103. package/node_modules/@engenty-wizards/plugin-sdk/dist/thread.js +6 -1
  104. package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +6 -2
  105. package/node_modules/@engenty-wizards/shared/dist/definition.js +13 -5
  106. package/node_modules/@engenty-wizards/shared/dist/knowledge.js +1 -1
  107. package/node_modules/@engenty-wizards/shared/dist/run.d.ts +11 -1
  108. package/node_modules/@engenty-wizards/shared/dist/run.js +11 -1
  109. package/node_modules/@engenty-wizards/shared/dist/runners.d.ts +12 -0
  110. package/node_modules/@engenty-wizards/shared/dist/store.d.ts +8 -0
  111. package/node_modules/@engenty-wizards/shared/dist/store.js +8 -0
  112. package/package.json +1 -1
  113. package/apps/web/dist/assets/ChatView-1VuHE2Q7.js +0 -10
  114. package/apps/web/dist/assets/EditorPage-Cp3jqQsk.js +0 -3
  115. package/apps/web/dist/assets/NewWizardChannels-DCF7JoL0.js +0 -1
  116. package/apps/web/dist/assets/NewWizardPage-BTlgq7yN.js +0 -7
  117. package/apps/web/dist/assets/ProjectPage-CoPSdDO9.js +0 -1
  118. package/apps/web/dist/assets/PublicRunner-CHwK3X75.js +0 -1
  119. package/apps/web/dist/assets/RunDrawer-CmGuqp0p.js +0 -4
  120. package/apps/web/dist/assets/RunnerView-DiaQiDbw.js +0 -1
  121. package/apps/web/dist/assets/SettingsPage-NzmFrJXb.js +0 -1
  122. package/apps/web/dist/assets/SetupPage-8mBuezdY.js +0 -1
  123. package/apps/web/dist/assets/SharePage-C-T5fkp1.js +0 -1
  124. package/apps/web/dist/assets/changelog-CsxPWKJ-.js +0 -1
  125. package/apps/web/dist/assets/esm-CnR8rDPi.js +0 -7
  126. package/apps/web/dist/assets/index-CmxVazOf.css +0 -1
  127. package/apps/web/dist/assets/index-D57rKyGN.js +0 -14
  128. package/apps/web/dist/assets/settings-menu-D-UqnL1p.js +0 -1
  129. package/apps/web/dist/assets/ui-DTuixH4y.js +0 -134
@@ -5,6 +5,7 @@ import { MODEL_CLASSES, TEXT_CLASSES, } from "@engenty-wizards/shared/definition
5
5
  import { CATEGORY_TYPES, whereSchema } from "@engenty-wizards/shared/knowledge";
6
6
  import { PROJECT_FILE_KINDS } from "@engenty-wizards/shared/projects";
7
7
  import { isCreditsRef } from "@engenty-wizards/shared/providers";
8
+ import { bilingual } from "@engenty-wizards/shared/run";
8
9
  import { PACKAGE_EXTENSION, PACKAGE_MAX_BYTES, PACKAGE_MIME, } from "@engenty-wizards/shared/wizard-package";
9
10
  import { generateText } from "ai";
10
11
  import { Hono } from "hono";
@@ -34,6 +35,7 @@ import { installMail, managed, renameTenant, tenantInfo, tenantMembers, userInfo
34
35
  import { seenApps } from "../mcp/seen.js";
35
36
  import { generateImageMedia, generateSpeechMedia, generateVideoMedia } from "../media/generate.js";
36
37
  import { transcribeAudio } from "../media/transcribe.js";
38
+ import { errorText, saidOf } from "../model-errors.js";
37
39
  import { checkKey, classProblem, classWays, embeddingModel, hasTextModel, LOCAL_KEYS, localModelSettings, ModelUnavailableError, modelSettings, saveLocalModels, saveModels, storedKey, textModel, } from "../models.js";
38
40
  import { refreshPro } from "../plugins/pro.js";
39
41
  import { HTML_RESPONSE_CSP } from "../render/guard.js";
@@ -48,7 +50,7 @@ import { categoryContents, listKnowledge, resolvePath, valueContents, } from "..
48
50
  import { writeSummary } from "../services/knowledge-model.js";
49
51
  import { applyWebsite, finishOnboarding, needsOnboarding } from "../services/onboarding.js";
50
52
  import { exportWizard, importWizard } from "../services/package.js";
51
- import { profileSchema, saveProfile, userProfile } from "../services/profile.js";
53
+ import { profileSchema, saveProfile, saveUserLang, userLang, userProfile, } from "../services/profile.js";
52
54
  import { addProjectFile, addProjectFileFromUrl, fileView, orderProjectFiles, projectFile, projectFileContent, projectFiles, reindexProjectFile, removeProjectFile, updateProjectFile, } from "../services/project-files.js";
53
55
  import { documentText, queueIndex, searchKnowledge } from "../services/project-index.js";
54
56
  import { createProject, deleteProject, gradientSchema, listProjects, maskedServers, ownedProject, projectLimitOrNull, projectPatchSchema, updateProject, } from "../services/projects.js";
@@ -63,6 +65,7 @@ import { codeOf } from "../tenants/control.js";
63
65
  import { currentTenant } from "../tenants/tenant.js";
64
66
  import { applyUpdate, updateStatus } from "../update.js";
65
67
  import { byteRange } from "./delivery.js";
68
+ import { inLang, requestLang } from "./lang.js";
66
69
  /** The model sources a test call has worked on: the setup is done for those, not for the others. */
67
70
  async function testedSources() {
68
71
  const tested = await readSetting("setup-done");
@@ -215,6 +218,8 @@ export const studio = new Hono()
215
218
  user: { id: user.id, name: profile.name, email: profile.email, image: user.image ?? null },
216
219
  /** What the person says about themselves; name and e-mail are the account's when managed. */
217
220
  profile,
221
+ /** The language the person picked, wherever they picked it; null until they do. */
222
+ lang: await userLang(user),
218
223
  tenant: {
219
224
  id: user.tenantId,
220
225
  role: user.role,
@@ -305,6 +310,11 @@ export const studio = new Hono()
305
310
  return c.json(await updateStatus());
306
311
  })
307
312
  .put("/profile", async (c) => c.json(await saveProfile(c.get("user"), profileSchema.parse(await c.req.json()))))
313
+ .put("/lang", async (c) => {
314
+ const { lang } = z.object({ lang: z.enum(["de", "en"]) }).parse(await c.req.json());
315
+ await saveUserLang(c.get("user"), lang);
316
+ return c.json({ lang });
317
+ })
308
318
  // --- the studio's assistant -------------------------------------------------
309
319
  // One open chat per person and tenant, on every page; turns stream as SSE events shaped like
310
320
  // AG-UI's (packages/shared/src/assistant.ts).
@@ -323,10 +333,10 @@ export const studio = new Hono()
323
333
  .post("/assistant/speak", async (c) => {
324
334
  const { text } = z.object({ text: z.string().min(1).max(4000) }).parse(await c.req.json());
325
335
  if ((await classWays()).speech.kind === "none") {
326
- throw new ServiceError("refused", "Hier spricht kein Sprachmodell.");
336
+ throw new ServiceError("refused", bilingual("Hier spricht kein Sprachmodell.", "No language model speaks here."));
327
337
  }
328
338
  if (!(await canSpend())) {
329
- throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
339
+ throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
330
340
  }
331
341
  const voice = await generateSpeechMedia({ text, abortSignal: c.req.raw.signal });
332
342
  return c.body(new Uint8Array(voice.bytes), 200, { "content-type": voice.mime });
@@ -346,10 +356,10 @@ export const studio = new Hono()
346
356
  .post("/assistant/turn", async (c) => {
347
357
  const user = c.get("user");
348
358
  const body = assistantTurnSchema
349
- .extend({ lang: z.enum(["de", "en"]).default("de") })
359
+ .extend({ lang: z.enum(["de", "en"]).default("en") })
350
360
  .parse(await c.req.json());
351
361
  if (!(await canSpend())) {
352
- throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
362
+ throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
353
363
  }
354
364
  const project = body.projectId ? await ownedProject(user.id, body.projectId) : null;
355
365
  const writable = project ? await projectWritable(project) : false;
@@ -460,7 +470,7 @@ export const studio = new Hono()
460
470
  }
461
471
  await send("error", {
462
472
  message: err instanceof ServiceError || err instanceof ModelUnavailableError
463
- ? err.message
473
+ ? errorText(err, body.lang)
464
474
  : body.lang === "de"
465
475
  ? "Da ist etwas schiefgelaufen. Bitte noch einmal versuchen."
466
476
  : "Something went wrong. Please try again.",
@@ -511,7 +521,7 @@ export const studio = new Hono()
511
521
  const kind = z.enum(PROJECT_FILE_KINDS).parse(c.req.query("kind"));
512
522
  const file = (await c.req.formData()).get("file");
513
523
  if (!(file instanceof File)) {
514
- throw new ServiceError("invalid", "Bitte eine Datei wählen.");
524
+ throw new ServiceError("invalid", bilingual("Bitte eine Datei wählen.", "Please choose a file."));
515
525
  }
516
526
  const row = await addProjectFile(project.id, {
517
527
  kind,
@@ -544,7 +554,7 @@ export const studio = new Hono()
544
554
  await ownedProject(c.get("user").id, c.req.param("id"));
545
555
  const { url } = z.object({ url: z.string().min(1).max(2000) }).parse(await c.req.json());
546
556
  const colors = await scanColors(url).catch(() => {
547
- throw new ServiceError("invalid", "Die Seite ist nicht erreichbar.");
557
+ throw new ServiceError("invalid", bilingual("Die Seite ist nicht erreichbar.", "The page cannot be reached."));
548
558
  });
549
559
  return c.json({ colors });
550
560
  })
@@ -560,7 +570,7 @@ export const studio = new Hono()
560
570
  })
561
571
  .parse(await c.req.json());
562
572
  if (!(await canSpend())) {
563
- throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
573
+ throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
564
574
  }
565
575
  try {
566
576
  return c.json({
@@ -569,7 +579,7 @@ export const studio = new Hono()
569
579
  }
570
580
  catch (err) {
571
581
  if (err instanceof ModelUnavailableError) {
572
- throw new ServiceError("refused", err.message);
582
+ throw new ServiceError("refused", saidOf(err));
573
583
  }
574
584
  throw err;
575
585
  }
@@ -578,7 +588,7 @@ export const studio = new Hono()
578
588
  await ownedProject(c.get("user").id, c.req.param("id"));
579
589
  const { url } = z.object({ url: z.string().min(1).max(2000) }).parse(await c.req.json());
580
590
  const fonts = await scanFonts(url).catch(() => {
581
- throw new ServiceError("invalid", "Die Seite ist nicht erreichbar.");
591
+ throw new ServiceError("invalid", bilingual("Die Seite ist nicht erreichbar.", "The page cannot be reached."));
582
592
  });
583
593
  return c.json({ fonts });
584
594
  })
@@ -841,7 +851,7 @@ export const studio = new Hono()
841
851
  format: z.enum(["faq"]).nullish(),
842
852
  })
843
853
  .parse(await c.req.json());
844
- return c.json(await createTable(c.get("user").id, c.req.param("id"), body));
854
+ return c.json(await createTable(c.get("user").id, c.req.param("id"), body, requestLang(c)));
845
855
  })
846
856
  .get("/tables/:id", async (c) => c.json(await getTable(c.get("user").id, c.req.param("id"))))
847
857
  .patch("/tables/:id", async (c) => {
@@ -934,9 +944,9 @@ export const studio = new Hono()
934
944
  .post("/projects/:id/wizards/import", async (c) => {
935
945
  const file = (await c.req.formData()).get("file");
936
946
  if (!(file instanceof File) || !file.size || file.size > PACKAGE_MAX_BYTES) {
937
- throw new ServiceError("invalid", `Bitte ein Wizard-Paket (${PACKAGE_EXTENSION}) bis 30 MB.`);
947
+ throw new ServiceError("invalid", bilingual(`Bitte ein Wizard-Paket (${PACKAGE_EXTENSION}) bis 30 MB.`, `Please a wizard package (${PACKAGE_EXTENSION}) of up to 30 MB.`));
938
948
  }
939
- return c.json(await importWizard(c.get("user").id, c.req.param("id"), new Uint8Array(await file.arrayBuffer()), file.name));
949
+ return c.json(await importWizard(c.get("user").id, c.req.param("id"), new Uint8Array(await file.arrayBuffer()), file.name, requestLang(c)));
940
950
  })
941
951
  .get("/wizards/:id/export", async (c) => {
942
952
  const found = await exportWizard(c.get("user").id, c.req.param("id"), {
@@ -944,7 +954,9 @@ export const studio = new Hono()
944
954
  knowledge: c.req.query("knowledge") === "1",
945
955
  });
946
956
  if (!found) {
947
- return c.json({ error: "Der Wizard ist noch nicht veröffentlicht." }, 404);
957
+ return c.json({
958
+ error: inLang(c, "Der Wizard ist noch nicht veröffentlicht.", "The wizard is not published yet."),
959
+ }, 404);
948
960
  }
949
961
  const { name, zip } = found;
950
962
  return c.body(new Uint8Array(zip), 200, {
@@ -1048,7 +1060,7 @@ export const studio = new Hono()
1048
1060
  // The models the draft needs that this runtime cannot serve, by class, with the steps that call them.
1049
1061
  .get("/wizards/:id/models", async (c) => {
1050
1062
  const w = await ownedWizard(c.get("user").id, c.req.param("id"));
1051
- return c.json({ missing: await missingModels(w.draft) });
1063
+ return c.json({ missing: await missingModels(w.draft, requestLang(c)) });
1052
1064
  })
1053
1065
  // A runtime that runs alone: the wizard's copy in the cloud of the linked account.
1054
1066
  .get("/wizards/:id/cloud", async (c) => {
@@ -1095,13 +1107,13 @@ export const studio = new Hono()
1095
1107
  .parse(await c.req.json());
1096
1108
  await writableWizard(user.id, wizardId);
1097
1109
  if (!(await canSpend())) {
1098
- throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
1110
+ throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
1099
1111
  }
1100
1112
  return streamSSE(c, async (stream) => {
1101
1113
  const abort = new AbortController();
1102
1114
  stream.onAbort(() => abort.abort());
1103
1115
  try {
1104
- const result = await architectTurn(user.id, wizardId, message, {
1116
+ const result = await architectTurn(user.id, wizardId, message, requestLang(c), {
1105
1117
  signal: abort.signal,
1106
1118
  onText: (delta) => {
1107
1119
  void stream.writeSSE({ event: "text", data: JSON.stringify(delta) });
@@ -1124,8 +1136,8 @@ export const studio = new Hono()
1124
1136
  event: "error",
1125
1137
  data: JSON.stringify({
1126
1138
  message: err instanceof ServiceError
1127
- ? err.message
1128
- : "Da ist etwas schiefgelaufen. Bitte noch einmal versuchen.",
1139
+ ? errorText(err, requestLang(c))
1140
+ : inLang(c, "Da ist etwas schiefgelaufen. Bitte noch einmal versuchen.", "Something went wrong. Please try again."),
1129
1141
  }),
1130
1142
  });
1131
1143
  }
@@ -1135,10 +1147,10 @@ export const studio = new Hono()
1135
1147
  .post("/transcribe", async (c) => {
1136
1148
  const file = (await c.req.formData()).get("file");
1137
1149
  if (!(file instanceof File) || !file.size || file.size > 10_000_000) {
1138
- throw new ServiceError("invalid", "Bitte eine Aufnahme bis 10 MB.");
1150
+ throw new ServiceError("invalid", bilingual("Bitte eine Aufnahme bis 10 MB.", "Please a recording of up to 10 MB."));
1139
1151
  }
1140
1152
  if (!(await canSpend())) {
1141
- throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
1153
+ throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
1142
1154
  }
1143
1155
  try {
1144
1156
  const { text } = await transcribeAudio({
@@ -1149,7 +1161,7 @@ export const studio = new Hono()
1149
1161
  }
1150
1162
  catch (err) {
1151
1163
  if (err instanceof ModelUnavailableError) {
1152
- throw new ServiceError("refused", err.message);
1164
+ throw new ServiceError("refused", saidOf(err));
1153
1165
  }
1154
1166
  throw err;
1155
1167
  }
@@ -1226,7 +1238,7 @@ export const studio = new Hono()
1226
1238
  })
1227
1239
  // --- models: where each class runs, own API keys, the credits behind them -----------------
1228
1240
  // Alone for this machine; on a Manage-App's runtime for the team, whose admins change it.
1229
- .get("/models", async (c) => c.json(await modelSettings()))
1241
+ .get("/models", async (c) => c.json(await modelSettings(requestLang(c))))
1230
1242
  .put("/models", async (c) => {
1231
1243
  if (!mayChangeModels(c.get("user"))) {
1232
1244
  return c.json({ error: "Only an admin of the team changes its models." }, 403);
@@ -1246,13 +1258,13 @@ export const studio = new Hono()
1246
1258
  Object.values(input.bindings ?? {}).some((ref) => ref && !isCreditsRef(ref));
1247
1259
  if (own && info && !feature("ownKeys", info.features)) {
1248
1260
  return c.json({
1249
- error: `Eigene API Keys gibt es nicht im Paket ${info.plan?.name ?? "des Teams"}.`,
1261
+ error: inLang(c, `Eigene API Keys gibt es nicht im Paket ${info.plan?.name ?? "des Teams"}.`, `Own API keys are not part of the ${info.plan?.name ?? "team's"} plan.`),
1250
1262
  code: "plan",
1251
1263
  }, 403);
1252
1264
  }
1253
1265
  }
1254
1266
  await saveModels(input);
1255
- return c.json(await modelSettings());
1267
+ return c.json(await modelSettings(requestLang(c)));
1256
1268
  })
1257
1269
  // Whether a provider takes a key — the one typed, or the one stored. Nothing is generated.
1258
1270
  .post("/models/keys/:provider/check", async (c) => {
@@ -1264,7 +1276,7 @@ export const studio = new Hono()
1264
1276
  if (!value) {
1265
1277
  return c.json({ ok: false, message: "Kein API Key hinterlegt." });
1266
1278
  }
1267
- return c.json(await checkKey(provider, value));
1279
+ return c.json(await checkKey(provider, value, requestLang(c)));
1268
1280
  })
1269
1281
  // One call on a class, as a run makes it: text answers a word, an image, a voice or a short
1270
1282
  // clip comes back to look at, a voice note comes back as text. Paid like any call.
@@ -1273,7 +1285,7 @@ export const studio = new Hono()
1273
1285
  return c.json({ error: "Only an admin of the team tests its models." }, 403);
1274
1286
  }
1275
1287
  const { cls, lang } = z
1276
- .object({ cls: z.enum(MODEL_CLASSES), lang: z.enum(["de", "en"]).default("de") })
1288
+ .object({ cls: z.enum(MODEL_CLASSES), lang: z.enum(["de", "en"]).default("en") })
1277
1289
  .parse(await c.req.json());
1278
1290
  return c.json(await testClass(cls, lang));
1279
1291
  })
@@ -1,4 +1,5 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { bilingual } from "@engenty-wizards/shared/run";
2
3
  import { and, eq } from "drizzle-orm";
3
4
  import { db, schema } from "../db/client.js";
4
5
  import { managed, tenantInfo } from "../manage.js";
@@ -30,7 +31,7 @@ export function currentRole() {
30
31
  return actor.getStore() ?? "owner";
31
32
  }
32
33
  export const isAdminRole = (role) => role === "owner" || role === "admin";
33
- export const MEMBERS_EDIT_ONLY = "Das dürfen nur Admins des Teams. Als Mitglied bearbeitest du die Wizards, die es gibt.";
34
+ export const MEMBERS_EDIT_ONLY = bilingual("Das dürfen nur Admins des Teams. Als Mitglied bearbeitest du die Wizards, die es gibt.", "Only the team's admins may do that. As a member you edit the wizards that are there.");
34
35
  /** Refuses a member: making and deleting is for the owner and the admins of the team. */
35
36
  export async function requireAdmin() {
36
37
  if (syncing() || !managed || isAdminRole(currentRole())) {
@@ -51,8 +52,8 @@ export async function mayBuild() {
51
52
  // An older Manage-App does not say: then it builds. One that cannot be reached: nothing changes.
52
53
  return info ? info.limits.build !== false : false;
53
54
  }
54
- export const READ_ONLY_PROJECT = "Dieses Projekt kommt aus einer lokalen Installation und wird dort geändert.";
55
- export const READ_ONLY_TENANT = "Hier wird nichts gebaut: Auf diesem Server laufen die Wizards, die deine lokale Installation veröffentlicht.";
55
+ export const READ_ONLY_PROJECT = bilingual("Dieses Projekt kommt aus einer lokalen Installation und wird dort geändert.", "This project comes from a local install and is changed there.");
56
+ export const READ_ONLY_TENANT = bilingual("Hier wird nichts gebaut: Auf diesem Server laufen die Wizards, die deine lokale Installation veröffentlicht.", "Nothing is built here: this server runs the wizards your local install publishes.");
56
57
  /** Refuses when the tenant builds nothing here. */
57
58
  export async function requireBuild() {
58
59
  if (syncing() || (await mayBuild())) {
@@ -30,7 +30,9 @@ async function attachedImages(userId, wizardId, message) {
30
30
  return images;
31
31
  }
32
32
  /** One studio chat turn: the architect reads the draft, its workspace and the thread, and changes them with tools. */
33
- export async function architectTurn(userId, wizardId, message, io) {
33
+ export async function architectTurn(userId, wizardId, message,
34
+ /** The language the admin reads: the lines about what it does, and its stock replies. */
35
+ lang, io) {
34
36
  // Asked before the first model call: a wizard that is not changed here costs nothing to refuse.
35
37
  const w = await writableWizard(userId, wizardId);
36
38
  const history = await wizardMessages(w.id);
@@ -51,6 +53,7 @@ export async function architectTurn(userId, wizardId, message, io) {
51
53
  onThought: io.onThought,
52
54
  onBuilding: io.onBuilding,
53
55
  images,
56
+ lang,
54
57
  })),
55
58
  unfinished: false,
56
59
  }
@@ -67,13 +70,21 @@ export async function architectTurn(userId, wizardId, message, io) {
67
70
  onThought: io.onThought,
68
71
  onBuilding: io.onBuilding,
69
72
  images,
73
+ lang,
70
74
  });
75
+ const de = lang === "de";
71
76
  const reply = result.unfinished
72
- ? `${result.reply ? `${result.reply}\n\n` : ""}Ich bin noch nicht ganz fertig geworden. Schreib „weiter“, dann mache ich genau dort weiter.`
77
+ ? `${result.reply ? `${result.reply}\n\n` : ""}${de
78
+ ? "Ich bin noch nicht ganz fertig geworden. Schreib „weiter“, dann mache ich genau dort weiter."
79
+ : "I have not quite finished. Write “continue” and I will carry on right there."}`
73
80
  : result.reply ||
74
81
  (result.changed
75
- ? "Erledigt."
76
- : "Das habe ich nicht verstanden – magst du es anders formulieren?");
82
+ ? de
83
+ ? "Erledigt."
84
+ : "Done."
85
+ : de
86
+ ? "Das habe ich nicht verstanden – magst du es anders formulieren?"
87
+ : "I did not understand that – could you put it differently?");
77
88
  await addMessage(w.id, { role: "assistant", content: reply, changed: result.changed });
78
89
  const after = await ownedWizard(userId, wizardId);
79
90
  return {
@@ -1,4 +1,5 @@
1
1
  import { ASSISTANT_LAYOUTS } from "@engenty-wizards/shared/assistant";
2
+ import { bilingual } from "@engenty-wizards/shared/run";
2
3
  import { generateText } from "ai";
3
4
  import { and, asc, desc, eq, isNotNull, isNull, sql } from "drizzle-orm";
4
5
  import { nanoid } from "nanoid";
@@ -252,7 +253,7 @@ export async function readChat(user, chatId) {
252
253
  where: and(mine(user), eq(schema.assistantChat.id, chatId)),
253
254
  });
254
255
  if (!chat) {
255
- throw new ServiceError("not_found", "Diesen Chat gibt es nicht.");
256
+ throw new ServiceError("not_found", bilingual("Diesen Chat gibt es nicht.", "There is no such chat."));
256
257
  }
257
258
  return {
258
259
  id: chat.id,
@@ -1,4 +1,5 @@
1
1
  import { GRADIENT_TYPES, gradientCss, mainLogo, } from "@engenty-wizards/shared/projects";
2
+ import { bilingual } from "@engenty-wizards/shared/run";
2
3
  import { generateText, Output } from "ai";
3
4
  import { and, asc, eq } from "drizzle-orm";
4
5
  import { z } from "zod";
@@ -65,14 +66,14 @@ export async function describeGradient(brand, text, current) {
65
66
  answer = result.output;
66
67
  }
67
68
  catch {
68
- throw new ServiceError("invalid", "Daraus ließ sich kein Verlauf machen. Beschreib ihn bitte anders.");
69
+ throw new ServiceError("invalid", bilingual("Daraus ließ sich kein Verlauf machen. Beschreib ihn bitte anders.", "No gradient could be made from that. Please describe it differently."));
69
70
  }
70
71
  const stops = answer.stops
71
72
  .filter((s) => HEX.test(s.color))
72
73
  .slice(0, 12)
73
74
  .map((s) => ({ color: s.color.toLowerCase(), at: Math.min(100, Math.max(0, s.at)) }));
74
75
  if (stops.length < 2) {
75
- throw new ServiceError("invalid", "Daraus ließ sich kein Verlauf machen. Beschreib ihn bitte anders.");
76
+ throw new ServiceError("invalid", bilingual("Daraus ließ sich kein Verlauf machen. Beschreib ihn bitte anders.", "No gradient could be made from that. Please describe it differently."));
76
77
  }
77
78
  return {
78
79
  name: answer.name.trim().slice(0, 80),
@@ -1,3 +1,4 @@
1
+ import { errorText, NotedError } from "../model-errors.js";
1
2
  const STATUS = {
2
3
  not_found: 404,
3
4
  invalid: 400,
@@ -8,19 +9,20 @@ const STATUS = {
8
9
  read_only: 403,
9
10
  forbidden: 403,
10
11
  };
11
- export class ServiceError extends Error {
12
+ export class ServiceError extends NotedError {
12
13
  code;
13
14
  data;
14
- constructor(code, message, data = {}) {
15
- super(message);
15
+ constructor(code, said, data = {}) {
16
+ super(said);
16
17
  this.code = code;
17
18
  this.data = data;
18
19
  }
19
20
  get status() {
20
21
  return STATUS[this.code];
21
22
  }
22
- toJSON() {
23
- return { error: this.message, code: this.code, ...this.data };
23
+ /** The answer, its words in the asker's language; English where none is known. */
24
+ toJSON(lang = "en") {
25
+ return { error: errorText(this, lang), code: this.code, ...this.data };
24
26
  }
25
27
  }
26
28
  export const notFound = () => new ServiceError("not_found", "not found");
@@ -1,4 +1,5 @@
1
1
  import { dayOf, slugOf } from "@engenty-wizards/shared/knowledge";
2
+ import { bilingual } from "@engenty-wizards/shared/run";
2
3
  import { columnIdOf } from "@engenty-wizards/shared/space-data";
3
4
  import { generateText, Output } from "ai";
4
5
  import { and, eq, inArray, isNull } from "drizzle-orm";
@@ -222,7 +223,7 @@ function titleOf(markdown, name) {
222
223
  function reviewOf(parsed, markdown) {
223
224
  const illegible = (markdown.match(/\[unleserlich\]/gi) ?? []).length;
224
225
  if (parsed.via === "vision" && (illegible >= 3 || markdown.replace(/\s/g, "").length < 200)) {
225
- return "Schwer lesbar: bitte mit dem Original vergleichen.";
226
+ return "Hard to read: please compare with the original.";
226
227
  }
227
228
  return null;
228
229
  }
@@ -503,7 +504,7 @@ export async function writeSummary(valueId) {
503
504
  where: eq(schema.spaceCategoryValue.id, valueId),
504
505
  });
505
506
  if (!value) {
506
- throw new ServiceError("not_found", "Diesen Wert gibt es nicht.");
507
+ throw new ServiceError("not_found", bilingual("Diesen Wert gibt es nicht.", "There is no such value."));
507
508
  }
508
509
  const category = await categoryRow(value.categoryId);
509
510
  const { items, total } = await itemsOfValue(value.id);
@@ -4,6 +4,7 @@ import { DEFINITION_VERSION, readable, wizardSchema, } from "@engenty-wizards/sh
4
4
  import { capabilitiesOf, costTierOf, effortOf, formatsOf, MARKETPLACE_LANGS, pluginsOf, } from "@engenty-wizards/shared/marketplace";
5
5
  import { wizardOutline } from "@engenty-wizards/shared/marketplace-entry";
6
6
  import { searchEntries } from "@engenty-wizards/shared/marketplace-search";
7
+ import { bilingual } from "@engenty-wizards/shared/run";
7
8
  import { and, eq, inArray, notInArray } from "drizzle-orm";
8
9
  import { formulaEstimate } from "../credits/estimate.js";
9
10
  import { control, controlDb, db, schema, withTenant } from "../db/client.js";
@@ -25,7 +26,7 @@ import { ServiceError } from "./errors.js";
25
26
  * listed first and never fetched. They are known as `<plugin id>.<starter id>`; a marketplace
26
27
  * entry's id has no dot, so the two never meet.
27
28
  */
28
- export const asLang = (raw) => MARKETPLACE_LANGS.includes(raw ?? "") ? raw : "de";
29
+ export const asLang = (raw) => MARKETPLACE_LANGS.includes(raw ?? "") ? raw : "en";
29
30
  /** A search waits this long for the marketplace; then it searches what is kept here. */
30
31
  const TIMEOUT_MS = 5000;
31
32
  /** One call to the marketplace's API; null without an answer, or with an error. */
@@ -114,7 +115,7 @@ function pluginSummary({ id, plugin, starter }) {
114
115
  .slice(0, 16),
115
116
  updatedAt: (changed?.mtime ?? new Date()).toISOString(),
116
117
  starter: true,
117
- language: starter.language ?? "de",
118
+ language: starter.language ?? "en",
118
119
  title: starter.title,
119
120
  pitch: starter.description,
120
121
  // Found by the plugin's name as well: "Termine" finds the appointment plugin's starters.
@@ -390,7 +391,7 @@ export async function marketplaceWizard(id, lang) {
390
391
  revision: null,
391
392
  plugin: plugin.source.id,
392
393
  title: starter.title,
393
- language: starter.language ?? "de",
394
+ language: starter.language ?? "en",
394
395
  definition: structuredClone(starter.definition),
395
396
  files: Object.fromEntries(Object.entries(starter.files ?? {}).map(([path, data]) => [
396
397
  path,
@@ -403,13 +404,13 @@ export async function marketplaceWizard(id, lang) {
403
404
  return null;
404
405
  }
405
406
  if (!readable(found.definition)) {
406
- throw new ServiceError("refused", "Diese Vorlage braucht eine neuere Version der App. Bitte aktualisiere die App.");
407
+ throw new ServiceError("refused", bilingual("Diese Vorlage braucht eine neuere Version der App. Bitte aktualisiere die App.", "This template needs a newer version of the app. Please update the app."));
407
408
  }
408
409
  // A wizard whose plugins are not here would stop at its first step that uses them.
409
410
  const have = await pluginIdsOf(currentTenant());
410
411
  const needs = pluginsOf(wizardSchema.parse(found.definition)).filter((p) => !have.has(p));
411
412
  if (needs.length) {
412
- throw new ServiceError("refused", `Diese Vorlage braucht ${needs.length === 1 ? "das Plugin" : "die Plugins"} ${needs.join(", ")}: mit Pro auf engenty.ai dabei.`);
413
+ throw new ServiceError("refused", bilingual(`Diese Vorlage braucht ${needs.length === 1 ? "das Plugin" : "die Plugins"} ${needs.join(", ")}: mit Pro auf engenty.ai dabei.`, `This template needs the ${needs.length === 1 ? "plugin" : "plugins"} ${needs.join(", ")}: included with Pro on engenty.ai.`));
413
414
  }
414
415
  return {
415
416
  id: found.id,
@@ -136,16 +136,23 @@ async function missing(userId, projectId, draft, manifest) {
136
136
  ];
137
137
  }
138
138
  /** Makes a new wizard of a package in the project: the draft and its workspace, not published. */
139
- export async function importWizard(userId, projectId, data, name) {
139
+ export async function importWizard(userId, projectId, data, name,
140
+ /** The language the admin reads the import's note in. */
141
+ lang = "en") {
140
142
  await requireAdmin();
141
143
  const { manifest, files } = readPackage(data);
142
144
  // The shape is checked before anything is written; a wrong one leaves no wizard behind.
143
145
  const { draft } = parseDraft(manifest.definition, files.map((f) => f.path));
144
146
  const lacks = await missing(userId, projectId, draft, manifest);
147
+ const de = lang === "de";
145
148
  const note = [
146
- `Importiert${name ? ` aus „${name}“` : ""}: ${draft.steps.length} Schritte, ${files.length} ${files.length === 1 ? "Datei" : "Dateien"}.`,
149
+ de
150
+ ? `Importiert${name ? ` aus „${name}“` : ""}: ${draft.steps.length} Schritte, ${files.length} ${files.length === 1 ? "Datei" : "Dateien"}.`
151
+ : `Imported${name ? ` from “${name}”` : ""}: ${draft.steps.length} steps, ${files.length} ${files.length === 1 ? "file" : "files"}.`,
147
152
  lacks.length
148
- ? `Diesem Projekt fehlt noch: ${lacks.join(", ")} – in den Einstellungen unter „Connectors“ hinzufügen.`
153
+ ? de
154
+ ? `Diesem Projekt fehlt noch: ${lacks.join(", ")} – in den Einstellungen unter „Connectors“ hinzufügen.`
155
+ : `This project still lacks: ${lacks.join(", ")} – add it under “Connectors” in the settings.`
149
156
  : "",
150
157
  ]
151
158
  .filter(Boolean)
@@ -27,6 +27,18 @@ export async function userProfile(user) {
27
27
  phone: stored.phone ?? "",
28
28
  };
29
29
  }
30
+ const langKey = (user) => `lang:${user.tenantId}:${user.id}`;
31
+ /**
32
+ * The language the person picked in the studio, kept with them rather than one browser: a new
33
+ * browser or device opens in it. Null until they pick one; the browser's language stands then.
34
+ */
35
+ export async function userLang(user) {
36
+ const lang = await readSetting(langKey(user));
37
+ return lang === "de" || lang === "en" ? lang : null;
38
+ }
39
+ export async function saveUserLang(user, lang) {
40
+ await writeSetting(langKey(user), lang);
41
+ }
30
42
  export async function saveProfile(user, profile) {
31
43
  // Name and e-mail an account gives stay the person's own underneath: they come back unlinked.
32
44
  const stored = (await readSetting(key(user))) ?? {};
@@ -1,4 +1,5 @@
1
1
  import { PROJECT_LIMITS, projectFileLimit, } from "@engenty-wizards/shared/projects";
2
+ import { bilingual } from "@engenty-wizards/shared/run";
2
3
  import { generateText } from "ai";
3
4
  import { and, asc, count, eq, max } from "drizzle-orm";
4
5
  import { db, schema } from "../db/client.js";
@@ -27,7 +28,7 @@ export function fileView(row) {
27
28
  description: row.description,
28
29
  source: row.source,
29
30
  status: stale ? "failed" : row.status,
30
- error: stale ? "Unterbrochen. Bitte neu einlesen." : row.error,
31
+ error: stale ? "Interrupted. Please read it in again." : row.error,
31
32
  indexed: row.indexed,
32
33
  pages: row.pages,
33
34
  chars: row.chars,
@@ -78,14 +79,14 @@ export async function addProjectFile(projectId, input) {
78
79
  const mime = documentMime(input.name, input.mime) || "application/octet-stream";
79
80
  if (!accepts(input.kind, mime)) {
80
81
  throw new ServiceError("invalid", input.kind === "logo"
81
- ? "Ein Logo ist ein Bild (PNG, SVG, JPEG, WebP)."
82
+ ? bilingual("Ein Logo ist ein Bild (PNG, SVG, JPEG, WebP).", "A logo is an image (PNG, SVG, JPEG, WebP).")
82
83
  : input.kind === "pattern"
83
- ? "Ein Muster ist ein Bild (SVG, PNG, JPEG, WebP)."
84
- : "Hierher gehören Bilder, Videos, Audio, PDFs, Schriften, Lottie und 3D-Modelle. Texte zum Nachschlagen bitte zu den Dokumenten.");
84
+ ? bilingual("Ein Muster ist ein Bild (SVG, PNG, JPEG, WebP).", "A pattern is an image (SVG, PNG, JPEG, WebP).")
85
+ : bilingual("Hierher gehören Bilder, Videos, Audio, PDFs, Schriften, Lottie und 3D-Modelle. Texte zum Nachschlagen bitte zu den Dokumenten.", "Images, videos, audio, PDFs, fonts, Lottie and 3D models belong here. Texts to look things up in go to the documents."));
85
86
  }
86
87
  const limit = projectFileLimit(input.kind, mime);
87
88
  if (!input.data.byteLength || input.data.byteLength > limit) {
88
- throw new ServiceError("invalid", `Bitte eine Datei bis ${Math.round(limit / 1_000_000)} MB wählen.`);
89
+ throw new ServiceError("invalid", bilingual(`Bitte eine Datei bis ${Math.round(limit / 1_000_000)} MB wählen.`, `Please choose a file of up to ${Math.round(limit / 1_000_000)} MB.`));
89
90
  }
90
91
  const scope = and(eq(schema.projectFile.projectId, projectId), eq(schema.projectFile.kind, input.kind));
91
92
  const [{ n, last }] = await db
@@ -93,7 +94,7 @@ export async function addProjectFile(projectId, input) {
93
94
  .from(schema.projectFile)
94
95
  .where(scope);
95
96
  if (n >= PROJECT_LIMITS.files[input.kind]) {
96
- throw new ServiceError("refused", `Höchstens ${PROJECT_LIMITS.files[input.kind]} Dateien.`);
97
+ throw new ServiceError("refused", bilingual(`Höchstens ${PROJECT_LIMITS.files[input.kind]} Dateien.`, `At most ${PROJECT_LIMITS.files[input.kind]} files.`));
97
98
  }
98
99
  const name = input.name.slice(0, 120) || "datei";
99
100
  const ref = await saveAsset({
@@ -149,21 +150,21 @@ export async function addProjectFileFromUrl(projectId, input) {
149
150
  headers: { "user-agent": "Mozilla/5.0 (compatible; engenty-wizards/0.1)" },
150
151
  signal: AbortSignal.any([AbortSignal.timeout(60_000), ...(signal ? [signal] : [])]),
151
152
  }).catch(() => {
152
- throw new ServiceError("invalid", "Die Adresse ist nicht erreichbar.");
153
+ throw new ServiceError("invalid", bilingual("Die Adresse ist nicht erreichbar.", "The address cannot be reached."));
153
154
  });
154
155
  if (!res.ok) {
155
- throw new ServiceError("invalid", `Die Adresse antwortete ${res.status}.`);
156
+ throw new ServiceError("invalid", bilingual(`Die Adresse antwortete ${res.status}.`, `The address answered ${res.status}.`));
156
157
  }
157
158
  const mime = (res.headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase();
158
159
  if (mime === "text/html" && kind !== "document") {
159
160
  await res.body?.cancel();
160
- throw new ServiceError("invalid", "Unter der Adresse steht eine Webseite, kein Bild.", {
161
+ throw new ServiceError("invalid", bilingual("Unter der Adresse steht eine Webseite, kein Bild.", "The address holds a web page, not an image."), {
161
162
  reason: "page",
162
163
  url: res.url,
163
164
  });
164
165
  }
165
166
  if (Number(res.headers.get("content-length") ?? 0) > projectFileLimit(kind, mime)) {
166
- throw new ServiceError("invalid", "Die Datei ist zu groß.");
167
+ throw new ServiceError("invalid", bilingual("Die Datei ist zu groß.", "The file is too large."));
167
168
  }
168
169
  const data = new Uint8Array(await res.arrayBuffer());
169
170
  const name = decodeURIComponent(new URL(res.url).pathname.split("/").pop() || kind);
@@ -297,7 +298,7 @@ async function prepare(row) {
297
298
  status: "failed",
298
299
  error: err instanceof UnreadableDocument
299
300
  ? err.message
300
- : "Das Lesen hat nicht geklappt. Bitte noch einmal versuchen.",
301
+ : "Reading did not work. Please try again.",
301
302
  });
302
303
  }
303
304
  });
@@ -306,7 +307,7 @@ export async function reindexProjectFile(projectId, fileId) {
306
307
  await requireWritableProject(projectId);
307
308
  const row = await projectFile(projectId, fileId);
308
309
  if (row.kind !== "document") {
309
- throw new ServiceError("invalid", "Nur Dokumente stehen im Index.");
310
+ throw new ServiceError("invalid", bilingual("Nur Dokumente stehen im Index.", "Only documents are in the index."));
310
311
  }
311
312
  const [pending] = await db
312
313
  .update(schema.projectFile)