@luxalgo/vela 0.8.5 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/{DataProvider-BUdlkx9I.d.ts → DataProvider-BGlB1zSc.d.ts} +1 -1
  2. package/dist/{DataProvider-DdqMxKdl.d.cts → DataProvider-zJGp58kb.d.cts} +1 -1
  3. package/dist/{chunk-BN4DNS3O.js → chunk-4WAHYL7Y.js} +1125 -468
  4. package/dist/chunk-O6L27KRB.js +1331 -0
  5. package/dist/{chunk-WY5H4S4I.js → chunk-PZN4RS4T.js} +91 -36
  6. package/dist/{chunk-AUOB7A5Y.js → chunk-WHPQQYH7.js} +29 -13
  7. package/dist/{chunk-QYIO37K7.js → chunk-XIRSR3D6.js} +2374 -1529
  8. package/dist/{chunk-ALTGHKIM.js → chunk-YRCL3WMD.js} +510 -406
  9. package/dist/{contributions-D8xuYc9U.d.cts → contributions-BjefWpxQ.d.ts} +71 -22
  10. package/dist/{contributions-P9fBBuY1.d.ts → contributions-CpCJisTC.d.cts} +71 -22
  11. package/dist/index.cjs +4022 -2088
  12. package/dist/index.d.cts +51 -25
  13. package/dist/index.d.ts +51 -25
  14. package/dist/index.js +5 -5
  15. package/dist/{keymap-CGOz5F5f.d.ts → keymap-B9_FLhfJ.d.cts} +9 -6
  16. package/dist/{keymap-CGOz5F5f.d.cts → keymap-UolhuXH1.d.ts} +9 -6
  17. package/dist/locales/ar.cjs +2260 -0
  18. package/dist/locales/ar.d.cts +5 -0
  19. package/dist/locales/ar.d.ts +5 -0
  20. package/dist/locales/ar.js +2258 -0
  21. package/dist/locales/de.cjs +2260 -0
  22. package/dist/locales/de.d.cts +5 -0
  23. package/dist/locales/de.d.ts +5 -0
  24. package/dist/locales/de.js +2258 -0
  25. package/dist/locales/en.cjs +2260 -0
  26. package/dist/locales/en.d.cts +5 -0
  27. package/dist/locales/en.d.ts +5 -0
  28. package/dist/locales/en.js +2258 -0
  29. package/dist/locales/es.cjs +2260 -0
  30. package/dist/locales/es.d.cts +5 -0
  31. package/dist/locales/es.d.ts +5 -0
  32. package/dist/locales/es.js +2258 -0
  33. package/dist/locales/fr.cjs +2260 -0
  34. package/dist/locales/fr.d.cts +5 -0
  35. package/dist/locales/fr.d.ts +5 -0
  36. package/dist/locales/fr.js +2258 -0
  37. package/dist/locales/he.cjs +2260 -0
  38. package/dist/locales/he.d.cts +5 -0
  39. package/dist/locales/he.d.ts +5 -0
  40. package/dist/locales/he.js +2258 -0
  41. package/dist/locales/hi.cjs +2260 -0
  42. package/dist/locales/hi.d.cts +5 -0
  43. package/dist/locales/hi.d.ts +5 -0
  44. package/dist/locales/hi.js +2258 -0
  45. package/dist/locales/id.cjs +2260 -0
  46. package/dist/locales/id.d.cts +5 -0
  47. package/dist/locales/id.d.ts +5 -0
  48. package/dist/locales/id.js +2258 -0
  49. package/dist/locales/it.cjs +2260 -0
  50. package/dist/locales/it.d.cts +5 -0
  51. package/dist/locales/it.d.ts +5 -0
  52. package/dist/locales/it.js +2258 -0
  53. package/dist/locales/ja.cjs +2260 -0
  54. package/dist/locales/ja.d.cts +5 -0
  55. package/dist/locales/ja.d.ts +5 -0
  56. package/dist/locales/ja.js +2258 -0
  57. package/dist/locales/ko.cjs +2260 -0
  58. package/dist/locales/ko.d.cts +5 -0
  59. package/dist/locales/ko.d.ts +5 -0
  60. package/dist/locales/ko.js +2258 -0
  61. package/dist/locales/ms.cjs +2260 -0
  62. package/dist/locales/ms.d.cts +5 -0
  63. package/dist/locales/ms.d.ts +5 -0
  64. package/dist/locales/ms.js +2258 -0
  65. package/dist/locales/pl.cjs +2260 -0
  66. package/dist/locales/pl.d.cts +5 -0
  67. package/dist/locales/pl.d.ts +5 -0
  68. package/dist/locales/pl.js +2258 -0
  69. package/dist/locales/pt-BR.cjs +2260 -0
  70. package/dist/locales/pt-BR.d.cts +5 -0
  71. package/dist/locales/pt-BR.d.ts +5 -0
  72. package/dist/locales/pt-BR.js +2258 -0
  73. package/dist/locales/ru.cjs +2260 -0
  74. package/dist/locales/ru.d.cts +5 -0
  75. package/dist/locales/ru.d.ts +5 -0
  76. package/dist/locales/ru.js +2258 -0
  77. package/dist/locales/th.cjs +2260 -0
  78. package/dist/locales/th.d.cts +5 -0
  79. package/dist/locales/th.d.ts +5 -0
  80. package/dist/locales/th.js +2258 -0
  81. package/dist/locales/tr.cjs +2260 -0
  82. package/dist/locales/tr.d.cts +5 -0
  83. package/dist/locales/tr.d.ts +5 -0
  84. package/dist/locales/tr.js +2258 -0
  85. package/dist/locales/vi.cjs +2260 -0
  86. package/dist/locales/vi.d.cts +5 -0
  87. package/dist/locales/vi.d.ts +5 -0
  88. package/dist/locales/vi.js +2258 -0
  89. package/dist/locales/zh-Hans.cjs +2260 -0
  90. package/dist/locales/zh-Hans.d.cts +5 -0
  91. package/dist/locales/zh-Hans.d.ts +5 -0
  92. package/dist/locales/zh-Hans.js +2258 -0
  93. package/dist/locales/zh-Hant.cjs +2260 -0
  94. package/dist/locales/zh-Hant.d.cts +5 -0
  95. package/dist/locales/zh-Hant.d.ts +5 -0
  96. package/dist/locales/zh-Hant.js +2258 -0
  97. package/dist/{options-BI5Tp32b.d.cts → options-CpegHml_.d.cts} +74 -9
  98. package/dist/{options-BI5Tp32b.d.ts → options-D4L3_hv_.d.ts} +74 -9
  99. package/dist/{plugin-D5Z-Q6TX.d.ts → plugin-DTmulpKM.d.ts} +81 -35
  100. package/dist/{plugin-B8F1nor8.d.cts → plugin-DpNp4aBi.d.cts} +81 -35
  101. package/dist/plugin.cjs +1267 -319
  102. package/dist/plugin.d.cts +7 -6
  103. package/dist/plugin.d.ts +7 -6
  104. package/dist/plugin.js +3 -3
  105. package/dist/providers/binance.cjs +1 -1
  106. package/dist/providers/binance.d.cts +3 -2
  107. package/dist/providers/binance.d.ts +3 -2
  108. package/dist/providers/binance.js +1 -1
  109. package/dist/providers/coinbase.cjs +1 -1
  110. package/dist/providers/coinbase.d.cts +3 -2
  111. package/dist/providers/coinbase.d.ts +3 -2
  112. package/dist/providers/coinbase.js +1 -1
  113. package/dist/providers/hyperliquid.cjs +1 -1
  114. package/dist/providers/hyperliquid.d.cts +3 -2
  115. package/dist/providers/hyperliquid.d.ts +3 -2
  116. package/dist/providers/hyperliquid.js +1 -1
  117. package/dist/{side-panel-_2UK1gSm.d.ts → side-panel-BqUnpfev.d.cts} +13 -2
  118. package/dist/{side-panel-_2UK1gSm.d.cts → side-panel-CYmH1Opm.d.ts} +13 -2
  119. package/dist/{statusline-model-CTwtDIca.d.cts → statusline-model-C3yIY9WE.d.ts} +11 -3
  120. package/dist/{statusline-model-D9kWC9X5.d.ts → statusline-model-pGs3Xvp-.d.cts} +11 -3
  121. package/dist/types-BlrXRLIR.d.cts +2515 -0
  122. package/dist/types-BlrXRLIR.d.ts +2515 -0
  123. package/dist/ui.cjs +1008 -56
  124. package/dist/ui.d.cts +21 -2
  125. package/dist/ui.d.ts +21 -2
  126. package/dist/ui.js +3 -3
  127. package/dist/vela.global.js +3987 -2055
  128. package/dist/vela.global.min.js +57 -57
  129. package/dist/widget.cjs +5330 -2699
  130. package/dist/widget.d.cts +100 -26
  131. package/dist/widget.d.ts +100 -26
  132. package/dist/widget.js +19 -11
  133. package/dist/workspace.cjs +5319 -2706
  134. package/dist/workspace.d.cts +63 -15
  135. package/dist/workspace.d.ts +63 -15
  136. package/dist/workspace.js +6 -6
  137. package/package.json +9 -2
  138. package/dist/chunk-I2U4NZD2.js +0 -340
  139. /package/dist/{chunk-W5YPYZ2P.js → chunk-3EYCX2L4.js} +0 -0
@@ -1,8 +1,8 @@
1
- import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, SecondClock, isGroupRow, defaultMemberOf, groupKeyOf, groupMembers, LEGEND_AT_TOP_ATTR, normalizeSession, Vela, TypedEventBus, timeframeToMs, MultiProviderFeed, registerBuiltinChartTypes, resolveTheme, buildToolbar, createCustomMark, createAttributionMark, DrawingToolbar, applyAttributionMarkTheme, sharedBarStore, CANDLE_OVERRIDE_KEYS } from './chunk-QYIO37K7.js';
2
- import { chartType, resolveTopbarComposition, topbarActionOverride, TOPBAR_BUILTIN_IDS, widgetActions, tzButtonLabel, resolveTimezone, timezoneMenuRows, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, symbolRanking, paneScaleAt, composeMenu, priceAxisItems, scaleChoiceOf, timeAxisItems, bodyItems, scaleWrites, invertWrite, settingsSectionOf, resolveEngines, legendActionsProviderFor, legendCalloutsProviderFor, normalizeTimezone, isExchangeTimezone, statePersistenceHandlers, topbarHas, rendererDefaults, mobilePlacement, widgetAttachments, getDrawingType, inputDeltas } from './chunk-ALTGHKIM.js';
3
- import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-AUOB7A5Y.js';
4
- import { Menu, Dialog, CalloutBubble, applyPlotOverlayTokens, ensureUIHost } from './chunk-WY5H4S4I.js';
5
- import { registerIcon, svg16, injectStyles, iconEl, iconMarkup, iconAt, SESSION_OFF, SESSION_POST, SESSION_PRE, announceSurface, holdForExit, icon, categoricalColor } from './chunk-I2U4NZD2.js';
1
+ import { priceStyleMessage, parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, SecondClock, isGroupRow, defaultMemberOf, groupKeyOf, groupMembers, LEGEND_AT_TOP_ATTR, normalizeSession, Vela, TypedEventBus, timeframeToMs, MultiProviderFeed, registerBuiltinChartTypes, resolveTheme, buildToolbar, createCustomMark, createAttributionMark, DrawingToolbar, relabelAttributionMark, applyAttributionMarkTheme, sharedBarStore, CANDLE_OVERRIDE_KEYS } from './chunk-XIRSR3D6.js';
2
+ import { resolveTopbarComposition, topbarActionOverride, TOPBAR_BUILTIN_IDS, widgetActions, tzButtonLabel, resolveTimezone, timezoneMenuRows, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, symbolRanking, paneScaleAt, composeMenu, priceAxisItems, scaleChoiceOf, timeAxisItems, bodyItems, scaleWrites, invertWrite, settingsSectionOf, resolveEngines, legendActionsProviderFor, legendCalloutsProviderFor, normalizeTimezone, isExchangeTimezone, statePersistenceHandlers, topbarHas, rendererDefaults, mobilePlacement, widgetAttachments, chartType, getDrawingType, nativeIndicatorEntry, inputDeltas } from './chunk-YRCL3WMD.js';
3
+ import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-WHPQQYH7.js';
4
+ import { Menu, Dialog, CalloutBubble, applyPlotOverlayTokens, ensureUIHost } from './chunk-PZN4RS4T.js';
5
+ import { msg, sourceText, registerIcon, svg16, textOf, localizeNumberText, injectStyles, iconEl, iconMarkup, englishTranslator, sliceGraphemes, iconAt, SESSION_OFF, SESSION_POST, SESSION_PRE, LocaleState, pickLocalization, eventTimeText, announceSurface, holdForExit, icon, withValues, categoricalColor } from './chunk-O6L27KRB.js';
6
6
  import { baseOf } from './chunk-W4EJWLEO.js';
7
7
 
8
8
  // src/widget/timeframe.ts
@@ -16,15 +16,26 @@ var UNIT_MS = {
16
16
  Y: 31536e6
17
17
  };
18
18
  var UNIT_NAME = {
19
- S: "second",
20
- MIN: "minute",
21
- H: "hour",
22
- D: "day",
23
- W: "week",
24
- M: "month",
25
- Y: "year"
19
+ S: msg("widget.timeframe.seconds", "{count, plural, one {{n} second} other {{n} seconds}}"),
20
+ MIN: msg("widget.timeframe.minutes", "{count, plural, one {{n} minute} other {{n} minutes}}"),
21
+ H: msg("widget.timeframe.hours", "{count, plural, one {{n} hour} other {{n} hours}}"),
22
+ D: msg("widget.timeframe.days", "{count, plural, one {{n} day} other {{n} days}}"),
23
+ W: msg("widget.timeframe.weeks", "{count, plural, one {{n} week} other {{n} weeks}}"),
24
+ M: msg("widget.timeframe.months", "{count, plural, one {{n} month} other {{n} months}}"),
25
+ Y: msg("widget.timeframe.years", "{count, plural, one {{n} year} other {{n} years}}")
26
26
  };
27
- var UNIT_SHORT = { S: "s", MIN: "m", H: "h", D: "D", W: "W", M: "M", Y: "Y" };
27
+ var UNIT_SHORT = {
28
+ S: msg("widget.timeframe.short.seconds", "{n}s"),
29
+ MIN: msg("widget.timeframe.short.minutes", "{n}m"),
30
+ H: msg("widget.timeframe.short.hours", "{n}h"),
31
+ D: msg("widget.timeframe.short.days", "{n}D"),
32
+ W: msg("widget.timeframe.short.weeks", "{n}W"),
33
+ M: msg("widget.timeframe.short.months", "{n}M"),
34
+ Y: msg("widget.timeframe.short.years", "{n}Y")
35
+ };
36
+ function shortLabel(key, count, i18n) {
37
+ return textOf(i18n, UNIT_SHORT[key], { n: localizeNumberText(String(count), i18n) });
38
+ }
28
39
  function unitKey(letter) {
29
40
  if (letter === "") return "MIN";
30
41
  return letter === "S" || letter === "H" || letter === "D" || letter === "W" || letter === "M" || letter === "Y" ? letter : null;
@@ -41,7 +52,7 @@ function canonicalFor(key, count) {
41
52
  if (key === "S") return `${count}S`;
42
53
  return `${count * UNIT_MINUTES[key]}`;
43
54
  }
44
- function parseTimeframe(text) {
55
+ function parseTimeframe(text, i18n) {
45
56
  const raw = String(text ?? "").trim();
46
57
  if (!raw) return { valid: false };
47
58
  const m = /^(\d*)\s*([a-zA-Z]?)$/.exec(raw);
@@ -53,15 +64,14 @@ function parseTimeframe(text) {
53
64
  const count = numStr === "" ? 1 : parseInt(numStr, 10);
54
65
  if (!Number.isFinite(count) || count < 1) return { valid: false };
55
66
  const ms = count * UNIT_MS[key];
56
- const name = UNIT_NAME[key];
57
67
  return {
58
68
  valid: true,
59
69
  count,
60
70
  unit: key,
61
71
  ms,
62
72
  canonical: canonicalFor(key, count),
63
- label: `${count} ${name}${count === 1 ? "" : "s"}`,
64
- short: `${count}${UNIT_SHORT[key]}`
73
+ label: textOf(i18n, UNIT_NAME[key], { count, n: localizeNumberText(String(count), i18n) }),
74
+ short: shortLabel(key, count, i18n)
65
75
  };
66
76
  }
67
77
  function timeframeMs(value) {
@@ -94,20 +104,26 @@ function favoriteTimeframeChips(favorites) {
94
104
  return 0;
95
105
  });
96
106
  }
97
- function timeframeLabel(value) {
98
- const parsed = parseTimeframe(value);
107
+ function timeframeLabel(value, i18n) {
108
+ const parsed = parseTimeframe(value, i18n);
99
109
  if (!parsed.valid) return value;
100
110
  const ms = parsed.ms;
101
111
  const order = ["Y", "M", "W", "D", "H", "MIN", "S"];
102
112
  for (const key of order) {
103
113
  if (ms % UNIT_MS[key] === 0 && ms / UNIT_MS[key] >= 1) {
104
- return `${ms / UNIT_MS[key]}${UNIT_SHORT[key]}`;
114
+ return shortLabel(key, ms / UNIT_MS[key], i18n);
105
115
  }
106
116
  }
107
117
  return parsed.short;
108
118
  }
109
119
 
110
120
  // src/widget/layout-picker.ts
121
+ var TEXT = {
122
+ layout: msg("widget.layoutPicker.layout", "Layout"),
123
+ sync: msg("widget.layoutPicker.sync", "Sync"),
124
+ gridHint: msg("widget.layoutPicker.gridHint", "Click a square to apply that columns × rows layout."),
125
+ gridSize: msg("widget.layoutPicker.gridSize", "{cols} × {rows}")
126
+ };
111
127
  var STYLE_ID = "vela-widget-layout-picker-v15";
112
128
  var CSS = `
113
129
  .vela-lp-layer { position: absolute; z-index: var(--vela-z-menu); }
@@ -172,7 +188,7 @@ var CSS = `
172
188
  transition: background var(--vela-dur-fast) var(--vela-ease), border-color var(--vela-dur-fast) var(--vela-ease);
173
189
  }
174
190
  .vela-lp-sq:hover { background: var(--vela-hover-strong); border-color: var(--vela-border-strong); }
175
- /* Selected/previewed squares \u2014 the shared monochrome selection chip. */
191
+ /* Selected/previewed squares — the shared monochrome selection chip. */
176
192
  .vela-lp-sq[data-on='1'] {
177
193
  background: var(--vela-selected-bg);
178
194
  border-color: var(--vela-selected-bg);
@@ -186,7 +202,7 @@ var CSS = `
186
202
  .vela-lp-sync-row { all: unset; display: flex; align-items: center; gap: 14px; padding: 6px 8px; border-radius: 5px; cursor: pointer; }
187
203
  .vela-lp-sync-row:hover { background: var(--vela-hover); }
188
204
  .vela-lp-sync-row .vela-lp-label { flex: 1 1 auto; }
189
- /* Toggle pill \u2014 same control language as the menu's switch rows, monochrome ON state. */
205
+ /* Toggle pill — same control language as the menu's switch rows, monochrome ON state. */
190
206
  .vela-lp-switch {
191
207
  position: relative;
192
208
  flex: none;
@@ -212,7 +228,10 @@ var CSS = `
212
228
  .vela-lp-switch.on::after { transform: translateX(16px); background: var(--vela-selected-fg); }
213
229
  `;
214
230
  var GRID = 4;
215
- function layoutGridCanvas(doc) {
231
+ function layoutGridLabel(cols, rows, i18n) {
232
+ return textOf(i18n, TEXT.gridSize, { cols, rows });
233
+ }
234
+ function layoutGridCanvas(doc, i18n) {
216
235
  injectStyles(STYLE_ID, CSS, doc);
217
236
  const el = doc.createElement("div");
218
237
  el.className = "vela-lp-grid";
@@ -223,7 +242,7 @@ function layoutGridCanvas(doc) {
223
242
  sq.className = "vela-lp-sq";
224
243
  sq.dataset.r = String(r);
225
244
  sq.dataset.c = String(c);
226
- sq.setAttribute("aria-label", `${c + 1} \xD7 ${r + 1}`);
245
+ sq.setAttribute("aria-label", textOf(i18n, TEXT.gridSize, { cols: c + 1, rows: r + 1 }));
227
246
  squares.push(sq);
228
247
  el.appendChild(sq);
229
248
  }
@@ -240,6 +259,8 @@ function paintLayoutGrid(squares, shape) {
240
259
  var LayoutPicker = class {
241
260
  constructor(opts) {
242
261
  this.squares = [];
262
+ /** Fixed text re-read in the current language on every open. */
263
+ this.fixedText = [];
243
264
  this.isOpen = false;
244
265
  /** The card's exit animation after a close; a reopen cancels it. */
245
266
  this.exit = null;
@@ -272,7 +293,8 @@ var LayoutPicker = class {
272
293
  const layoutHeading = doc.createElement("div");
273
294
  layoutHeading.className = "vela-lp-heading";
274
295
  const headingText = doc.createElement("span");
275
- headingText.textContent = "Layout";
296
+ headingText.textContent = textOf(opts.i18n, TEXT.layout);
297
+ this.fixedText.push({ el: headingText, text: TEXT.layout });
276
298
  const badge = doc.createElement("span");
277
299
  badge.className = "vela-lp-badge";
278
300
  badge.textContent = "?";
@@ -283,7 +305,7 @@ var LayoutPicker = class {
283
305
  placement: "bottom",
284
306
  content: () => this.tipNode()
285
307
  });
286
- const { el: grid, squares } = layoutGridCanvas(doc);
308
+ const { el: grid, squares } = layoutGridCanvas(doc, opts.i18n);
287
309
  this.squares.push(...squares);
288
310
  layoutCol.appendChild(grid);
289
311
  this.presetsEl = doc.createElement("div");
@@ -296,7 +318,8 @@ var LayoutPicker = class {
296
318
  const syncCol = doc.createElement("div");
297
319
  const syncHeading = doc.createElement("div");
298
320
  syncHeading.className = "vela-lp-heading";
299
- syncHeading.textContent = "Sync";
321
+ syncHeading.textContent = textOf(opts.i18n, TEXT.sync);
322
+ this.fixedText.push({ el: syncHeading, text: TEXT.sync });
300
323
  syncCol.appendChild(syncHeading);
301
324
  this.syncEl = doc.createElement("div");
302
325
  this.syncEl.className = "vela-lp-sync";
@@ -340,6 +363,7 @@ var LayoutPicker = class {
340
363
  this.exit?.cancel();
341
364
  this.exit = null;
342
365
  this.hover = null;
366
+ this.relabel();
343
367
  this.refresh();
344
368
  this.layer.style.display = "";
345
369
  this.position();
@@ -375,6 +399,16 @@ var LayoutPicker = class {
375
399
  this.infoTip.destroy();
376
400
  this.layer.remove();
377
401
  }
402
+ /** Fixed text and square names in the current language. */
403
+ relabel() {
404
+ const i18n = this.opts.i18n;
405
+ for (const { el, text } of this.fixedText) el.textContent = textOf(i18n, text);
406
+ this.infoTip.setContent(() => this.tipNode());
407
+ for (const sq of this.squares) {
408
+ const { r, c } = this.squarePos(sq);
409
+ sq.setAttribute("aria-label", textOf(i18n, TEXT.gridSize, { cols: c + 1, rows: r + 1 }));
410
+ }
411
+ }
378
412
  // ── internals ──
379
413
  squareAt(e) {
380
414
  const sq = e.target?.closest?.(".vela-lp-sq");
@@ -387,7 +421,7 @@ var LayoutPicker = class {
387
421
  tipNode() {
388
422
  const tip = this.doc.createElement("div");
389
423
  tip.className = "vela-lp-tip";
390
- tip.textContent = "Click a square to apply that columns \xD7 rows layout.";
424
+ tip.textContent = textOf(this.opts.i18n, TEXT.gridHint);
391
425
  return tip;
392
426
  }
393
427
  /** Project the interaction state onto the DOM (the hover/current rectangle). */
@@ -504,7 +538,7 @@ var CSS2 = `
504
538
  color: var(--vela-fg-muted);
505
539
  }
506
540
  .vela-widget-tf-caret:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
507
- /* The merged trigger is a plain button (hover feedback only) \u2014 the highlight
541
+ /* The merged trigger is a plain button (hover feedback only) — the highlight
508
542
  background marks the CURRENT chip among favorites, and a lone trigger with a
509
543
  permanent highlight would read as stuck-pressed. */
510
544
  .vela-widget-tf-caret[data-solo='1'] {
@@ -518,8 +552,8 @@ var CSS2 = `
518
552
  }
519
553
  .vela-widget-topbar .vela-widget-tf-caret .vela-icon { font-size: 14px; width: 14px; height: 14px; }
520
554
  .vela-widget-topbar .vela-icon { color: inherit; font-size: 16px; width: 16px; height: 16px; }
521
- /* Width is set in syncHairlines() to exactly one device pixel \u2014 a CSS 1px at
522
- fractional DPR (1.25, 1.5\u2026) straddles two physical pixels and siblings end
555
+ /* Width is set in syncHairlines() to exactly one device pixel — a CSS 1px at
556
+ fractional DPR (1.25, 1.5…) straddles two physical pixels and siblings end
523
557
  up looking like different thicknesses depending on subpixel placement. */
524
558
  .vela-sep { height: 22px; margin: 0 2px; flex: none; background: var(--vela-border-strong); }
525
559
  .vela-alerts-badge {
@@ -538,15 +572,15 @@ var CSS2 = `
538
572
  align-items: center;
539
573
  justify-content: center;
540
574
  }
541
- /* The right side of the bar \u2014 whatever the composition puts there rides this one
575
+ /* The right side of the bar — whatever the composition puts there rides this one
542
576
  auto-margin push (the flow-actions host used to carry it; composition can omit it). */
543
577
  .vela-topbar-right { margin-left: auto; display: inline-flex; align-items: center; gap: var(--vela-space-1); }
544
578
  .vela-widget-actions { display: inline-flex; gap: var(--vela-space-1); }
545
- /* Left-aligned contributed actions \u2014 the primary-chrome cluster after the dropdowns. */
579
+ /* Left-aligned contributed actions — the primary-chrome cluster after the dropdowns. */
546
580
  .vela-widget-actions-left { display: inline-flex; align-items: center; gap: var(--vela-space-1); }
547
581
  /* One PINNED contributed action's slot (a composition entry naming the action's id). */
548
582
  .vela-widget-action-pin { display: inline-flex; align-items: center; }
549
- /* The side-panel toggles, one per docked panel \u2014 a group so the dock can rebuild them
583
+ /* The side-panel toggles, one per docked panel — a group so the dock can rebuild them
550
584
  without disturbing the tools around it. */
551
585
  .vela-widget-panels { display: inline-flex; align-items: center; gap: var(--vela-space-1); }
552
586
  .vela-widget-tool {
@@ -576,15 +610,8 @@ var CSS2 = `
576
610
  }
577
611
  .vela-widget-action:hover { background: var(--vela-hover); }
578
612
  `;
579
- var BUILTIN_STYLE_LABELS = {
580
- candles: "Candles",
581
- bars: "Bars",
582
- line: "Line",
583
- area: "Area",
584
- baseline: "Baseline"
585
- };
586
- function priceStyleLabel(id) {
587
- return chartType(id)?.label ?? BUILTIN_STYLE_LABELS[id] ?? id;
613
+ function priceStyleLabel(id, i18n) {
614
+ return textOf(i18n, priceStyleMessage(id));
588
615
  }
589
616
  function priceStyleIcon(id) {
590
617
  const iconId = `style-${id}`;
@@ -596,6 +623,21 @@ function priceStyleIcon(id) {
596
623
  }
597
624
  return void 0;
598
625
  }
626
+ var TEXT2 = {
627
+ timeframes: msg("widget.topbar.timeframes", "Timeframes"),
628
+ indicators: msg("widget.topbar.indicators", "Indicators"),
629
+ undo: msg("widget.topbar.undo", "Undo"),
630
+ redo: msg("widget.topbar.redo", "Redo"),
631
+ screenshot: msg("widget.topbar.screenshot", "Download screenshot"),
632
+ alerts: msg("widget.topbar.alerts", "Alerts"),
633
+ timeframe: msg("widget.topbar.timeframe", "Timeframe"),
634
+ chartStyle: msg("widget.topbar.chartStyle", "Chart style"),
635
+ layout: msg("widget.topbar.layout", "Layout"),
636
+ switchTimeframe: msg("widget.topbar.switchTimeframe", "Switch timeframe to {timeframe}"),
637
+ currentTimeframe: msg("widget.topbar.currentTimeframe", "Timeframe — {timeframe}"),
638
+ currentLayout: msg("widget.topbar.currentLayout", "Layout — {layout}"),
639
+ currentStyle: msg("widget.topbar.currentStyle", "Chart style — {style}")
640
+ };
599
641
  var Topbar = class {
600
642
  constructor(host, opts) {
601
643
  this.layoutButton = null;
@@ -625,6 +667,8 @@ var Topbar = class {
625
667
  };
626
668
  this.hairlineRo = null;
627
669
  this.hairlineRaf = 0;
670
+ /** Re-apply each fixed label in the current language (see {@link relocalize}). */
671
+ this.relabels = [];
628
672
  this.opts = opts;
629
673
  this.host = host;
630
674
  this.timeframe = opts.timeframe;
@@ -645,7 +689,7 @@ var Topbar = class {
645
689
  this.tfCaret = doc.createElement("button");
646
690
  this.tfCaret.className = "vela-widget-tf-caret";
647
691
  this.tfCaret.appendChild(iconEl("chevron-down", doc));
648
- this.tfCaret.setAttribute("aria-label", "Timeframes");
692
+ this.tfCaret.setAttribute("aria-label", this.t(TEXT2.timeframes));
649
693
  const tfGroup = doc.createElement("span");
650
694
  tfGroup.className = "vela-widget-tf-group";
651
695
  tfGroup.append(this.tfChipsHost, this.tfCaret);
@@ -656,7 +700,9 @@ var Topbar = class {
656
700
  if (opts.onIndicatorsClick && !topbarActionOverride("indicators")) {
657
701
  indicatorsBtn = doc.createElement("button");
658
702
  indicatorsBtn.className = "vela-widget-indicators";
659
- indicatorsBtn.append(iconEl("indicators", doc), doc.createTextNode("Indicators"));
703
+ const label = doc.createTextNode(this.t(TEXT2.indicators));
704
+ indicatorsBtn.append(iconEl("indicators", doc), label);
705
+ this.relabels.push(() => label.textContent = this.t(TEXT2.indicators));
660
706
  indicatorsBtn.addEventListener("click", opts.onIndicatorsClick);
661
707
  }
662
708
  this.actionsHost = doc.createElement("span");
@@ -667,15 +713,15 @@ var Topbar = class {
667
713
  this.panelsHost.className = "vela-widget-panels";
668
714
  const tool = (cls, icon2, tip, onClick) => this.toolButton(cls, icon2, tip, onClick, this.tooltips);
669
715
  if (vis("undo-redo")) {
670
- this.undoBtn = tool("vela-widget-undo", "undo", "Undo", opts.onUndoClick);
671
- this.redoBtn = tool("vela-widget-redo", "redo", "Redo", opts.onRedoClick);
716
+ this.undoBtn = tool("vela-widget-undo", "undo", TEXT2.undo, opts.onUndoClick);
717
+ this.redoBtn = tool("vela-widget-redo", "redo", TEXT2.redo, opts.onRedoClick);
672
718
  } else {
673
719
  this.undoBtn = doc.createElement("button");
674
720
  this.redoBtn = doc.createElement("button");
675
721
  }
676
722
  this.setHistoryState(false, false);
677
- const screenshotBtn = vis("screenshot") && !topbarActionOverride("screenshot") ? tool("vela-widget-screenshot", "camera", "Download screenshot", opts.onScreenshotClick) : null;
678
- this.alertsBtn = vis("alerts") ? tool("vela-widget-alerts", "bell", "Alerts") : doc.createElement("button");
723
+ const screenshotBtn = vis("screenshot") && !topbarActionOverride("screenshot") ? tool("vela-widget-screenshot", "camera", TEXT2.screenshot, opts.onScreenshotClick) : null;
724
+ this.alertsBtn = vis("alerts") ? tool("vela-widget-alerts", "bell", TEXT2.alerts) : doc.createElement("button");
679
725
  this.alertsBtn.style.position = "relative";
680
726
  if (opts.onAlertsClick) this.alertsBtn.addEventListener("click", () => opts.onAlertsClick(this.alertsBtn));
681
727
  this.alertsBadge = doc.createElement("span");
@@ -759,14 +805,15 @@ var Topbar = class {
759
805
  this.hairlineRo.observe(this.el);
760
806
  doc.defaultView?.addEventListener("resize", this.onHairlineSync);
761
807
  this.renderActions();
762
- this.tooltips.push(new Tooltip(this.tfCaret, { content: "Timeframe", triggerId: "vela-topbar-tf", host }));
763
- this.tooltips.push(new Tooltip(this.styleButton, { content: "Chart style", triggerId: "vela-topbar-style", host }));
808
+ this.labeledTooltip(this.tfCaret, TEXT2.timeframe, "vela-topbar-tf", this.tooltips);
809
+ this.labeledTooltip(this.styleButton, TEXT2.chartStyle, "vela-topbar-style", this.tooltips);
764
810
  if (this.layoutButton && opts.layout) {
765
- this.tooltips.push(new Tooltip(this.layoutButton, { content: "Layout", triggerId: "vela-topbar-layout", host }));
811
+ this.labeledTooltip(this.layoutButton, TEXT2.layout, "vela-topbar-layout", this.tooltips);
766
812
  const layout = opts.layout;
767
813
  this.layoutPicker = new LayoutPicker({
768
814
  trigger: this.layoutButton,
769
815
  host,
816
+ ...opts.i18n ? { i18n: opts.i18n } : {},
770
817
  shape: () => layout.shape(),
771
818
  presets: () => layout.presets().map((p) => ({ ...p, checked: p.id === this.layoutId })),
772
819
  onSelectGrid: (rows, cols) => layout.onSelectGrid(rows, cols),
@@ -779,6 +826,7 @@ var Topbar = class {
779
826
  trigger: this.tfCaret,
780
827
  triggerId: "vela-topbar-tf",
781
828
  host,
829
+ i18n: opts.i18n ?? null,
782
830
  items: this.tfItems(),
783
831
  onSelect: (id) => opts.onTimeframe(id),
784
832
  onFavorite: (id, on) => opts.onTimeframeFavorite?.(id, on),
@@ -795,6 +843,12 @@ var Topbar = class {
795
843
  iconBadges: true
796
844
  });
797
845
  }
846
+ get i18n() {
847
+ return this.opts.i18n ?? null;
848
+ }
849
+ t(m, values) {
850
+ return textOf(this.i18n, m, values);
851
+ }
798
852
  setSymbol(symbol) {
799
853
  this.symbolEl.textContent = parseSymbol(symbol).ticker;
800
854
  }
@@ -820,13 +874,13 @@ var Topbar = class {
820
874
  for (const tf of shown) {
821
875
  const b = doc.createElement("button");
822
876
  b.className = "vela-widget-tf";
823
- const label = timeframeLabel(tf);
877
+ const label = timeframeLabel(tf, this.i18n);
824
878
  b.textContent = label;
825
879
  if (tf === this.timeframe) {
826
880
  b.dataset.current = "1";
827
881
  b.setAttribute("aria-current", "true");
828
882
  } else {
829
- b.setAttribute("aria-label", `Switch timeframe to ${label}`);
883
+ b.setAttribute("aria-label", this.t(TEXT2.switchTimeframe, { timeframe: label }));
830
884
  b.addEventListener("click", () => this.opts.onTimeframe(tf));
831
885
  }
832
886
  this.tfChipsHost.appendChild(b);
@@ -834,12 +888,12 @@ var Topbar = class {
834
888
  this.tfCaret.replaceChildren();
835
889
  if (chips.length === 0) {
836
890
  this.tfCaret.dataset.solo = "1";
837
- this.tfCaret.append(doc.createTextNode(timeframeLabel(this.timeframe)), iconEl("chevron-down", doc));
838
- this.tfCaret.setAttribute("aria-label", `Timeframe \u2014 ${timeframeLabel(this.timeframe)}`);
891
+ this.tfCaret.append(doc.createTextNode(timeframeLabel(this.timeframe, this.i18n)), iconEl("chevron-down", doc));
892
+ this.tfCaret.setAttribute("aria-label", this.t(TEXT2.currentTimeframe, { timeframe: timeframeLabel(this.timeframe, this.i18n) }));
839
893
  } else {
840
894
  delete this.tfCaret.dataset.solo;
841
895
  this.tfCaret.appendChild(iconEl("chevron-down", doc));
842
- this.tfCaret.setAttribute("aria-label", "Timeframes");
896
+ this.tfCaret.setAttribute("aria-label", this.t(TEXT2.timeframes));
843
897
  }
844
898
  this.onHairlineSync();
845
899
  }
@@ -866,14 +920,14 @@ var Topbar = class {
866
920
  } else {
867
921
  this.layoutButton.appendChild(doc.createTextNode(this.layoutId ?? ""));
868
922
  }
869
- this.layoutButton.setAttribute("aria-label", `Layout \u2014 ${this.layoutId ?? ""}`);
923
+ this.layoutButton.setAttribute("aria-label", this.t(TEXT2.currentLayout, { layout: this.layoutId ?? "" }));
870
924
  }
871
925
  renderStyleButton(doc) {
872
926
  this.styleButton.replaceChildren();
873
927
  const icon2 = priceStyleIcon(this.priceStyle);
874
928
  if (icon2) this.styleButton.appendChild(iconEl(icon2, doc));
875
- else this.styleButton.appendChild(doc.createTextNode(priceStyleLabel(this.priceStyle)));
876
- this.styleButton.setAttribute("aria-label", `Chart style \u2014 ${priceStyleLabel(this.priceStyle)}`);
929
+ else this.styleButton.appendChild(doc.createTextNode(priceStyleLabel(this.priceStyle, this.i18n)));
930
+ this.styleButton.setAttribute("aria-label", this.t(TEXT2.currentStyle, { style: priceStyleLabel(this.priceStyle, this.i18n) }));
877
931
  }
878
932
  /** Re-project the contributed topbar actions (call after registrations change).
879
933
  * An action PINNED by the composition renders into its named slot (list position
@@ -898,16 +952,17 @@ var Topbar = class {
898
952
  const iconOnly = action.iconOnly === true && !!action.icon;
899
953
  if (action.iconOnly === true && !action.icon && !this.warnedIconless.has(action.id)) {
900
954
  this.warnedIconless.add(action.id);
901
- console.warn(`[vela] widget action "${action.id}": iconOnly needs an \`icon\` \u2014 rendering the label instead.`);
955
+ console.warn(`[vela] widget action "${action.id}": iconOnly needs an \`icon\` — rendering the label instead.`);
902
956
  }
903
957
  const b = doc.createElement("button");
904
958
  b.className = left ? "vela-widget-action-left" : iconOnly ? "vela-widget-tool" : "vela-widget-action";
905
959
  if (action.icon) b.appendChild(iconEl(action.icon, doc));
960
+ const label = textOf(ctx?.i18n ?? this.i18n, action.label);
906
961
  if (iconOnly) {
907
- b.setAttribute("aria-label", action.label);
908
- this.actionTooltips.push(new Tooltip(b, { content: action.label, triggerId: `vela-action-${action.id}`, host: this.host }));
962
+ b.setAttribute("aria-label", label);
963
+ this.actionTooltips.push(new Tooltip(b, { content: label, triggerId: `vela-action-${action.id}`, host: this.host }));
909
964
  } else {
910
- b.appendChild(doc.createTextNode(action.label));
965
+ b.appendChild(doc.createTextNode(label));
911
966
  }
912
967
  b.addEventListener("click", () => {
913
968
  const c = this.opts.getContext?.();
@@ -968,11 +1023,33 @@ var Topbar = class {
968
1023
  const b = doc.createElement("button");
969
1024
  b.className = `vela-widget-tool ${cls}`;
970
1025
  b.appendChild(iconEl(icon2, doc));
971
- b.setAttribute("aria-label", tip);
972
- sink.push(new Tooltip(b, { content: tip, triggerId: `vela-tool-${cls}`, host: this.host }));
1026
+ b.setAttribute("aria-label", this.t(tip));
1027
+ const tooltip = new Tooltip(b, { content: this.t(tip), triggerId: `vela-tool-${cls}`, host: this.host });
1028
+ sink.push(tooltip);
1029
+ this.relabels.push(() => {
1030
+ b.setAttribute("aria-label", this.t(tip));
1031
+ tooltip.setContent(this.t(tip));
1032
+ });
973
1033
  if (onClick) b.addEventListener("click", onClick);
974
1034
  return b;
975
1035
  }
1036
+ /** A kit tooltip whose text follows the language. */
1037
+ labeledTooltip(el, text, triggerId, sink) {
1038
+ const tooltip = new Tooltip(el, { content: this.t(text), triggerId, host: this.host });
1039
+ sink.push(tooltip);
1040
+ this.relabels.push(() => tooltip.setContent(this.t(text)));
1041
+ }
1042
+ /** Re-read every label, tooltip and menu in the current language. */
1043
+ relocalize() {
1044
+ for (const relabel of this.relabels) relabel();
1045
+ this.renderTfChips();
1046
+ this.renderStyleButton(this.styleButton.ownerDocument);
1047
+ if (this.layoutButton) this.renderLayoutButton(this.layoutButton.ownerDocument);
1048
+ this.tfMenu.setItems(this.tfItems());
1049
+ this.styleMenu.setItems(this.styleItems());
1050
+ this.layoutPicker?.relabel();
1051
+ this.renderActions();
1052
+ }
976
1053
  /** Paint each `.vela-sep` as exactly one device pixel, snapped to the pixel grid. */
977
1054
  syncHairlines() {
978
1055
  const win = this.el.ownerDocument.defaultView;
@@ -990,7 +1067,7 @@ var Topbar = class {
990
1067
  const stars = this.opts.onTimeframeFavorite !== void 0;
991
1068
  return this.opts.timeframes.map((tf) => ({
992
1069
  id: tf,
993
- label: timeframeLabel(tf),
1070
+ label: timeframeLabel(tf, this.i18n),
994
1071
  checked: tf === this.timeframe,
995
1072
  ...stars ? { favorite: this.tfFavs.includes(tf) } : {}
996
1073
  }));
@@ -998,7 +1075,7 @@ var Topbar = class {
998
1075
  styleItems() {
999
1076
  return priceStyleIds().map((id, i) => ({
1000
1077
  id,
1001
- label: priceStyleLabel(id),
1078
+ label: priceStyleLabel(id, this.i18n),
1002
1079
  icon: priceStyleIcon(id),
1003
1080
  checked: id === this.priceStyle,
1004
1081
  separatorBefore: i === BUILTIN_PRICE_STYLES.length
@@ -1098,12 +1175,34 @@ var CSS3 = `
1098
1175
  }
1099
1176
  .vela-bb-settings:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
1100
1177
  `;
1178
+ var TEXT3 = {
1179
+ timeZone: msg("widget.bottombar.timeZone", "Time zone"),
1180
+ session: msg("widget.bottombar.session", "Session — regular (RTH) vs extended (ETH) hours"),
1181
+ regular: msg("widget.bottombar.regular", "RTH"),
1182
+ extended: msg("widget.bottombar.extended", "ETH"),
1183
+ chartSettings: msg("widget.bottombar.chartSettings", "Chart settings")
1184
+ };
1185
+ var RANGE_LABELS = {
1186
+ "1D": msg("widget.bottombar.range.1d", "1D"),
1187
+ "7D": msg("widget.bottombar.range.7d", "7D"),
1188
+ "1M": msg("widget.bottombar.range.1m", "1M"),
1189
+ "3M": msg("widget.bottombar.range.3m", "3M"),
1190
+ "6M": msg("widget.bottombar.range.6m", "6M"),
1191
+ YTD: msg("widget.bottombar.range.ytd", "YTD"),
1192
+ "1Y": msg("widget.bottombar.range.1y", "1Y"),
1193
+ "5Y": msg("widget.bottombar.range.5y", "5Y"),
1194
+ ALL: msg("widget.bottombar.range.all", "ALL")
1195
+ };
1196
+ function rangeLabel(id, i18n) {
1197
+ return textOf(i18n, RANGE_LABELS[id] ?? id);
1198
+ }
1101
1199
  var Bottombar = class {
1102
1200
  constructor(host, opts) {
1103
1201
  this.settingsTip = null;
1104
1202
  this.rangeButtons = /* @__PURE__ */ new Map();
1105
1203
  this.sessionButtons = /* @__PURE__ */ new Map();
1106
1204
  this.sessionEl = null;
1205
+ this.i18n = opts.i18n ?? null;
1107
1206
  this.timezone = opts.timezone;
1108
1207
  this.exchangeTimezone = opts.exchangeTimezone;
1109
1208
  const doc = host.ownerDocument;
@@ -1113,7 +1212,7 @@ var Bottombar = class {
1113
1212
  for (const preset of RANGE_PRESETS) {
1114
1213
  const b = doc.createElement("button");
1115
1214
  b.className = "vela-bb-range";
1116
- b.textContent = preset.id;
1215
+ b.textContent = this.t(RANGE_LABELS[preset.id] ?? preset.id);
1117
1216
  b.addEventListener("click", () => {
1118
1217
  this.setActiveRange(preset.id);
1119
1218
  opts.onRange(preset);
@@ -1125,7 +1224,7 @@ var Bottombar = class {
1125
1224
  spacer.className = "vela-bb-spacer";
1126
1225
  this.tzButton = doc.createElement("button");
1127
1226
  this.tzButton.className = "vela-bb-tz";
1128
- this.tzButton.setAttribute("aria-label", "Time zone");
1227
+ this.tzButton.setAttribute("aria-label", this.t(TEXT3.timeZone));
1129
1228
  this.clockEl = doc.createElement("span");
1130
1229
  this.clockEl.className = "vela-bb-clock";
1131
1230
  this.tzLabelEl = doc.createElement("span");
@@ -1135,11 +1234,11 @@ var Bottombar = class {
1135
1234
  session.className = "vela-bb-session";
1136
1235
  this.sessionEl = session;
1137
1236
  session.style.display = "none";
1138
- session.title = "Session \u2014 regular (RTH) vs extended (ETH) hours";
1139
- for (const [key, label] of [["regular", "RTH"], ["extended", "ETH"]]) {
1237
+ session.title = this.t(TEXT3.session);
1238
+ for (const [key, label] of [["regular", TEXT3.regular], ["extended", TEXT3.extended]]) {
1140
1239
  const b = doc.createElement("button");
1141
1240
  b.className = "vela-bb-session-btn" + (key === "regular" ? " is-active" : "");
1142
- b.textContent = label;
1241
+ b.textContent = this.t(label);
1143
1242
  b.disabled = true;
1144
1243
  b.addEventListener("click", () => {
1145
1244
  if (b.disabled) return;
@@ -1150,11 +1249,12 @@ var Bottombar = class {
1150
1249
  session.appendChild(b);
1151
1250
  }
1152
1251
  const settingsBtn = doc.createElement("button");
1252
+ this.settingsBtn = settingsBtn;
1153
1253
  settingsBtn.className = "vela-bb-settings";
1154
1254
  settingsBtn.appendChild(iconEl("gear", doc));
1155
- settingsBtn.setAttribute("aria-label", "Chart settings");
1255
+ settingsBtn.setAttribute("aria-label", this.t(TEXT3.chartSettings));
1156
1256
  if (opts.onSettingsClick) settingsBtn.addEventListener("click", opts.onSettingsClick);
1157
- this.settingsTip = new Tooltip(settingsBtn, { content: "Chart settings", triggerId: "vela-bb-settings", host });
1257
+ this.settingsTip = new Tooltip(settingsBtn, { content: this.t(TEXT3.chartSettings), triggerId: "vela-bb-settings", host });
1158
1258
  this.el.append(spacer, this.tzButton, session, settingsBtn);
1159
1259
  host.appendChild(this.el);
1160
1260
  this.tzMenu = new Menu({
@@ -1217,20 +1317,25 @@ var Bottombar = class {
1217
1317
  this.el.remove();
1218
1318
  }
1219
1319
  tzItems() {
1220
- return timezoneMenuRows(this.timezone).map((r) => ({ id: r.value, label: r.label, checked: r.checked }));
1320
+ return timezoneMenuRows(this.timezone, this.i18n).map((r) => ({ id: r.value, label: r.label, checked: r.checked }));
1321
+ }
1322
+ t(m) {
1323
+ return textOf(this.i18n, m);
1324
+ }
1325
+ /** Re-read every label, the zone menu and the clock in the current language. */
1326
+ relocalize() {
1327
+ for (const [id, b] of this.rangeButtons) b.textContent = this.t(RANGE_LABELS[id] ?? id);
1328
+ this.tzButton.setAttribute("aria-label", this.t(TEXT3.timeZone));
1329
+ if (this.sessionEl) this.sessionEl.title = this.t(TEXT3.session);
1330
+ this.sessionButtons.get("regular").textContent = this.t(TEXT3.regular);
1331
+ this.sessionButtons.get("extended").textContent = this.t(TEXT3.extended);
1332
+ this.settingsBtn.setAttribute("aria-label", this.t(TEXT3.chartSettings));
1333
+ this.settingsTip?.setContent(this.t(TEXT3.chartSettings));
1334
+ this.tzMenu.setItems(this.tzItems());
1335
+ this.tick();
1221
1336
  }
1222
1337
  tick() {
1223
- try {
1224
- this.clockEl.textContent = new Intl.DateTimeFormat("en-GB", {
1225
- hour: "2-digit",
1226
- minute: "2-digit",
1227
- second: "2-digit",
1228
- hour12: false,
1229
- timeZone: this.displayZone
1230
- }).format(/* @__PURE__ */ new Date());
1231
- } catch {
1232
- this.clockEl.textContent = "";
1233
- }
1338
+ this.clockEl.textContent = (this.i18n ?? englishTranslator()).format.clock(Date.now(), { timeZone: this.displayZone, surface: "clock" });
1234
1339
  }
1235
1340
  };
1236
1341
 
@@ -1269,23 +1374,29 @@ function tickerIconEl(doc, base, name, className, iconUrl) {
1269
1374
  }
1270
1375
 
1271
1376
  // src/widget/object-tree-model.ts
1377
+ var TEXT4 = {
1378
+ mainChart: msg("widget.objectTree.mainChart", "Main chart"),
1379
+ pane: msg("widget.objectTree.pane", "Panel {n}"),
1380
+ drawingWithText: msg("widget.objectTree.drawingWithText", "{name} — {text}"),
1381
+ group: msg("widget.objectTree.group", "Group {n}")
1382
+ };
1272
1383
  var PRICE_PANE_ID = "price";
1273
- var PRICE_PANE_LABEL = "Main chart";
1274
- function drawingMeta(type) {
1275
- const meta = getDrawingType(type);
1384
+ sourceText(TEXT4.mainChart);
1385
+ function drawingMeta(type, i18n) {
1386
+ const meta = getDrawingType(type, i18n ?? void 0);
1276
1387
  if (meta) return { label: meta.label, icon: meta.icon };
1277
1388
  return { label: String(type || "drawing").replace(/(^|\s)\S/g, (c) => c.toUpperCase()), icon: null };
1278
1389
  }
1279
- function drawingLabel(d) {
1280
- const { label } = drawingMeta(d.type);
1390
+ function drawingLabel(d, i18n) {
1391
+ const { label } = drawingMeta(d.type, i18n);
1281
1392
  const text = d.text?.value?.trim();
1282
- return text ? `${label} \u2014 ${text}` : label;
1393
+ return text ? textOf(i18n, TEXT4.drawingWithText, { name: label, text }) : label;
1283
1394
  }
1284
- function paneLabel(pane, fallbackTitle) {
1285
- if (pane.kind === "price") return PRICE_PANE_LABEL;
1395
+ function paneLabel(pane, fallbackTitle, i18n) {
1396
+ if (pane.kind === "price") return textOf(i18n, TEXT4.mainChart);
1286
1397
  const master = pane.indicators.find((i) => !i.ownScale) ?? pane.indicators[0];
1287
1398
  if (master) return indicatorLabel(master, fallbackTitle);
1288
- return `Pane ${pane.order + 1}`;
1399
+ return textOf(i18n, TEXT4.pane, { n: String(pane.order + 1) });
1289
1400
  }
1290
1401
  function indicatorLabel(i, fallbackTitle) {
1291
1402
  return i.title || fallbackTitle(i.id) || i.id;
@@ -1372,13 +1483,14 @@ function groupState(members) {
1372
1483
  function canGroup(ids, groups) {
1373
1484
  return ids.length > 0 && ids.every((id) => groupOf(groups, id) === null);
1374
1485
  }
1375
- function nextGroupName(groups) {
1486
+ function nextGroupName(groups, i18n) {
1376
1487
  const taken = new Set(groups.map((g) => g.name));
1488
+ const nameOf = (n) => textOf(i18n, TEXT4.group, { n: String(n) });
1377
1489
  for (let n = 1; n <= groups.length; n += 1) {
1378
- const name = `Group ${n}`;
1490
+ const name = nameOf(n);
1379
1491
  if (!taken.has(name)) return name;
1380
1492
  }
1381
- return `Group ${groups.length + 1}`;
1493
+ return nameOf(groups.length + 1);
1382
1494
  }
1383
1495
  function withMembers(groups, keep) {
1384
1496
  return groups.map((g) => ({ ...g, ids: g.ids.filter(keep) })).filter((g) => g.ids.length > 0);
@@ -1453,7 +1565,7 @@ function buildTree(snap) {
1453
1565
  return snap.panes.map((pane) => ({
1454
1566
  id: pane.id,
1455
1567
  kind: pane.kind,
1456
- label: paneLabel(pane, snap.handleTitle),
1568
+ label: paneLabel(pane, snap.handleTitle, snap.i18n),
1457
1569
  order: pane.order,
1458
1570
  collapsed: pane.collapsed,
1459
1571
  maximized: pane.maximized,
@@ -1465,6 +1577,56 @@ function treeIsEmpty(panes) {
1465
1577
  }
1466
1578
 
1467
1579
  // src/widget/object-tree.ts
1580
+ var TEXT5 = {
1581
+ objectTree: msg("widget.objectTree.objectTree", "Object tree"),
1582
+ price: msg("widget.objectTree.price", "Price"),
1583
+ emptyHint: msg("widget.objectTree.emptyHint", "Add an indicator or a drawing to populate the tree"),
1584
+ groupSelected: msg("widget.objectTree.groupSelected", "Group the selected drawings"),
1585
+ groupDrawings: msg("widget.objectTree.groupDrawings", "{count, plural, one {Group this drawing} other {Group these drawings}}"),
1586
+ alreadyInAGroup: msg("widget.objectTree.alreadyInAGroup", "Already in a group"),
1587
+ duplicateSelected: msg("widget.objectTree.duplicateSelected", "Duplicate the selected drawings"),
1588
+ duplicate: msg("widget.objectTree.duplicate", "Duplicate"),
1589
+ movePaneUp: msg("widget.objectTree.movePaneUp", "Move panel up"),
1590
+ movePaneDown: msg("widget.objectTree.movePaneDown", "Move panel down"),
1591
+ expandPane: msg("widget.objectTree.expandPane", "Expand panel"),
1592
+ collapsePane: msg("widget.objectTree.collapsePane", "Collapse panel"),
1593
+ restorePanes: msg("widget.objectTree.restorePanes", "Restore panels"),
1594
+ maximizePane: msg("widget.objectTree.maximizePane", "Maximize panel"),
1595
+ hide: msg("widget.objectTree.hide", "Hide"),
1596
+ show: msg("widget.objectTree.show", "Show"),
1597
+ restackHint: msg("widget.objectTree.restackHint", "Drag to change what draws in front"),
1598
+ remove: msg("widget.objectTree.remove", "Remove"),
1599
+ moveIndicatorHint: msg("widget.objectTree.moveIndicatorHint", "Drag to another panel, or between panels for a new one"),
1600
+ unlockAll: msg("widget.objectTree.unlockAll", "Unlock all"),
1601
+ lockAll: msg("widget.objectTree.lockAll", "Lock all"),
1602
+ showAll: msg("widget.objectTree.showAll", "Show all"),
1603
+ hideAll: msg("widget.objectTree.hideAll", "Hide all"),
1604
+ removeAll: msg("widget.objectTree.removeAll", "Remove all"),
1605
+ unlock: msg("widget.objectTree.unlock", "Unlock"),
1606
+ lock: msg("widget.objectTree.lock", "Lock"),
1607
+ moveDrawingHint: msg("widget.objectTree.moveDrawingHint", "Drag to restack, or onto another panel to move it there"),
1608
+ ownScaleHint: msg("widget.objectTree.ownScaleHint", "Draws against its own price scale"),
1609
+ bringToFront: msg("widget.objectTree.bringToFront", "Bring to front"),
1610
+ sendToBack: msg("widget.objectTree.sendToBack", "Send to back"),
1611
+ indicatorSettings: msg("widget.objectTree.indicatorSettings", "Indicator settings"),
1612
+ moveTo: msg("widget.objectTree.moveTo", "Move to"),
1613
+ newPaneAbove: msg("widget.objectTree.newPaneAbove", "New panel above"),
1614
+ newPaneBelow: msg("widget.objectTree.newPaneBelow", "New panel below"),
1615
+ newGroup: msg("widget.objectTree.newGroup", "New group"),
1616
+ addToGroup: msg("widget.objectTree.addToGroup", "Add to group"),
1617
+ moveToGroup: msg("widget.objectTree.moveToGroup", "Move to group"),
1618
+ rename: msg("widget.objectTree.rename", "Rename…"),
1619
+ selectAll: msg("widget.objectTree.selectAll", "Select all"),
1620
+ bringAllToFront: msg("widget.objectTree.bringAllToFront", "Bring all to front"),
1621
+ sendAllToBack: msg("widget.objectTree.sendAllToBack", "Send all to back"),
1622
+ ungroup: msg("widget.objectTree.ungroup", "Ungroup"),
1623
+ selected: msg("widget.objectTree.selected", "{count, plural, one {# selected} other {# selected}}"),
1624
+ groupMembers: msg("widget.objectTree.groupMembers", "{count, plural, one {# drawing — drag to move it} other {# drawings — drag to move them together}}"),
1625
+ groupPick: msg("widget.objectTree.groupPick", "Group selection ({count})"),
1626
+ removeFromGroup: msg("widget.objectTree.removeFromGroup", "Remove from {group}"),
1627
+ scaleTag: msg("widget.objectTree.scaleTag", "scale")
1628
+ };
1629
+ var OBJECT_TREE_TITLE = TEXT5.objectTree;
1468
1630
  var STYLE_ID4 = "vela-widget-objtree";
1469
1631
  var CSS4 = `
1470
1632
  /* One pane's block. The transparent border reserves the drop-target outline. */
@@ -1498,7 +1660,7 @@ var CSS4 = `
1498
1660
  }
1499
1661
  .vela-ot-row:hover { background: var(--vela-hover); }
1500
1662
  .vela-ot-row > .vela-icon { color: var(--vela-fg-muted); width: 14px; height: 14px; font-size: 14px; justify-content: center; flex: none; }
1501
- /* Drawing glyphs come from the type registry at toolbar scale \u2014 bring them down to row size. */
1663
+ /* Drawing glyphs come from the type registry at toolbar scale — bring them down to row size. */
1502
1664
  .vela-ot-row > .vela-icon svg { width: 14px; height: 14px; }
1503
1665
  .vela-ot-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1504
1666
  .vela-ot-row[data-hidden] .vela-ot-name,
@@ -1558,9 +1720,9 @@ var CSS4 = `
1558
1720
  .vela-ot-btn:disabled { opacity: 0.35; cursor: default; }
1559
1721
  .vela-ot-empty { padding: 20px 10px; text-align: center; color: var(--vela-fg-muted); font-size: 12px; }
1560
1722
 
1561
- /* \u2500\u2500 drawing groups \u2500\u2500 */
1723
+ /* ── drawing groups ── */
1562
1724
  /* One top-level entry in a pane's drawing list: a lone drawing, or a whole group block. No
1563
- border of its own \u2014 units stack flush, so consecutive highlighted rows read as one block; the
1725
+ border of its own — units stack flush, so consecutive highlighted rows read as one block; the
1564
1726
  drop-into-group outline is drawn inside the box instead (see the drag-and-drop rules). */
1565
1727
  .vela-ot-unit { border-radius: 6px; }
1566
1728
  /* Adjacent picked rows merge into one contiguous highlight: the shared edge loses its rounding. */
@@ -1579,7 +1741,7 @@ var CSS4 = `
1579
1741
  color: var(--vela-fg-bright);
1580
1742
  font: inherit;
1581
1743
  }
1582
- /* The selection bar. Always there \u2014 the actions it holds are the panel's, not a row's, so they
1744
+ /* The selection bar. Always there — the actions it holds are the panel's, not a row's, so they
1583
1745
  stay in place and simply go dim until a drawing is selected. It stays put at the top of the
1584
1746
  list while the list scrolls under it, so they never scroll out of reach. */
1585
1747
  .vela-ot-selbar {
@@ -1598,7 +1760,7 @@ var CSS4 = `
1598
1760
  .vela-ot-selbar .vela-ot-btn { visibility: visible; width: 24px; height: 22px; }
1599
1761
  .vela-ot-selbar .vela-ot-btn[data-icon='group'] .vela-icon { width: 16px; height: 16px; font-size: 16px; }
1600
1762
 
1601
- /* \u2500\u2500 drag-and-drop \u2500\u2500 */
1763
+ /* ── drag-and-drop ── */
1602
1764
  .vela-ot-row[data-drag] { cursor: grab; }
1603
1765
  .vela-ot .vela-panel-body[data-dragging] .vela-ot-row[data-drag] { cursor: grabbing; }
1604
1766
  /* The row in flight fades: the ghost under the pointer is the thing being moved. */
@@ -1684,8 +1846,8 @@ var MenuBuilder = class {
1684
1846
  }
1685
1847
  };
1686
1848
  var ObjectTree = class extends SidePanel {
1687
- constructor(host, iconFor) {
1688
- super(host, "Object tree", "vela-ot");
1849
+ constructor(host, iconFor, i18n) {
1850
+ super(host, TEXT5.objectTree, "vela-ot", i18n ? { i18n } : {});
1689
1851
  this.iconFor = iconFor;
1690
1852
  this.chart = null;
1691
1853
  /** What the CHART has selected — every member of a multi-selection, mirrored as rows. */
@@ -1768,16 +1930,21 @@ var ObjectTree = class extends SidePanel {
1768
1930
  panes: chart.panes.list(),
1769
1931
  indicatorVisible: (id) => handle(id)?.visible !== false,
1770
1932
  handleTitle: (id) => handle(id)?.title,
1933
+ i18n: this.i18n,
1771
1934
  stackable,
1772
1935
  interleave: stackable && chart.drawings.supported && chart.renderer.capabilities.drawingDepth === true,
1773
1936
  zOrder: stackable ? chart.renderer.get("seriesOrder") ?? [] : [],
1774
1937
  candleZ: stackable ? Number(chart.renderer.get("candleZOrder")) || 0 : 0,
1775
- priceLabel: this.symbolName || "Price",
1938
+ priceLabel: this.symbolName || this.t(TEXT5.price),
1776
1939
  priceVisible: chart.renderer.get("candleVisible") !== false,
1777
1940
  drawings: chart.drawings.supported ? chart.drawings.all() : [],
1778
1941
  groups: this.groups
1779
1942
  };
1780
1943
  }
1944
+ relocalize() {
1945
+ super.relocalize();
1946
+ this.refresh();
1947
+ }
1781
1948
  /**
1782
1949
  * Repaint because something changed — unless the panel is mid-interaction. A live drag
1783
1950
  * resolves drops against the rows as currently rendered, and a rename lives in an input
@@ -1808,7 +1975,7 @@ var ObjectTree = class extends SidePanel {
1808
1975
  if (treeIsEmpty(panes)) {
1809
1976
  const empty = doc.createElement("div");
1810
1977
  empty.className = "vela-ot-empty";
1811
- empty.textContent = "Add an indicator or a drawing to populate the tree";
1978
+ empty.textContent = this.t(TEXT5.emptyHint);
1812
1979
  this.body.appendChild(empty);
1813
1980
  }
1814
1981
  }
@@ -1823,7 +1990,7 @@ var ObjectTree = class extends SidePanel {
1823
1990
  bar.appendChild(
1824
1991
  this.btn({
1825
1992
  icon: "group",
1826
- title: ids.length === 0 ? "Group the selected drawings" : groupable ? `Group ${ids.length === 1 ? "this drawing" : "these drawings"}` : "Already in a group",
1993
+ title: ids.length === 0 ? this.t(TEXT5.groupSelected) : groupable ? this.t(TEXT5.groupDrawings, { count: ids.length }) : this.t(TEXT5.alreadyInAGroup),
1827
1994
  disabled: !groupable,
1828
1995
  run: () => this.makeGroup(ids)
1829
1996
  })
@@ -1831,7 +1998,7 @@ var ObjectTree = class extends SidePanel {
1831
1998
  bar.appendChild(
1832
1999
  this.btn({
1833
2000
  icon: "clone",
1834
- title: ids.length === 0 ? "Duplicate the selected drawings" : "Duplicate",
2001
+ title: ids.length === 0 ? this.t(TEXT5.duplicateSelected) : this.t(TEXT5.duplicate),
1835
2002
  disabled: ids.length === 0,
1836
2003
  run: () => {
1837
2004
  for (const id of ids) this.cloneInto(pass, id);
@@ -1841,7 +2008,7 @@ var ObjectTree = class extends SidePanel {
1841
2008
  );
1842
2009
  const count = doc.createElement("span");
1843
2010
  count.className = "vela-ot-selcount";
1844
- count.textContent = ids.length > 1 ? `${ids.length} selected` : "";
2011
+ count.textContent = ids.length > 1 ? this.t(TEXT5.selected, { count: ids.length }) : "";
1845
2012
  bar.appendChild(count);
1846
2013
  return bar;
1847
2014
  }
@@ -1916,11 +2083,11 @@ var ObjectTree = class extends SidePanel {
1916
2083
  acts.appendChild(this.btn({ icon: icon2, title, run }));
1917
2084
  };
1918
2085
  if (pane.kind !== "price") {
1919
- if (pane.order > 1) op("arrow-up", "Move pane up", () => chart.panes.move(pane.id, "up"));
1920
- if (pane.order < pass.panes.length - 1) op("arrow-down", "Move pane down", () => chart.panes.move(pane.id, "down"));
1921
- op(pane.collapsed ? "expand" : "collapse", pane.collapsed ? "Expand pane" : "Collapse pane", () => chart.panes.collapse(pane.id, !pane.collapsed));
2086
+ if (pane.order > 1) op("arrow-up", this.t(TEXT5.movePaneUp), () => chart.panes.move(pane.id, "up"));
2087
+ if (pane.order < pass.panes.length - 1) op("arrow-down", this.t(TEXT5.movePaneDown), () => chart.panes.move(pane.id, "down"));
2088
+ op(pane.collapsed ? "expand" : "collapse", pane.collapsed ? this.t(TEXT5.expandPane) : this.t(TEXT5.collapsePane), () => chart.panes.collapse(pane.id, !pane.collapsed));
1922
2089
  }
1923
- op(pane.maximized ? "restore" : "maximize", pane.maximized ? "Restore panes" : "Maximize pane", () => chart.panes.maximize(pane.maximized ? null : pane.id));
2090
+ op(pane.maximized ? "restore" : "maximize", pane.maximized ? this.t(TEXT5.restorePanes) : this.t(TEXT5.maximizePane), () => chart.panes.maximize(pane.maximized ? null : pane.id));
1924
2091
  return head;
1925
2092
  }
1926
2093
  rowEl(pass, pane, row) {
@@ -1928,11 +2095,11 @@ var ObjectTree = class extends SidePanel {
1928
2095
  const { chart } = pass;
1929
2096
  if (row.kind === "price") {
1930
2097
  const base = this.symbolName.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || this.symbolName;
1931
- const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar", this.symbolRaw ? this.iconFor?.(this.symbolRaw) : void 0);
2098
+ const icon2 = tickerIconEl(doc, base || sliceGraphemes(this.t(TEXT5.price), 1), this.symbolName || this.t(TEXT5.price), "vela-ot-avatar", this.symbolRaw ? this.iconFor?.(this.symbolRaw) : void 0);
1932
2099
  const el2 = this.row(icon2, row.label, row.visible, [
1933
2100
  {
1934
2101
  icon: row.visible ? "eye" : "eye-off",
1935
- title: row.visible ? "Hide" : "Show",
2102
+ title: row.visible ? this.t(TEXT5.hide) : this.t(TEXT5.show),
1936
2103
  engaged: !row.visible,
1937
2104
  run: () => {
1938
2105
  chart.renderer.set("candleVisible", !row.visible);
@@ -1944,7 +2111,7 @@ var ObjectTree = class extends SidePanel {
1944
2111
  el2.dataset.pane = pane.id;
1945
2112
  if (pass.stackable) {
1946
2113
  el2.dataset.drag = "1";
1947
- el2.title = "Drag to change what draws in front";
2114
+ el2.title = this.t(TEXT5.restackHint);
1948
2115
  }
1949
2116
  return el2;
1950
2117
  }
@@ -1956,7 +2123,7 @@ var ObjectTree = class extends SidePanel {
1956
2123
  [
1957
2124
  {
1958
2125
  icon: row.visible ? "eye" : "eye-off",
1959
- title: row.visible ? "Hide" : "Show",
2126
+ title: row.visible ? this.t(TEXT5.hide) : this.t(TEXT5.show),
1960
2127
  engaged: !row.visible,
1961
2128
  run: () => {
1962
2129
  handle?.setVisible(!row.visible);
@@ -1965,21 +2132,21 @@ var ObjectTree = class extends SidePanel {
1965
2132
  },
1966
2133
  {
1967
2134
  icon: "trash",
1968
- title: "Remove",
2135
+ title: this.t(TEXT5.remove),
1969
2136
  run: () => {
1970
2137
  handle?.remove();
1971
2138
  this.refresh();
1972
2139
  }
1973
2140
  }
1974
2141
  ],
1975
- row.ownScale ? "scale" : void 0
2142
+ row.ownScale ? this.t(TEXT5.scaleTag) : void 0
1976
2143
  );
1977
2144
  el.dataset.rowKind = "indicator";
1978
2145
  el.dataset.id = row.id;
1979
2146
  el.dataset.pane = pane.id;
1980
2147
  if (pass.repanable || pass.stackable) {
1981
2148
  el.dataset.drag = "1";
1982
- el.title = pass.repanable ? "Drag to another pane, or between panes for a new one" : "Drag to change what draws in front";
2149
+ el.title = pass.repanable ? this.t(TEXT5.moveIndicatorHint) : this.t(TEXT5.restackHint);
1983
2150
  }
1984
2151
  return el;
1985
2152
  }
@@ -1998,22 +2165,22 @@ var ObjectTree = class extends SidePanel {
1998
2165
  const el = this.row(iconEl(folded ? "chevron-right" : "chevron-down", doc), g.name, !allHidden, [
1999
2166
  {
2000
2167
  icon: allLocked ? "lock" : "unlock",
2001
- title: allLocked ? "Unlock all" : "Lock all",
2168
+ title: allLocked ? this.t(TEXT5.unlockAll) : this.t(TEXT5.lockAll),
2002
2169
  engaged: allLocked,
2003
2170
  run: () => this.setMembers(pass, ids, { locked: !allLocked })
2004
2171
  },
2005
2172
  {
2006
2173
  icon: allHidden ? "eye-off" : "eye",
2007
- title: allHidden ? "Show all" : "Hide all",
2174
+ title: allHidden ? this.t(TEXT5.showAll) : this.t(TEXT5.hideAll),
2008
2175
  engaged: allHidden,
2009
2176
  run: () => this.setMembers(pass, ids, { visible: allHidden })
2010
2177
  },
2011
- { icon: "trash", title: "Remove all", run: () => this.removeGroup(pass, g.id, true) }
2178
+ { icon: "trash", title: this.t(TEXT5.removeAll), run: () => this.removeGroup(pass, g.id, true) }
2012
2179
  ]);
2013
2180
  el.dataset.rowKind = "group";
2014
2181
  el.dataset.id = g.id;
2015
2182
  el.dataset.pane = pane.id;
2016
- el.title = `${members.length} drawing${members.length === 1 ? "" : "s"} \u2014 drag to move them together`;
2183
+ el.title = this.t(TEXT5.groupMembers, { count: members.length });
2017
2184
  if (this.renaming === g.id) {
2018
2185
  const name = el.querySelector(".vela-ot-name");
2019
2186
  const input = doc.createElement("input");
@@ -2047,7 +2214,7 @@ var ObjectTree = class extends SidePanel {
2047
2214
  drawRow(pass, d, inGroup = false) {
2048
2215
  const doc = this.el.ownerDocument;
2049
2216
  const { chart } = pass;
2050
- const markup = drawingMeta(d.type).icon;
2217
+ const markup = drawingMeta(d.type, this.i18n).icon;
2051
2218
  let icon2;
2052
2219
  if (markup) {
2053
2220
  icon2 = doc.createElement("span");
@@ -2057,10 +2224,10 @@ var ObjectTree = class extends SidePanel {
2057
2224
  } else {
2058
2225
  icon2 = iconEl("pen", doc);
2059
2226
  }
2060
- const el = this.row(icon2, drawingLabel(d), d.visible, [
2227
+ const el = this.row(icon2, drawingLabel(d, this.i18n), d.visible, [
2061
2228
  {
2062
2229
  icon: d.locked ? "lock" : "unlock",
2063
- title: d.locked ? "Unlock" : "Lock",
2230
+ title: d.locked ? this.t(TEXT5.unlock) : this.t(TEXT5.lock),
2064
2231
  engaged: d.locked,
2065
2232
  run: () => {
2066
2233
  chart.drawings.lock(d.id, !d.locked);
@@ -2069,7 +2236,7 @@ var ObjectTree = class extends SidePanel {
2069
2236
  },
2070
2237
  {
2071
2238
  icon: d.visible ? "eye" : "eye-off",
2072
- title: d.visible ? "Hide" : "Show",
2239
+ title: d.visible ? this.t(TEXT5.hide) : this.t(TEXT5.show),
2073
2240
  engaged: !d.visible,
2074
2241
  run: () => {
2075
2242
  chart.drawings.show(d.id, !d.visible);
@@ -2078,7 +2245,7 @@ var ObjectTree = class extends SidePanel {
2078
2245
  },
2079
2246
  {
2080
2247
  icon: "trash",
2081
- title: "Remove",
2248
+ title: this.t(TEXT5.remove),
2082
2249
  run: () => {
2083
2250
  chart.drawings.remove(d.id);
2084
2251
  this.refresh();
@@ -2089,7 +2256,7 @@ var ObjectTree = class extends SidePanel {
2089
2256
  el.dataset.id = d.id;
2090
2257
  el.dataset.pane = d.paneId;
2091
2258
  el.dataset.drag = "1";
2092
- el.title = "Drag to restack, or onto another pane to move it there";
2259
+ el.title = this.t(TEXT5.moveDrawingHint);
2093
2260
  if (inGroup) el.classList.add("vela-ot-subrow");
2094
2261
  if (this.selectedDrawings.has(d.id)) el.dataset.selected = "1";
2095
2262
  if (this.picked.has(d.id)) el.dataset.picked = "1";
@@ -2137,7 +2304,7 @@ var ObjectTree = class extends SidePanel {
2137
2304
  const t = doc.createElement("span");
2138
2305
  t.className = "vela-ot-tag";
2139
2306
  t.textContent = tag;
2140
- t.title = "Draws against its own price scale";
2307
+ t.title = this.t(TEXT5.ownScaleHint);
2141
2308
  el.appendChild(t);
2142
2309
  }
2143
2310
  const acts = doc.createElement("span");
@@ -2168,7 +2335,7 @@ var ObjectTree = class extends SidePanel {
2168
2335
  makeGroup(ids) {
2169
2336
  if (!canGroup(ids, this.groups)) return;
2170
2337
  this.groupSeq += 1;
2171
- this.groups = [...this.groups, { id: `grp-${this.groupSeq}`, name: nextGroupName(this.groups), ids: [...ids] }];
2338
+ this.groups = [...this.groups, { id: `grp-${this.groupSeq}`, name: nextGroupName(this.groups, this.i18n), ids: [...ids] }];
2172
2339
  this.picked.clear();
2173
2340
  this.render();
2174
2341
  }
@@ -2517,20 +2684,20 @@ var ObjectTree = class extends SidePanel {
2517
2684
  const { chart } = pass;
2518
2685
  const visible = chart.renderer.get("candleVisible") !== false;
2519
2686
  const items = [
2520
- b.entry(visible ? "Hide" : "Show", visible ? "eye-off" : "eye", () => {
2687
+ b.entry(visible ? this.t(TEXT5.hide) : this.t(TEXT5.show), visible ? "eye-off" : "eye", () => {
2521
2688
  chart.renderer.set("candleVisible", !visible);
2522
2689
  this.refresh();
2523
2690
  })
2524
2691
  ];
2525
2692
  if (pass.stackable) {
2526
2693
  items.push(
2527
- b.entry("Bring to front", "arrow-up", () => {
2694
+ b.entry(this.t(TEXT5.bringToFront), "arrow-up", () => {
2528
2695
  chart.renderer.set("candleZOrder", this.stackBounds(pass, PRICE_PANE_ID).top + 1);
2529
2696
  this.refresh();
2530
2697
  }, true)
2531
2698
  );
2532
2699
  items.push(
2533
- b.entry("Send to back", "arrow-down", () => {
2700
+ b.entry(this.t(TEXT5.sendToBack), "arrow-down", () => {
2534
2701
  chart.renderer.set("candleZOrder", this.stackBounds(pass, PRICE_PANE_ID).bottom - 1);
2535
2702
  this.refresh();
2536
2703
  })
@@ -2554,24 +2721,24 @@ var ObjectTree = class extends SidePanel {
2554
2721
  const { chart } = pass;
2555
2722
  const handle = pass.handle(row.id);
2556
2723
  const items = [
2557
- b.entry(row.visible ? "Hide" : "Show", row.visible ? "eye-off" : "eye", () => {
2724
+ b.entry(row.visible ? this.t(TEXT5.hide) : this.t(TEXT5.show), row.visible ? "eye-off" : "eye", () => {
2558
2725
  handle?.setVisible(!row.visible);
2559
2726
  this.refresh();
2560
2727
  })
2561
2728
  ];
2562
2729
  if (chart.renderer.supportsIndicatorSettings) {
2563
- items.push(b.entry("Indicator settings", "gear", () => chart.renderer.openIndicatorSettings(row.id)));
2730
+ items.push(b.entry(this.t(TEXT5.indicatorSettings), "gear", () => chart.renderer.openIndicatorSettings(row.id)));
2564
2731
  }
2565
2732
  if (chart.panes.supported) {
2566
2733
  const moves = this.moveItems(b, pass, row.id, paneId);
2567
- if (moves.length > 0) items.push(b.submenu("Move to", "move-vertical", moves));
2734
+ if (moves.length > 0) items.push(b.submenu(this.t(TEXT5.moveTo), "move-vertical", moves));
2568
2735
  }
2569
2736
  if (pass.stackable) {
2570
- items.push(b.entry("Bring to front", "arrow-up", () => chart.renderer.set("seriesOrder", { id: row.id, z: this.stackBounds(pass, paneId).top + 1 }), true));
2571
- items.push(b.entry("Send to back", "arrow-down", () => chart.renderer.set("seriesOrder", { id: row.id, z: this.stackBounds(pass, paneId).bottom - 1 })));
2737
+ items.push(b.entry(this.t(TEXT5.bringToFront), "arrow-up", () => chart.renderer.set("seriesOrder", { id: row.id, z: this.stackBounds(pass, paneId).top + 1 }), true));
2738
+ items.push(b.entry(this.t(TEXT5.sendToBack), "arrow-down", () => chart.renderer.set("seriesOrder", { id: row.id, z: this.stackBounds(pass, paneId).bottom - 1 })));
2572
2739
  }
2573
2740
  items.push(
2574
- b.entry("Remove", "trash", () => {
2741
+ b.entry(this.t(TEXT5.remove), "trash", () => {
2575
2742
  handle?.remove();
2576
2743
  this.refresh();
2577
2744
  }, true)
@@ -2589,33 +2756,33 @@ var ObjectTree = class extends SidePanel {
2589
2756
  const current = pass.panes.find((p) => p.id === fromPane);
2590
2757
  const alone = current !== void 0 && current.kind !== "price" && paneRows(current).length <= 1;
2591
2758
  if (!alone) {
2592
- if (fromPane !== PRICE_PANE_ID) items.push(b.entry("New pane above", void 0, () => chart.panes.moveIndicator(id, { newPane: { before: fromPane } })));
2593
- items.push(b.entry("New pane below", void 0, () => chart.panes.moveIndicator(id, { newPane: { after: fromPane } })));
2759
+ if (fromPane !== PRICE_PANE_ID) items.push(b.entry(this.t(TEXT5.newPaneAbove), void 0, () => chart.panes.moveIndicator(id, { newPane: { before: fromPane } })));
2760
+ items.push(b.entry(this.t(TEXT5.newPaneBelow), void 0, () => chart.panes.moveIndicator(id, { newPane: { after: fromPane } })));
2594
2761
  }
2595
2762
  return items;
2596
2763
  }
2597
2764
  drawingMenu(b, pass, d) {
2598
2765
  const { chart } = pass;
2599
2766
  const items = [
2600
- b.entry(d.visible ? "Hide" : "Show", d.visible ? "eye-off" : "eye", () => {
2767
+ b.entry(d.visible ? this.t(TEXT5.hide) : this.t(TEXT5.show), d.visible ? "eye-off" : "eye", () => {
2601
2768
  chart.drawings.show(d.id, !d.visible);
2602
2769
  this.refresh();
2603
2770
  }),
2604
- b.entry(d.locked ? "Unlock" : "Lock", d.locked ? "unlock" : "lock", () => {
2771
+ b.entry(d.locked ? this.t(TEXT5.unlock) : this.t(TEXT5.lock), d.locked ? "unlock" : "lock", () => {
2605
2772
  chart.drawings.lock(d.id, !d.locked);
2606
2773
  this.refresh();
2607
2774
  }),
2608
- b.entry("Duplicate", "clone", () => {
2775
+ b.entry(this.t(TEXT5.duplicate), "clone", () => {
2609
2776
  this.cloneInto(pass, d.id);
2610
2777
  this.render();
2611
2778
  }),
2612
2779
  // Front/back clear the WHOLE stack on a shared-z renderer — over or under the
2613
2780
  // candles and every indicator, not just the other drawings.
2614
- b.entry("Bring to front", "arrow-up", () => chart.drawings.bringToFront(d.id), true),
2615
- b.entry("Send to back", "arrow-down", () => chart.drawings.sendToBack(d.id))
2781
+ b.entry(this.t(TEXT5.bringToFront), "arrow-up", () => chart.drawings.bringToFront(d.id), true),
2782
+ b.entry(this.t(TEXT5.sendToBack), "arrow-down", () => chart.drawings.sendToBack(d.id))
2616
2783
  ];
2617
2784
  items.push(...this.groupingItems(b, d.id));
2618
- items.push(b.entry("Remove", "trash", () => chart.drawings.remove(d.id), true));
2785
+ items.push(b.entry(this.t(TEXT5.remove), "trash", () => chart.drawings.remove(d.id), true));
2619
2786
  return items;
2620
2787
  }
2621
2788
  /** The grouping half of a drawing's menu: what it can do about the bundle it is (or isn't)
@@ -2625,15 +2792,15 @@ var ObjectTree = class extends SidePanel {
2625
2792
  const mine = groupOf(this.groups, id);
2626
2793
  const pick = this.picked.has(id) && this.picked.size > 1 ? [...this.picked] : null;
2627
2794
  if (pick && canGroup(pick, this.groups)) {
2628
- items.push(b.entry(`Group selection (${pick.length})`, "group", () => this.makeGroup(pick), true));
2795
+ items.push(b.entry(this.t(TEXT5.groupPick, { count: pick.length }), "group", () => this.makeGroup(pick), true));
2629
2796
  } else if (!pick && mine === null) {
2630
- items.push(b.entry("New group", "group", () => this.makeGroup([id]), true));
2797
+ items.push(b.entry(this.t(TEXT5.newGroup), "group", () => this.makeGroup([id]), true));
2631
2798
  }
2632
2799
  const others = this.groups.filter((g) => g.id !== mine?.id);
2633
2800
  if (!pick && others.length > 0) {
2634
2801
  items.push(
2635
2802
  b.submenu(
2636
- mine === null ? "Add to group" : "Move to group",
2803
+ mine === null ? this.t(TEXT5.addToGroup) : this.t(TEXT5.moveToGroup),
2637
2804
  "folder-plus",
2638
2805
  others.map(
2639
2806
  (g) => b.entry(g.name, void 0, () => {
@@ -2647,7 +2814,7 @@ var ObjectTree = class extends SidePanel {
2647
2814
  }
2648
2815
  if (mine !== null) {
2649
2816
  items.push(
2650
- b.entry(`Remove from ${mine.name}`, "folder-minus", () => {
2817
+ b.entry(this.t(TEXT5.removeFromGroup, { group: mine.name }), "folder-minus", () => {
2651
2818
  this.groups = removeFromGroups(this.groups, [id]);
2652
2819
  this.render();
2653
2820
  }, items.length === 0)
@@ -2660,24 +2827,24 @@ var ObjectTree = class extends SidePanel {
2660
2827
  const members = pass.chart.drawings.all().filter((d) => g.ids.includes(d.id));
2661
2828
  const { allHidden, allLocked } = groupState(members);
2662
2829
  const items = [
2663
- b.entry("Rename\u2026", "pen", () => {
2830
+ b.entry(this.t(TEXT5.rename), "pen", () => {
2664
2831
  this.renaming = g.id;
2665
2832
  this.render();
2666
2833
  }),
2667
- b.entry(allHidden ? "Show all" : "Hide all", allHidden ? "eye" : "eye-off", () => this.setMembers(pass, ids, { visible: allHidden }), true),
2668
- b.entry(allLocked ? "Unlock all" : "Lock all", allLocked ? "unlock" : "lock", () => this.setMembers(pass, ids, { locked: !allLocked })),
2669
- b.entry("Select all", "group", () => {
2834
+ b.entry(allHidden ? this.t(TEXT5.showAll) : this.t(TEXT5.hideAll), allHidden ? "eye" : "eye-off", () => this.setMembers(pass, ids, { visible: allHidden }), true),
2835
+ b.entry(allLocked ? this.t(TEXT5.unlockAll) : this.t(TEXT5.lockAll), allLocked ? "unlock" : "lock", () => this.setMembers(pass, ids, { locked: !allLocked })),
2836
+ b.entry(this.t(TEXT5.selectAll), "group", () => {
2670
2837
  this.picked = new Set(ids);
2671
2838
  this.selectOnChart(ids);
2672
2839
  this.render();
2673
2840
  })
2674
2841
  ];
2675
2842
  if (members.length > 0) {
2676
- items.push(b.entry("Bring all to front", "arrow-up", () => this.restackGroup(pass, g, "front")));
2677
- items.push(b.entry("Send all to back", "arrow-down", () => this.restackGroup(pass, g, "back")));
2843
+ items.push(b.entry(this.t(TEXT5.bringAllToFront), "arrow-up", () => this.restackGroup(pass, g, "front")));
2844
+ items.push(b.entry(this.t(TEXT5.sendAllToBack), "arrow-down", () => this.restackGroup(pass, g, "back")));
2678
2845
  }
2679
- items.push(b.entry("Ungroup", "ungroup", () => this.removeGroup(pass, g.id, false), true));
2680
- items.push(b.entry("Remove all", "trash", () => this.removeGroup(pass, g.id, true)));
2846
+ items.push(b.entry(this.t(TEXT5.ungroup), "ungroup", () => this.removeGroup(pass, g.id, false), true));
2847
+ items.push(b.entry(this.t(TEXT5.removeAll), "trash", () => this.removeGroup(pass, g.id, true)));
2681
2848
  return items;
2682
2849
  }
2683
2850
  /** Forget everything that refers to a drawing or group the chart no longer has — otherwise a
@@ -2695,6 +2862,21 @@ var ObjectTree = class extends SidePanel {
2695
2862
  };
2696
2863
 
2697
2864
  // src/widget/data-window.ts
2865
+ var TEXT6 = {
2866
+ title: msg("widget.dataWindow.title", "Data window"),
2867
+ timeSection: msg("widget.dataWindow.timeSection", "Time"),
2868
+ date: msg("widget.dataWindow.date", "Date"),
2869
+ time: msg("widget.dataWindow.time", "Time"),
2870
+ open: msg("widget.dataWindow.open", "Open"),
2871
+ high: msg("widget.dataWindow.high", "High"),
2872
+ low: msg("widget.dataWindow.low", "Low"),
2873
+ close: msg("widget.dataWindow.close", "Close"),
2874
+ volume: msg("widget.dataWindow.volume", "Volume"),
2875
+ priceSection: msg("widget.dataWindow.priceSection", "Price"),
2876
+ noData: msg("widget.dataWindow.noData", "No data"),
2877
+ noReadout: msg("widget.dataWindow.noReadout", "This renderer provides no data readout.")
2878
+ };
2879
+ var DATA_WINDOW_TITLE = TEXT6.title;
2698
2880
  var STYLE_ID5 = "vela-widget-datawindow";
2699
2881
  var CSS5 = `
2700
2882
  .vela-dw-group {
@@ -2720,19 +2902,21 @@ var CSS5 = `
2720
2902
  }
2721
2903
  .vela-dw-row:hover { background: var(--vela-hover); }
2722
2904
  .vela-dw-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--vela-fg-bright); }
2723
- .vela-dw-value { margin-left: auto; font-variant-numeric: tabular-nums; white-space: nowrap; }
2905
+ /* Values are left-to-right isolates (dir="ltr"), so a sign keeps its place in a right-to-left page. */
2906
+ .vela-dw-value { margin-left: auto; font-variant-numeric: tabular-nums; white-space: nowrap; unicode-bidi: isolate; }
2724
2907
  .vela-dw-empty { padding: 20px 10px; text-align: center; color: var(--vela-fg-muted); font-size: 12px; }
2725
2908
  `;
2726
2909
  var REFRESH_EVENTS2 = ["bar", "context:changed", "indicator:added", "indicator:removed", "indicator:visibility", "indicator:moved", "pane:changed", "market:changed", "history:complete"];
2727
- var EM_DASH = "\u2014";
2728
- function dataWindowSections(readout) {
2910
+ var EM_DASH = "—";
2911
+ function dataWindowSections(readout, i18n) {
2912
+ const t = (m) => textOf(i18n, m);
2729
2913
  const sections = [];
2730
2914
  if (readout.date || readout.time) {
2731
2915
  sections.push({
2732
- title: "Time",
2916
+ title: t(TEXT6.timeSection),
2733
2917
  lines: [
2734
- { label: "Date", value: readout.date || EM_DASH, color: "" },
2735
- { label: "Time", value: readout.time || EM_DASH, color: "" }
2918
+ { label: t(TEXT6.date), value: readout.date || EM_DASH, color: "" },
2919
+ { label: t(TEXT6.time), value: readout.time || EM_DASH, color: "" }
2736
2920
  ]
2737
2921
  });
2738
2922
  }
@@ -2740,13 +2924,13 @@ function dataWindowSections(readout) {
2740
2924
  if (ohlc) {
2741
2925
  const color = ohlc.up ? "var(--vela-up)" : "var(--vela-down)";
2742
2926
  const lines = [
2743
- { label: "Open", value: ohlc.o, color },
2744
- { label: "High", value: ohlc.h, color },
2745
- { label: "Low", value: ohlc.l, color },
2746
- { label: "Close", value: ohlc.c, color }
2927
+ { label: t(TEXT6.open), value: ohlc.o, color },
2928
+ { label: t(TEXT6.high), value: ohlc.h, color },
2929
+ { label: t(TEXT6.low), value: ohlc.l, color },
2930
+ { label: t(TEXT6.close), value: ohlc.c, color }
2747
2931
  ];
2748
- if (ohlc.vol !== void 0) lines.push({ label: "Volume", value: ohlc.vol, color });
2749
- sections.push({ title: "Price", lines });
2932
+ if (ohlc.vol !== void 0) lines.push({ label: t(TEXT6.volume), value: ohlc.vol, color });
2933
+ sections.push({ title: t(TEXT6.priceSection), lines });
2750
2934
  }
2751
2935
  for (const group of readout.groups) {
2752
2936
  sections.push({ title: group.name, lines: group.rows.map((r) => ({ label: r.label, value: r.value, color: r.color })) });
@@ -2754,8 +2938,8 @@ function dataWindowSections(readout) {
2754
2938
  return sections;
2755
2939
  }
2756
2940
  var DataWindow = class extends SidePanel {
2757
- constructor(host) {
2758
- super(host, "Data window", "vela-dw");
2941
+ constructor(host, i18n) {
2942
+ super(host, TEXT6.title, "vela-dw", i18n ? { i18n } : {});
2759
2943
  this.chart = null;
2760
2944
  this.unsubs = [];
2761
2945
  injectStyles(STYLE_ID5, CSS5, host.ownerDocument);
@@ -2776,6 +2960,10 @@ var DataWindow = class extends SidePanel {
2776
2960
  this.detach();
2777
2961
  super.destroy();
2778
2962
  }
2963
+ relocalize() {
2964
+ super.relocalize();
2965
+ this.refresh();
2966
+ }
2779
2967
  detach() {
2780
2968
  for (const u of this.unsubs) u();
2781
2969
  this.unsubs = [];
@@ -2786,11 +2974,11 @@ var DataWindow = class extends SidePanel {
2786
2974
  const doc = this.el.ownerDocument;
2787
2975
  const readout = this.chart.renderer.dataWindowReadout();
2788
2976
  this.body.replaceChildren();
2789
- const sections = readout ? dataWindowSections(readout) : [];
2977
+ const sections = readout ? dataWindowSections(readout, this.i18n) : [];
2790
2978
  if (sections.length === 0) {
2791
2979
  const empty = doc.createElement("div");
2792
2980
  empty.className = "vela-dw-empty";
2793
- empty.textContent = readout ? "No data" : "This renderer provides no data readout.";
2981
+ empty.textContent = this.t(readout ? TEXT6.noData : TEXT6.noReadout);
2794
2982
  this.body.appendChild(empty);
2795
2983
  return;
2796
2984
  }
@@ -2807,6 +2995,7 @@ var DataWindow = class extends SidePanel {
2807
2995
  label.textContent = line.label;
2808
2996
  const value = doc.createElement("span");
2809
2997
  value.className = "vela-dw-value";
2998
+ value.dir = "ltr";
2810
2999
  value.textContent = line.value;
2811
3000
  if (line.color) value.style.color = line.color;
2812
3001
  row.append(label, value);
@@ -2852,7 +3041,8 @@ var PanelDock = class {
2852
3041
  minWidth: desc.minWidth,
2853
3042
  maxWidth: desc.maxWidth,
2854
3043
  overlay: desc.overlay,
2855
- maximizable: desc.maximizable
3044
+ maximizable: desc.maximizable,
3045
+ ...this.deps.i18n ? { i18n: this.deps.i18n } : {}
2856
3046
  });
2857
3047
  const entry = {
2858
3048
  id: desc.id,
@@ -2901,7 +3091,12 @@ var PanelDock = class {
2901
3091
  /** The docked panels in dock order — what a non-topbar chrome (the mobile
2902
3092
  * three-dots drawer) lists so every panel stays reachable there too. */
2903
3093
  list() {
2904
- return this.entries.map((e) => ({ id: e.id, title: e.title, icon: e.icon }));
3094
+ return this.entries.map((e) => ({ id: e.id, title: textOf(this.deps.i18n, e.title), icon: e.icon }));
3095
+ }
3096
+ /** Re-read every panel's header and the toggle tooltips in the current language. */
3097
+ relocalize() {
3098
+ for (const entry of this.entries) entry.panel.relocalize();
3099
+ this.publish();
2905
3100
  }
2906
3101
  /** The dock's persistable state, or null when there is nothing worth saving. */
2907
3102
  getState() {
@@ -2978,7 +3173,7 @@ var PanelDock = class {
2978
3173
  }
2979
3174
  /** Push the current toggle group to the chrome, pressed states included. */
2980
3175
  publish() {
2981
- const buttons = this.entries.map((e) => ({ id: e.id, title: e.title, icon: e.icon }));
3176
+ const buttons = this.list().slice();
2982
3177
  this.deps.chrome.setPanelButtons(buttons, (id) => this.toggle(id));
2983
3178
  for (const entry of this.entries) this.deps.chrome.setPanelActive(entry.id, entry.panel.open);
2984
3179
  }
@@ -3053,7 +3248,7 @@ var STYLE_ID6 = "vela-widget-symbolpicker";
3053
3248
  var CSS6 = `
3054
3249
  /* The dialog body is flush (a flex column): search + market tabs are a fixed head
3055
3250
  and the result list is the ONLY scroller, in both layouts. A sticky head over a
3056
- scrolling padded body is not an option \u2014 browsers pin sticky boxes to the
3251
+ scrolling padded body is not an option — browsers pin sticky boxes to the
3057
3252
  scroller's content box, so the head lands one padding below the top and covers
3058
3253
  the first row. */
3059
3254
  .vela-sp-head { flex: none; padding: var(--vela-space-4) var(--vela-space-4) 0; }
@@ -3081,7 +3276,7 @@ var CSS6 = `
3081
3276
  .vela-sp-input::placeholder { text-transform: none; }
3082
3277
  .vela-sp-tabs { display: flex; gap: 14px; margin: 12px 2px 0; border-bottom: 1px solid var(--vela-border); padding-bottom: 8px; }
3083
3278
  /* Mobile (fullscreen dialog): the asset-class strip scrolls sideways instead of
3084
- overflowing the body, and the result list stops capping itself \u2014 it fills the
3279
+ overflowing the body, and the result list stops capping itself — it fills the
3085
3280
  rest of the screen. The bottom inset (+ safe area) is the kit's: every mobile
3086
3281
  dialog body carries it, flush or not, so the list adds none of its own. */
3087
3282
  [data-layout='mobile'] .vela-sp-tabs { overflow-x: auto; scrollbar-width: none; gap: 6px; }
@@ -3148,7 +3343,7 @@ var CSS6 = `
3148
3343
  letter-spacing: 0.06em;
3149
3344
  text-transform: uppercase;
3150
3345
  }
3151
- /* Provider brand marks \u2014 fixed by the venue, deliberately outside the theme palette. */
3346
+ /* Provider brand marks — fixed by the venue, deliberately outside the theme palette. */
3152
3347
  .vela-sp-badge[data-p='binance'] { color: #f0b90b; } /* palette-exempt: venue brand mark */
3153
3348
  .vela-sp-badge[data-p='hyperliquid'] { color: #50d2c1; } /* palette-exempt: venue brand mark */
3154
3349
  .vela-sp-empty { padding: var(--vela-space-3); color: var(--vela-fg-muted); text-align: center; }
@@ -3168,6 +3363,30 @@ var CSS6 = `
3168
3363
  .vela-sp-expander:hover { background: var(--vela-surface-elev); color: var(--vela-fg); }
3169
3364
  .vela-sp-row[data-member] { padding-left: 34px; }
3170
3365
  `;
3366
+ var TEXT7 = {
3367
+ title: msg("widget.symbolPicker.title", "Symbol Search"),
3368
+ search: msg("widget.symbolPicker.search", "Search symbol…"),
3369
+ noMatch: msg("widget.symbolPicker.noMatch", "No symbols match."),
3370
+ empty: msg("widget.symbolPicker.empty", "No symbols available yet."),
3371
+ showContracts: msg("widget.symbolPicker.showContracts", "Show contracts")
3372
+ };
3373
+ var TABS = [
3374
+ { id: "all", label: msg("widget.symbolPicker.tab.all", "All") },
3375
+ { id: "stocks", label: msg("widget.symbolPicker.tab.stocks", "Stocks") },
3376
+ { id: "etfs", label: msg("widget.symbolPicker.tab.etfs", "ETFs") },
3377
+ { id: "crypto", label: msg("widget.symbolPicker.tab.crypto", "Crypto") },
3378
+ { id: "futures", label: msg("widget.symbolPicker.tab.futures", "Futures") },
3379
+ { id: "forex", label: msg("widget.symbolPicker.tab.forex", "Forex") },
3380
+ { id: "commodities", label: msg("widget.symbolPicker.tab.commodities", "Commodities") }
3381
+ ];
3382
+ var TAB_TYPES = {
3383
+ crypto: ["crypto"],
3384
+ stocks: ["stock"],
3385
+ etfs: ["etf"],
3386
+ futures: ["futures", "root"],
3387
+ forex: ["forex"],
3388
+ commodities: ["commodity"]
3389
+ };
3171
3390
  var PAGE = 100;
3172
3391
  var SymbolPicker = class {
3173
3392
  constructor(opts) {
@@ -3176,7 +3395,8 @@ var SymbolPicker = class {
3176
3395
  this.rows = [];
3177
3396
  this.highlighted = 0;
3178
3397
  this.seed = "";
3179
- this.activeTab = "All";
3398
+ this.activeTab = "all";
3399
+ this.tabButtons = [];
3180
3400
  this.visible = PAGE;
3181
3401
  /** The last filter pass returned fewer raw rows than asked — the pool is drained
3182
3402
  * (checked BEFORE folding: folding shortens pages without meaning exhaustion). */
@@ -3190,7 +3410,7 @@ var SymbolPicker = class {
3190
3410
  injectStyles(STYLE_ID6, CSS6, doc);
3191
3411
  this.input = doc.createElement("input");
3192
3412
  this.input.className = "vela-sp-input";
3193
- this.input.placeholder = "Search symbol\u2026";
3413
+ this.input.placeholder = textOf(opts.i18n, TEXT7.search);
3194
3414
  this.input.setAttribute("spellcheck", "false");
3195
3415
  const searchRow = doc.createElement("div");
3196
3416
  searchRow.className = "vela-sp-searchrow";
@@ -3199,13 +3419,15 @@ var SymbolPicker = class {
3199
3419
  this.tabs.className = "vela-sp-tabs";
3200
3420
  const head = doc.createElement("div");
3201
3421
  head.className = "vela-sp-head";
3202
- for (const t of ["All", "Stocks", "ETFs", "Crypto", "Futures", "Forex", "Commodities"]) {
3422
+ for (const tab of TABS) {
3203
3423
  const b = doc.createElement("button");
3204
3424
  b.className = "vela-sp-tab";
3205
- b.textContent = t;
3206
- if (t === this.activeTab) b.dataset.active = "1";
3425
+ b.dataset.tab = tab.id;
3426
+ b.textContent = textOf(opts.i18n, tab.label);
3427
+ this.tabButtons.push({ el: b, label: tab.label });
3428
+ if (tab.id === this.activeTab) b.dataset.active = "1";
3207
3429
  b.addEventListener("click", () => {
3208
- this.activeTab = t;
3430
+ this.activeTab = tab.id;
3209
3431
  for (const c of this.tabs.children) delete c.dataset.active;
3210
3432
  b.dataset.active = "1";
3211
3433
  this.refresh();
@@ -3222,13 +3444,14 @@ var SymbolPicker = class {
3222
3444
  this.grow();
3223
3445
  });
3224
3446
  this.dialog = new Dialog({
3225
- title: "Symbol Search",
3447
+ title: textOf(opts.i18n, TEXT7.title),
3226
3448
  host: opts.host,
3227
3449
  closeOnInteractOutside: true,
3228
3450
  flush: true,
3229
3451
  content: (body) => body.append(head, this.list),
3230
3452
  onOpenChange: (open) => {
3231
3453
  if (open) {
3454
+ this.relabel();
3232
3455
  this.input.value = this.seed.toUpperCase();
3233
3456
  this.seed = "";
3234
3457
  this.refresh();
@@ -3352,7 +3575,7 @@ var SymbolPicker = class {
3352
3575
  Promise.resolve([...raw]).then((copy) => hook(copy)).then((out) => {
3353
3576
  this.ranked = { key, result: dedupeSymbols(out) };
3354
3577
  }).catch((err) => {
3355
- console.warn("[vela] symbol ranking failed \u2014 pool order kept:", err);
3578
+ console.warn("[vela] symbol ranking failed — pool order kept:", err);
3356
3579
  this.ranked = { key, result: [...raw] };
3357
3580
  }).finally(() => {
3358
3581
  this.ranking = false;
@@ -3361,10 +3584,17 @@ var SymbolPicker = class {
3361
3584
  }
3362
3585
  return this.ranked?.result ?? raw;
3363
3586
  }
3587
+ /** The dialog's fixed text in the shell's language, re-read on every open. */
3588
+ relabel() {
3589
+ const i18n = this.opts.i18n;
3590
+ this.dialog.titleEl.textContent = textOf(i18n, TEXT7.title);
3591
+ this.input.placeholder = textOf(i18n, TEXT7.search);
3592
+ for (const { el, label } of this.tabButtons) el.textContent = textOf(i18n, label);
3593
+ }
3364
3594
  computeRows() {
3365
- const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Futures: ["futures", "root"], Forex: ["forex"], Commodities: ["commodity"] };
3366
3595
  const all = this.pool();
3367
- const pool = this.activeTab === "All" ? all : all.filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
3596
+ const tab = this.activeTab;
3597
+ const pool = tab === "all" ? all : all.filter((s) => TAB_TYPES[tab].includes((s.type ?? "").toLowerCase()) || tab === "crypto" && (s.type ?? "").toLowerCase() === "futures");
3368
3598
  const filtered = filterSymbols(pool, this.input.value, this.visible, symbolRanking() ? false : TOP_TICKERS);
3369
3599
  this.exhausted = filtered.length < this.visible;
3370
3600
  const folded = foldGroups(filtered);
@@ -3392,7 +3622,7 @@ var SymbolPicker = class {
3392
3622
  if (!this.rows.length) {
3393
3623
  const empty = doc.createElement("div");
3394
3624
  empty.className = "vela-sp-empty";
3395
- empty.textContent = this.input.value ? "No symbols match." : "No symbols indexed (provider still loading?).";
3625
+ empty.textContent = textOf(this.opts.i18n, this.input.value ? TEXT7.noMatch : TEXT7.empty);
3396
3626
  this.list.appendChild(empty);
3397
3627
  return;
3398
3628
  }
@@ -3428,7 +3658,7 @@ var SymbolPicker = class {
3428
3658
  row.dataset.group = "1";
3429
3659
  const expander = doc.createElement("button");
3430
3660
  expander.className = "vela-sp-expander";
3431
- expander.setAttribute("aria-label", "Show contracts");
3661
+ expander.setAttribute("aria-label", textOf(this.opts.i18n, TEXT7.showContracts));
3432
3662
  expander.appendChild(iconEl(this.expanded.has(groupKeyOf(s)) ? "chevron-down" : "chevron-right", doc));
3433
3663
  row.appendChild(expander);
3434
3664
  } else if (s.group != null && this.expanded.has(groupKeyOf(s))) row.dataset.member = "1";
@@ -3444,6 +3674,17 @@ var SymbolPicker = class {
3444
3674
  };
3445
3675
 
3446
3676
  // src/widget/indicator-picker.ts
3677
+ var TEXT8 = {
3678
+ title: msg("widget.indicatorPicker.title", "Indicators"),
3679
+ search: msg("widget.indicatorPicker.search", "Search…"),
3680
+ beta: msg("widget.indicatorPicker.beta", "beta"),
3681
+ remove: msg("widget.indicatorPicker.remove", "Remove from chart"),
3682
+ onChart: msg("widget.indicatorPicker.onChart", "On chart · {count}"),
3683
+ general: msg("widget.indicatorPicker.general", "General"),
3684
+ noMatch: msg("widget.indicatorPicker.noMatch", "No indicators match."),
3685
+ empty: msg("widget.indicatorPicker.empty", "No indicators in the manifest.")
3686
+ };
3687
+ var GENERAL = "General";
3447
3688
  var STYLE_ID7 = "vela-widget-indpicker";
3448
3689
  var CSS7 = `
3449
3690
  .vela-ip-searchrow {
@@ -3461,7 +3702,7 @@ var CSS7 = `
3461
3702
  .vela-ip-searchrow .vela-icon { color: var(--vela-fg-muted); }
3462
3703
  .vela-ip-search { flex: 1; background: transparent; color: var(--vela-fg); border: none; font-size: 14px; outline: none; }
3463
3704
  .vela-ip-list { max-height: 50vh; overflow: auto; min-width: 380px; }
3464
- /* Mobile (fullscreen dialog): no width floor \u2014 380px would overflow a phone \u2014
3705
+ /* Mobile (fullscreen dialog): no width floor — 380px would overflow a phone —
3465
3706
  and no height cap; the fullscreen body owns the scrolling. */
3466
3707
  [data-layout='mobile'] .vela-ip-list { min-width: 0; max-height: none; }
3467
3708
  .vela-ip-list::-webkit-scrollbar { width: 8px; }
@@ -3525,7 +3766,7 @@ var IndicatorPicker = class {
3525
3766
  injectStyles(STYLE_ID7, CSS7, doc);
3526
3767
  this.search = doc.createElement("input");
3527
3768
  this.search.className = "vela-ip-search";
3528
- this.search.placeholder = "Search\u2026";
3769
+ this.search.placeholder = this.t(TEXT8.search);
3529
3770
  this.search.spellcheck = false;
3530
3771
  this.search.addEventListener("input", () => this.refresh());
3531
3772
  const searchRow = doc.createElement("div");
@@ -3534,7 +3775,7 @@ var IndicatorPicker = class {
3534
3775
  this.list = doc.createElement("div");
3535
3776
  this.list.className = "vela-ip-list";
3536
3777
  this.dialog = new Dialog({
3537
- title: "Indicators",
3778
+ title: this.t(TEXT8.title),
3538
3779
  host: opts.host,
3539
3780
  draggable: true,
3540
3781
  closeOnInteractOutside: true,
@@ -3544,6 +3785,7 @@ var IndicatorPicker = class {
3544
3785
  this.isOpen = open;
3545
3786
  if (open) {
3546
3787
  this.search.value = "";
3788
+ this.relabel();
3547
3789
  this.refresh();
3548
3790
  if (!this.search.closest('[data-layout="mobile"]')) setTimeout(() => this.search.focus(), 0);
3549
3791
  }
@@ -3565,6 +3807,14 @@ var IndicatorPicker = class {
3565
3807
  open() {
3566
3808
  this.dialog.show();
3567
3809
  }
3810
+ /** The dialog's fixed text in the shell's language, re-read on every open. */
3811
+ relabel() {
3812
+ this.dialog.titleEl.textContent = this.t(TEXT8.title);
3813
+ this.search.placeholder = this.t(TEXT8.search);
3814
+ }
3815
+ t(text, values) {
3816
+ return textOf(this.opts.i18n, text, values);
3817
+ }
3568
3818
  /** Re-render the lists if the dialog is open (async catalog updates land late). */
3569
3819
  sync() {
3570
3820
  if (this.isOpen) this.refresh();
@@ -3577,11 +3827,21 @@ var IndicatorPicker = class {
3577
3827
  }
3578
3828
  refresh() {
3579
3829
  const doc = this.list.ownerDocument;
3580
- const q = this.search.value.trim().toLowerCase();
3830
+ const raw = this.search.value.trim();
3831
+ const locale = this.opts.i18n?.locale;
3832
+ const lower = (s) => {
3833
+ try {
3834
+ return s.toLocaleLowerCase(locale);
3835
+ } catch {
3836
+ return s.toLowerCase();
3837
+ }
3838
+ };
3839
+ const q = lower(raw);
3840
+ const qPlain = raw.toLowerCase();
3581
3841
  const library = this.opts.library();
3582
3842
  const instances = this.opts.onChart();
3583
3843
  this.list.replaceChildren();
3584
- const match = (r) => !q || r.name.toLowerCase().includes(q);
3844
+ const match = (r) => !raw || [r.name, ...r.aliases ?? []].some((n) => lower(n).includes(q) || n.toLowerCase().includes(qPlain));
3585
3845
  const rowEl = (r, opts) => {
3586
3846
  const row = doc.createElement("div");
3587
3847
  row.className = "vela-ip-row";
@@ -3595,7 +3855,7 @@ var IndicatorPicker = class {
3595
3855
  if (r.beta) {
3596
3856
  const beta = doc.createElement("span");
3597
3857
  beta.className = "vela-ip-badge";
3598
- beta.textContent = "beta";
3858
+ beta.textContent = this.t(TEXT8.beta);
3599
3859
  row.appendChild(beta);
3600
3860
  }
3601
3861
  if (opts.instance !== void 0) {
@@ -3609,7 +3869,7 @@ var IndicatorPicker = class {
3609
3869
  const trash = doc.createElement("button");
3610
3870
  trash.className = "vela-ip-trash";
3611
3871
  trash.appendChild(iconEl("trash", doc));
3612
- trash.title = "Remove from chart";
3872
+ trash.title = this.t(TEXT8.remove);
3613
3873
  row.appendChild(trash);
3614
3874
  }
3615
3875
  return row;
@@ -3620,7 +3880,7 @@ var IndicatorPicker = class {
3620
3880
  card.className = "vela-ip-oncard";
3621
3881
  const title = doc.createElement("div");
3622
3882
  title.className = "vela-ip-group";
3623
- title.textContent = `On chart \xB7 ${onChart.length}`;
3883
+ title.textContent = this.t(TEXT8.onChart, { count: onChart.length });
3624
3884
  card.appendChild(title);
3625
3885
  for (const [r, i] of onChart) card.appendChild(rowEl(r, { instance: i }));
3626
3886
  this.list.appendChild(card);
@@ -3628,7 +3888,7 @@ var IndicatorPicker = class {
3628
3888
  const byCat = /* @__PURE__ */ new Map();
3629
3889
  library.forEach((r, i) => {
3630
3890
  if (!match(r)) return;
3631
- const cat = r.category ?? "General";
3891
+ const cat = r.category ?? GENERAL;
3632
3892
  const bucket = byCat.get(cat) ?? [];
3633
3893
  bucket.push([r, i]);
3634
3894
  byCat.set(cat, bucket);
@@ -3636,14 +3896,14 @@ var IndicatorPicker = class {
3636
3896
  for (const [cat, bucket] of byCat) {
3637
3897
  const title = doc.createElement("div");
3638
3898
  title.className = "vela-ip-group";
3639
- title.textContent = cat;
3899
+ title.textContent = cat === GENERAL ? this.t(TEXT8.general) : cat;
3640
3900
  this.list.appendChild(title);
3641
3901
  for (const [r, i] of bucket) this.list.appendChild(rowEl(r, { library: i }));
3642
3902
  }
3643
3903
  if (!onChart.length && byCat.size === 0) {
3644
3904
  const empty = doc.createElement("div");
3645
3905
  empty.className = "vela-ip-empty";
3646
- empty.textContent = library.length ? "No indicators match." : "No indicators in the manifest.";
3906
+ empty.textContent = this.t(library.length ? TEXT8.noMatch : TEXT8.empty);
3647
3907
  this.list.appendChild(empty);
3648
3908
  }
3649
3909
  }
@@ -3671,8 +3931,18 @@ var CSS8 = `
3671
3931
  .vela-tq-hint { margin-top: var(--vela-space-2); text-align: center; color: var(--vela-fg-muted); min-height: 1.2em; }
3672
3932
  .vela-tq-hint[data-invalid] { color: var(--vela-danger); }
3673
3933
  `;
3934
+ var TEXT9 = {
3935
+ title: msg("widget.timeframeQuick.title", "Change timeframe"),
3936
+ // The codes are what the field accepts, typed as is in every language.
3937
+ example: msg("widget.timeframeQuick.example", "e.g. 15, 4H, D, 3M"),
3938
+ invalid: msg("widget.timeframeQuick.invalid", "Not a timeframe")
3939
+ };
3674
3940
  var TimeframeQuick = class {
3941
+ t(text) {
3942
+ return textOf(this.i18n, text);
3943
+ }
3675
3944
  constructor(opts) {
3945
+ this.i18n = opts.i18n ?? null;
3676
3946
  const doc = (opts.host ?? document.body).ownerDocument;
3677
3947
  injectStyles(STYLE_ID8, CSS8, doc);
3678
3948
  this.input = doc.createElement("input");
@@ -3681,7 +3951,7 @@ var TimeframeQuick = class {
3681
3951
  this.hint = doc.createElement("div");
3682
3952
  this.hint.className = "vela-tq-hint";
3683
3953
  this.dialog = new Dialog({
3684
- title: "Change timeframe",
3954
+ title: this.t(TEXT9.title),
3685
3955
  host: opts.host,
3686
3956
  draggable: true,
3687
3957
  closeOnInteractOutside: true,
@@ -3703,7 +3973,12 @@ var TimeframeQuick = class {
3703
3973
  }
3704
3974
  });
3705
3975
  }
3976
+ /** The dialog's title in the shell's language, re-read on every open. */
3977
+ relabel() {
3978
+ this.dialog.titleEl.textContent = this.t(TEXT9.title);
3979
+ }
3706
3980
  open(seed = "") {
3981
+ this.relabel();
3707
3982
  this.dialog.show();
3708
3983
  this.input.value = seed.toUpperCase();
3709
3984
  this.renderHint();
@@ -3734,21 +4009,32 @@ var TimeframeQuick = class {
3734
4009
  }
3735
4010
  renderHint() {
3736
4011
  const raw = this.input.value.trim();
3737
- const parsed = parseTimeframe(raw);
4012
+ const parsed = parseTimeframe(raw, this.i18n);
3738
4013
  if (!raw) {
3739
- this.hint.textContent = "e.g. 15, 4H, D, 3M";
4014
+ this.hint.textContent = this.t(TEXT9.example);
3740
4015
  delete this.hint.dataset.invalid;
3741
4016
  } else if (parsed.valid) {
3742
4017
  this.hint.textContent = parsed.label ?? "";
3743
4018
  delete this.hint.dataset.invalid;
3744
4019
  } else {
3745
- this.hint.textContent = "Not a timeframe";
4020
+ this.hint.textContent = this.t(TEXT9.invalid);
3746
4021
  this.hint.dataset.invalid = "1";
3747
4022
  }
3748
4023
  }
3749
4024
  };
3750
4025
 
3751
4026
  // src/widget/shortcuts-help.ts
4027
+ var TEXT10 = {
4028
+ title: msg("widget.shortcuts.title", "Keyboard shortcuts"),
4029
+ mouse: msg("widget.shortcuts.mouse", "Mouse"),
4030
+ scroll: msg("widget.shortcuts.scrollHistory", "Scroll through history"),
4031
+ scrollKeys: msg("widget.shortcuts.scrollHistoryKeys", "Shift+Scroll"),
4032
+ measure: msg("widget.shortcuts.measure", "Measure from the press point"),
4033
+ measureKeys: msg("widget.shortcuts.measureKeys", "Shift+Click"),
4034
+ erase: msg("widget.shortcuts.deleteDrawing", "Delete the drawing under the cursor"),
4035
+ eraseKeys: msg("widget.shortcuts.deleteDrawingKeys", "Middle-click"),
4036
+ typing: msg("widget.shortcuts.typing", "Typing a letter or 0 opens the symbol search; typing a digit from 1 to 9 opens the timeframe entry.")
4037
+ };
3752
4038
  var STYLE_ID9 = "vela-widget-shortcuts";
3753
4039
  var CSS9 = `
3754
4040
  .vela-sh-cat {
@@ -3772,13 +4058,15 @@ var CSS9 = `
3772
4058
  .vela-sh-static { color: var(--vela-fg-muted); font-size: var(--vela-font-size-sm); margin-top: var(--vela-space-2); }
3773
4059
  `;
3774
4060
  var ShortcutsHelp = class {
3775
- constructor(keymap, host, onOpenChange) {
4061
+ /** `i18n` returns the shell's current translator; the panel reads it each time it opens. */
4062
+ constructor(keymap, host, onOpenChange, i18n) {
3776
4063
  this.keymap = keymap;
4064
+ this.i18n = i18n ?? englishTranslator;
3777
4065
  const doc = (host ?? document.body).ownerDocument;
3778
4066
  injectStyles(STYLE_ID9, CSS9, doc);
3779
4067
  this.list = doc.createElement("div");
3780
4068
  this.dialog = new Dialog({
3781
- title: "Keyboard shortcuts",
4069
+ title: this.i18n().t(TEXT10.title),
3782
4070
  host,
3783
4071
  closeOnInteractOutside: true,
3784
4072
  content: (body) => body.appendChild(this.list),
@@ -3799,6 +4087,9 @@ var ShortcutsHelp = class {
3799
4087
  }
3800
4088
  refresh() {
3801
4089
  const doc = this.list.ownerDocument;
4090
+ const i18n = this.i18n();
4091
+ const t = (m) => i18n.t(m);
4092
+ this.dialog.titleEl.textContent = t(TEXT10.title);
3802
4093
  this.list.replaceChildren();
3803
4094
  const addCategory = (text) => {
3804
4095
  const cat = doc.createElement("div");
@@ -3824,7 +4115,7 @@ var ShortcutsHelp = class {
3824
4115
  this.list.appendChild(row);
3825
4116
  };
3826
4117
  const byCategory = /* @__PURE__ */ new Map();
3827
- for (const b of this.keymap.bindings()) {
4118
+ for (const b of this.keymap.bindings(i18n)) {
3828
4119
  const bucket = byCategory.get(b.category) ?? [];
3829
4120
  bucket.push(b);
3830
4121
  byCategory.set(b.category, bucket);
@@ -3833,13 +4124,13 @@ var ShortcutsHelp = class {
3833
4124
  addCategory(category);
3834
4125
  for (const b of bindings) addRow(b.label, b.display);
3835
4126
  }
3836
- addCategory("Mouse");
3837
- addRow("Scroll through history", ["Shift+Scroll"]);
3838
- addRow("Measure from the press point", ["Shift+Click"]);
3839
- addRow("Delete the drawing under the cursor", ["Middle-click"]);
4127
+ addCategory(t(TEXT10.mouse));
4128
+ addRow(t(TEXT10.scroll), [t(TEXT10.scrollKeys)]);
4129
+ addRow(t(TEXT10.measure), [t(TEXT10.measureKeys)]);
4130
+ addRow(t(TEXT10.erase), [t(TEXT10.eraseKeys)]);
3840
4131
  const s = doc.createElement("div");
3841
4132
  s.className = "vela-sh-static";
3842
- s.textContent = "Typing a letter or 0 opens the symbol search; typing a digit from 1 to 9 opens the timeframe entry.";
4133
+ s.textContent = t(TEXT10.typing);
3843
4134
  this.list.appendChild(s);
3844
4135
  }
3845
4136
  };
@@ -4190,7 +4481,7 @@ function decimalsFor(ref) {
4190
4481
  return a >= 1 ? 2 : a >= 0.01 ? 4 : 6;
4191
4482
  }
4192
4483
  function fmtPrice(n, dp) {
4193
- if (n == null || !Number.isFinite(n)) return "\u2014";
4484
+ if (n == null || !Number.isFinite(n)) return "—";
4194
4485
  const d = dp ?? decimalsFor(n);
4195
4486
  return n.toLocaleString("en-US", { minimumFractionDigits: d, maximumFractionDigits: d });
4196
4487
  }
@@ -4201,13 +4492,25 @@ function fmtChange(open, close) {
4201
4492
  const sign = diff >= 0 ? "+" : "";
4202
4493
  return `${sign}${fmtPrice(diff, decimalsFor(close))} (${sign}${pct.toFixed(2)}%)`;
4203
4494
  }
4204
- function fmtChangePct(open, close) {
4205
- if (open == null || close == null || !Number.isFinite(open) || !Number.isFinite(close) || open === 0) return "";
4206
- const pct = (close - open) / open * 100;
4207
- return `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`;
4208
- }
4209
4495
 
4210
4496
  // src/widget/statusline-model.ts
4497
+ var CELL_LABELS = {
4498
+ open: msg("widget.statusline.openShort", "O"),
4499
+ high: msg("widget.statusline.highShort", "H"),
4500
+ low: msg("widget.statusline.lowShort", "L"),
4501
+ close: msg("widget.statusline.closeShort", "C")
4502
+ };
4503
+ var STATUSLINE_PART_LABELS = {
4504
+ logo: msg("widget.statusline.part.logo", "Symbol logo"),
4505
+ name: msg("widget.statusline.part.name", "Symbol name"),
4506
+ market: msg("widget.statusline.part.market", "Market status"),
4507
+ ohlc: msg("widget.statusline.part.ohlc", "OHLC values"),
4508
+ change: msg("widget.statusline.part.change", "Bar change values")
4509
+ };
4510
+ var TEXT11 = {
4511
+ hideChart: msg("widget.statusline.hideChart", "Hide chart"),
4512
+ showChart: msg("widget.statusline.showChart", "Show chart")
4513
+ };
4211
4514
  function segmentVisibility(parts, chartHidden = false) {
4212
4515
  return { avatar: parts.logo, symbol: parts.name, meta: parts.name, market: parts.market, ohlc: parts.ohlc && !chartHidden, change: parts.change && !chartHidden, eye: chartHidden };
4213
4516
  }
@@ -4217,28 +4520,31 @@ var STATUSLINE_LADDER = [
4217
4520
  { stacked: true, level: "compact" },
4218
4521
  { stacked: true, level: "minimal" }
4219
4522
  ];
4220
- function readoutCells(level, readout) {
4523
+ function readoutCells(level, readout, i18n) {
4221
4524
  if (readout === "value") return [{ key: "close", label: "" }];
4525
+ const label = (key) => textOf(i18n, CELL_LABELS[key]);
4222
4526
  if (level === "full") {
4223
4527
  return [
4224
- { key: "open", label: "O" },
4225
- { key: "high", label: "H" },
4226
- { key: "low", label: "L" },
4227
- { key: "close", label: "C" }
4528
+ { key: "open", label: label("open") },
4529
+ { key: "high", label: label("high") },
4530
+ { key: "low", label: label("low") },
4531
+ { key: "close", label: label("close") }
4228
4532
  ];
4229
4533
  }
4230
- return [{ key: "close", label: level === "compact" ? "C" : "" }];
4534
+ return [{ key: "close", label: level === "compact" ? label("close") : "" }];
4231
4535
  }
4232
- function statuslineMenuItems(parts, chartVisible) {
4536
+ function statuslineMenuItems(parts, chartVisible, i18n) {
4537
+ const t = (m) => textOf(i18n, m);
4233
4538
  return [
4234
- { id: "part:logo", label: "Symbol logo", checked: parts.logo },
4235
- { id: "part:name", label: "Symbol name", checked: parts.name },
4236
- { id: "part:market", label: "Market status", checked: parts.market },
4237
- { id: "part:ohlc", label: "OHLC values", checked: parts.ohlc },
4238
- { id: "part:change", label: "Bar change values", checked: parts.change },
4239
- { id: "chart", label: chartVisible ? "Hide chart" : "Show chart", separatorBefore: true }
4539
+ { id: "part:logo", label: t(STATUSLINE_PART_LABELS.logo), checked: parts.logo },
4540
+ { id: "part:name", label: t(STATUSLINE_PART_LABELS.name), checked: parts.name },
4541
+ { id: "part:market", label: t(STATUSLINE_PART_LABELS.market), checked: parts.market },
4542
+ { id: "part:ohlc", label: t(STATUSLINE_PART_LABELS.ohlc), checked: parts.ohlc },
4543
+ { id: "part:change", label: t(STATUSLINE_PART_LABELS.change), checked: parts.change },
4544
+ { id: "chart", label: t(chartVisible ? TEXT11.hideChart : TEXT11.showChart), separatorBefore: true }
4240
4545
  ];
4241
4546
  }
4547
+ var SHOW_CHART_LABEL = TEXT11.showChart;
4242
4548
 
4243
4549
  // src/widget/statusline.ts
4244
4550
  var STYLE_ID10 = "vela-widget-statusline";
@@ -4247,7 +4553,7 @@ var CSS10 = `
4247
4553
  position: absolute;
4248
4554
  top: var(--vela-space-2);
4249
4555
  /* Track the indicator legend's left edge: the renderer publishes its toolbar gutter
4250
- * on the mount container, and the legend sits 10px into the plot to its right \u2014
4556
+ * on the mount container, and the legend sits 10px into the plot to its right —
4251
4557
  * so the two columns stay aligned whether the toolbar is docked (44px), collapsed
4252
4558
  * (16px), or absent entirely (a workspace cell: 0). */
4253
4559
  left: calc(var(--vela-toolbar-gutter, 0px) + 10px);
@@ -4263,8 +4569,8 @@ var CSS10 = `
4263
4569
  color: var(--vela-fg);
4264
4570
  font-size: var(--vela-font-size-md);
4265
4571
  /* Same chip treatment as the indicator legend rows (InputsUI): a translucent wash of
4266
- * the chart background when idle \u2014 enough to keep the readout legible when candles
4267
- * reach it \u2014 and the solid chart background on hover. Symmetric 7px padding with a
4572
+ * the chart background when idle — enough to keep the readout legible when candles
4573
+ * reach it — and the solid chart background on hover. Symmetric 7px padding with a
4268
4574
  * compensating negative margin (mirroring the legend rows) keeps the avatar's left
4269
4575
  * edge on the legend column's left edge (both at left:10px) while the chip itself
4270
4576
  * extends 7px further left, so both columns' chips share the same left edge. */
@@ -4276,12 +4582,12 @@ var CSS10 = `
4276
4582
  }
4277
4583
  /* Hovering opens the chip the same way a legend row opens: solid chart background
4278
4584
  * plus the same inset neutral outline the indicator rows wear (InputsUI's
4279
- * setRowHighlighted) \u2014 the two columns read as one family. */
4585
+ * setRowHighlighted) — the two columns read as one family. */
4280
4586
  .vela-statusline:hover { background: var(--vela-bg); box-shadow: inset 0 0 0 1px var(--vela-border); }
4281
- /* Chart hidden (the price series' eye \u2014 renderer 'candleVisible'): the line dims to
4587
+ /* Chart hidden (the price series' eye — renderer 'candleVisible'): the line dims to
4282
4588
  * the same 0.5 wash a hidden indicator's legend row wears. */
4283
4589
  .vela-statusline.vela-sl-chart-hidden { opacity: 0.5; }
4284
- /* Two rows inside the chip \u2014 the identity (logo / symbol / meta / market badge) and the
4590
+ /* Two rows inside the chip — the identity (logo / symbol / meta / market badge) and the
4285
4591
  * value readout (O/H/L/C + change, or the show-chart eye). In the widest layout they sit
4286
4592
  * side by side on one line; the STACKED layouts (fit() decides, see the ladder in
4287
4593
  * statusline-model) turn the chip into a column so the values drop under the symbol. */
@@ -4298,7 +4604,7 @@ var CSS10 = `
4298
4604
  * that follows the chip (see the stacked legend shift below). */
4299
4605
  row-gap: var(--vela-space-1);
4300
4606
  }
4301
- /* Stacked, the identity row may shrink (the meta carries the ellipsis) \u2014 the values row
4607
+ /* Stacked, the identity row may shrink (the meta carries the ellipsis) — the values row
4302
4608
  * never does, so its overflow is what fit() measures to keep descending the ladder. */
4303
4609
  .vela-statusline.vela-sl-stacked .vela-sl-identity { max-width: 100%; }
4304
4610
  .vela-statusline.vela-sl-stacked .vela-sl-meta {
@@ -4321,25 +4627,28 @@ var CSS10 = `
4321
4627
  }
4322
4628
  .vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
4323
4629
  .vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
4324
- /* The meta opens with "\xB7 " \u2014 sit its dot one space-width after the ticker, as far as the
4630
+ /* The meta opens with "· " — sit its dot one space-width after the ticker, as far as the
4325
4631
  * venue sits after it, not a full row gap away. */
4326
4632
  .vela-statusline .vela-sl-symbol + .vela-sl-meta { margin-left: calc(var(--vela-space-1) - var(--vela-space-2)); }
4327
- /* Market status badge \u2014 a kit callout bubble (icon-only 16px circle, label on hover
4633
+ /* Market status badge — a kit callout bubble (icon-only 16px circle, label on hover
4328
4634
  * via the kit tooltip); the session tint is applied per status in setMarketStatus. While
4329
4635
  * the chart replays past bars it wears the replay badge instead (the inverse chip). */
4330
4636
  .vela-statusline .vela-sl-market { align-self: center; display: inline-flex; }
4331
4637
  .vela-statusline .vela-sl-market > [hidden] { display: none !important; }
4332
4638
  .vela-statusline .vela-sl-replay-badge, .vela-statusline .vela-sl-replay-badge svg { display: block; width: 16px; height: 16px; }
4333
4639
  /* Tabular figures: every digit has one advance, so a readout whose text changes only in
4334
- * its digits keeps its width \u2014 render() skips the ladder for those (see widthKey). */
4640
+ * its digits keeps its width — render() skips the ladder for those (see widthKey). */
4335
4641
  .vela-statusline .vela-sl-values { font-variant-numeric: tabular-nums; }
4336
4642
  .vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
4337
4643
  .vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
4338
- /* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
4644
+ /* Prices and the signed change are left-to-right isolates (each carries dir="ltr"): inside a
4645
+ * right-to-left page the row's order mirrors, but a sign stays in front of its number. */
4646
+ .vela-statusline .vela-sl-ohlc b, .vela-statusline .vela-sl-change { unicode-bidi: isolate; }
4647
+ /* The change value wears the SAME ink as the OHLC values (set inline per render) —
4339
4648
  * these are the pre-ink fallbacks only. */
4340
4649
  .vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
4341
4650
  .vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
4342
- /* The show-chart eye \u2014 out only while the chart is hidden (syncParts drives display),
4651
+ /* The show-chart eye — out only while the chart is hidden (syncParts drives display),
4343
4652
  * replacing the value readout it took away. Same footprint as a legend action button. */
4344
4653
  .vela-statusline .vela-sl-eye {
4345
4654
  align-self: center;
@@ -4358,18 +4667,18 @@ var CSS10 = `
4358
4667
  }
4359
4668
  .vela-statusline .vela-sl-eye:hover { color: var(--vela-fg); background: color-mix(in srgb, var(--vela-fg) 12%, transparent); }
4360
4669
  /* Stack the TOP pane's legend below the status line (lower study panes stay put).
4361
- * The renderer marks whichever legend sits at the plot's top edge \u2014 the price pane
4362
- * normally, or a maximized study pane filling the plot \u2014 so the legend never merges
4670
+ * The renderer marks whichever legend sits at the plot's top edge — the price pane
4671
+ * normally, or a maximized study pane filling the plot — so the legend never merges
4363
4672
  * with the status line whichever pane owns the top. The renderer sets the legend's
4364
- * inline top \u2014 shift with a transform, don't fight it. Scoped to hosts that actually
4365
- * CARRY a status line (the marker class set by the Statusline constructor) \u2014 the
4673
+ * inline top — shift with a transform, don't fight it. Scoped to hosts that actually
4674
+ * CARRY a status line (the marker class set by the Statusline constructor) — the
4366
4675
  * stylesheet is document-global, so a bare attribute selector here would shift every
4367
4676
  * chart on the page, including statusline-less ones. */
4368
4677
  .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
4369
- /* A stacked chip is two lines tall \u2014 the legend shifts one more line. fit() flags the
4678
+ /* A stacked chip is two lines tall — the legend shifts one more line. fit() flags the
4370
4679
  * host whenever the ladder lands on a stacked layout. */
4371
4680
  .vela-has-statusline.vela-sl-stacked-host [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
4372
- /* Mobile: the same ladder at the phone's type scale \u2014 the width alone decides how much
4681
+ /* Mobile: the same ladder at the phone's type scale — the width alone decides how much
4373
4682
  * of the readout fits, exactly as on a narrow desktop chart. */
4374
4683
  [data-layout='mobile'] .vela-statusline { font-size: var(--vela-font-size-sm); }
4375
4684
  [data-layout='mobile'] .vela-statusline .vela-sl-symbol { font-size: var(--vela-font-size-md); }
@@ -4378,14 +4687,16 @@ function baseOfTicker(ticker) {
4378
4687
  return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
4379
4688
  }
4380
4689
  var MARKET_LABELS = {
4381
- open: "Market Open",
4382
- pre: "Pre-Market",
4383
- post: "Post-Market",
4384
- extended: "Extended Hours",
4385
- closed: "Market Closed",
4386
- holiday: "Market Holiday"
4690
+ open: msg("widget.statusline.market.open", "Market Open"),
4691
+ pre: msg("widget.statusline.market.pre", "Pre-Market"),
4692
+ post: msg("widget.statusline.market.post", "Post-Market"),
4693
+ extended: msg("widget.statusline.market.extended", "Extended Hours"),
4694
+ closed: msg("widget.statusline.market.closed", "Market Closed"),
4695
+ holiday: msg("widget.statusline.market.holiday", "Market Holiday")
4387
4696
  };
4388
- var REPLAY_LABEL = "Replay Mode";
4697
+ var REPLAY_LABEL = msg("widget.statusline.replay", "Replay Mode");
4698
+ var CHANGE_TEXT = msg("widget.statusline.change", "{change} ({percent})");
4699
+ var EM_DASH2 = "—";
4389
4700
  var REPLAY_BADGE_SVG = '<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><circle cx="8" cy="8" r="8" style="fill: var(--vela-selected-bg)"/><path d="M11.7 4.75v6.5L7.2 8zM7.2 4.75v6.5L2.7 8z" style="fill: var(--vela-selected-fg); stroke: var(--vela-selected-fg); stroke-width: 0.8; stroke-linejoin: round"/></svg>';
4390
4701
  var MARKET_INKS = {
4391
4702
  open: "var(--vela-up)",
@@ -4454,13 +4765,16 @@ var Statusline = class {
4454
4765
  this.fitKey = null;
4455
4766
  /** Web fonts landing after the last fit change every width without a resize. */
4456
4767
  this.onFontsLoaded = () => this.fit();
4768
+ /** The bound chart's translator (English until {@link onChart}). */
4769
+ this.i18n = null;
4770
+ this.meta = null;
4457
4771
  this.onContextMenu = (e) => {
4458
4772
  if (!this.menu || !this.menuHooks) return;
4459
4773
  e.preventDefault();
4460
4774
  e.stopPropagation();
4461
4775
  const chartVisible = this.menuHooks.chartVisible();
4462
4776
  this.setChartHidden(!chartVisible);
4463
- this.menu.setItems(statuslineMenuItems(this.parts, chartVisible));
4777
+ this.menu.setItems(statuslineMenuItems(this.parts, chartVisible, this.i18n));
4464
4778
  this.menu.openAt(e.clientX, e.clientY);
4465
4779
  };
4466
4780
  const doc = host.ownerDocument;
@@ -4480,7 +4794,7 @@ var Statusline = class {
4480
4794
  icon: "market-open",
4481
4795
  background: `color-mix(in srgb, ${MARKET_INKS.open} 20%, transparent)`,
4482
4796
  color: MARKET_INKS.open,
4483
- label: MARKET_LABELS.open,
4797
+ label: this.t(MARKET_LABELS.open),
4484
4798
  host
4485
4799
  });
4486
4800
  this.replayBadge = doc.createElement("span");
@@ -4494,11 +4808,12 @@ var Statusline = class {
4494
4808
  this.ohlcEl.className = "vela-sl-ohlc";
4495
4809
  this.changeEl = doc.createElement("span");
4496
4810
  this.changeEl.className = "vela-sl-change";
4811
+ this.changeEl.dir = "ltr";
4497
4812
  this.eyeEl = doc.createElement("button");
4498
4813
  this.eyeEl.type = "button";
4499
4814
  this.eyeEl.className = "vela-sl-eye";
4500
4815
  this.eyeEl.innerHTML = iconAt("eye-off", 14);
4501
- this.eyeEl.setAttribute("aria-label", "Show chart");
4816
+ this.eyeEl.setAttribute("aria-label", this.t(SHOW_CHART_LABEL));
4502
4817
  this.eyeEl.style.display = "none";
4503
4818
  this.eyeEl.addEventListener("click", (e) => {
4504
4819
  e.stopPropagation();
@@ -4518,11 +4833,14 @@ var Statusline = class {
4518
4833
  this.fitRO.observe(host);
4519
4834
  }
4520
4835
  doc.fonts?.addEventListener("loadingdone", this.onFontsLoaded);
4521
- this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
4522
- this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
4836
+ this.marketTip = new Tooltip(this.marketEl, { content: this.t(MARKET_LABELS.open), placement: "bottom" });
4837
+ this.eyeTip = new Tooltip(this.eyeEl, { content: this.t(SHOW_CHART_LABEL), placement: "bottom" });
4523
4838
  this.setMarketStatus("open");
4524
4839
  this.render();
4525
4840
  }
4841
+ t(text) {
4842
+ return textOf(this.i18n, text);
4843
+ }
4526
4844
  setSymbol(symbol) {
4527
4845
  const ticker = parseSymbol(symbol).ticker;
4528
4846
  this.symbolEl.textContent = ticker;
@@ -4607,7 +4925,17 @@ var Statusline = class {
4607
4925
  }
4608
4926
  /** The "· BINANCE · 1h" segment after the symbol — venue first, then resolution. */
4609
4927
  setMeta(timeframe, provider) {
4610
- this.metaEl.textContent = `${provider ? `\xB7 ${provider.toUpperCase()} ` : ""}\xB7 ${timeframeLabel(timeframe)}`;
4928
+ this.meta = { timeframe, provider };
4929
+ this.metaEl.textContent = `${provider ? `· ${provider.toUpperCase()} ` : ""}· ${timeframeLabel(timeframe, this.i18n)}`;
4930
+ this.fit();
4931
+ }
4932
+ /** Re-read every label and value in the chart's language. */
4933
+ relocalize() {
4934
+ this.eyeEl.setAttribute("aria-label", this.t(SHOW_CHART_LABEL));
4935
+ this.eyeTip.setContent(this.t(SHOW_CHART_LABEL));
4936
+ this.dressBadge();
4937
+ if (this.meta) this.setMeta(this.meta.timeframe, this.meta.provider);
4938
+ this.fitKey = null;
4611
4939
  this.fit();
4612
4940
  }
4613
4941
  /** Dress the market badge for a session state: its icon, tinted circle, and the
@@ -4628,7 +4956,7 @@ var Statusline = class {
4628
4956
  this.replayBadge.hidden = !this.replaying;
4629
4957
  if (this.replaying) {
4630
4958
  this.marketEl.dataset.status = "replay";
4631
- this.marketTip.setContent(REPLAY_LABEL);
4959
+ this.marketTip.setContent(this.t(REPLAY_LABEL));
4632
4960
  return;
4633
4961
  }
4634
4962
  const status = this.marketStatus;
@@ -4638,9 +4966,9 @@ var Statusline = class {
4638
4966
  icon: `market-${status}`,
4639
4967
  background: `color-mix(in srgb, ${ink} 20%, transparent)`,
4640
4968
  color: ink,
4641
- label: MARKET_LABELS[status]
4969
+ label: this.t(MARKET_LABELS[status])
4642
4970
  });
4643
- this.marketTip.setContent(MARKET_LABELS[status]);
4971
+ this.marketTip.setContent(this.t(MARKET_LABELS[status]));
4644
4972
  }
4645
4973
  /** Show/hide one part — the settings dialog's Status line tab and the right-click
4646
4974
  * menu both drive these. 'name' owns the venue/timeframe meta too (see
@@ -4697,6 +5025,9 @@ var Statusline = class {
4697
5025
  /** (Re)bind to a chart instance — called after every widget rebuild. */
4698
5026
  onChart(chart) {
4699
5027
  this.detach();
5028
+ const relabel = this.i18n !== chart.i18n;
5029
+ this.i18n = chart.i18n;
5030
+ if (relabel) this.relocalize();
4700
5031
  this.lastBar = null;
4701
5032
  this.hoverBar = null;
4702
5033
  this.unsubs.push(
@@ -4772,18 +5103,29 @@ var Statusline = class {
4772
5103
  const doc = this.el.ownerDocument;
4773
5104
  const up = this.isUp ? this.isUp(bar) : bar.close >= bar.open;
4774
5105
  const ink = up ? this.upColor ?? "var(--vela-up)" : this.downColor ?? "var(--vela-down)";
5106
+ const format = this.i18n?.format;
5107
+ const price = (v) => !Number.isFinite(v) ? EM_DASH2 : format ? format.price(v, { decimals: dp, grouping: true, surface: "statusLine" }) : v.toLocaleString("en-US", { minimumFractionDigits: dp, maximumFractionDigits: dp });
5108
+ const percent = (v) => format ? format.percent(v, { surface: "statusLine" }) : `${v >= 0 ? "+" : "-"}${Math.abs(v).toFixed(2)}%`;
4775
5109
  const cell = (k, v) => {
4776
5110
  const s = doc.createElement("span");
4777
5111
  if (k) s.append(`${k} `);
4778
5112
  const b = doc.createElement("b");
4779
- b.textContent = fmtPrice(v, dp);
5113
+ b.dir = "ltr";
5114
+ b.textContent = price(v);
4780
5115
  b.style.color = ink;
4781
5116
  s.appendChild(b);
4782
5117
  return s;
4783
5118
  };
4784
5119
  const { level } = this.layout;
4785
- this.ohlcEl.replaceChildren(...readoutCells(level, this.readout).map((c) => cell(c.label, bar[c.key])));
4786
- this.changeEl.textContent = level === "minimal" ? fmtChangePct(bar.open, bar.close) : fmtChange(bar.open, bar.close);
5120
+ this.ohlcEl.replaceChildren(...readoutCells(level, this.readout, this.i18n).map((c) => cell(c.label, bar[c.key])));
5121
+ let change = "";
5122
+ if (Number.isFinite(bar.open) && Number.isFinite(bar.close) && bar.open !== 0) {
5123
+ const diff = bar.close - bar.open;
5124
+ const pct = percent(diff / bar.open * 100);
5125
+ const signed = (diff >= 0 ? "+" : "") + price(diff);
5126
+ change = level === "minimal" ? pct : textOf(this.i18n, CHANGE_TEXT, { change: signed, percent: pct });
5127
+ }
5128
+ this.changeEl.textContent = change;
4787
5129
  this.changeEl.dataset.dir = up ? "up" : "down";
4788
5130
  this.changeEl.style.color = ink;
4789
5131
  }
@@ -4798,7 +5140,7 @@ var CSS11 = `
4798
5140
  .vela-watermark {
4799
5141
  position: absolute;
4800
5142
  /* Insets follow the renderer-published gutters AND the price-pane vertical
4801
- * bounds (mount container), so the mark centers on the PRICE PANE \u2014 never
5143
+ * bounds (mount container), so the mark centers on the PRICE PANE — never
4802
5144
  * the study panes below, the drawings toolbar, or the price scale. */
4803
5145
  top: var(--vela-price-pane-top, 0px);
4804
5146
  bottom: var(--vela-price-pane-bottom, 0px);
@@ -4832,8 +5174,14 @@ function watermarkFontPx(availPx, textPxAtMax) {
4832
5174
  if (textPxAtMax <= 0) return MAX_FONT_PX;
4833
5175
  return Math.max(MIN_FONT_PX, Math.min(MAX_FONT_PX, Math.floor(MAX_FONT_PX * availPx * FILL / textPxAtMax)));
4834
5176
  }
5177
+ var TEXT12 = {
5178
+ replay: msg("widget.watermark.replay", "Replay"),
5179
+ mark: msg("widget.watermark.mark", "{symbol} · {timeframe}")
5180
+ };
4835
5181
  var Watermark = class {
4836
- constructor(host, symbol, timeframe) {
5182
+ /** `i18n` reads the chart's translator (absent: English). */
5183
+ constructor(host, symbol, timeframe, i18n = () => null) {
5184
+ this.i18n = i18n;
4837
5185
  this.resizeObserver = null;
4838
5186
  /** The host's visibility preference (the persisted watermark toggle). */
4839
5187
  this.shown = true;
@@ -4845,6 +5193,8 @@ var Watermark = class {
4845
5193
  * canvas, so the mark stays out of its way. Starts true: the FIRST `load:start`
4846
5194
  * fires during chart construction, before any subscriber can see it. */
4847
5195
  this.loading = true;
5196
+ this.symbol = "";
5197
+ this.timeframe = "";
4848
5198
  injectStyles(STYLE_ID11, CSS11, host.ownerDocument);
4849
5199
  this.el = host.ownerDocument.createElement("div");
4850
5200
  this.el.className = "vela-watermark";
@@ -4852,7 +5202,8 @@ var Watermark = class {
4852
5202
  this.text = host.ownerDocument.createElement("span");
4853
5203
  this.replayLine = host.ownerDocument.createElement("span");
4854
5204
  this.replayLine.className = "vela-watermark-replay";
4855
- this.replayLine.append(iconEl("replay", host.ownerDocument), host.ownerDocument.createTextNode("Replay"));
5205
+ this.replayText = host.ownerDocument.createTextNode(textOf(this.i18n(), TEXT12.replay));
5206
+ this.replayLine.append(iconEl("replay", host.ownerDocument), this.replayText);
4856
5207
  this.el.append(this.text, this.replayLine);
4857
5208
  host.appendChild(this.el);
4858
5209
  if (typeof ResizeObserver !== "undefined") {
@@ -4888,9 +5239,17 @@ var Watermark = class {
4888
5239
  this.el.style.display = (this.shown || replay) && !this.loading ? "" : "none";
4889
5240
  }
4890
5241
  update(symbol, timeframe) {
4891
- this.text.textContent = symbol ? `${parseSymbol(symbol).ticker} \xB7 ${timeframeLabel(timeframe)}` : "";
5242
+ this.symbol = symbol;
5243
+ this.timeframe = timeframe;
5244
+ const i18n = this.i18n();
5245
+ this.text.textContent = symbol ? textOf(i18n, TEXT12.mark, { symbol: parseSymbol(symbol).ticker, timeframe: timeframeLabel(timeframe, i18n) }) : "";
4892
5246
  this.fit();
4893
5247
  }
5248
+ /** Re-read the mark and the replay line in the chart's language. */
5249
+ relocalize() {
5250
+ this.replayText.data = textOf(this.i18n(), TEXT12.replay);
5251
+ this.update(this.symbol, this.timeframe);
5252
+ }
4894
5253
  /** Measure the text at the cap and shrink it to the price pane's width. */
4895
5254
  fit() {
4896
5255
  if (this.el.clientWidth <= 0 || !this.text.textContent) return;
@@ -4986,7 +5345,7 @@ var ChartContextMenu = class {
4986
5345
  }
4987
5346
  contributed(zone) {
4988
5347
  return widgetActions(`context:${zone}`, this.context()).map((a) => ({
4989
- item: { id: `action:${a.id}`, label: a.label, icon: a.icon },
5348
+ item: { id: `action:${a.id}`, label: textOf(this.chart?.i18n, a.label), icon: a.icon },
4990
5349
  order: a.order
4991
5350
  }));
4992
5351
  }
@@ -4994,6 +5353,7 @@ var ChartContextMenu = class {
4994
5353
  return composeMenu(zone, this.builtinItems(zone), this.contributed(zone));
4995
5354
  }
4996
5355
  builtinItems(zone) {
5356
+ const chart = this.chart;
4997
5357
  if (zone === "price-axis") {
4998
5358
  const pane = this.lastPane;
4999
5359
  return priceAxisItems({
@@ -5004,16 +5364,18 @@ var ChartContextMenu = class {
5004
5364
  priceLabel: this.flag("priceLabel"),
5005
5365
  countdown: this.flag("countdown"),
5006
5366
  priceLine: this.flag("currentPriceLine")
5007
- });
5367
+ }, this.chart?.i18n);
5008
5368
  }
5009
5369
  if (zone === "time-axis") {
5010
- return timeAxisItems(this.cbs.timezone?.() ?? String(this.chart?.renderer.get("timezone") ?? "Etc/UTC"));
5370
+ return timeAxisItems(this.cbs.timezone?.() ?? String(this.chart?.renderer.get("timezone") ?? "Etc/UTC"), chart?.i18n);
5011
5371
  }
5012
- const chart = this.chart;
5013
- return bodyItems({
5014
- drawings: chart?.drawings.supported ? chart.drawings.all().length : 0,
5015
- indicators: chart?.indicators().length ?? 0
5016
- });
5372
+ return bodyItems(
5373
+ {
5374
+ drawings: chart?.drawings.supported ? chart.drawings.all().length : 0,
5375
+ indicators: chart?.indicators().length ?? 0
5376
+ },
5377
+ chart?.i18n
5378
+ );
5017
5379
  }
5018
5380
  run(id) {
5019
5381
  const chart = this.chart;
@@ -5410,6 +5772,14 @@ var CONTROLS_BOTTOM_PX = TIME_AXIS_H2 + 12;
5410
5772
  var CLUSTER_H = 24;
5411
5773
  var CLUSTER_PILL = "rgba(0,0,0,0.65)";
5412
5774
  var CLUSTER_LEFT_CSS = "calc((100% + var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px)) / 2)";
5775
+ var TEXT13 = {
5776
+ zoomOut: msg("widget.cellControls.zoomOut", "Zoom out"),
5777
+ zoomIn: msg("widget.cellControls.zoomIn", "Zoom in"),
5778
+ restoreLayout: msg("widget.cellControls.restoreLayout", "Restore layout"),
5779
+ maximizeChart: msg("widget.cellControls.maximizeChart", "Maximize chart"),
5780
+ resetChart: msg("widget.cellControls.resetChart", "Reset chart"),
5781
+ dragToMove: msg("widget.cellControls.dragToMove", "Drag to move chart")
5782
+ };
5413
5783
  var STYLE_ID12 = "vela-cell-controls";
5414
5784
  var CSS12 = `
5415
5785
  .vela-cc-btn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:none;border-radius:var(--vela-radius-sm);background:transparent;line-height:0;font-size:12px;color:var(--vela-fg-muted);cursor:pointer;}
@@ -5470,17 +5840,17 @@ var CellControls = class {
5470
5840
  this.host.appendChild(this.root);
5471
5841
  this.refresh();
5472
5842
  }
5473
- /** Rebuild the buttons (the multi-cell gate or the maximized state changed). */
5843
+ /** Rebuild the buttons (the multi-cell gate, the maximized state or the language changed). */
5474
5844
  refresh() {
5475
5845
  this.root.textContent = "";
5476
5846
  const multi = this.deps.multiCell();
5477
5847
  const maximized = multi && this.deps.isMaximized();
5478
5848
  if (multi && !maximized) this.root.appendChild(this.makeGrip());
5479
- this.root.appendChild(this.button("minus", "Zoom out", () => this.glider.zoom(ZOOM_OUT)));
5480
- this.root.appendChild(this.button("plus", "Zoom in", () => this.glider.zoom(ZOOM_IN)));
5849
+ this.root.appendChild(this.button("minus", TEXT13.zoomOut, () => this.glider.zoom(ZOOM_OUT)));
5850
+ this.root.appendChild(this.button("plus", TEXT13.zoomIn, () => this.glider.zoom(ZOOM_IN)));
5481
5851
  if (multi) {
5482
5852
  this.root.appendChild(
5483
- this.button(maximized ? "restore" : "maximize", maximized ? "Restore layout" : "Maximize chart", () => this.deps.toggleMaximize(), {
5853
+ this.button(maximized ? "restore" : "maximize", maximized ? TEXT13.restoreLayout : TEXT13.maximizeChart, () => this.deps.toggleMaximize(), {
5484
5854
  // The maximized state reads as an inverse chip (white-on-dark, dark-on-light),
5485
5855
  // the same active-state affordance as a collapsed pane's expand button.
5486
5856
  selected: maximized
@@ -5488,14 +5858,19 @@ var CellControls = class {
5488
5858
  );
5489
5859
  }
5490
5860
  this.root.appendChild(
5491
- this.button("reset", "Reset chart", () => {
5861
+ this.button("reset", TEXT13.resetChart, () => {
5492
5862
  this.glider.stop();
5493
5863
  this.deps.reset();
5494
5864
  })
5495
5865
  );
5496
5866
  }
5497
- button(iconId, title, onClick, opts = {}) {
5867
+ /** Text in the chart's language. */
5868
+ t(text) {
5869
+ return textOf(this.deps.chart()?.i18n, text);
5870
+ }
5871
+ button(iconId, text, onClick, opts = {}) {
5498
5872
  const b = this.host.ownerDocument.createElement("button");
5873
+ const title = this.t(text);
5499
5874
  b.type = "button";
5500
5875
  b.title = title;
5501
5876
  b.setAttribute("aria-label", title);
@@ -5513,8 +5888,8 @@ var CellControls = class {
5513
5888
  makeGrip() {
5514
5889
  const b = this.host.ownerDocument.createElement("button");
5515
5890
  b.type = "button";
5516
- b.title = "Drag to move chart";
5517
- b.setAttribute("aria-label", "Drag to move chart");
5891
+ b.title = this.t(TEXT13.dragToMove);
5892
+ b.setAttribute("aria-label", b.title);
5518
5893
  b.className = "vela-cc-btn vela-cc-grip";
5519
5894
  b.innerHTML = icon("grip");
5520
5895
  b.addEventListener("pointerdown", (e) => this.onGripDown(b, e));
@@ -5657,6 +6032,28 @@ var WidgetHistory = class {
5657
6032
  };
5658
6033
 
5659
6034
  // src/workspace/ChartCell.ts
6035
+ var TEXT14 = {
6036
+ regularHours: msg("workspace.settings.regularHours", "Regular hours (RTH)"),
6037
+ extendedHours: msg("workspace.settings.extendedHours", "Extended hours (ETH)"),
6038
+ extendedColor: msg("workspace.settings.extendedColor", "Extended hours"),
6039
+ premarketColor: msg("workspace.settings.premarketColor", "Pre-market"),
6040
+ postmarketColor: msg("workspace.settings.postmarketColor", "Post-market"),
6041
+ tradingSession: msg("workspace.settings.tradingSession", "Trading session"),
6042
+ session: msg("workspace.settings.session", "Session"),
6043
+ advanced: msg("workspace.settings.advanced", "Advanced"),
6044
+ barsToFetch: msg("workspace.settings.barsToFetch", "Bars to fetch"),
6045
+ timeZone: msg("workspace.settings.timeZone", "Time zone"),
6046
+ timeZoneRow: msg("workspace.settings.timeZoneRow", "Time zone"),
6047
+ watermark: msg("workspace.settings.watermark", "Watermark"),
6048
+ symbolWatermark: msg("workspace.settings.symbolWatermark", "Symbol watermark"),
6049
+ replayWatermark: msg("workspace.settings.replayWatermark", "Replay watermark"),
6050
+ statusLine: msg("workspace.settings.statusLine", "Status line"),
6051
+ statusLineParts: msg("workspace.settings.statusLineParts", "Status line"),
6052
+ indicators: msg("workspace.settings.indicators", "Indicators"),
6053
+ indicatorTitles: msg("workspace.settings.indicatorTitles", "Titles"),
6054
+ indicatorValues: msg("workspace.settings.indicatorValues", "Values"),
6055
+ builtIn: msg("workspace.indicators.builtIn", "Built-in")
6056
+ };
5660
6057
  function seedDefaults(opts) {
5661
6058
  return {
5662
6059
  symbol: opts.symbol,
@@ -5688,6 +6085,13 @@ function instanceDeltas(handle) {
5688
6085
  const props = inputDeltas(handle.props, handle.propValues());
5689
6086
  return inputs || props ? { ...inputs ? { inputs } : {}, ...props ? { props } : {} } : void 0;
5690
6087
  }
6088
+ function nativeAliases(type, i18n) {
6089
+ const entry = nativeIndicatorEntry(type);
6090
+ if (!entry) return {};
6091
+ const names = [sourceText(entry.title)];
6092
+ if (entry.shortTitle !== void 0) names.push(textOf(i18n, entry.shortTitle), sourceText(entry.shortTitle));
6093
+ return { aliases: [...new Set(names)] };
6094
+ }
5691
6095
  var ChartCell = class {
5692
6096
  constructor(id, gridHost, seed, deps) {
5693
6097
  this.id = id;
@@ -5763,6 +6167,7 @@ var ChartCell = class {
5763
6167
  this.host,
5764
6168
  {
5765
6169
  ...deps.chartDefaults,
6170
+ ...deps.localization?.(),
5766
6171
  symbol,
5767
6172
  timeframe: seed.timeframe,
5768
6173
  bars: seed.bars,
@@ -5848,7 +6253,7 @@ var ChartCell = class {
5848
6253
  if (!this.indicatorValuesOn) this.inner.renderer.set("indicatorValues", false);
5849
6254
  this.watermarkOn = seed.watermark ?? deps.watermark;
5850
6255
  this.replayWatermarkOn = seed.replayWatermark ?? true;
5851
- this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60") : null;
6256
+ this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60", () => this.inner?.i18n) : null;
5852
6257
  if (!this.watermarkOn) this.watermark?.setVisible(false);
5853
6258
  if (!this.replayWatermarkOn) this.watermark?.setReplayVisible(false);
5854
6259
  this.inner.on("replay:start", () => this.watermark?.setReplaying(true));
@@ -6076,12 +6481,10 @@ var ChartCell = class {
6076
6481
  pushSettingsSections() {
6077
6482
  const chart = this.inner;
6078
6483
  if (!chart) return;
6079
- const rth = "Regular hours (RTH)";
6080
- const eth = "Extended hours (ETH)";
6081
6484
  const shadeRows = this.sessionOvernightFlag ? [
6082
6485
  {
6083
6486
  kind: "color",
6084
- label: "Extended hours",
6487
+ label: TEXT14.extendedColor,
6085
6488
  id: "extended-color",
6086
6489
  get: () => this.sessionShadeColor("extendedColor"),
6087
6490
  set: (v) => this.setSessionShadeColor("extendedColor", v)
@@ -6089,43 +6492,46 @@ var ChartCell = class {
6089
6492
  ] : [
6090
6493
  {
6091
6494
  kind: "color",
6092
- label: "Pre-market",
6495
+ label: TEXT14.premarketColor,
6093
6496
  id: "premarket-color",
6094
6497
  get: () => this.sessionShadeColor("premarketColor"),
6095
6498
  set: (v) => this.setSessionShadeColor("premarketColor", v)
6096
6499
  },
6097
6500
  {
6098
6501
  kind: "color",
6099
- label: "Post-market",
6502
+ label: TEXT14.postmarketColor,
6100
6503
  id: "postmarket-color",
6101
6504
  get: () => this.sessionShadeColor("postmarketColor"),
6102
6505
  set: (v) => this.setSessionShadeColor("postmarketColor", v)
6103
6506
  }
6104
6507
  ];
6105
6508
  const sessionSection = {
6106
- title: "Trading session",
6509
+ title: TEXT14.tradingSession,
6107
6510
  id: "trading-session",
6108
6511
  placement: "symbol",
6109
6512
  rows: [
6110
6513
  {
6111
6514
  kind: "select",
6112
- label: "Session",
6515
+ label: TEXT14.session,
6113
6516
  id: "session",
6114
- options: [rth, eth],
6115
- get: () => this.session === "extended" ? eth : rth,
6116
- set: (v) => this.setSession(v === eth ? "extended" : "regular")
6517
+ options: [
6518
+ ["regular", TEXT14.regularHours],
6519
+ ["extended", TEXT14.extendedHours]
6520
+ ],
6521
+ get: () => this.session === "extended" ? "extended" : "regular",
6522
+ set: (v) => this.setSession(v === "extended" ? "extended" : "regular")
6117
6523
  },
6118
6524
  ...shadeRows
6119
6525
  ]
6120
6526
  };
6121
6527
  const advanced = {
6122
- title: "Advanced",
6528
+ title: TEXT14.advanced,
6123
6529
  id: "advanced",
6124
6530
  placement: "end",
6125
6531
  rows: [
6126
6532
  {
6127
6533
  kind: "select",
6128
- label: "Bars to fetch",
6534
+ label: TEXT14.barsToFetch,
6129
6535
  id: "bars",
6130
6536
  options: ["500", "1000", "2000", "5000", "10000", "20000", "50000", "60000", "80000", "100000"],
6131
6537
  get: () => String(this.state.bars ?? 1e3),
@@ -6138,15 +6544,15 @@ var ChartCell = class {
6138
6544
  ]
6139
6545
  };
6140
6546
  const timezoneSection = {
6141
- title: "Time zone",
6547
+ title: TEXT14.timeZone,
6142
6548
  id: "time-zone",
6143
6549
  placement: "symbol",
6144
6550
  rows: [
6145
6551
  {
6146
6552
  kind: "select",
6147
- label: "Time zone",
6553
+ label: TEXT14.timeZoneRow,
6148
6554
  id: "zone",
6149
- options: timezoneMenuRows(this.deps.timezone()).map((r) => [r.value, r.label]),
6555
+ options: timezoneMenuRows(this.deps.timezone(), chart.i18n).map((r) => [r.value, r.label]),
6150
6556
  get: () => {
6151
6557
  const choice = this.deps.timezone();
6152
6558
  return isExchangeTimezone(choice) ? choice : normalizeTimezone(choice);
@@ -6156,20 +6562,20 @@ var ChartCell = class {
6156
6562
  ]
6157
6563
  };
6158
6564
  const watermarkSection = {
6159
- title: "Watermark",
6565
+ title: TEXT14.watermark,
6160
6566
  id: "watermark",
6161
6567
  placement: "symbol",
6162
6568
  rows: [
6163
6569
  {
6164
6570
  kind: "toggle",
6165
- label: "Symbol watermark",
6571
+ label: TEXT14.symbolWatermark,
6166
6572
  id: "visible",
6167
6573
  get: () => this.watermarkOn,
6168
6574
  set: (v) => this.setWatermarkVisible(v)
6169
6575
  },
6170
6576
  {
6171
6577
  kind: "toggle",
6172
- label: "Replay watermark",
6578
+ label: TEXT14.replayWatermark,
6173
6579
  id: "replay",
6174
6580
  get: () => this.replayWatermarkOn,
6175
6581
  set: (v) => this.setReplayWatermarkVisible(v)
@@ -6180,49 +6586,49 @@ var ChartCell = class {
6180
6586
  if (this.statusline) {
6181
6587
  const sl = this.statusline;
6182
6588
  sections.push({
6183
- title: "Status line",
6589
+ title: TEXT14.statusLine,
6184
6590
  id: "status-line",
6185
6591
  rows: [
6186
- { kind: "heading", label: "Status line", id: "parts" },
6592
+ { kind: "heading", label: TEXT14.statusLineParts, id: "parts" },
6187
6593
  {
6188
6594
  kind: "toggle",
6189
- label: "Symbol name",
6595
+ label: STATUSLINE_PART_LABELS.name,
6190
6596
  id: "name",
6191
6597
  get: () => sl.partVisible("name"),
6192
6598
  set: (v) => this.setStatuslinePart("name", v)
6193
6599
  },
6194
6600
  {
6195
6601
  kind: "toggle",
6196
- label: "Market status",
6602
+ label: STATUSLINE_PART_LABELS.market,
6197
6603
  id: "market",
6198
6604
  get: () => sl.partVisible("market"),
6199
6605
  set: (v) => this.setStatuslinePart("market", v)
6200
6606
  },
6201
6607
  {
6202
6608
  kind: "toggle",
6203
- label: "OHLC values",
6609
+ label: STATUSLINE_PART_LABELS.ohlc,
6204
6610
  id: "ohlc",
6205
6611
  get: () => sl.partVisible("ohlc"),
6206
6612
  set: (v) => this.setStatuslinePart("ohlc", v)
6207
6613
  },
6208
6614
  {
6209
6615
  kind: "toggle",
6210
- label: "Bar change values",
6616
+ label: STATUSLINE_PART_LABELS.change,
6211
6617
  id: "change",
6212
6618
  get: () => sl.partVisible("change"),
6213
6619
  set: (v) => this.setStatuslinePart("change", v)
6214
6620
  },
6215
- { kind: "heading", label: "Indicators", id: "indicators" },
6621
+ { kind: "heading", label: TEXT14.indicators, id: "indicators" },
6216
6622
  {
6217
6623
  kind: "toggle",
6218
- label: "Titles",
6624
+ label: TEXT14.indicatorTitles,
6219
6625
  id: "indicator-titles",
6220
6626
  get: () => this.indicatorTitlesOn,
6221
6627
  set: (v) => this.setIndicatorTitlesVisible(v)
6222
6628
  },
6223
6629
  {
6224
6630
  kind: "toggle",
6225
- label: "Values",
6631
+ label: TEXT14.indicatorValues,
6226
6632
  id: "indicator-values",
6227
6633
  get: () => this.indicatorValuesOn,
6228
6634
  set: (v) => this.setIndicatorValuesVisible(v)
@@ -6299,6 +6705,16 @@ var ChartCell = class {
6299
6705
  if (!this.inner) throw new Error(`[vela] cell "${this.id}" is destroyed`);
6300
6706
  return this.inner;
6301
6707
  }
6708
+ /** Re-read the cell's own chrome (status line, watermark, view controls, settings rows,
6709
+ * native indicator names) in the chart's language, after `chart.setLocale`. */
6710
+ relocalize() {
6711
+ if (!this.inner) return;
6712
+ this.statusline?.relocalize();
6713
+ this.watermark?.relocalize();
6714
+ this.cellControls.refresh();
6715
+ this.pushSettingsSections();
6716
+ this.refreshNativeCatalog();
6717
+ }
6302
6718
  get symbol() {
6303
6719
  return this.state.symbol ?? "";
6304
6720
  }
@@ -6487,20 +6903,29 @@ var ChartCell = class {
6487
6903
  * index space the picker hands back, so `libraryRows` and `addFromLibrary` MUST both
6488
6904
  * read it — indexing the unsorted catalog on add would land on a different study. */
6489
6905
  supportedNatives() {
6490
- return this.nativeCatalog.filter((n) => n.supported).sort((a, b) => a.title.localeCompare(b.title, "en", { sensitivity: "base" }));
6906
+ const locale = this.inner?.i18n.locale ?? "en";
6907
+ const collator = new Intl.Collator(locale, { sensitivity: "base" });
6908
+ return this.nativeCatalog.filter((n) => n.supported).sort((a, b) => collator.compare(a.title, b.title));
6491
6909
  }
6492
6910
  /** The picker's library rows: supported natives first (see {@link supportedNatives}),
6493
6911
  * then the manifest in the host's own order. */
6494
6912
  libraryRows() {
6495
6913
  return [
6496
- ...this.supportedNatives().map((n) => ({ name: n.title, category: "Built-in", native: true, nativeType: n.type, beta: n.beta })),
6914
+ ...this.supportedNatives().map((n) => ({
6915
+ name: n.title,
6916
+ ...nativeAliases(n.type, this.inner?.i18n),
6917
+ category: textOf(this.inner?.i18n, TEXT14.builtIn),
6918
+ native: true,
6919
+ nativeType: n.type,
6920
+ beta: n.beta
6921
+ })),
6497
6922
  ...this.manifest.map((e) => ({ name: e.name, language: e.language, category: e.category }))
6498
6923
  ];
6499
6924
  }
6500
6925
  /** The picker's on-chart rows: native instances first, then live script instances. */
6501
6926
  onChartRows() {
6502
6927
  return [
6503
- ...this.nativeHandles().map((h) => ({ name: h.title, native: true, nativeType: h.nativeType })),
6928
+ ...this.nativeHandles().map((h) => ({ name: h.title, ...nativeAliases(h.nativeType, this.inner?.i18n), native: true, nativeType: h.nativeType })),
6504
6929
  ...this.instances.map((it) => ({ name: it.entry.name, language: it.entry.language }))
6505
6930
  ];
6506
6931
  }
@@ -6870,7 +7295,7 @@ var WorkspaceReplay = class {
6870
7295
  if (!this.session) return this;
6871
7296
  if (intervalMs !== void 0) {
6872
7297
  if (!(Number.isFinite(intervalMs) && intervalMs > 0)) {
6873
- console.warn(`[vela] workspace.replay.play(${intervalMs}) ignored \u2014 the interval must be a positive number of ms`);
7298
+ console.warn(`[vela] workspace.replay.play(${intervalMs}) ignored — the interval must be a positive number of ms`);
6874
7299
  return this;
6875
7300
  }
6876
7301
  this.intervalMs = intervalMs;
@@ -7098,27 +7523,37 @@ function monthSpan(timeframe) {
7098
7523
 
7099
7524
  // src/workspace/layouts.ts
7100
7525
  var registry = /* @__PURE__ */ new Map();
7526
+ var labels = /* @__PURE__ */ new Map();
7101
7527
  function registerLayout(def) {
7102
- registry.set(def.id, def);
7528
+ registry.set(def.id, typeof def.label === "string" ? def : { ...def, label: sourceText(def.label) });
7529
+ labels.set(def.id, def.label);
7103
7530
  }
7104
7531
  function unregisterLayout(id) {
7105
7532
  registry.delete(id);
7533
+ labels.delete(id);
7106
7534
  }
7107
- function layoutDefinition(id) {
7108
- return registry.get(id);
7535
+ function inLanguage(def, i18n) {
7536
+ const grid = GRID_ID_RE.exec(def.id);
7537
+ const message = labels.get(def.id) ?? (grid ? gridLabel(Number(grid[1]), Number(grid[2])) : def.label);
7538
+ const label = textOf(i18n, message);
7539
+ return label === def.label ? def : { ...def, label };
7109
7540
  }
7110
- function layouts() {
7111
- return [...registry.values()];
7541
+ function layoutDefinition(id, i18n) {
7542
+ const def = registry.get(id);
7543
+ return def && inLanguage(def, i18n);
7544
+ }
7545
+ function layouts(i18n) {
7546
+ return [...registry.values()].map((def) => inLanguage(def, i18n));
7112
7547
  }
7113
7548
  function slots(n) {
7114
7549
  return Array.from({ length: n }, (_, i) => ({ id: `c${i + 1}` }));
7115
7550
  }
7116
7551
  function registerBuiltinLayouts() {
7117
- registerLayout({ id: "1", label: "Single", cols: [1], rows: [1], cells: slots(1) });
7118
- registerLayout({ id: "2h", label: "2 side by side", cols: [1, 1], rows: [1], cells: slots(2) });
7119
- registerLayout({ id: "2v", label: "2 stacked", cols: [1], rows: [1, 1], cells: slots(2) });
7120
- registerLayout({ id: "4", label: "4 grid", cols: [1, 1], rows: [1, 1], cells: slots(4) });
7121
- registerLayout({ id: "8", label: "8 grid", cols: [1, 1, 1, 1], rows: [1, 1], cells: slots(8) });
7552
+ registerLayout({ id: "1", label: msg("workspace.layout.single", "Single"), cols: [1], rows: [1], cells: slots(1) });
7553
+ registerLayout({ id: "2h", label: msg("workspace.layout.sideBySide", "2 side by side"), cols: [1, 1], rows: [1], cells: slots(2) });
7554
+ registerLayout({ id: "2v", label: msg("workspace.layout.stacked", "2 stacked"), cols: [1], rows: [1, 1], cells: slots(2) });
7555
+ registerLayout({ id: "4", label: msg("workspace.layout.grid4", "4 grid"), cols: [1, 1], rows: [1, 1], cells: slots(4) });
7556
+ registerLayout({ id: "8", label: msg("workspace.layout.grid8", "8 grid"), cols: [1, 1, 1, 1], rows: [1, 1], cells: slots(8) });
7122
7557
  }
7123
7558
  var GRID_PICKER_MAX = 4;
7124
7559
  var GRID_BUILTIN_IDS = {
@@ -7128,17 +7563,20 @@ var GRID_BUILTIN_IDS = {
7128
7563
  "2x2": "4",
7129
7564
  "2x4": "8"
7130
7565
  };
7566
+ var GRID_LABEL = msg("workspace.layout.grid", "{cols} × {rows} grid");
7567
+ function gridLabel(rows, cols) {
7568
+ return withValues(GRID_LABEL, { cols, rows });
7569
+ }
7131
7570
  var clampTrack = (n) => Math.max(1, Math.min(GRID_PICKER_MAX, Math.round(n)));
7132
- function layoutForGrid(rows, cols) {
7571
+ function layoutForGrid(rows, cols, i18n) {
7133
7572
  const r = clampTrack(rows);
7134
7573
  const c = clampTrack(cols);
7135
7574
  const builtin = GRID_BUILTIN_IDS[`${r}x${c}`];
7136
7575
  const registered = builtin ? registry.get(builtin) : void 0;
7137
- if (registered) return registered;
7576
+ if (registered) return inLanguage(registered, i18n);
7138
7577
  return {
7139
7578
  id: `g${r}x${c}`,
7140
- // Width-first label — matches the picker's caption ("3 × 2" = 3 wide, 2 tall).
7141
- label: `${c} \xD7 ${r} grid`,
7579
+ label: textOf(i18n, gridLabel(r, c)),
7142
7580
  cols: Array.from({ length: c }, () => 1),
7143
7581
  rows: Array.from({ length: r }, () => 1),
7144
7582
  cells: slots(r * c)
@@ -7161,7 +7599,7 @@ function layoutShape(def) {
7161
7599
  function occupancyGrid(def) {
7162
7600
  if (def.areas) return def.areas.map((row) => row.trim().split(/\s+/));
7163
7601
  const cols = def.cols.length;
7164
- return def.rows.map((_, r) => def.cols.map((_2, c) => def.cells[r * cols + c]?.id ?? `\xB7${r}x${c}`));
7602
+ return def.rows.map((_, r) => def.cols.map((_2, c) => def.cells[r * cols + c]?.id ?? `·${r}x${c}`));
7165
7603
  }
7166
7604
  var GLYPH_MIN = 1.5;
7167
7605
  var GLYPH_SPAN = 13;
@@ -7452,8 +7890,8 @@ var TOOL_BINDINGS = [
7452
7890
  ["hline", "drawings.hline-cursor"],
7453
7891
  ["vline", "drawings.vline-cursor"]
7454
7892
  ];
7455
- function toolShortcutHints(keymap) {
7456
- const byId = new Map(keymap.bindings().map((b) => [b.id, b.display[0]]));
7893
+ function toolShortcutHints(keymap, i18n) {
7894
+ const byId = new Map(keymap.bindings(i18n).map((b) => [b.id, b.display[0]]));
7457
7895
  const out = {};
7458
7896
  for (const [type, id] of TOOL_BINDINGS) {
7459
7897
  const display = byId.get(id);
@@ -7551,10 +7989,10 @@ var CSS14 = `
7551
7989
  .vela-mb-item:active { background: var(--vela-hover); }
7552
7990
  .vela-mb-item .vela-icon { font-size: 18px; width: 18px; height: 18px; }
7553
7991
  /* A lit stop (the maximize toggle while something is isolated): the inverse
7554
- "selected" chip \u2014 white on the dark theme, dark on the light one. */
7992
+ "selected" chip — white on the dark theme, dark on the light one. */
7555
7993
  .vela-mb-item.vela-mb-on, .vela-mb-item.vela-mb-on:active { background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
7556
7994
  /* Left-aligned contributed actions get their own stops (the built-in indicators
7557
- slot) \u2014 the wrapper is layout-transparent so each stop flexes like a sibling. */
7995
+ slot) — the wrapper is layout-transparent so each stop flexes like a sibling. */
7558
7996
  .vela-mb-actions { display: contents; }
7559
7997
  .vela-mb-symbol {
7560
7998
  flex: 1.6 1 0;
@@ -7568,39 +8006,57 @@ var CSS14 = `
7568
8006
  text-align: center;
7569
8007
  }
7570
8008
  `;
8009
+ var TEXT15 = {
8010
+ symbolSearch: msg("widget.mobileBar.symbolSearch", "Symbol search"),
8011
+ timeframe: msg("widget.mobileBar.timeframe", "Timeframe"),
8012
+ indicators: msg("widget.mobileBar.indicators", "Indicators"),
8013
+ drawings: msg("widget.mobileBar.drawings", "Drawings"),
8014
+ maximizeChart: msg("widget.mobileBar.maximizeChart", "Maximize chart"),
8015
+ restoreLayout: msg("widget.mobileBar.restoreLayout", "Restore layout"),
8016
+ more: msg("widget.mobileBar.more", "More"),
8017
+ chartSettings: msg("widget.mobileBar.chartSettings", "Chart settings")
8018
+ };
7571
8019
  var MobileBar = class {
7572
8020
  constructor(host, opts) {
8021
+ /** Each fixed stop's accessible name, re-read by {@link relocalize}. */
8022
+ this.labels = [];
8023
+ this.maximizeActive = false;
7573
8024
  this.opts = opts;
7574
8025
  const doc = host.ownerDocument;
7575
8026
  injectStyles(STYLE_ID14, CSS14, doc);
7576
8027
  this.el = doc.createElement("div");
7577
8028
  this.el.className = "vela-mobilebar";
8029
+ this.timeframe = opts.timeframe;
7578
8030
  const item = (cls, label, onClick, icon2) => {
7579
8031
  const b = doc.createElement("button");
7580
8032
  b.className = `vela-mb-item ${cls}`;
7581
- b.setAttribute("aria-label", label);
8033
+ b.setAttribute("aria-label", this.t(label));
8034
+ this.labels.push({ el: b, label });
7582
8035
  if (icon2) b.appendChild(iconEl(icon2, doc));
7583
8036
  b.addEventListener("click", onClick);
7584
8037
  return b;
7585
8038
  };
7586
- this.symbolEl = item("vela-mb-symbol", "Symbol search", opts.onSymbolClick);
8039
+ this.symbolEl = item("vela-mb-symbol", TEXT15.symbolSearch, opts.onSymbolClick);
7587
8040
  this.symbolEl.textContent = parseSymbol(opts.symbol).ticker;
7588
- this.tfEl = item("vela-mb-tf", "Timeframe", opts.onTimeframeClick);
7589
- this.tfEl.textContent = timeframeLabel(opts.timeframe);
8041
+ this.tfEl = item("vela-mb-tf", TEXT15.timeframe, opts.onTimeframeClick);
8042
+ this.tfEl.textContent = timeframeLabel(opts.timeframe, opts.i18n);
7590
8043
  const onIndicators = opts.onIndicatorsClick;
7591
- const indicators = onIndicators ? item("vela-mb-indicators", "Indicators", onIndicators, "indicators") : null;
8044
+ const indicators = onIndicators ? item("vela-mb-indicators", TEXT15.indicators, onIndicators, "indicators") : null;
7592
8045
  this.actionsHost = doc.createElement("span");
7593
8046
  this.actionsHost.className = "vela-mb-actions";
7594
8047
  const onDrawings = opts.onDrawingsClick;
7595
- const drawings = onDrawings ? item("vela-mb-drawings", "Drawings", onDrawings, "pen") : null;
8048
+ const drawings = onDrawings ? item("vela-mb-drawings", TEXT15.drawings, onDrawings, "pen") : null;
7596
8049
  const onMaximize = opts.onMaximizeClick;
7597
- this.maxEl = onMaximize ? item("vela-mb-maximize", "Maximize chart", onMaximize, "maximize") : null;
7598
- const more = item("vela-mb-more", "More", opts.onMoreClick, "kebab");
7599
- const settings = item("vela-mb-settings", "Chart settings", opts.onSettingsClick, "gear");
8050
+ this.maxEl = onMaximize ? item("vela-mb-maximize", TEXT15.maximizeChart, onMaximize, "maximize") : null;
8051
+ const more = item("vela-mb-more", TEXT15.more, opts.onMoreClick, "kebab");
8052
+ const settings = item("vela-mb-settings", TEXT15.chartSettings, opts.onSettingsClick, "gear");
7600
8053
  this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], ...this.maxEl ? [this.maxEl] : [], more, settings);
7601
8054
  host.appendChild(this.el);
7602
8055
  this.renderActions();
7603
8056
  }
8057
+ t(text) {
8058
+ return textOf(this.opts.i18n, text);
8059
+ }
7604
8060
  /** Re-project the contributed actions placed on the bar (left-aligned by default —
7605
8061
  * see `mobilePlacement`) as icon-only stops in the indicators slot (call after
7606
8062
  * registrations change). The rest are three-dots drawer rows. */
@@ -7613,9 +8069,10 @@ var MobileBar = class {
7613
8069
  for (const action of widgetActions("topbar", ctx).filter((a) => mobilePlacement(a) === "bar" && !builtin.has(a.id))) {
7614
8070
  const b = doc.createElement("button");
7615
8071
  b.className = "vela-mb-item";
7616
- b.setAttribute("aria-label", action.label);
8072
+ const label = textOf(ctx.i18n ?? this.opts.i18n, action.label);
8073
+ b.setAttribute("aria-label", label);
7617
8074
  if (action.icon) b.appendChild(iconEl(action.icon, doc));
7618
- else b.appendChild(doc.createTextNode(action.label));
8075
+ else b.appendChild(doc.createTextNode(label));
7619
8076
  b.addEventListener("click", () => {
7620
8077
  const c = this.opts.getContext?.();
7621
8078
  if (c) action.run(c);
@@ -7627,14 +8084,23 @@ var MobileBar = class {
7627
8084
  this.symbolEl.textContent = parseSymbol(symbol).ticker;
7628
8085
  }
7629
8086
  setTimeframe(tf) {
7630
- this.tfEl.textContent = timeframeLabel(tf);
8087
+ this.timeframe = tf;
8088
+ this.tfEl.textContent = timeframeLabel(tf, this.opts.i18n);
8089
+ }
8090
+ /** Re-read every label in the current language. */
8091
+ relocalize() {
8092
+ for (const { el, label } of this.labels) el.setAttribute("aria-label", this.t(label));
8093
+ this.setTimeframe(this.timeframe);
8094
+ if (this.maxEl) this.setMaximizeActive(this.maximizeActive);
8095
+ this.renderActions();
7631
8096
  }
7632
8097
  /** Light the maximize stop while something is isolated (a chart over the grid,
7633
8098
  * or a maximized pane inside the active chart) — inverse chip + restore glyph. */
7634
8099
  setMaximizeActive(on) {
7635
8100
  if (!this.maxEl) return;
8101
+ this.maximizeActive = on;
7636
8102
  this.maxEl.classList.toggle("vela-mb-on", on);
7637
- this.maxEl.setAttribute("aria-label", on ? "Restore layout" : "Maximize chart");
8103
+ this.maxEl.setAttribute("aria-label", this.t(on ? TEXT15.restoreLayout : TEXT15.maximizeChart));
7638
8104
  this.maxEl.replaceChildren(iconEl(on ? "restore" : "maximize", this.el.ownerDocument));
7639
8105
  }
7640
8106
  /** Show the maximize stop only while the grid holds more than one chart — on a
@@ -7700,23 +8166,28 @@ var CSS15 = `
7700
8166
  background: var(--vela-hover);
7701
8167
  }
7702
8168
  `;
8169
+ var TEXT16 = {
8170
+ dateRange: msg("widget.timeframeDrawer.dateRange", "Date Range"),
8171
+ timeframes: msg("widget.timeframeDrawer.timeframes", "Timeframes")
8172
+ };
7703
8173
  var TimeframeDrawer = class {
7704
8174
  constructor(opts) {
7705
8175
  this.opts = opts;
7706
8176
  this.rangeChips = /* @__PURE__ */ new Map();
7707
8177
  this.tfChips = /* @__PURE__ */ new Map();
8178
+ this.headings = [];
7708
8179
  const doc = opts.host.ownerDocument;
7709
8180
  injectStyles(STYLE_ID15, CSS15, doc);
7710
8181
  this.drawer = new Drawer({ host: opts.host, onOpenChange: opts.onOpenChange });
7711
8182
  const rangeHeading = doc.createElement("div");
7712
8183
  rangeHeading.className = "vela-tfd-heading";
7713
- rangeHeading.textContent = "Date Range";
8184
+ this.headings.push({ el: rangeHeading, text: TEXT16.dateRange });
7714
8185
  const ranges = doc.createElement("div");
7715
8186
  ranges.className = "vela-tfd-ranges";
7716
8187
  for (const preset of opts.ranges) {
7717
8188
  const chip = doc.createElement("button");
7718
8189
  chip.className = "vela-tfd-chip";
7719
- chip.textContent = preset.id;
8190
+ chip.textContent = rangeLabel(preset.id, opts.i18n);
7720
8191
  chip.addEventListener("click", () => {
7721
8192
  opts.onRange(preset);
7722
8193
  this.drawer.hide();
@@ -7726,13 +8197,13 @@ var TimeframeDrawer = class {
7726
8197
  }
7727
8198
  const tfHeading = doc.createElement("div");
7728
8199
  tfHeading.className = "vela-tfd-heading";
7729
- tfHeading.textContent = "Timeframes";
8200
+ this.headings.push({ el: tfHeading, text: TEXT16.timeframes });
7730
8201
  const grid = doc.createElement("div");
7731
8202
  grid.className = "vela-tfd-grid";
7732
8203
  for (const tf of opts.timeframes) {
7733
8204
  const chip = doc.createElement("button");
7734
8205
  chip.className = "vela-tfd-chip";
7735
- chip.textContent = timeframeLabel(tf);
8206
+ chip.textContent = timeframeLabel(tf, opts.i18n);
7736
8207
  chip.addEventListener("click", () => {
7737
8208
  opts.onTimeframe(tf);
7738
8209
  this.drawer.hide();
@@ -7741,8 +8212,17 @@ var TimeframeDrawer = class {
7741
8212
  grid.appendChild(chip);
7742
8213
  }
7743
8214
  this.drawer.body.append(rangeHeading, ranges, tfHeading, grid);
8215
+ this.relabel();
8216
+ }
8217
+ /** Headings and chips in the current language. */
8218
+ relabel() {
8219
+ const i18n = this.opts.i18n;
8220
+ for (const { el, text } of this.headings) el.textContent = textOf(i18n, text);
8221
+ for (const [id, chip] of this.rangeChips) chip.textContent = rangeLabel(id, i18n);
8222
+ for (const [tf, chip] of this.tfChips) chip.textContent = timeframeLabel(tf, i18n);
7744
8223
  }
7745
8224
  open() {
8225
+ this.relabel();
7746
8226
  const tf = this.opts.currentTimeframe();
7747
8227
  const range = this.opts.activeRange();
7748
8228
  for (const [id, chip] of this.rangeChips) {
@@ -7764,6 +8244,12 @@ var TimeframeDrawer = class {
7764
8244
  };
7765
8245
 
7766
8246
  // src/widget/drawings-drawer.ts
8247
+ var TEXT17 = {
8248
+ title: msg("widget.drawingsDrawer.title", "Drawings"),
8249
+ search: msg("widget.drawingsDrawer.search", "Search tools…"),
8250
+ favorite: msg("widget.drawingsDrawer.favorite", "Favorite {tool}"),
8251
+ noMatch: msg("widget.drawingsDrawer.noMatch", "No tools match.")
8252
+ };
7767
8253
  var STYLE_ID16 = "vela-widget-drawings-drawer";
7768
8254
  var CSS16 = `
7769
8255
  /* Search + tabs stay pinned while the tool list scrolls underneath. */
@@ -7797,7 +8283,7 @@ var CSS16 = `
7797
8283
  gap: 4px;
7798
8284
  overflow-x: auto;
7799
8285
  scrollbar-width: none;
7800
- /* The strip scrolls sideways itself \u2014 its touches are native scroll, not tab swipes
8286
+ /* The strip scrolls sideways itself — its touches are native scroll, not tab swipes
7801
8287
  (the drawer body is pan-y so everywhere ELSE a sideways move swipes the tabs). */
7802
8288
  touch-action: pan-x;
7803
8289
  padding: 0 2px 8px;
@@ -7870,7 +8356,7 @@ var DrawingsDrawer = class {
7870
8356
  injectStyles(STYLE_ID16, CSS16, doc);
7871
8357
  this.drawer = new Drawer({
7872
8358
  host: opts.host,
7873
- title: "Drawings",
8359
+ title: this.t(TEXT17.title),
7874
8360
  onOpenChange: opts.onOpenChange,
7875
8361
  // Swiping across the tool list pages through the group tabs.
7876
8362
  onSwipe: (dir) => this.stepGroup(dir === "left" ? 1 : -1)
@@ -7882,7 +8368,7 @@ var DrawingsDrawer = class {
7882
8368
  search.appendChild(iconEl("search", doc));
7883
8369
  this.input = doc.createElement("input");
7884
8370
  this.input.type = "text";
7885
- this.input.placeholder = "Search tools\u2026";
8371
+ this.input.placeholder = this.t(TEXT17.search);
7886
8372
  this.input.addEventListener("input", () => this.renderList());
7887
8373
  search.appendChild(this.input);
7888
8374
  this.tabs = doc.createElement("div");
@@ -7892,7 +8378,12 @@ var DrawingsDrawer = class {
7892
8378
  this.list.className = "vela-dd-list";
7893
8379
  this.drawer.body.append(sticky, this.list);
7894
8380
  }
8381
+ t(m, values) {
8382
+ return textOf(this.opts.i18n?.() ?? null, m, values);
8383
+ }
7895
8384
  open() {
8385
+ this.drawer.setTitle(this.t(TEXT17.title));
8386
+ this.input.placeholder = this.t(TEXT17.search);
7896
8387
  this.definition = this.opts.toolbar();
7897
8388
  if (!this.definition.groups.some((g) => g.id === this.activeGroup)) this.activeGroup = this.definition.groups[0]?.id ?? "";
7898
8389
  this.input.value = "";
@@ -7924,7 +8415,7 @@ var DrawingsDrawer = class {
7924
8415
  for (const group of this.definition.groups) {
7925
8416
  const tab = doc.createElement("button");
7926
8417
  tab.className = "vela-dd-tab";
7927
- tab.textContent = group.label;
8418
+ tab.textContent = this.t(group.label);
7928
8419
  if (group.id === this.activeGroup) tab.dataset.active = "1";
7929
8420
  tab.addEventListener("click", () => {
7930
8421
  this.activeGroup = group.id;
@@ -7938,7 +8429,15 @@ var DrawingsDrawer = class {
7938
8429
  renderList() {
7939
8430
  const doc = this.list.ownerDocument;
7940
8431
  this.list.replaceChildren();
7941
- const query = this.input.value.trim().toLowerCase();
8432
+ const locale = this.opts.i18n?.().locale;
8433
+ const lower = (s) => {
8434
+ try {
8435
+ return s.toLocaleLowerCase(locale);
8436
+ } catch {
8437
+ return s.toLowerCase();
8438
+ }
8439
+ };
8440
+ const query = lower(this.input.value.trim());
7942
8441
  const active = this.opts.currentTool();
7943
8442
  const section = (label) => {
7944
8443
  const el = doc.createElement("div");
@@ -7955,7 +8454,7 @@ var DrawingsDrawer = class {
7955
8454
  glyph.innerHTML = tool.icon;
7956
8455
  const label = doc.createElement("span");
7957
8456
  label.className = "vela-dd-label";
7958
- label.textContent = tool.label;
8457
+ label.textContent = this.t(tool.label);
7959
8458
  const star = doc.createElement("button");
7960
8459
  star.className = "vela-dd-star";
7961
8460
  const paintStar = (on) => {
@@ -7964,7 +8463,7 @@ var DrawingsDrawer = class {
7964
8463
  else delete star.dataset.on;
7965
8464
  };
7966
8465
  paintStar(this.opts.isFavorite(tool.type));
7967
- star.setAttribute("aria-label", `Favorite ${tool.label}`);
8466
+ star.setAttribute("aria-label", this.t(TEXT17.favorite, { tool: this.t(tool.label) }));
7968
8467
  star.addEventListener("click", (e) => {
7969
8468
  e.stopPropagation();
7970
8469
  const on = !this.opts.isFavorite(tool.type);
@@ -7981,16 +8480,16 @@ var DrawingsDrawer = class {
7981
8480
  if (query) {
7982
8481
  let any = false;
7983
8482
  for (const group2 of this.definition.groups) {
7984
- const hits = group2.tools.filter((t) => t.label.toLowerCase().includes(query));
8483
+ const hits = group2.tools.filter((t) => lower(this.t(t.label)).includes(query) || sourceText(t.label).toLowerCase().includes(query));
7985
8484
  if (hits.length === 0) continue;
7986
8485
  any = true;
7987
- section(group2.label);
8486
+ section(this.t(group2.label));
7988
8487
  for (const tool of hits) row(tool);
7989
8488
  }
7990
8489
  if (!any) {
7991
8490
  const empty = doc.createElement("div");
7992
8491
  empty.className = "vela-dd-empty";
7993
- empty.textContent = "No tools match.";
8492
+ empty.textContent = this.t(TEXT17.noMatch);
7994
8493
  this.list.appendChild(empty);
7995
8494
  }
7996
8495
  return;
@@ -7999,7 +8498,7 @@ var DrawingsDrawer = class {
7999
8498
  if (!group) return;
8000
8499
  if (group.sections && group.sections.length > 0) {
8001
8500
  for (const s of group.sections) {
8002
- section(s.label);
8501
+ section(this.t(s.label));
8003
8502
  for (const tool of s.tools) row(tool);
8004
8503
  }
8005
8504
  } else {
@@ -8009,6 +8508,17 @@ var DrawingsDrawer = class {
8009
8508
  };
8010
8509
 
8011
8510
  // src/widget/more-drawer.ts
8511
+ var TEXT18 = {
8512
+ chartType: msg("widget.moreDrawer.chartType", "Chart type"),
8513
+ layout: msg("widget.moreDrawer.layout", "Layout"),
8514
+ alerts: msg("widget.moreDrawer.alerts", "Alerts"),
8515
+ undo: msg("widget.moreDrawer.undo", "Undo"),
8516
+ redo: msg("widget.moreDrawer.redo", "Redo"),
8517
+ screenshot: msg("widget.moreDrawer.screenshot", "Screenshot"),
8518
+ sync: msg("widget.moreDrawer.sync", "Sync"),
8519
+ noAlerts: msg("widget.moreDrawer.noAlerts", "No alerts yet."),
8520
+ alert: msg("widget.moreDrawer.alert", "{title}: {message}")
8521
+ };
8012
8522
  var STYLE_ID17 = "vela-widget-more-drawer";
8013
8523
  var CSS17 = `
8014
8524
  .vela-md-actions {
@@ -8099,6 +8609,9 @@ var MoreDrawer = class {
8099
8609
  destroy() {
8100
8610
  this.drawer.destroy();
8101
8611
  }
8612
+ t(text, values) {
8613
+ return textOf(this.opts.i18n, text, values);
8614
+ }
8102
8615
  show(view) {
8103
8616
  this.view = view;
8104
8617
  this.render();
@@ -8110,7 +8623,7 @@ var MoreDrawer = class {
8110
8623
  const back = doc.createElement("button");
8111
8624
  back.className = "vela-md-back";
8112
8625
  back.appendChild(iconEl("chevron-left", doc));
8113
- back.appendChild(doc.createTextNode(this.view === "style" ? "Chart type" : this.view === "layout" ? "Layout" : "Alerts"));
8626
+ back.appendChild(doc.createTextNode(this.t(this.view === "style" ? TEXT18.chartType : this.view === "layout" ? TEXT18.layout : TEXT18.alerts)));
8114
8627
  back.addEventListener("click", () => this.show("main"));
8115
8628
  this.drawer.body.appendChild(back);
8116
8629
  }
@@ -8150,25 +8663,25 @@ var MoreDrawer = class {
8150
8663
  b.className = "vela-md-action";
8151
8664
  b.disabled = !enabled;
8152
8665
  b.appendChild(iconEl(icon2, doc));
8153
- b.appendChild(doc.createTextNode(label));
8666
+ b.appendChild(doc.createTextNode(this.t(label)));
8154
8667
  b.addEventListener("click", () => {
8155
8668
  run();
8156
8669
  this.drawer.hide();
8157
8670
  });
8158
8671
  actions.appendChild(b);
8159
8672
  };
8160
- if (this.opts.onUndo) action("undo", "Undo", this.opts.canUndo(), this.opts.onUndo);
8161
- if (this.opts.onRedo) action("redo", "Redo", this.opts.canRedo(), this.opts.onRedo);
8162
- if (this.opts.onScreenshot) action("camera", "Screenshot", true, this.opts.onScreenshot);
8673
+ if (this.opts.onUndo) action("undo", TEXT18.undo, this.opts.canUndo(), this.opts.onUndo);
8674
+ if (this.opts.onRedo) action("redo", TEXT18.redo, this.opts.canRedo(), this.opts.onRedo);
8675
+ if (this.opts.onScreenshot) action("camera", TEXT18.screenshot, true, this.opts.onScreenshot);
8163
8676
  if (actions.childElementCount > 0) this.drawer.body.appendChild(actions);
8164
8677
  const list = doc.createElement("div");
8165
8678
  list.className = "vela-md-list";
8166
8679
  const current = this.opts.priceStyles().find((s) => s.id === this.opts.priceStyle());
8167
- list.appendChild(this.row(doc, "Chart type", { icon: "style-line", value: current?.label, chevron: true, onClick: () => this.show("style") }));
8680
+ list.appendChild(this.row(doc, this.t(TEXT18.chartType), { icon: "style-line", value: current?.label, chevron: true, onClick: () => this.show("style") }));
8168
8681
  if (this.opts.layout) {
8169
8682
  const shape = this.opts.layout.shape();
8170
- const value = shape ? `${shape.cols} \xD7 ${shape.rows}` : this.opts.layout.presets().find((p) => p.checked)?.label;
8171
- list.appendChild(this.row(doc, "Layout", { icon: "layout", glyph: this.opts.layout.glyph?.(), value, chevron: true, onClick: () => this.show("layout") }));
8683
+ const value = shape ? layoutGridLabel(shape.cols, shape.rows, this.opts.i18n) : this.opts.layout.presets().find((p) => p.checked)?.label;
8684
+ list.appendChild(this.row(doc, this.t(TEXT18.layout), { icon: "layout", glyph: this.opts.layout.glyph?.(), value, chevron: true, onClick: () => this.show("layout") }));
8172
8685
  }
8173
8686
  for (const act of this.opts.primaryActions?.() ?? []) list.appendChild(this.actionRow(doc, act));
8174
8687
  for (const panel of this.opts.panels()) {
@@ -8184,7 +8697,7 @@ var MoreDrawer = class {
8184
8697
  }
8185
8698
  if (this.opts.alerts) {
8186
8699
  const alertCount = this.opts.alerts().length;
8187
- list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
8700
+ list.appendChild(this.row(doc, this.t(TEXT18.alerts), { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
8188
8701
  }
8189
8702
  for (const act of this.opts.actions()) list.appendChild(this.actionRow(doc, act));
8190
8703
  this.drawer.body.appendChild(list);
@@ -8224,7 +8737,7 @@ var MoreDrawer = class {
8224
8737
  list.className = "vela-md-list";
8225
8738
  const wrap = doc.createElement("div");
8226
8739
  wrap.className = "vela-md-gridwrap";
8227
- const { el: grid, squares } = layoutGridCanvas(doc);
8740
+ const { el: grid, squares } = layoutGridCanvas(doc, this.opts.i18n);
8228
8741
  paintLayoutGrid(squares, layout.shape());
8229
8742
  grid.addEventListener("click", (e) => {
8230
8743
  const sq = e.target?.closest?.(".vela-lp-sq");
@@ -8249,7 +8762,7 @@ var MoreDrawer = class {
8249
8762
  if (syncs.length > 0) {
8250
8763
  const heading = doc.createElement("div");
8251
8764
  heading.className = "vela-md-section";
8252
- heading.textContent = "Sync";
8765
+ heading.textContent = this.t(TEXT18.sync);
8253
8766
  list.appendChild(heading);
8254
8767
  for (const s of syncs) {
8255
8768
  list.appendChild(
@@ -8270,20 +8783,23 @@ var MoreDrawer = class {
8270
8783
  if (alerts.length === 0) {
8271
8784
  const empty = doc.createElement("div");
8272
8785
  empty.className = "vela-md-empty";
8273
- empty.textContent = "No alerts yet.";
8786
+ empty.textContent = this.t(TEXT18.noAlerts);
8274
8787
  this.drawer.body.appendChild(empty);
8275
8788
  return;
8276
8789
  }
8277
8790
  const list = doc.createElement("div");
8278
8791
  list.className = "vela-md-list";
8279
8792
  for (const a of alerts) {
8280
- list.appendChild(this.row(doc, `${a.title}: ${a.message}`, { value: new Date(a.time).toLocaleTimeString(), onClick: () => void 0 }));
8793
+ list.appendChild(this.row(doc, this.t(TEXT18.alert, { title: a.title, message: a.message }), { value: eventTimeText(a.time, this.opts.i18n), onClick: () => void 0 }));
8281
8794
  }
8282
8795
  this.drawer.body.appendChild(list);
8283
8796
  }
8284
8797
  };
8285
8798
 
8286
8799
  // src/widget/timezone-drawer.ts
8800
+ var TEXT19 = {
8801
+ title: msg("widget.timezoneDrawer.title", "Time zone")
8802
+ };
8287
8803
  var STYLE_ID18 = "vela-widget-timezone-drawer";
8288
8804
  var CSS18 = `
8289
8805
  .vela-tzd-list { padding: 2px 0 4px; }
@@ -8305,9 +8821,10 @@ var TimezoneDrawer = class {
8305
8821
  constructor(opts) {
8306
8822
  this.opts = opts;
8307
8823
  injectStyles(STYLE_ID18, CSS18, opts.host.ownerDocument);
8308
- this.drawer = new Drawer({ host: opts.host, title: "Time zone", onOpenChange: opts.onOpenChange });
8824
+ this.drawer = new Drawer({ host: opts.host, title: textOf(opts.i18n, TEXT19.title), onOpenChange: opts.onOpenChange });
8309
8825
  }
8310
8826
  open() {
8827
+ this.drawer.setTitle(textOf(this.opts.i18n, TEXT19.title));
8311
8828
  this.render();
8312
8829
  this.drawer.show();
8313
8830
  }
@@ -8322,7 +8839,7 @@ var TimezoneDrawer = class {
8322
8839
  this.drawer.body.replaceChildren();
8323
8840
  const list = doc.createElement("div");
8324
8841
  list.className = "vela-tzd-list";
8325
- for (const tz of timezoneMenuRows(this.opts.timezone())) {
8842
+ for (const tz of timezoneMenuRows(this.opts.timezone(), this.opts.i18n)) {
8326
8843
  const row = doc.createElement("div");
8327
8844
  row.className = "vela-tzd-row";
8328
8845
  const label = doc.createElement("span");
@@ -8367,17 +8884,21 @@ var CSS19 = `
8367
8884
  color: var(--vela-fg-muted);
8368
8885
  }
8369
8886
  `;
8887
+ var TEXT20 = {
8888
+ title: msg("widget.priceScaleDrawer.title", "Price scale")
8889
+ };
8370
8890
  var PriceScaleDrawer = class {
8371
8891
  constructor(opts) {
8372
8892
  this.opts = opts;
8373
8893
  this.pane = null;
8374
8894
  injectStyles(STYLE_ID19, CSS19, opts.host.ownerDocument);
8375
- this.drawer = new Drawer({ host: opts.host, title: "Price scale", onOpenChange: opts.onOpenChange });
8895
+ this.drawer = new Drawer({ host: opts.host, title: textOf(opts.chart()?.i18n, TEXT20.title), onOpenChange: opts.onOpenChange });
8376
8896
  }
8377
8897
  open() {
8378
8898
  const chart = this.opts.chart();
8379
8899
  const panes = chart?.renderer.get("paneScales");
8380
8900
  this.pane = Array.isArray(panes) ? paneScaleAt(panes, this.opts.pressY()) : null;
8901
+ this.drawer.setTitle(textOf(chart?.i18n, TEXT20.title));
8381
8902
  this.render();
8382
8903
  this.drawer.show();
8383
8904
  }
@@ -8404,7 +8925,7 @@ var PriceScaleDrawer = class {
8404
8925
  priceLabel: this.flag("priceLabel"),
8405
8926
  countdown: this.flag("countdown"),
8406
8927
  priceLine: this.flag("currentPriceLine")
8407
- });
8928
+ }, chart.i18n);
8408
8929
  const list = doc.createElement("div");
8409
8930
  list.className = "vela-psd-list";
8410
8931
  const row = (label, opts) => {
@@ -8427,7 +8948,7 @@ var PriceScaleDrawer = class {
8427
8948
  };
8428
8949
  for (const item of items) {
8429
8950
  if (item.id === "labels" && item.submenu) {
8430
- section("Labels");
8951
+ section(item.label);
8431
8952
  for (const sub of item.submenu) {
8432
8953
  row(sub.label, {
8433
8954
  checked: sub.checked,
@@ -8441,7 +8962,7 @@ var PriceScaleDrawer = class {
8441
8962
  continue;
8442
8963
  }
8443
8964
  if (item.id === "levels" && item.submenu) {
8444
- section("Levels");
8965
+ section(item.label);
8445
8966
  for (const sub of item.submenu) {
8446
8967
  row(sub.label, {
8447
8968
  checked: sub.checked,
@@ -8507,6 +9028,12 @@ var PriceScaleDrawer = class {
8507
9028
  };
8508
9029
 
8509
9030
  // src/widget/drawing-pill.ts
9031
+ var TEXT21 = {
9032
+ magnet: msg("widget.drawingPill.magnet", "Magnet snap"),
9033
+ stay: msg("widget.drawingPill.stay", "Stay in drawing mode"),
9034
+ eraser: msg("widget.drawingPill.eraser", "Eraser"),
9035
+ exit: msg("widget.drawingPill.exit", "Exit drawing mode")
9036
+ };
8510
9037
  var STYLE_ID20 = "vela-widget-drawing-pill";
8511
9038
  var CSS20 = `
8512
9039
  .vela-drawpill { display: none; }
@@ -8556,17 +9083,24 @@ var CSS20 = `
8556
9083
  .vela-drawpill-btn .vela-icon { font-size: 17px; width: 17px; height: 17px; }
8557
9084
  .vela-drawpill-badge {
8558
9085
  position: absolute;
8559
- top: 2px;
8560
- right: 2px;
8561
- font-size: 8px;
8562
- font-weight: 700;
9086
+ top: 5px;
9087
+ right: 5px;
9088
+ display: none;
9089
+ align-items: flex-end;
9090
+ gap: 1px;
8563
9091
  color: var(--vela-accent);
8564
9092
  }
9093
+ .vela-drawpill-badge[data-strength] { display: inline-flex; }
9094
+ .vela-drawpill-badge > i { display: block; width: 2px; height: 4px; border-radius: 1px; background: currentColor; }
9095
+ .vela-drawpill-badge > i + i { height: 7px; opacity: 0.3; }
9096
+ .vela-drawpill-badge[data-strength='strong'] > i + i { opacity: 1; }
8565
9097
  `;
8566
9098
  var DrawingPill = class {
8567
9099
  constructor(host) {
8568
9100
  this.chart = null;
8569
9101
  this.chartSubs = [];
9102
+ /** Each button's accessible name, re-read in the chart's language on every sync. */
9103
+ this.labels = [];
8570
9104
  const doc = host.ownerDocument;
8571
9105
  injectStyles(STYLE_ID20, CSS20, doc);
8572
9106
  this.el = doc.createElement("div");
@@ -8577,32 +9111,38 @@ var DrawingPill = class {
8577
9111
  const btn = (icon2, label, onClick) => {
8578
9112
  const b = doc.createElement("button");
8579
9113
  b.className = "vela-drawpill-btn";
8580
- b.setAttribute("aria-label", label);
9114
+ b.setAttribute("aria-label", this.t(label));
9115
+ this.labels.push({ el: b, label });
8581
9116
  b.appendChild(iconEl(icon2, doc));
8582
9117
  b.addEventListener("click", onClick);
8583
9118
  return b;
8584
9119
  };
8585
- this.magnetBtn = btn("magnet", "Magnet snap", () => {
9120
+ this.magnetBtn = btn("magnet", TEXT21.magnet, () => {
8586
9121
  const order = ["off", "weak", "strong"];
8587
9122
  const cur = this.chart?.drawings.getSnapMode() ?? "off";
8588
9123
  this.chart?.drawings.setSnapMode(order[(order.indexOf(cur) + 1) % order.length]);
8589
9124
  });
8590
9125
  this.magnetBadge = doc.createElement("span");
8591
9126
  this.magnetBadge.className = "vela-drawpill-badge";
9127
+ this.magnetBadge.setAttribute("aria-hidden", "true");
9128
+ this.magnetBadge.append(doc.createElement("i"), doc.createElement("i"));
8592
9129
  this.magnetBtn.appendChild(this.magnetBadge);
8593
- this.stayBtn = btn(iconMarkup("pen-lock") ? "pen-lock" : "pen", "Stay in drawing mode", () => {
9130
+ this.stayBtn = btn(iconMarkup("pen-lock") ? "pen-lock" : "pen", TEXT21.stay, () => {
8594
9131
  this.chart?.drawings.setStayMode(!this.chart.drawings.getStayMode());
8595
9132
  });
8596
- this.eraserBtn = btn("eraser", "Eraser", () => {
9133
+ this.eraserBtn = btn("eraser", TEXT21.eraser, () => {
8597
9134
  this.chart?.drawings.setMode(this.chart.drawings.getMode() === "eraser" ? null : "eraser");
8598
9135
  });
8599
- const close = btn("close", "Exit drawing mode", () => {
9136
+ const close = btn("close", TEXT21.exit, () => {
8600
9137
  this.chart?.drawings.setTool(null);
8601
9138
  this.chart?.drawings.setMode(null);
8602
9139
  });
8603
9140
  this.el.append(this.toolGlyph, this.magnetBtn, this.stayBtn, this.eraserBtn, close);
8604
9141
  host.appendChild(this.el);
8605
9142
  }
9143
+ t(text) {
9144
+ return textOf(this.chart?.i18n, text);
9145
+ }
8606
9146
  /** Rebind to another chart (a rebuild, or the workspace's active cell changing) —
8607
9147
  * the previous chart's subscriptions are dropped so only one chart drives the pill. */
8608
9148
  onChart(chart) {
@@ -8616,7 +9156,12 @@ var DrawingPill = class {
8616
9156
  ];
8617
9157
  this.sync();
8618
9158
  }
9159
+ /** Re-read the labels in the chart's language. */
9160
+ relocalize() {
9161
+ this.sync();
9162
+ }
8619
9163
  sync() {
9164
+ for (const { el, label } of this.labels) el.setAttribute("aria-label", this.t(label));
8620
9165
  const drawings = this.chart?.drawings;
8621
9166
  if (!drawings || !drawings.supported) {
8622
9167
  this.el.hidden = true;
@@ -8629,7 +9174,8 @@ var DrawingPill = class {
8629
9174
  this.toolGlyph.innerHTML = tool !== null ? getDrawingType(tool)?.icon ?? "" : iconMarkup("eraser") ?? "";
8630
9175
  const snap = drawings.getSnapMode();
8631
9176
  this.magnetBtn.dataset.on = snap !== "off" ? "1" : "";
8632
- this.magnetBadge.textContent = snap === "weak" ? "W" : snap === "strong" ? "S" : "";
9177
+ if (snap === "off") delete this.magnetBadge.dataset.strength;
9178
+ else this.magnetBadge.dataset.strength = snap;
8633
9179
  this.stayBtn.dataset.on = drawings.getStayMode() ? "1" : "";
8634
9180
  this.eraserBtn.dataset.on = eraser ? "1" : "";
8635
9181
  }
@@ -8664,6 +9210,7 @@ function buildContext(host) {
8664
9210
  togglePanel: (id, open) => host.togglePanel(id, open),
8665
9211
  dockStrip: (el) => host.dockStrip(el),
8666
9212
  host: host.root,
9213
+ i18n: host.i18n,
8667
9214
  toast: (message, kind) => host.toast(message, kind),
8668
9215
  addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
8669
9216
  addNativeIndicator: (type) => host.active()?.addNative(type),
@@ -8743,7 +9290,7 @@ var CSS21 = `
8743
9290
  .vela-ws-grid { position: relative; flex: 1 1 auto; min-width: 0; display: grid; gap: ${GAP_PX}px; background: var(--vela-border-soft); }
8744
9291
  .vela-cell { background: var(--vela-bg); position: relative; }
8745
9292
  /* Active-cell highlight: an overlay ring ABOVE the chart's own canvas stack (a plain
8746
- outline on the cell is painted under them) \u2014 inert to the pointer. Scoped to
9293
+ outline on the cell is painted under them) — inert to the pointer. Scoped to
8747
9294
  multi-cell grids ([data-multi]): a single-cell layout always has an active cell,
8748
9295
  and ringing the only chart would just be noise. */
8749
9296
  .vela-ws-grid[data-multi='1'] .vela-cell[data-active='1']::after {
@@ -8760,11 +9307,11 @@ var CSS21 = `
8760
9307
  .vela-ws-splitter:hover::after { content: ''; position: absolute; background: var(--vela-separator-hover-line); }
8761
9308
  .vela-ws-splitter[data-axis='cols']:hover::after { left: calc(50% - 1px); top: 0; width: 2px; height: 100%; }
8762
9309
  .vela-ws-splitter[data-axis='rows']:hover::after { top: calc(50% - 1px); left: 0; height: 2px; width: 100%; }
8763
- /* Mobile: the docked drawing-toolbar column would eat a phone-width grid \u2014 the shell's
9310
+ /* Mobile: the docked drawing-toolbar column would eat a phone-width grid — the shell's
8764
9311
  drawings drawer + on-chart pill replace it (same policy as the widget's in-chart bar). */
8765
9312
  [data-layout='mobile'] .vela-ws-toolbar { display: none; }
8766
9313
  /* A maximized cell owns the whole grid: the splitter strips have no seams to grab and
8767
- the active ring would just outline the only visible chart \u2014 both are noise here. */
9314
+ the active ring would just outline the only visible chart — both are noise here. */
8768
9315
  .vela-ws-grid[data-maximized='1'] .vela-ws-splitter { display: none; }
8769
9316
  .vela-ws-grid[data-maximized='1'] .vela-cell[data-active='1']::after { display: none; }
8770
9317
  /* Drop-target preview while a cell's drag handle is held: a dashed ring + the same
@@ -8784,24 +9331,58 @@ function declaredOrder(cells) {
8784
9331
  const names = Object.keys(cells ?? {});
8785
9332
  for (const n of names) {
8786
9333
  if (/^\d+$/.test(n)) {
8787
- console.warn(`[vela] workspace cell "${n}" ignored \u2014 a purely-numeric name cannot keep its declaration order (JS object key semantics); use e.g. "cell${n}"`);
9334
+ console.warn(`[vela] workspace cell "${n}" ignored — a purely-numeric name cannot keep its declaration order (JS object key semantics); use e.g. "cell${n}"`);
8788
9335
  }
8789
9336
  }
8790
9337
  return names.filter((n) => !/^\d+$/.test(n));
8791
9338
  }
9339
+ var TEXT22 = {
9340
+ syncSymbol: msg("workspace.sync.symbol", "Symbol"),
9341
+ syncInterval: msg("workspace.sync.interval", "Interval"),
9342
+ syncCrosshair: msg("workspace.sync.crosshair", "Crosshair"),
9343
+ syncStyle: msg("workspace.sync.style", "Style"),
9344
+ alert: msg("workspace.alerts.untitled", "Alert"),
9345
+ alertToast: msg("workspace.alerts.toast", "{source} — {title} — {message}"),
9346
+ alertToastUntitled: msg("workspace.alerts.toastUntitled", "{source} — {message}"),
9347
+ alertRow: msg("workspace.alerts.row", "{source} · {time} · {title}: {message}"),
9348
+ alertDrawerTitle: msg("workspace.alerts.drawerTitle", "{source} · {title}"),
9349
+ noAlerts: msg("workspace.alerts.none", "No alerts yet"),
9350
+ screenshot: msg("workspace.keys.screenshot", "Download a screenshot of the layout"),
9351
+ resetView: msg("workspace.keys.resetView", "Reset view (all history)"),
9352
+ toggleLog: msg("workspace.keys.toggleLog", "Toggle logarithmic scale"),
9353
+ togglePercent: msg("workspace.keys.togglePercent", "Toggle percent scale"),
9354
+ trendline: msg("workspace.keys.trendline", "Arm the trend line tool"),
9355
+ hlineCursor: msg("workspace.keys.hlineCursor", "Horizontal line at the cursor price"),
9356
+ vlineCursor: msg("workspace.keys.vlineCursor", "Vertical line at the cursor time"),
9357
+ undo: msg("workspace.keys.undo", "Undo (active chart)"),
9358
+ redo: msg("workspace.keys.redo", "Redo (active chart)"),
9359
+ zoomIn: msg("workspace.keys.zoomIn", "Zoom in"),
9360
+ zoomOut: msg("workspace.keys.zoomOut", "Zoom out"),
9361
+ panLeft: msg("workspace.keys.panLeft", "Pan toward history"),
9362
+ panRight: msg("workspace.keys.panRight", "Pan toward now"),
9363
+ openIndicators: msg("workspace.keys.openIndicators", "Open the indicator picker"),
9364
+ showShortcuts: msg("workspace.keys.showShortcuts", "Show this shortcuts panel"),
9365
+ categoryChart: msg("workspace.keys.category.chart", "Chart"),
9366
+ categoryDrawings: msg("workspace.keys.category.drawings", "Drawings"),
9367
+ categoryEdit: msg("workspace.keys.category.edit", "Edit"),
9368
+ categoryIndicators: msg("workspace.keys.category.indicators", "Indicators"),
9369
+ categoryHelp: msg("workspace.keys.category.help", "Help")
9370
+ };
9371
+ var ALERT_ROW_MAX = 80;
8792
9372
  function nextAutoCellId(taken) {
8793
9373
  for (let i = 1; ; i += 1) {
8794
9374
  if (!taken.has(`c${i}`)) return `c${i}`;
8795
9375
  }
8796
9376
  }
8797
9377
  function resolveTyping(key, open) {
9378
+ const printable = [...key].length === 1;
8798
9379
  if (open.dialogs > 0) {
8799
- if (key.length !== 1) return null;
9380
+ if (!printable) return null;
8800
9381
  if (open.symbolSearch) return { target: "symbol", text: key.toUpperCase() };
8801
9382
  if (open.timeframeEntry) return { target: "timeframe", text: key.toUpperCase() };
8802
9383
  return null;
8803
9384
  }
8804
- if (/^[a-zA-Z0]$/.test(key)) return { target: "symbol", text: key.toUpperCase() };
9385
+ if (printable && /^[\p{L}0]$/u.test(key)) return { target: "symbol", text: key.toUpperCase() };
8805
9386
  if (/^[1-9]$/.test(key)) return { target: "timeframe", text: key };
8806
9387
  return null;
8807
9388
  }
@@ -8917,6 +9498,7 @@ var VelaWorkspace = class {
8917
9498
  const hostEl = typeof container === "string" ? document.querySelector(container) : container;
8918
9499
  if (!hostEl) throw new Error(`VelaWorkspace: container not found: ${String(container)}`);
8919
9500
  this.opts = opts;
9501
+ this.localeState = new LocaleState(pickLocalization(opts));
8920
9502
  this.replay = new WorkspaceReplay({
8921
9503
  cells: () => this.cells(),
8922
9504
  activeId: () => this.activeId,
@@ -8967,12 +9549,14 @@ var VelaWorkspace = class {
8967
9549
  this.root = doc.createElement("div");
8968
9550
  this.root.className = "vela-workspace";
8969
9551
  ensureUIHost(this.root, resolveTheme(opts.theme));
9552
+ this.applyRootAttributes();
8970
9553
  for (const [name, make] of Object.entries(opts.providers ?? {})) void this.feed.registerProvider(name, make());
8971
9554
  void this.feed.ready().then(() => {
8972
9555
  if (!this.destroyed) this.refreshRetention();
8973
9556
  });
8974
9557
  this.symbolPicker = new SymbolPicker({
8975
9558
  host: this.root,
9559
+ i18n: this.localeState,
8976
9560
  // Row icons come from each descriptor's OWNING provider (resolveSymbolIcon).
8977
9561
  iconFor: (d) => this.feed.symbolIconOf(d),
8978
9562
  onSelect: (ticker) => this.active.setSymbol(ticker),
@@ -8984,6 +9568,7 @@ var VelaWorkspace = class {
8984
9568
  this.symbolPicker.setSource(() => this.feed.symbols());
8985
9569
  this.indicatorPicker = opts.indicatorPicker !== false && !this.indicatorsOverride && topbarHas(this.topbarComp, "indicators") ? new IndicatorPicker({
8986
9570
  host: this.root,
9571
+ i18n: this.localeState,
8987
9572
  library: () => this.active.libraryRows(),
8988
9573
  onChart: () => this.active.onChartRows(),
8989
9574
  onAdd: (i) => this.active.addFromLibrary(i),
@@ -8996,11 +9581,13 @@ var VelaWorkspace = class {
8996
9581
  const picker = this.indicatorPicker;
8997
9582
  this.tfQuick = new TimeframeQuick({
8998
9583
  host: this.root,
9584
+ i18n: this.localeState,
8999
9585
  onApply: (tf) => this.setActiveTimeframe(tf),
9000
9586
  onOpenChange: (open) => this.trackDialog(open)
9001
9587
  });
9002
9588
  this.topbar = new Topbar(this.root, {
9003
9589
  symbol: "",
9590
+ i18n: this.localeState,
9004
9591
  // RAW option, not the resolved lists — the bar distinguishes a host-declared
9005
9592
  // side (list is law) from a default one (an override's `order` may flow).
9006
9593
  composition: opts.topbar,
@@ -9024,18 +9611,13 @@ var VelaWorkspace = class {
9024
9611
  // The picker composes dynamic layouts on its grid canvas; registered
9025
9612
  // presets the canvas cannot express (bespoke plugin areas) list as rows.
9026
9613
  shape: () => layoutShape(this.def),
9027
- presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label })),
9614
+ presets: () => layouts(this.localeState).filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label })),
9028
9615
  onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
9029
9616
  onSelectPreset: (id) => this.setLayout(id),
9030
9617
  // The SYNC switches reflect the simple all-cells form; flipping one
9031
9618
  // OVERRIDES a host-set group record with plain on/off (groups stay an
9032
9619
  // API-only shape).
9033
- syncs: () => [
9034
- { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
9035
- { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
9036
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
9037
- { id: "style", label: "Style", checked: this.syncOpts.style === true }
9038
- ],
9620
+ syncs: () => this.syncRows(),
9039
9621
  onToggleSync: (id) => {
9040
9622
  const kind = id;
9041
9623
  this.sync.set(kind, this.syncOpts[kind] ? false : true);
@@ -9046,7 +9628,7 @@ var VelaWorkspace = class {
9046
9628
  });
9047
9629
  const main = doc.createElement("div");
9048
9630
  main.className = "vela-ws-main";
9049
- const toolbar = buildToolbar(opts.drawings);
9631
+ const toolbar = buildToolbar(opts.drawings, this.localeState);
9050
9632
  this.drawingsEnabled = opts.drawings !== false;
9051
9633
  this.toolbarDef = toolbar.definition;
9052
9634
  let toolbarHost = null;
@@ -9061,12 +9643,13 @@ var VelaWorkspace = class {
9061
9643
  this.dock = new PanelDock(main, {
9062
9644
  chrome: this.topbar,
9063
9645
  context: () => this.context(),
9064
- changed: () => this.markStateDirty()
9646
+ changed: () => this.markStateDirty(),
9647
+ i18n: this.localeState
9065
9648
  });
9066
- this.objectTree = new ObjectTree(main, (sym) => this.feed.symbolIcon(sym));
9067
- this.dataWindow = new DataWindow(main);
9068
- this.dock.addBuiltIn({ id: "dataWindow", title: "Data window", icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
9069
- this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
9649
+ this.objectTree = new ObjectTree(main, (sym) => this.feed.symbolIcon(sym), this.localeState);
9650
+ this.dataWindow = new DataWindow(main, this.localeState);
9651
+ this.dock.addBuiltIn({ id: "dataWindow", title: DATA_WINDOW_TITLE, icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
9652
+ this.dock.addBuiltIn({ id: "objects", title: OBJECT_TREE_TITLE, icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
9070
9653
  this.dock.refresh();
9071
9654
  if (boot?.panels) this.dock.applyState(boot.panels);
9072
9655
  this.root.appendChild(main);
@@ -9077,7 +9660,7 @@ var VelaWorkspace = class {
9077
9660
  const attribution = rendererDefaults().attribution;
9078
9661
  if (attribution !== false) {
9079
9662
  const background = resolveTheme(opts.theme).background;
9080
- const mark = typeof attribution === "string" && attribution.trim() ? createCustomMark(doc, attribution, background) : createAttributionMark(doc, background);
9663
+ const mark = typeof attribution === "string" && attribution.trim() ? createCustomMark(doc, attribution, background) : createAttributionMark(doc, background, this.localeState);
9081
9664
  Object.assign(mark.style, {
9082
9665
  left: "calc(var(--vela-toolbar-gutter, 0px) + 12px)",
9083
9666
  bottom: `calc(var(--vela-bottom-gutter, ${TIME_AXIS_H3}px) + 10px)`,
@@ -9116,6 +9699,7 @@ var VelaWorkspace = class {
9116
9699
  },
9117
9700
  {
9118
9701
  dock: "static",
9702
+ i18n: () => this.localeState,
9119
9703
  // Drawings sync is a WORKSPACE link (same model as the layout
9120
9704
  // dropdown's switches) — the bar only hosts its toggle.
9121
9705
  onDrawingsSync: (on) => {
@@ -9128,6 +9712,7 @@ var VelaWorkspace = class {
9128
9712
  this.drawToolbar?.setVisible(true);
9129
9713
  this.drawToolbar?.setDrawingsSyncMode(!!this.syncOpts.drawings);
9130
9714
  this.bottombar = opts.bottombar !== false ? new Bottombar(this.root, {
9715
+ i18n: this.localeState,
9131
9716
  timezone: this.timezone,
9132
9717
  clock: this.clock,
9133
9718
  onRange: (preset) => {
@@ -9141,6 +9726,7 @@ var VelaWorkspace = class {
9141
9726
  onSettingsClick: () => this.active.chart.renderer.openSettings()
9142
9727
  }) : null;
9143
9728
  this.mobileBar = opts.bottombar !== false ? new MobileBar(this.root, {
9729
+ i18n: this.localeState,
9144
9730
  symbol: "",
9145
9731
  timeframe: "60",
9146
9732
  onSymbolClick: () => this.symbolPicker.open(),
@@ -9174,7 +9760,7 @@ var VelaWorkspace = class {
9174
9760
  this.keymap = new KeymapManager();
9175
9761
  this.keymap.attach(this.root);
9176
9762
  this.registerDefaultKeys();
9177
- this.drawToolbar?.setShortcuts(toolShortcutHints(this.keymap));
9763
+ this.drawToolbar?.setShortcuts(toolShortcutHints(this.keymap, this.localeState));
9178
9764
  this.root.addEventListener("keydown", this.onRootKeydown);
9179
9765
  this.root.tabIndex = -1;
9180
9766
  this.cellBackend = this.backendFor(this.def);
@@ -9298,6 +9884,61 @@ var VelaWorkspace = class {
9298
9884
  on(event, handler) {
9299
9885
  return this.events.on(event, handler);
9300
9886
  }
9887
+ /**
9888
+ * The workspace's translator: its resolved locale and direction, `t()` for messages and
9889
+ * `format` for numbers and dates. Every cell's chart carries the same settings
9890
+ * (`cell.chart.i18n`), and contributed actions read it as `ctx.i18n`. The same object
9891
+ * for the workspace's lifetime; it reads the new settings after {@link setLocale}.
9892
+ */
9893
+ get i18n() {
9894
+ return this.localeState;
9895
+ }
9896
+ /**
9897
+ * Change the language, direction or formatting of the whole workspace at runtime — a
9898
+ * locale tag (`ws.setLocale('de')`) or any {@link LocalizationOptions} fields. Fields
9899
+ * you pass replace their current value (an explicit `undefined` restores the default);
9900
+ * the others are kept. Every cell re-renders in place, the shared chrome follows, and
9901
+ * `locale:changed` fires. Register the locale's pack (`registerLocale`) first for
9902
+ * translated text.
9903
+ */
9904
+ setLocale(next) {
9905
+ if (this.destroyed) return;
9906
+ const patch = typeof next === "string" ? { locale: next } : pickLocalization(next);
9907
+ this.localeState.update(patch);
9908
+ this.applyRootAttributes();
9909
+ for (const cell of this.cellsById.values()) {
9910
+ cell.chart.setLocale(patch);
9911
+ cell.relocalize();
9912
+ }
9913
+ this.relocalizeChrome();
9914
+ this.events.emit("locale:changed", { locale: this.localeState.locale, direction: this.localeState.direction });
9915
+ }
9916
+ /** Re-read the shared chrome in the current language. Dialogs, menus and drawers read
9917
+ * it on their next open; what stays on screen is relabeled here. */
9918
+ relocalizeChrome() {
9919
+ this.symbolPicker.relabel();
9920
+ this.indicatorPicker?.relabel();
9921
+ this.tfQuick.relabel();
9922
+ this.topbar.relocalize();
9923
+ this.bottombar?.relocalize();
9924
+ this.mobileBar?.relocalize();
9925
+ this.dock.relocalize();
9926
+ this.toolbarDef = buildToolbar(this.opts.drawings, this.localeState).definition;
9927
+ this.drawToolbar?.setDefinition(this.toolbarDef);
9928
+ this.drawToolbar?.relabel();
9929
+ this.drawToolbar?.setShortcuts(toolShortcutHints(this.keymap, this.localeState));
9930
+ this.drawingPill?.relocalize();
9931
+ if (this.attributionMark) relabelAttributionMark(this.attributionMark, this.localeState);
9932
+ }
9933
+ /** `lang` once the host set a locale, `dir` once it set a direction; otherwise the
9934
+ * attributes stay off and the host page decides. */
9935
+ applyRootAttributes() {
9936
+ const s = this.localeState;
9937
+ if (s.localeExplicit) this.root.setAttribute("lang", s.locale);
9938
+ else this.root.removeAttribute("lang");
9939
+ if (s.directionExplicit) this.root.setAttribute("dir", s.direction);
9940
+ else this.root.removeAttribute("dir");
9941
+ }
9301
9942
  /** The context handed to contributed actions/attachments (rebuilt per invocation). */
9302
9943
  context() {
9303
9944
  return buildContext({
@@ -9314,6 +9955,7 @@ var VelaWorkspace = class {
9314
9955
  };
9315
9956
  },
9316
9957
  root: this.root,
9958
+ i18n: this.localeState,
9317
9959
  toast: (message, kind) => this.toastHost.show(message, kind),
9318
9960
  stateDirty: () => this.markStateDirty(),
9319
9961
  replay: this.replay
@@ -9814,7 +10456,7 @@ var VelaWorkspace = class {
9814
10456
  resolveLayout(layout) {
9815
10457
  if (typeof layout !== "string") return layout;
9816
10458
  const def = ensureLayout(layout);
9817
- if (!def) throw new Error(`[vela] unknown workspace layout "${layout}" \u2014 register it with registerLayout().`);
10459
+ if (!def) throw new Error(`[vela] unknown workspace layout "${layout}" — register it with registerLayout().`);
9818
10460
  return def;
9819
10461
  }
9820
10462
  backendFor(def) {
@@ -9942,6 +10584,7 @@ var VelaWorkspace = class {
9942
10584
  feed: this.feed,
9943
10585
  engines: this.opts.engines ?? {},
9944
10586
  chartDefaults: cellChartDefaults(this.opts),
10587
+ localization: () => this.localeState.options,
9945
10588
  theme,
9946
10589
  live: this.opts.live ?? false,
9947
10590
  volume: this.opts.volume ?? true,
@@ -9994,10 +10637,11 @@ var VelaWorkspace = class {
9994
10637
  chart.on("script:run", (run) => this.events.emit("script:run", { ...run, cell: cell.id }));
9995
10638
  chart.on("priceStyle:change", ({ from, to }) => this.events.emit("cell:priceStyle", { id: cell.id, from, to }));
9996
10639
  chart.on("alert", (alert) => {
9997
- const source = [parseSymbol(cell.symbol).ticker || cell.symbol, timeframeLabel(cell.timeframe), alert.indicator].filter(Boolean).join(" ");
9998
- this.alerts.unshift({ cellId: cell.id, source, title: alert.title ?? "Alert", message: alert.message, time: alert.time });
10640
+ const source = [parseSymbol(cell.symbol).ticker || cell.symbol, timeframeLabel(cell.timeframe, this.localeState), alert.indicator].filter(Boolean).join(" ");
10641
+ this.alerts.unshift({ cellId: cell.id, source, title: alert.title ?? this.t(TEXT22.alert), message: alert.message, time: alert.time });
9999
10642
  if (this.alerts.length > this.alertCap) this.alerts.pop();
10000
- this.toastHost.show(`${source} \u2014 ${alert.title ? alert.title + " \u2014 " : ""}${alert.message}`, "info", 4e3);
10643
+ const toast = alert.title ? this.t(TEXT22.alertToast, { source, title: alert.title, message: alert.message }) : this.t(TEXT22.alertToastUntitled, { source, message: alert.message });
10644
+ this.toastHost.show(toast, "info", 4e3);
10001
10645
  this.topbar.setAlertCount(this.alerts.length);
10002
10646
  });
10003
10647
  chart.on("drawing:favorites", ({ favorites }) => {
@@ -10083,7 +10727,7 @@ var VelaWorkspace = class {
10083
10727
  if (kind === "crosshair") {
10084
10728
  for (const cell of this.cellsById.values()) cell.chart.renderer.setExternalCrosshair(null);
10085
10729
  if (setting && ![...this.cellsById.values()].some((c) => c.chart.renderer.supportsExternalCrosshair)) {
10086
- console.warn("[vela] crosshair sync: no cell renderer supports an external crosshair \u2014 nothing will show.");
10730
+ console.warn("[vela] crosshair sync: no cell renderer supports an external crosshair — nothing will show.");
10087
10731
  }
10088
10732
  if (this.topbar && this.def) this.topbar.setLayout(this.def.id);
10089
10733
  this.markStateDirty();
@@ -10466,7 +11110,8 @@ var VelaWorkspace = class {
10466
11110
  this.active.applyRange(preset);
10467
11111
  this.bottombar?.setActiveRange(preset.id);
10468
11112
  },
10469
- onOpenChange: (open) => this.trackDialog(open)
11113
+ onOpenChange: (open) => this.trackDialog(open),
11114
+ i18n: this.localeState
10470
11115
  }));
10471
11116
  this.tfDrawer.open();
10472
11117
  }
@@ -10479,7 +11124,8 @@ var VelaWorkspace = class {
10479
11124
  isFavorite: (type) => this.active.chart.drawings.isFavorite(type),
10480
11125
  onFavorite: (type, on) => this.active.chart.drawings.setFavorite(type, on),
10481
11126
  onSelect: (type) => this.active.chart.drawings.setTool(type),
10482
- onOpenChange: (open) => this.trackDialog(open)
11127
+ onOpenChange: (open) => this.trackDialog(open),
11128
+ i18n: () => this.localeState
10483
11129
  }));
10484
11130
  this.drawingsDrawer.open();
10485
11131
  }
@@ -10487,16 +11133,17 @@ var VelaWorkspace = class {
10487
11133
  const has = (id) => topbarHas(this.topbarComp, id);
10488
11134
  this.moreDrawer ?? (this.moreDrawer = new MoreDrawer({
10489
11135
  host: this.root,
11136
+ i18n: this.localeState,
10490
11137
  ...has("undo-redo") ? { onUndo: () => this.active.history.undo(), onRedo: () => this.active.history.redo() } : {},
10491
11138
  ...has("screenshot") ? { onScreenshot: this.screenshotOverride ? () => this.runOverride(this.screenshotOverride) : () => this.downloadScreenshot() } : {},
10492
11139
  canUndo: () => this.active.history.canUndo,
10493
11140
  canRedo: () => this.active.history.canRedo,
10494
- priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id), icon: priceStyleIcon(id) })),
11141
+ priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id, this.localeState), icon: priceStyleIcon(id) })),
10495
11142
  priceStyle: () => this.active.priceStyle,
10496
11143
  onPriceStyle: (id) => this.active.setPriceStyle(id),
10497
11144
  panels: () => has("panels") ? [...this.dock.list()] : [],
10498
11145
  onTogglePanel: (id) => this.dock.toggle(id),
10499
- ...has("alerts") ? { alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })) } : {},
11146
+ ...has("alerts") ? { alerts: () => this.alerts.map((a) => ({ title: this.t(TEXT22.alertDrawerTitle, { source: a.source, title: a.title }), message: a.message, time: a.time })) } : {},
10500
11147
  // Actions placed on the bottom bar (left-aligned by default) have their stop
10501
11148
  // there — only the menu-placed ones land in the drawer, or they would appear
10502
11149
  // twice: left (primary) ones with the primary rows, right ones at the end.
@@ -10510,15 +11157,10 @@ var VelaWorkspace = class {
10510
11157
  // single-chart mode and a composition without 'layout' omit it here too.
10511
11158
  layout: this.monoLayout || !has("layout") ? void 0 : {
10512
11159
  shape: () => layoutShape(this.def),
10513
- presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.def.id })),
11160
+ presets: () => layouts(this.localeState).filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.def.id })),
10514
11161
  onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
10515
11162
  onSelectPreset: (id) => this.setLayout(id),
10516
- syncs: () => [
10517
- { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
10518
- { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
10519
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
10520
- { id: "style", label: "Style", checked: this.syncOpts.style === true }
10521
- ],
11163
+ syncs: () => this.syncRows(),
10522
11164
  onToggleSync: (id) => {
10523
11165
  const kind = id;
10524
11166
  this.sync.set(kind, this.syncOpts[kind] ? false : true);
@@ -10532,14 +11174,28 @@ var VelaWorkspace = class {
10532
11174
  /** The contributed topbar actions the mobile menu lists, from one cluster. */
10533
11175
  drawerActions(cluster) {
10534
11176
  const builtin = new Set(TOPBAR_BUILTIN_IDS);
10535
- return widgetActions("topbar", this.context()).filter((a) => mobilePlacement(a) === "menu" && a.align === "left" === (cluster === "left") && !builtin.has(a.id)).map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) }));
11177
+ return widgetActions("topbar", this.context()).filter((a) => mobilePlacement(a) === "menu" && a.align === "left" === (cluster === "left") && !builtin.has(a.id)).map((a) => ({ label: this.t(a.label), icon: a.icon, run: () => a.run(this.context()) }));
11178
+ }
11179
+ /** Display text in the workspace's language. */
11180
+ t(text, values) {
11181
+ return textOf(this.localeState, text, values);
11182
+ }
11183
+ /** The layout SYNC switches (the topbar dropdown and the mobile drawer list the same). */
11184
+ syncRows() {
11185
+ return [
11186
+ { id: "symbol", label: this.t(TEXT22.syncSymbol), checked: this.syncOpts.symbol === true },
11187
+ { id: "timeframe", label: this.t(TEXT22.syncInterval), checked: this.syncOpts.timeframe === true },
11188
+ { id: "crosshair", label: this.t(TEXT22.syncCrosshair), checked: this.syncOpts.crosshair === true },
11189
+ { id: "style", label: this.t(TEXT22.syncStyle), checked: this.syncOpts.style === true }
11190
+ ];
10536
11191
  }
10537
11192
  openTimezoneDrawer() {
10538
11193
  this.timezoneDrawer ?? (this.timezoneDrawer = new TimezoneDrawer({
10539
11194
  host: this.root,
10540
11195
  timezone: () => this.timezone,
10541
11196
  onTimezone: (zone) => this.setTimezone(zone),
10542
- onOpenChange: (open) => this.trackDialog(open)
11197
+ onOpenChange: (open) => this.trackDialog(open),
11198
+ i18n: this.localeState
10543
11199
  }));
10544
11200
  this.timezoneDrawer.open();
10545
11201
  }
@@ -10558,8 +11214,8 @@ var VelaWorkspace = class {
10558
11214
  this.alertsMenu?.destroy();
10559
11215
  const items = this.alerts.length ? this.alerts.map((a, i) => ({
10560
11216
  id: String(i),
10561
- label: `${a.source} \xB7 ${new Date(a.time).toLocaleTimeString()} \xB7 ${a.title}: ${a.message}`.slice(0, 80)
10562
- })) : [{ id: "none", label: "No alerts yet", disabled: true }];
11217
+ label: sliceGraphemes(this.t(TEXT22.alertRow, { source: a.source, time: eventTimeText(a.time, this.localeState), title: a.title, message: a.message }), ALERT_ROW_MAX)
11218
+ })) : [{ id: "none", label: this.t(TEXT22.noAlerts), disabled: true }];
10563
11219
  this.alertsMenu = new Menu({
10564
11220
  host: this.root,
10565
11221
  items,
@@ -10594,29 +11250,29 @@ var VelaWorkspace = class {
10594
11250
  this.keymap.register({
10595
11251
  id: "chart.screenshot",
10596
11252
  keys: "mod+alt+s",
10597
- label: ov ? ov.label : "Download a screenshot of the layout",
10598
- category: "Chart",
11253
+ label: ov ? ov.label : TEXT22.screenshot,
11254
+ category: TEXT22.categoryChart,
10599
11255
  run: ov ? () => this.runOverride(ov) : () => this.downloadScreenshot()
10600
11256
  });
10601
11257
  }
10602
- this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: "Reset view (all history)", category: "Chart", run: () => this.active.chart.setVisibleRangePreset("ALL") });
10603
- this.keymap.register({ id: "chart.toggle-log", keys: "alt+l", label: "Toggle logarithmic scale", category: "Chart", run: () => this.active.chart.renderer.set("logScale", !this.active.chart.renderer.get("logScale")) });
11258
+ this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: TEXT22.resetView, category: TEXT22.categoryChart, run: () => this.active.chart.setVisibleRangePreset("ALL") });
11259
+ this.keymap.register({ id: "chart.toggle-log", keys: "alt+l", label: TEXT22.toggleLog, category: TEXT22.categoryChart, run: () => this.active.chart.renderer.set("logScale", !this.active.chart.renderer.get("logScale")) });
10604
11260
  this.keymap.register({
10605
11261
  id: "chart.toggle-percent",
10606
11262
  keys: "alt+p",
10607
- label: "Toggle percent scale",
10608
- category: "Chart",
11263
+ label: TEXT22.togglePercent,
11264
+ category: TEXT22.categoryChart,
10609
11265
  run: () => {
10610
11266
  const mode = this.active.chart.renderer.get("scaleMode");
10611
11267
  this.active.chart.renderer.set("scaleMode", mode === "percent" ? "price" : "percent");
10612
11268
  }
10613
11269
  });
10614
- this.keymap.register({ id: "drawings.trendline", keys: "alt+t", label: "Arm the trend line tool", category: "Drawings", run: () => this.active.chart.drawings.setTool("trendline") });
11270
+ this.keymap.register({ id: "drawings.trendline", keys: "alt+t", label: TEXT22.trendline, category: TEXT22.categoryDrawings, run: () => this.active.chart.drawings.setTool("trendline") });
10615
11271
  this.keymap.register({
10616
11272
  id: "drawings.hline-cursor",
10617
11273
  keys: "alt+h",
10618
- label: "Horizontal line at the cursor price",
10619
- category: "Drawings",
11274
+ label: TEXT22.hlineCursor,
11275
+ category: TEXT22.categoryDrawings,
10620
11276
  run: () => {
10621
11277
  const c = this.active;
10622
11278
  if (c.lastCrossTime != null && c.lastCrossPrice != null) c.chart.drawings.add("hline", { anchors: [{ time: c.lastCrossTime, price: c.lastCrossPrice }] });
@@ -10625,32 +11281,32 @@ var VelaWorkspace = class {
10625
11281
  this.keymap.register({
10626
11282
  id: "drawings.vline-cursor",
10627
11283
  keys: "alt+v",
10628
- label: "Vertical line at the cursor time",
10629
- category: "Drawings",
11284
+ label: TEXT22.vlineCursor,
11285
+ category: TEXT22.categoryDrawings,
10630
11286
  run: () => {
10631
11287
  const c = this.active;
10632
11288
  if (c.lastCrossTime != null && c.lastCrossPrice != null) c.chart.drawings.add("vline", { anchors: [{ time: c.lastCrossTime, price: c.lastCrossPrice }] });
10633
11289
  }
10634
11290
  });
10635
- this.keymap.register({ id: "history.undo", keys: ["mod+z"], label: "Undo (active chart)", category: "Edit", run: () => this.active.history.undo() });
10636
- this.keymap.register({ id: "history.redo", keys: ["mod+y", "mod+shift+z"], label: "Redo (active chart)", category: "Edit", run: () => this.active.history.redo() });
10637
- this.keymap.register({ id: "view.zoom-in", keys: "mod+arrowup", label: "Zoom in", category: "Chart", run: () => this.glider.zoom(ZOOM_IN) });
10638
- this.keymap.register({ id: "view.zoom-out", keys: "mod+arrowdown", label: "Zoom out", category: "Chart", run: () => this.glider.zoom(ZOOM_OUT) });
10639
- this.keymap.register({ id: "view.pan-left", keys: "mod+arrowleft", label: "Pan toward history", category: "Chart", run: () => this.active.chart.panBy(-PAN_FAST) });
10640
- this.keymap.register({ id: "view.pan-right", keys: "mod+arrowright", label: "Pan toward now", category: "Chart", run: () => this.active.chart.panBy(PAN_FAST) });
11291
+ this.keymap.register({ id: "history.undo", keys: ["mod+z"], label: TEXT22.undo, category: TEXT22.categoryEdit, run: () => this.active.history.undo() });
11292
+ this.keymap.register({ id: "history.redo", keys: ["mod+y", "mod+shift+z"], label: TEXT22.redo, category: TEXT22.categoryEdit, run: () => this.active.history.redo() });
11293
+ this.keymap.register({ id: "view.zoom-in", keys: "mod+arrowup", label: TEXT22.zoomIn, category: TEXT22.categoryChart, run: () => this.glider.zoom(ZOOM_IN) });
11294
+ this.keymap.register({ id: "view.zoom-out", keys: "mod+arrowdown", label: TEXT22.zoomOut, category: TEXT22.categoryChart, run: () => this.glider.zoom(ZOOM_OUT) });
11295
+ this.keymap.register({ id: "view.pan-left", keys: "mod+arrowleft", label: TEXT22.panLeft, category: TEXT22.categoryChart, run: () => this.active.chart.panBy(-PAN_FAST) });
11296
+ this.keymap.register({ id: "view.pan-right", keys: "mod+arrowright", label: TEXT22.panRight, category: TEXT22.categoryChart, run: () => this.active.chart.panBy(PAN_FAST) });
10641
11297
  const indOv = this.indicatorsOverride;
10642
11298
  if (indOv && topbarHas(this.topbarComp, "indicators")) {
10643
- this.keymap.register({ id: "indicators.open", keys: "/", label: indOv.label, category: "Indicators", run: () => this.runOverride(indOv) });
11299
+ this.keymap.register({ id: "indicators.open", keys: "/", label: indOv.label, category: TEXT22.categoryIndicators, run: () => this.runOverride(indOv) });
10644
11300
  } else if (this.indicatorPicker) {
10645
- this.keymap.register({ id: "indicators.open", keys: "/", label: "Open the indicator picker", category: "Indicators", run: () => this.indicatorPicker?.open() });
11301
+ this.keymap.register({ id: "indicators.open", keys: "/", label: TEXT22.openIndicators, category: TEXT22.categoryIndicators, run: () => this.indicatorPicker?.open() });
10646
11302
  }
10647
11303
  this.keymap.register({
10648
11304
  id: "help.shortcuts",
10649
11305
  keys: "?",
10650
- label: "Show this shortcuts panel",
10651
- category: "Help",
11306
+ label: TEXT22.showShortcuts,
11307
+ category: TEXT22.categoryHelp,
10652
11308
  run: () => {
10653
- this.shortcutsHelp ?? (this.shortcutsHelp = new ShortcutsHelp(this.keymap, this.root, (open) => this.trackDialog(open)));
11309
+ this.shortcutsHelp ?? (this.shortcutsHelp = new ShortcutsHelp(this.keymap, this.root, (open) => this.trackDialog(open), () => this.localeState));
10654
11310
  this.shortcutsHelp.open();
10655
11311
  }
10656
11312
  });
@@ -10659,6 +11315,7 @@ var VelaWorkspace = class {
10659
11315
  routeTyping(ev) {
10660
11316
  if (this.destroyed) return;
10661
11317
  if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
11318
+ if (ev.isComposing || ev.key === "Process") return;
10662
11319
  if (isEditableTarget(ev)) return;
10663
11320
  const hit = resolveTyping(ev.key, { dialogs: this.openDialogs, symbolSearch: this.symbolPicker.isOpen, timeframeEntry: this.tfQuick.isOpen });
10664
11321
  if (!hit) return;