wizards 0.5.7 → 0.5.9

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 (109) hide show
  1. package/apps/runtime/dist/agents/architect.js +13 -26
  2. package/apps/runtime/dist/agents/studio-assistant.js +19 -22
  3. package/apps/runtime/dist/agents/subscription.js +10 -9
  4. package/apps/runtime/dist/app.js +6 -6
  5. package/apps/runtime/dist/assistant/knows.js +10 -9
  6. package/apps/runtime/dist/connectors/external.js +3 -3
  7. package/apps/runtime/dist/connectors/index.js +23 -23
  8. package/apps/runtime/dist/connectors/via-account.js +6 -6
  9. package/apps/runtime/dist/documents/parse.js +3 -3
  10. package/apps/runtime/dist/engine/input.js +11 -11
  11. package/apps/runtime/dist/engine/requirements.js +7 -8
  12. package/apps/runtime/dist/engine/runner.js +10 -14
  13. package/apps/runtime/dist/engine/steps.js +6 -6
  14. package/apps/runtime/dist/engine/surface.js +2 -2
  15. package/apps/runtime/dist/film/step.js +5 -5
  16. package/apps/runtime/dist/harness/apple.js +2 -1
  17. package/apps/runtime/dist/harness/claude.js +2 -2
  18. package/apps/runtime/dist/harness/codex.js +2 -2
  19. package/apps/runtime/dist/harness/model.js +4 -5
  20. package/apps/runtime/dist/harness/terminal.js +4 -4
  21. package/apps/runtime/dist/media/conversation.js +18 -35
  22. package/apps/runtime/dist/media/transcribe.js +2 -1
  23. package/apps/runtime/dist/model-errors.js +1 -1
  24. package/apps/runtime/dist/models.js +32 -40
  25. package/apps/runtime/dist/plugins/runs.js +3 -5
  26. package/apps/runtime/dist/routes/api.js +9 -9
  27. package/apps/runtime/dist/routes/lang.js +14 -9
  28. package/apps/runtime/dist/routes/runs.js +5 -5
  29. package/apps/runtime/dist/routes/studio.js +38 -37
  30. package/apps/runtime/dist/services/access.js +4 -4
  31. package/apps/runtime/dist/services/architect.js +3 -12
  32. package/apps/runtime/dist/services/assistant-chats.js +2 -2
  33. package/apps/runtime/dist/services/brand.js +3 -3
  34. package/apps/runtime/dist/services/cloud.js +5 -5
  35. package/apps/runtime/dist/services/errors.js +2 -2
  36. package/apps/runtime/dist/services/knowledge-model.js +3 -3
  37. package/apps/runtime/dist/services/marketplace.js +3 -3
  38. package/apps/runtime/dist/services/package-file.js +9 -11
  39. package/apps/runtime/dist/services/package.js +8 -9
  40. package/apps/runtime/dist/services/profile.js +2 -1
  41. package/apps/runtime/dist/services/project-files.js +13 -13
  42. package/apps/runtime/dist/services/projects.js +7 -11
  43. package/apps/runtime/dist/services/runners.js +4 -4
  44. package/apps/runtime/dist/services/runs.js +7 -10
  45. package/apps/runtime/dist/services/shares.js +2 -2
  46. package/apps/runtime/dist/services/site.js +8 -11
  47. package/apps/runtime/dist/services/space-categories.js +7 -7
  48. package/apps/runtime/dist/services/space-data.js +20 -20
  49. package/apps/runtime/dist/services/spaces.js +7 -9
  50. package/apps/runtime/dist/services/wizards.js +11 -13
  51. package/apps/runtime/dist/store/index.js +2 -2
  52. package/apps/runtime/dist/texts.js +195 -0
  53. package/apps/runtime/dist/tools/plugin.js +7 -3
  54. package/apps/runtime/dist/widgets/step.js +2 -2
  55. package/apps/web/dist/assets/AboutDialog-CpI-cGpz.js +1 -0
  56. package/apps/web/dist/assets/{ChangelogDialog-rAtYZjxz.js → ChangelogDialog-BW56Y5UR.js} +1 -1
  57. package/apps/web/dist/assets/ChatView-CH8U-tRy.js +10 -0
  58. package/apps/web/dist/assets/EditorPage-Be5J_y4F.js +3 -0
  59. package/apps/web/dist/assets/{FlowPreview--3-yI51e.js → FlowPreview-CzmH4JUQ.js} +1 -1
  60. package/apps/web/dist/assets/{Harness-B9tvFe0U.js → Harness-CJKWO3AK.js} +2 -2
  61. package/apps/web/dist/assets/{NewWizardChannels-BxrBhC9U.js → NewWizardChannels-Ca2niMTY.js} +1 -1
  62. package/apps/web/dist/assets/NewWizardPage-D5gi4TYE.js +7 -0
  63. package/apps/web/dist/assets/{OnboardingPage-DPdH9sSj.js → OnboardingPage-Bm-c-D21.js} +1 -1
  64. package/apps/web/dist/assets/ProjectPage-C8nJCBZP.js +1 -0
  65. package/apps/web/dist/assets/{PublicRunner-Bv3TMKw4.js → PublicRunner-anxg3Rxz.js} +1 -1
  66. package/apps/web/dist/assets/{ReadOnly-CmqIrOAR.js → ReadOnly-i-6WBMSE.js} +1 -1
  67. package/apps/web/dist/assets/RunDrawer-4yvfW_2c.js +4 -0
  68. package/apps/web/dist/assets/{RunnerView-Cd1VAlIA.js → RunnerView-BkWQBa_M.js} +1 -1
  69. package/apps/web/dist/assets/SettingsPage-NL8r9S6R.js +1 -0
  70. package/apps/web/dist/assets/SetupPage-Crcfx4-p.js +1 -0
  71. package/apps/web/dist/assets/{SharePage-BT80wI1I.js → SharePage-CDEAtmBC.js} +1 -1
  72. package/apps/web/dist/assets/changelog-BGBuldNF.js +1 -0
  73. package/apps/web/dist/assets/{esm-BTVd6EFo.js → esm-BF1nonbH.js} +1 -1
  74. package/apps/web/dist/assets/index-C74LSJ-W.js +14 -0
  75. package/apps/web/dist/assets/{lock-ByHFI96G.js → lock-DVvM6IsA.js} +1 -1
  76. package/apps/web/dist/assets/{phone-KPwxAM1R.js → phone-3fG1okFs.js} +1 -1
  77. package/apps/web/dist/assets/{settings-menu-K5bg3QE3.js → settings-menu-D9fbi1Z0.js} +1 -1
  78. package/apps/web/dist/assets/ui-EuR3jezP.js +134 -0
  79. package/apps/web/dist/assets/{upload-BsIL8tNR.js → upload-BsMUhEfX.js} +1 -1
  80. package/apps/web/dist/index.html +2 -2
  81. package/apps/web/dist/mcp/flow.html +77 -77
  82. package/bin/wizards.mjs +15 -2
  83. package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +26 -9
  84. package/node_modules/@engenty-wizards/shared/dist/definition.js +125 -3
  85. package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.d.ts +1 -1
  86. package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.js +9 -8
  87. package/node_modules/@engenty-wizards/shared/dist/marketplace-search.js +6 -6
  88. package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +78 -205
  89. package/node_modules/@engenty-wizards/shared/dist/marketplace.js +167 -68
  90. package/node_modules/@engenty-wizards/shared/dist/projects.d.ts +3 -6
  91. package/node_modules/@engenty-wizards/shared/dist/projects.js +9 -9
  92. package/node_modules/@engenty-wizards/shared/dist/providers.d.ts +33 -7
  93. package/node_modules/@engenty-wizards/shared/dist/providers.js +88 -37
  94. package/node_modules/@engenty-wizards/shared/dist/run.d.ts +298 -365
  95. package/node_modules/@engenty-wizards/shared/dist/run.js +582 -240
  96. package/node_modules/@engenty-wizards/shared/dist/texts.d.ts +36 -0
  97. package/node_modules/@engenty-wizards/shared/dist/texts.js +46 -0
  98. package/package.json +5 -1
  99. package/apps/web/dist/assets/AboutDialog-t9bayaIs.js +0 -1
  100. package/apps/web/dist/assets/ChatView-C3VqLT98.js +0 -10
  101. package/apps/web/dist/assets/EditorPage-D3Ef24b9.js +0 -3
  102. package/apps/web/dist/assets/NewWizardPage-C1_hsQPr.js +0 -7
  103. package/apps/web/dist/assets/ProjectPage-P40R1cPE.js +0 -1
  104. package/apps/web/dist/assets/RunDrawer-9qG1G-OY.js +0 -4
  105. package/apps/web/dist/assets/SettingsPage-DFz4UMfo.js +0 -1
  106. package/apps/web/dist/assets/SetupPage-B0UBNMB9.js +0 -1
  107. package/apps/web/dist/assets/changelog-BpSBMjpm.js +0 -1
  108. package/apps/web/dist/assets/index-D8vwgldH.js +0 -14
  109. package/apps/web/dist/assets/ui-4bQx9I1f.js +0 -134
@@ -1,6 +1,6 @@
1
1
  import { marginOf, } from "@engenty-wizards/shared/decision";
2
2
  import { branchValues, cardsFromData, conditionsOf, dataRef, followRules, formatsFor, isDecidedField, LIST_FORMATS, listRef, loopKey, MAX_SHOWN_FIELDS, optionsFromData, otherwiseOf, pageNeeds, whenHolds, wizardLang, } from "@engenty-wizards/shared/definition";
3
- import { bilingual, saidText, } from "@engenty-wizards/shared/run";
3
+ import { note, saidText, } from "@engenty-wizards/shared/run";
4
4
  import { CONVERSATION_RUNNERS, DEFAULT_RUNNER_SETTINGS } from "@engenty-wizards/shared/runners";
5
5
  import { eq } from "drizzle-orm";
6
6
  import { nanoid } from "nanoid";
@@ -43,7 +43,7 @@ export class RunInputError extends NotedError {
43
43
  }
44
44
  export class NoCreditsError extends NotedError {
45
45
  constructor() {
46
- super(bilingual("Dieser Wizard hat gerade kein Guthaben mehr. Versuch es später noch einmal.", "This wizard has run out of credits for now. Try again later."));
46
+ super(note("wizardRunOutCredits"));
47
47
  }
48
48
  }
49
49
  /** The runs this process drives right now, with the handle that stops each. */
@@ -113,19 +113,19 @@ function friendly(err) {
113
113
  }
114
114
  // A runtime that runs alone pays its provider itself: its admin can top the account up.
115
115
  if (!managed && /minimum balance|insufficient_funds|top up your credits/i.test(msg)) {
116
- return bilingual("Das Guthaben beim Modellanbieter reicht dafür nicht aus (Videos brauchen dort ein Mindestguthaben). Lade dort auf und versuch es noch einmal.", "Your credit with the model provider isn’t enough for this (videos need a minimum balance there). Top up there and try again.");
116
+ return note("providerCreditLow");
117
117
  }
118
118
  if (/rate.?limit|429/i.test(msg)) {
119
- return bilingual("Der KI-Dienst ist gerade ausgelastet. Versuch es gleich noch einmal.", "The AI service is busy right now. Try again in a moment.");
119
+ return note("aiServiceBusyRight");
120
120
  }
121
121
  if (/timeout|timed out/i.test(msg)) {
122
- return bilingual("Der Schritt hat zu lange gedauert. Versuch es noch einmal.", "The step took too long. Try again.");
122
+ return note("stepTookTooLong");
123
123
  }
124
124
  // The platform's own provider account (budget, quota, key): nothing the person can fix.
125
125
  if (/budget|quota|insufficient|billing|credit balance|api key|unauthori[sz]ed|401|403/i.test(msg)) {
126
- return bilingual("Der KI-Dienst ist gerade nicht erreichbar. Versuch es später noch einmal.", "The AI service can’t be reached right now. Try again later.");
126
+ return note("aiServiceNotReached");
127
127
  }
128
- return bilingual(`Dieser Schritt hat nicht geklappt: ${msg.slice(0, 300)}`, `This step failed: ${msg.slice(0, 300)}`);
128
+ return note("stepFailed", { detail: msg.slice(0, 300) });
129
129
  }
130
130
  /**
131
131
  * A step that failed ends the run. A failure with a note (Apple Intelligence's) goes into the log
@@ -637,7 +637,7 @@ export async function submitPage(runId, stepId, input) {
637
637
  (choice.values.length === 0 || choice.values.some((v) => String(v) === String(answer)))) {
638
638
  errors.push({
639
639
  field: id,
640
- message: bilingual("Das ist hier gerade nicht verfügbar.", "That isn’t available here right now."),
640
+ message: note("notAvailableNow"),
641
641
  });
642
642
  }
643
643
  }
@@ -653,9 +653,7 @@ export async function submitPage(runId, stepId, input) {
653
653
  for (const id of Array.isArray(value) ? value : value ? [value] : []) {
654
654
  const asset = await db.query.asset.findFirst({ where: eq(schema.asset.id, String(id)) });
655
655
  if (asset?.runId !== runId) {
656
- throw new RunInputError([
657
- { field: field.id, message: bilingual("Datei nicht gefunden", "File not found") },
658
- ]);
656
+ throw new RunInputError([{ field: field.id, message: note("fileNotFound") }]);
659
657
  }
660
658
  }
661
659
  }
@@ -666,9 +664,7 @@ export async function submitPage(runId, stepId, input) {
666
664
  values[field.id] = account.label;
667
665
  }
668
666
  else if (field.required) {
669
- throw new RunInputError([
670
- { field: field.id, message: bilingual("Verbinde zuerst", "Connect first") },
671
- ]);
667
+ throw new RunInputError([{ field: field.id, message: note("connectFirst") }]);
672
668
  }
673
669
  }
674
670
  }
@@ -1,7 +1,7 @@
1
1
  import { pOf } from "@engenty-wizards/shared/decision";
2
2
  import { allFields, dataRef, isDecisionStep, LOCALES, templateRefs, wizardLang, } from "@engenty-wizards/shared/definition";
3
3
  import { mainLogo, ruleLines } from "@engenty-wizards/shared/projects";
4
- import { bilingual } from "@engenty-wizards/shared/run";
4
+ import { note as runNote } from "@engenty-wizards/shared/run";
5
5
  import { SURFACE_GUIDE, validateSurface, } from "@engenty-wizards/shared/surface";
6
6
  import { Agent } from "@mastra/core/agent";
7
7
  import { generateText, Output } from "ai";
@@ -379,7 +379,7 @@ function agentEntries(step, ctx) {
379
379
  async function runAgentEach(step, ctx) {
380
380
  const entries = agentEntries(step, ctx);
381
381
  if (!entries.length) {
382
- throw new StepError(bilingual("Es gibt keine Einträge, für die dieser Schritt arbeiten könnte.", "There are no entries for this step to work on."));
382
+ throw new StepError(runNote("noEntriesStepWork"));
383
383
  }
384
384
  // The view, if any, is composed once over the table of all entries.
385
385
  const single = {
@@ -554,7 +554,7 @@ export async function runAgentStep(step, ctx) {
554
554
  const text = step.output.format === "json" ? everything : closing || everything;
555
555
  if (step.output.format !== "json") {
556
556
  if (!text && !assets.length) {
557
- throw new StepError(bilingual("Der Schritt hat kein Ergebnis geliefert.", "The step didn’t return a result."));
557
+ throw new StepError(runNote("stepNotReturnResult"));
558
558
  }
559
559
  return { text, assets: assets.length ? assets : undefined, at: new Date().toISOString() };
560
560
  }
@@ -755,7 +755,7 @@ async function writeHtml(step, ctx, kind) {
755
755
  .replace(/```\s*$/, "")
756
756
  .trim();
757
757
  if (!/<html|<!doctype/i.test(html)) {
758
- throw new StepError(bilingual("Das Dokument konnte nicht erstellt werden.", "The document couldn’t be created."));
758
+ throw new StepError(runNote("documentNotCreated"));
759
759
  }
760
760
  // On screen (preview, PNG) a print document needs the margin its @page rule gives it on paper.
761
761
  return kind === "document"
@@ -806,7 +806,7 @@ async function inParallel(items, limit, work) {
806
806
  async function runMediaStep(step, ctx, asset) {
807
807
  const entries = entriesOf(step, ctx);
808
808
  if (!entries.length) {
809
- throw new StepError(bilingual("Für diesen Schritt fehlen die Vorlagen (Bilder oder Szenen).", "This step is missing its sources (images or scenes)."));
809
+ throw new StepError(runNote("stepMissingSourcesImages"));
810
810
  }
811
811
  const previous = ctx.state.outputs[step.id];
812
812
  const before = previous?.assets?.filter((a) => a.kind === asset) ?? [];
@@ -898,7 +898,7 @@ export async function runGenerateStep(step, ctx) {
898
898
  case "voice": {
899
899
  const text = renderTemplate(step.prompt, ctx.scope).trim().slice(0, 4000);
900
900
  if (!text) {
901
- throw new StepError(bilingual("Es gibt keinen Text zum Vorlesen.", "There is no text to read aloud."));
901
+ throw new StepError(runNote("noTextReadAloud"));
902
902
  }
903
903
  const note = ctx.state.notes[step.id];
904
904
  const media = await generateSpeechMedia({
@@ -1,5 +1,5 @@
1
1
  import { assembleSurface, dataRef, listRef, } from "@engenty-wizards/shared/definition";
2
- import { bilingual } from "@engenty-wizards/shared/run";
2
+ import { note } from "@engenty-wizards/shared/run";
3
3
  import { validateSurface } from "@engenty-wizards/shared/surface";
4
4
  import { decide } from "./decide.js";
5
5
  import { answersAsText, resolveRef } from "./template.js";
@@ -14,7 +14,7 @@ export async function runSurfaceStep(step, ctx) {
14
14
  const components = step.components ?? (await decidedComponents(step, ctx));
15
15
  const issues = validateSurface(components, Object.keys(data));
16
16
  if (issues.length) {
17
- throw new StepError(bilingual(`Die Ansicht hat Fehler: ${issues.map((i) => i.message).join(" ")}`, `The view has errors: ${issues.map((i) => i.message).join(" ")}`));
17
+ throw new StepError(note("viewErrors", { detail: issues.map((i) => i.message).join(" ") }));
18
18
  }
19
19
  return { surface: { components, data }, at: new Date().toISOString() };
20
20
  }
@@ -2,12 +2,12 @@ import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { readFile, rm, stat } from "node:fs/promises";
4
4
  import { isAbsolute, join, normalize } from "node:path";
5
- import { bilingual } from "@engenty-wizards/shared/run";
5
+ import { note as runNote } from "@engenty-wizards/shared/run";
6
6
  import { StepError } from "../engine/types.js";
7
7
  import { CLAUDE_BIN, claudeEnv } from "../harness/claude.js";
8
8
  import { failureOf } from "../harness/model.js";
9
9
  import { openBridge } from "../mcp/bridge.js";
10
- import { errorText } from "../model-errors.js";
10
+ import { saidOf } from "../model-errors.js";
11
11
  import { filmClient } from "../models.js";
12
12
  import { prepareFilmFolder, savedSession, saveSession } from "./project.js";
13
13
  import { runHyperframes } from "./tools.js";
@@ -229,13 +229,13 @@ export async function runFilmStep(step, ctx) {
229
229
  if (ctx.signal.aborted) {
230
230
  throw err;
231
231
  }
232
- throw new StepError(bilingual(`${client.name} hat beim Film aufgehört: ${err.message}. Die Arbeit ist gespeichert. Ein neuer Versuch macht dort weiter.`, `${client.name} stopped working on the film: ${err.message}. The work is saved. A new try carries on from there.`));
232
+ throw new StepError(runNote("filmClientStopped", { name: client.name, detail: err.message }));
233
233
  });
234
234
  const result = last;
235
235
  if (!result || result.is_error) {
236
236
  const detail = String(result?.result ?? (stderr.trim() || "no result")).slice(0, 400);
237
237
  const failure = failureOf(client, detail);
238
- throw new StepError(bilingual(`${errorText(failure, "de")} Die Arbeit ist gespeichert. Ein neuer Versuch macht dort weiter.`, `${errorText(failure, "en")} The work is saved. A new try carries on from there.`));
238
+ throw new StepError(runNote("filmWorkSaved", { problem: saidOf(failure) }));
239
239
  }
240
240
  return result;
241
241
  };
@@ -265,7 +265,7 @@ export async function runFilmStep(step, ctx) {
265
265
  }
266
266
  const problem = renderProblem(render.out);
267
267
  if (attempt >= RENDER_FIXES) {
268
- throw new StepError(bilingual(`Der Film ließ sich nicht rendern: ${problem.slice(0, 400)}`, `The film couldn’t be rendered: ${problem.slice(0, 400)}`));
268
+ throw new StepError(runNote("filmRenderFailed", { detail: problem.slice(0, 400) }));
269
269
  }
270
270
  await ctx.emit("tool", { code: "filmFix" });
271
271
  const fixed = await turn(`The final render of film/ failed:\n\n${problem}\n\nFind the cause in the composition's code, fix it, run \`check\` and a draft render through the hyperframes tool until both pass, and end with one line on what you fixed.`, await savedSession(root));
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { release } from "node:os";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { langOf, SPEECH_LOCALES } from "@engenty-wizards/shared/definition";
5
6
  import { nanoid } from "nanoid";
6
7
  import { env } from "../env.js";
7
8
  import { ffmpeg, hasFfmpeg } from "../media/ffmpeg.js";
@@ -168,7 +169,7 @@ export function appleModel(alias) {
168
169
  /** The Mac's own language: what a voice note is most likely in, unless the call names one. */
169
170
  function systemLocale() {
170
171
  const locale = Intl.DateTimeFormat().resolvedOptions().locale;
171
- return /^[a-z]{2}-[A-Z]{2}$/.test(locale) ? locale : locale.startsWith("de") ? "de-DE" : "en-US";
172
+ return /^[a-z]{2}-[A-Z]{2}$/.test(locale) ? locale : SPEECH_LOCALES[langOf(locale) ?? "en"];
172
173
  }
173
174
  /** What AVAudioFile reads as it is; a browser's WebM or Ogg (Opus) is turned into WAV first. */
174
175
  function audioExtension(mediaType) {
@@ -1,4 +1,4 @@
1
- import { bilingual } from "@engenty-wizards/shared/run";
1
+ import { note } from "@engenty-wizards/shared/run";
2
2
  import { resolveEnv, run } from "./env.js";
3
3
  import { failureOf, HarnessModel, lastJson, runClient, } from "./model.js";
4
4
  /**
@@ -143,5 +143,5 @@ export const claude = {
143
143
  },
144
144
  login: { args: ["auth", "login"], interactive: false },
145
145
  model: (alias, effort) => new ClaudeCodeModel(claude, alias || "sonnet", effort),
146
- exhausted: bilingual("Das Anthropic-Konto, mit dem Claude Code angemeldet ist, hat kein Guthaben. Melde Claude Code unter Einstellungen → Modelle mit deinem Abo an.", "The Anthropic account Claude Code is signed in with has no credit. Sign Claude Code in with your subscription under Settings → Models."),
146
+ exhausted: note("anthropicAccountClaudeCode"),
147
147
  };
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { bilingual } from "@engenty-wizards/shared/run";
4
+ import { note } from "@engenty-wizards/shared/run";
5
5
  import { resolveEnv, run } from "./env.js";
6
6
  import { failureOf, HarnessImageModel, HarnessModel, runClient, } from "./model.js";
7
7
  /**
@@ -225,5 +225,5 @@ export const codex = {
225
225
  login: { args: ["login"], interactive: false },
226
226
  model: (alias, effort) => new CodexModel(codex, alias || "default", effort),
227
227
  image: { alias: "image", model: (alias) => new CodexImageModel(codex, alias || "image") },
228
- exhausted: bilingual("Das Kontingent des ChatGPT-Kontos, mit dem Codex angemeldet ist, ist aufgebraucht. Versuch es später noch einmal oder wähle einen anderen Client.", "The ChatGPT account Codex is signed in with has used up its quota. Try again later or choose another client."),
228
+ exhausted: note("chatgptAccountCodexSigned"),
229
229
  };
@@ -1,17 +1,16 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { bilingual } from "@engenty-wizards/shared/run";
4
+ import { note } from "@engenty-wizards/shared/run";
5
5
  import { nanoid } from "nanoid";
6
6
  import { env } from "../env.js";
7
7
  import { openBridge } from "../mcp/bridge.js";
8
8
  import { ModelUnavailableError } from "../model-errors.js";
9
9
  import { extensionOf, jsonInstruction, parseJsonAnswer, renderPrompt } from "./prompt.js";
10
10
  const workDir = join(env.dataDir, "harness");
11
- export const signedOut = (h) => bilingual(`${h.name} ist auf diesem Computer nicht angemeldet. Melde dich unter Einstellungen → Modelle an.`, `${h.name} isn’t signed in on this computer. Sign in under Settings → Models.`);
12
- export const notInstalled = (h) => bilingual(`${h.name} ist auf diesem Computer nicht installiert. Installiere es unter Einstellungen → Modelle.`, `${h.name} isn’t installed on this computer. Install it under Settings → Models.`);
13
- export const exhausted = (h) => h.exhausted ??
14
- bilingual(`Das Konto, mit dem ${h.name} angemeldet ist, hat kein Guthaben oder Kontingent mehr.`, `The account ${h.name} is signed in with has no credit or quota left.`);
11
+ export const signedOut = (h) => note("clientNotSignedIn", { name: h.name });
12
+ export const notInstalled = (h) => note("clientNotInstalled", { name: h.name });
13
+ export const exhausted = (h) => h.exhausted ?? note("clientNoCredit", { name: h.name });
15
14
  /** The failure a client reports, as the person should read it. */
16
15
  export function failureOf(h, detail) {
17
16
  // The client's own words stay in the log: the sentence below sums them up for the person.
@@ -1,7 +1,7 @@
1
1
  import { accessSync, chmodSync, constants } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname, join } from "node:path";
4
- import { bilingual } from "@engenty-wizards/shared/run";
4
+ import { note } from "@engenty-wizards/shared/run";
5
5
  import { nanoid } from "nanoid";
6
6
  import { ServiceError } from "../services/errors.js";
7
7
  const BUFFER = 200_000;
@@ -37,7 +37,7 @@ async function ptyModule() {
37
37
  return { spawn };
38
38
  }
39
39
  catch {
40
- throw new ServiceError("refused", bilingual("Das eingebaute Terminal ist auf diesem Computer nicht verfügbar. Führe den Befehl in deinem eigenen Terminal aus.", "The built-in terminal isn’t available on this computer. Run the command in your own terminal."));
40
+ throw new ServiceError("refused", note("builtTerminalNotAvailable"));
41
41
  }
42
42
  }
43
43
  function emit(session, event) {
@@ -85,7 +85,7 @@ export async function openTerminal(opts) {
85
85
  });
86
86
  }
87
87
  catch (err) {
88
- throw new ServiceError("refused", bilingual(`Der Befehl konnte nicht starten: ${err.message}`, `The command couldn’t start: ${err.message}`));
88
+ throw new ServiceError("refused", note("commandNotStarted", { detail: err.message }));
89
89
  }
90
90
  const session = {
91
91
  id,
@@ -147,7 +147,7 @@ export async function openTerminal(opts) {
147
147
  function sessionOf(id) {
148
148
  const session = sessions.get(id);
149
149
  if (!session) {
150
- throw new ServiceError("not_found", bilingual("Dieses Terminal gibt es nicht mehr.", "This terminal no longer exists."));
150
+ throw new ServiceError("not_found", note("terminalNoLongerExists"));
151
151
  }
152
152
  return session;
153
153
  }
@@ -1,3 +1,4 @@
1
+ import { LANG_NAMES } from "@engenty-wizards/shared/definition";
1
2
  import { NotedError } from "../model-errors.js";
2
3
  import { conversationAccess } from "../models.js";
3
4
  /**
@@ -10,8 +11,8 @@ const SECRETS_URL = "https://api.openai.com/v1/realtime/client_secrets";
10
11
  const CALLS_URL = "https://api.openai.com/v1/realtime/calls";
11
12
  /** How long a minted secret opens a session; the session itself goes on until it ends. */
12
13
  const SECRET_TTL_SECONDS = 600;
13
- /** The voice the wizard speaks with, by language. */
14
- const VOICES = { de: "marin", en: "marin" };
14
+ /** The voice the wizard speaks with: one that speaks every language engenty does. */
15
+ const VOICE = "marin";
15
16
  /** What the model may do: one tool per command of the run page, run by the browser. */
16
17
  export const CONVERSATION_TOOLS = [
17
18
  {
@@ -97,41 +98,23 @@ export const CONVERSATION_TOOLS = [
97
98
  parameters: { type: "object", properties: {}, additionalProperties: false },
98
99
  },
99
100
  ];
100
- /** The rules the model speaks by, in the wizard's language. */
101
+ /** The rules the model speaks by: written in English, it speaks the wizard's language. */
101
102
  export function conversationInstructions(view) {
102
- const de = view.lang === "de";
103
- const about = [
104
- `${de ? "Du bist" : "You are"} ${view.wizard.title}.`,
105
- view.wizard.description,
106
- view.wizard.intro,
107
- ]
103
+ const about = [`You are ${view.wizard.title}.`, view.wizard.description, view.wizard.intro]
108
104
  .filter(Boolean)
109
105
  .join(" ");
110
- const rules = de
111
- ? [
112
- "Du führst ein Gespräch, das einen Wizard Schritt für Schritt ausfüllt. Die Person sieht den Wizard auch auf dem Bildschirm.",
113
- "Sprich Deutsch, kurz und freundlich, wie am Telefon. Ein Satz, dann die nächste Frage.",
114
- "Frag ein Feld nach dem anderen, in der Reihenfolge des Zustands. Nach jeder Antwort rufst du set_field auf. Sind alle Felder beantwortet, die du per Sprache füllen kannst, rufst du submit_page auf.",
115
- "Bei einer Auswahl nennst du die Möglichkeiten (höchstens vier auf einmal) und gibst genau eine davon weiter.",
116
- "E-Mail-Adressen, Links und Zahlen wiederholst du zur Bestätigung, bevor du sie weitergibst.",
117
- "Ein Feld, das den Bildschirm braucht (ein Foto, eine Datei, eine Unterschrift, eine Verbindung), füllt die Person dort aus: sag das in einem Satz und warte auf den nächsten Zustand.",
118
- "Während der Wizard arbeitet, sagst du in einem Satz, was er gerade tut, und wartest. Erfinde nichts.",
119
- "Bei einer Prüfung liest du den Text kurz zusammengefasst vor und fragst, ob er passt. Dann accept_review, oder regenerate mit dem Wunsch der Person.",
120
- "Am Ende sagst du, was entstanden ist und dass es auf dem Bildschirm liegt.",
121
- "Ist die Kamera an, kannst du mit look sehen, was die Person zeigt, und mit take_photo ein Foto in ein Fotofeld legen; ein Fotofeld fragst du dann so: „Halte es in die Kamera, ich mache das Foto.“ Ist die Kamera aus, bitte die Person, sie einzuschalten, oder lass das Feld dem Bildschirm.",
122
- ]
123
- : [
124
- "You hold a conversation that fills in a wizard step by step. The person also sees the wizard on the screen.",
125
- "Speak English, short and friendly, as on the phone. One sentence, then the next question.",
126
- "Ask one field at a time, in the order of the state. After each answer call set_field. When every field you can fill by voice is answered, call submit_page.",
127
- "For a choice, name the options (at most four at a time) and pass exactly one of them on.",
128
- "Read e-mail addresses, links and numbers back before passing them on.",
129
- "A field that needs the screen (a photo, a file, a signature, a connection) is filled there by the person: say so in one sentence and wait for the next state.",
130
- "While the wizard works, say in one sentence what it is doing and wait. Never make anything up.",
131
- "At a review, sum the text up in a few words and ask whether it fits. Then accept_review, or regenerate with what the person wants changed.",
132
- "At the end, say what was made and that it is on the screen.",
133
- "With the camera on you can see what the person shows with look, and put a photo into a photo field with take_photo; ask for a photo field like this: 'Hold it to the camera, I will take the picture.' With the camera off, ask the person to switch it on, or leave the field to the screen.",
134
- ];
106
+ const rules = [
107
+ "You hold a conversation that fills in a wizard step by step. The person also sees the wizard on the screen.",
108
+ `Speak ${LANG_NAMES[view.lang]} (${view.lang}), short and friendly, as on the phone. One sentence, then the next question.`,
109
+ "Ask one field at a time, in the order of the state. After each answer call set_field. When every field you can fill by voice is answered, call submit_page.",
110
+ "For a choice, name the options (at most four at a time) and pass exactly one of them on.",
111
+ "Read e-mail addresses, links and numbers back before passing them on.",
112
+ "A field that needs the screen (a photo, a file, a signature, a connection) is filled there by the person: say so in one sentence and wait for the next state.",
113
+ "While the wizard works, say in one sentence what it is doing and wait. Never make anything up.",
114
+ "At a review, sum the text up in a few words and ask whether it fits. Then accept_review, or regenerate with what the person wants changed.",
115
+ "At the end, say what was made and that it is on the screen.",
116
+ "With the camera on you can see what the person shows with look, and put a photo into a photo field with take_photo; ask for a photo field like this: 'Hold it to the camera, I will take the picture.' With the camera off, ask the person to switch it on, or leave the field to the screen.",
117
+ ];
135
118
  return `${about}\n\n${rules.join("\n")}`;
136
119
  }
137
120
  /** Mints the secret a browser opens a session with; a failure carries a note for the run's language. */
@@ -157,7 +140,7 @@ export async function mintConversation(view) {
157
140
  noise_reduction: { type: "far_field" },
158
141
  turn_detection: { type: "server_vad", threshold: 0.7, silence_duration_ms: 600 },
159
142
  },
160
- output: { voice: VOICES[view.lang] },
143
+ output: { voice: VOICE },
161
144
  },
162
145
  },
163
146
  }),
@@ -1,5 +1,6 @@
1
1
  import { readFile, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { SPEECH_LOCALES } from "@engenty-wizards/shared/definition";
3
4
  import { generateText, experimental_transcribe as transcribe } from "ai";
4
5
  import { costOf, listenerModel, transcriptionCostUsd } from "../models.js";
5
6
  import { ffmpeg, hasFfmpeg, probeMedia, withTempDir } from "./ffmpeg.js";
@@ -18,7 +19,7 @@ export async function transcribeAudio(input) {
18
19
  audio: input.bytes,
19
20
  abortSignal: input.abortSignal,
20
21
  ...(input.language
21
- ? { providerOptions: { apple: { locale: input.language === "en" ? "en-US" : "de-DE" } } }
22
+ ? { providerOptions: { apple: { locale: SPEECH_LOCALES[input.language] } } }
22
23
  : {}),
23
24
  });
24
25
  return {
@@ -1,6 +1,6 @@
1
1
  import { noteText } from "@engenty-wizards/shared/run";
2
2
  /**
3
- * A failure the person reads, given as a note of `RUN_NOTES` (or `bilingual` words) where it has
3
+ * A failure the person reads, given as a note of `RUN_NOTES` where it has
4
4
  * one: a run shows it in the wizard's language, a response in the asker's, and the message keeps
5
5
  * the English line for the log. Free text has no note.
6
6
  */
@@ -8,7 +8,7 @@ import { createOpenAI } from "@ai-sdk/openai";
8
8
  import { createReplicate } from "@ai-sdk/replicate";
9
9
  import { MODEL_CLASSES, TEXT_CLASSES, } from "@engenty-wizards/shared/definition";
10
10
  import { creditsModelOf, isCreditsRef, PROVIDER_IDS, providerInfo, providerOfRef, } from "@engenty-wizards/shared/providers";
11
- import { bilingual, saidText } from "@engenty-wizards/shared/run";
11
+ import { note, saidText } from "@engenty-wizards/shared/run";
12
12
  import { generateSpeech, generateText, } from "ai";
13
13
  import { accountToken, linkedAccount } from "./auth/account.js";
14
14
  import { DeviceModel, deviceThinks } from "./engine/device.js";
@@ -22,6 +22,7 @@ import { seal, unseal } from "./secrets/crypto.js";
22
22
  import { vaultDelete, vaultGet, vaultSet } from "./secrets/vault.js";
23
23
  import { readSetting, writeSetting } from "./settings.js";
24
24
  import { currentTenant, currentTenantOrNull } from "./tenants/tenant.js";
25
+ import { serverText } from "./texts.js";
25
26
  export { isHarnessVendor } from "./harness/index.js";
26
27
  /**
27
28
  * A step names a model class, never a model. Where the class runs is decided here, in this order:
@@ -234,27 +235,26 @@ function ownRef(cfg, cls) {
234
235
  }
235
236
  /** What a class makes, to say that nothing makes it. */
236
237
  const MAKES = {
237
- classifier: { de: "Text", en: "text" },
238
- standard: { de: "Text", en: "text" },
239
- high: { de: "Text", en: "text" },
240
- highest: { de: "Text", en: "text" },
241
- image: { de: "Bilder", en: "images" },
242
- video: { de: "Videos", en: "videos" },
243
- speech: { de: "Sprachausgabe", en: "speech" },
244
- audio: { de: "Sprachnotizen", en: "voice notes" },
238
+ classifier: "makesText",
239
+ standard: "makesText",
240
+ high: "makesText",
241
+ highest: "makesText",
242
+ image: "makesImages",
243
+ video: "makesVideos",
244
+ speech: "makesSpeech",
245
+ audio: "makesVoiceNotes",
245
246
  };
246
247
  /** Why a class has no way: the provider picked for it lacks its key, or nothing is set up. */
247
248
  function missingText(cfg, cls) {
248
249
  const bound = cfg.settings.bindings[cls];
249
250
  if (isCreditsRef(bound)) {
250
- return bilingual("Dafür sind Credits gewählt, aber du bist mit keinem Konto angemeldet.", "This is set to run on credits, but you’re not signed in to an account.");
251
+ return note("creditsNotSignedIn");
251
252
  }
252
- const makes = MAKES[cls];
253
253
  const provider = bound ? providerInfo(providerOfRef(bound) ?? "") : null;
254
254
  if (provider) {
255
- return bilingual(`Für ${makes.de} ist ${provider.name} gewählt, aber kein API-Key hinterlegt.`, `${provider.name} is chosen for ${makes.en}, but no API key is set.`);
255
+ return note("providerKeyMissing", { provider: provider.name, makes: note(MAKES[cls]) });
256
256
  }
257
- return bilingual(`Für ${makes.de} ist kein Modell eingerichtet.`, `No model is set up for ${makes.en}.`);
257
+ return note("noModelFor", { makes: note(MAKES[cls]) });
258
258
  }
259
259
  /**
260
260
  * The way to the credits. On a Manage-App's runtime always; alone, through the linked account —
@@ -274,7 +274,7 @@ async function creditAccess(cfg, chosen) {
274
274
  const token = await accountToken();
275
275
  if (!token) {
276
276
  if (chosen) {
277
- throw new ModelUnavailableError(bilingual("Das Konto ist nicht mehr angemeldet. Melde dich unter Einstellungen → Konto neu an.", "The account is no longer signed in. Sign in again under Settings → Account."));
277
+ throw new ModelUnavailableError(note("accountNoLongerSigned"));
278
278
  }
279
279
  return null;
280
280
  }
@@ -301,7 +301,7 @@ async function routeOf(cls) {
301
301
  function need(cfg, name) {
302
302
  const key = cfg.keys[name];
303
303
  if (!key) {
304
- throw new ModelUnavailableError(bilingual(`Für ${providerInfo(name)?.name ?? name} ist kein API-Key hinterlegt.`, `No API key is set for ${providerInfo(name)?.name ?? name}.`));
304
+ throw new ModelUnavailableError(note("noApiKeyFor", { provider: providerInfo(name)?.name ?? name }));
305
305
  }
306
306
  return key;
307
307
  }
@@ -412,7 +412,7 @@ async function ownLanguageModel(cfg, ref, meta) {
412
412
  };
413
413
  }
414
414
  }
415
- throw new ModelUnavailableError(bilingual(`Das Modell „${ref}“ lässt sich noch nicht nutzen. Hinterleg unter Einstellungen → Modelle einen API-Key oder melde dich mit einem Konto an.`, `The model “${ref}” can’t be used yet. Add an API key under Settings → Models, or sign in to an account.`));
415
+ throw new ModelUnavailableError(note("modelNotUsedYet", { ref }));
416
416
  }
417
417
  /**
418
418
  * The language model a text class runs on. A run watched from a device that offers the class
@@ -492,7 +492,7 @@ export async function imageModel(meta = {}) {
492
492
  return own(createOpenAI({ apiKey: cfg.keys.openai }).image(aliasOf(id)));
493
493
  }
494
494
  }
495
- throw new ModelUnavailableError(bilingual("Für Bilder ist kein Modell eingerichtet.", "No model is set up for images."));
495
+ throw new ModelUnavailableError(note("noModelForImages"));
496
496
  }
497
497
  export async function videoModel(meta = {}) {
498
498
  const route = await routeOf("video");
@@ -521,7 +521,7 @@ export async function videoModel(meta = {}) {
521
521
  return own(createGateway({ apiKey: cfg.keys.gateway }).videoModel(id), true);
522
522
  }
523
523
  }
524
- throw new ModelUnavailableError(bilingual("Für Videos ist kein Modell eingerichtet.", "No model is set up for videos."));
524
+ throw new ModelUnavailableError(note("noModelForVideos"));
525
525
  }
526
526
  /** The model that reads a text aloud. */
527
527
  export async function speechModel(meta = {}) {
@@ -529,7 +529,7 @@ export async function speechModel(meta = {}) {
529
529
  if (route.kind === "credits") {
530
530
  const resolved = await viaCredits(route, "speech", meta, (c, id) => c.speechModel(id));
531
531
  if (!resolved.ref) {
532
- throw new ModelUnavailableError(bilingual("Für Sprachausgabe ist kein Modell eingerichtet.", "No model is set up for speech."));
532
+ throw new ModelUnavailableError(note("noModelForSpeech"));
533
533
  }
534
534
  return resolved;
535
535
  }
@@ -560,7 +560,7 @@ export async function speechModel(meta = {}) {
560
560
  return own(createOpenAI({ apiKey: cfg.keys.openai }).speech(aliasOf(id)));
561
561
  }
562
562
  }
563
- throw new ModelUnavailableError(bilingual("Für Sprachausgabe ist kein Modell eingerichtet.", "No model is set up for speech."));
563
+ throw new ModelUnavailableError(note("noModelForSpeech"));
564
564
  }
565
565
  /**
566
566
  * What turns a voice note into text: a transcription model (ElevenLabs Scribe, Whisper), or a
@@ -711,7 +711,10 @@ export function freeTierRefusal(err) {
711
711
  }
712
712
  /** What the person reads when Vercel's free tier leaves a model out, and how to fix it. */
713
713
  export function freeTierText(model) {
714
- return bilingual(`Dein AI-Gateway-Key nutzt bei Vercel die kostenlose Stufe, und die schließt ${model ? `„${model}“` : "dieses Modell"} aus. Lade bei Vercel Guthaben auf (${GATEWAY_TOP_UP}) oder wähle unter Einstellungen → Modelle ein anderes Modell.`, `Your AI Gateway key is on Vercel’s free tier, which doesn’t include ${model ? `“${model}”` : "this model"}. Top up at Vercel (${GATEWAY_TOP_UP}) or choose another model under Settings → Models.`);
714
+ return note("freeTier", {
715
+ model: model ? note("quoted", { text: model }) : note("thisModel"),
716
+ url: GATEWAY_TOP_UP,
717
+ });
715
718
  }
716
719
  /** What a key was answered for a model: refusals are asked again soon, as a top-up lifts them. */
717
720
  const gatewayProbes = new Map();
@@ -765,7 +768,7 @@ const CONVERSATION_MODEL = env.conversationModel || "gpt-realtime";
765
768
  export async function conversationAccess() {
766
769
  const cfg = await config();
767
770
  if (!cfg.keys.openai) {
768
- throw new ModelUnavailableError(bilingual("Für Sprachgespräche ist kein OpenAI-API-Key hinterlegt (Einstellungen → Modelle).", "No OpenAI API key is set for voice conversations (Settings → Models)."));
771
+ throw new ModelUnavailableError(note("noOpenaiApiKey"));
769
772
  }
770
773
  return { apiKey: cfg.keys.openai, model: CONVERSATION_MODEL };
771
774
  }
@@ -802,12 +805,10 @@ export async function classProblem(cls) {
802
805
  const named = creditsModelOf((await config()).settings.bindings[cls]);
803
806
  if (named) {
804
807
  return catalog?.models && !catalog.models.some((m) => m.id === named)
805
- ? bilingual("Dieses Modell gibt es mit deinem Konto nicht.", "This model isn’t available with your account.")
808
+ ? note("modelNotAvailableAccount")
806
809
  : null;
807
810
  }
808
- return catalog?.classes && !catalog.classes[cls]
809
- ? bilingual("Dafür ist bei deinem Konto kein Modell eingerichtet.", "Your account has no model set up for this.")
810
- : null;
811
+ return catalog?.classes && !catalog.classes[cls] ? note("accountNoModelSet") : null;
811
812
  }
812
813
  const client = harness(resolved.vendor);
813
814
  if (client && (await detectHarness(client.id))?.auth === "none") {
@@ -833,7 +834,7 @@ export async function filmClient(cls) {
833
834
  const client = managed ? null : harness(cfg.settings.source);
834
835
  if (!client || !FILM_CLIENTS.has(client.id)) {
835
836
  return {
836
- problem: bilingual("Filme macht Claude Code auf diesem Computer mit deinem Abo. Richte es unter Einstellungen → Modelle als KI-Abo ein.", "Claude Code makes films on this computer, using your subscription. Set it up as an AI subscription under Settings → Models."),
837
+ problem: note("claudeCodeMakesFilms"),
837
838
  };
838
839
  }
839
840
  const status = await detectHarness(client.id);
@@ -918,9 +919,7 @@ async function appleProblem(cls) {
918
919
  if (!status.supported || status.reason === "missing") {
919
920
  return appleUnavailableNote(status);
920
921
  }
921
- return (await hasFfmpeg())
922
- ? null
923
- : bilingual("Aufnahmen aus Chrome oder Firefox (WebM, Ogg) schreibt dieser Mac erst mit ffmpeg auf. Safari und die App brauchen es nicht. Installiere ffmpeg, z. B. mit „brew install ffmpeg“.", "This Mac needs ffmpeg to transcribe recordings from Chrome or Firefox (WebM, Ogg). Safari and the app don’t. Install ffmpeg, e.g. with “brew install ffmpeg”.");
922
+ return (await hasFfmpeg()) ? null : note("macNeedsFfmpegTranscribe");
924
923
  }
925
924
  /** At start the ways are asked once, so a model the key cannot reach shows before a run meets it. */
926
925
  export async function checkModelsAtStart() {
@@ -958,7 +957,6 @@ export async function modelSettings(lang = "en") {
958
957
  // --- Checking a key ------------------------------------------------------------------
959
958
  /** A cheap authenticated call: whether the provider takes the key. Nothing is generated. */
960
959
  export async function checkKey(name, key, lang = "en") {
961
- const de = lang === "de";
962
960
  const requests = {
963
961
  openai: ["https://api.openai.com/v1/models", { authorization: `Bearer ${key}` }],
964
962
  anthropic: [
@@ -991,20 +989,14 @@ export async function checkKey(name, key, lang = "en") {
991
989
  return {
992
990
  ok: false,
993
991
  message: res.status === 401
994
- ? de
995
- ? "Der Anbieter kennt diesen API-Key nicht."
996
- : "The provider doesn’t recognise this key."
997
- : de
998
- ? `Der Anbieter antwortet mit Fehler ${res.status}.`
999
- : `The provider answered with error ${res.status}.`,
992
+ ? serverText(lang, "keyUnknown")
993
+ : serverText(lang, "providerError", { status: String(res.status) }),
1000
994
  };
1001
995
  }
1002
996
  catch {
1003
997
  return {
1004
998
  ok: false,
1005
- message: de
1006
- ? "Der Anbieter ist gerade nicht erreichbar."
1007
- : "The provider cannot be reached right now.",
999
+ message: serverText(lang, "providerUnreachable"),
1008
1000
  };
1009
1001
  }
1010
1002
  }