@luxalgo/vela 0.8.4 → 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-Byo8pEv0.d.ts → DataProvider-BGlB1zSc.d.ts} +1 -1
  2. package/dist/{DataProvider-Bw58CjYy.d.cts → DataProvider-zJGp58kb.d.cts} +1 -1
  3. package/dist/{chunk-MOJ4T26N.js → chunk-4WAHYL7Y.js} +1151 -474
  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-UNUVPQFW.js → chunk-XIRSR3D6.js} +2837 -1604
  8. package/dist/{chunk-ALTGHKIM.js → chunk-YRCL3WMD.js} +510 -406
  9. package/dist/{contributions-BY3zSYiZ.d.ts → contributions-BjefWpxQ.d.ts} +106 -25
  10. package/dist/{contributions-DOm40g5-.d.cts → contributions-CpCJisTC.d.cts} +106 -25
  11. package/dist/index.cjs +4485 -2163
  12. package/dist/index.d.cts +67 -25
  13. package/dist/index.d.ts +67 -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-CSXbjMM5.d.cts → options-CpegHml_.d.cts} +110 -10
  98. package/dist/{options-CSXbjMM5.d.ts → options-D4L3_hv_.d.ts} +110 -10
  99. package/dist/{plugin-BDxUhGO3.d.ts → plugin-DTmulpKM.d.ts} +92 -40
  100. package/dist/{plugin-CiQVyETg.d.cts → plugin-DpNp4aBi.d.cts} +92 -40
  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-C9tYXuux.d.ts → statusline-model-C3yIY9WE.d.ts} +11 -3
  120. package/dist/{statusline-model-DUBX6Fib.d.cts → 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 +4451 -2131
  128. package/dist/vela.global.min.js +57 -57
  129. package/dist/widget.cjs +5819 -2780
  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 +5808 -2787
  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-UNUVPQFW.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);
@@ -1435,9 +1547,14 @@ function paneItems(snap, pane, units) {
1435
1547
  return [...unitItems, ...rows2];
1436
1548
  }
1437
1549
  const zOf = new Map(snap.zOrder.map((e) => [e.id, e.z]));
1438
- const rows = pane.indicators.map((i) => ({ item: { kind: "row", row: indicatorRow(i) }, z: zOf.get(i.id) ?? 0 }));
1439
- if (pane.kind === "price") rows.push({ item: { kind: "row", row: priceRow() }, z: snap.candleZ });
1440
- rows.sort((a, b) => b.z - a.z);
1550
+ const rank = new Map(snap.zOrder.map((e, i) => [e.id, i]));
1551
+ const rows = pane.indicators.map((i) => ({
1552
+ item: { kind: "row", row: indicatorRow(i) },
1553
+ z: zOf.get(i.id) ?? 0,
1554
+ rank: rank.get(i.id) ?? -1
1555
+ }));
1556
+ if (pane.kind === "price") rows.push({ item: { kind: "row", row: priceRow() }, z: snap.candleZ, rank: -Infinity });
1557
+ rows.sort((a, b) => b.z - a.z || b.rank - a.rank);
1441
1558
  if (!snap.interleave) return [...unitItems, ...rows.map((r) => r.item)];
1442
1559
  const entries = [...rows, ...units.map((unit) => ({ item: { kind: "unit", unit }, z: unitZ(unit) }))];
1443
1560
  entries.sort((a, b) => a.z === b.z ? Number(a.item.kind === "unit") - Number(b.item.kind === "unit") : b.z - a.z);
@@ -1448,7 +1565,7 @@ function buildTree(snap) {
1448
1565
  return snap.panes.map((pane) => ({
1449
1566
  id: pane.id,
1450
1567
  kind: pane.kind,
1451
- label: paneLabel(pane, snap.handleTitle),
1568
+ label: paneLabel(pane, snap.handleTitle, snap.i18n),
1452
1569
  order: pane.order,
1453
1570
  collapsed: pane.collapsed,
1454
1571
  maximized: pane.maximized,
@@ -1460,6 +1577,56 @@ function treeIsEmpty(panes) {
1460
1577
  }
1461
1578
 
1462
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;
1463
1630
  var STYLE_ID4 = "vela-widget-objtree";
1464
1631
  var CSS4 = `
1465
1632
  /* One pane's block. The transparent border reserves the drop-target outline. */
@@ -1493,7 +1660,7 @@ var CSS4 = `
1493
1660
  }
1494
1661
  .vela-ot-row:hover { background: var(--vela-hover); }
1495
1662
  .vela-ot-row > .vela-icon { color: var(--vela-fg-muted); width: 14px; height: 14px; font-size: 14px; justify-content: center; flex: none; }
1496
- /* 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. */
1497
1664
  .vela-ot-row > .vela-icon svg { width: 14px; height: 14px; }
1498
1665
  .vela-ot-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1499
1666
  .vela-ot-row[data-hidden] .vela-ot-name,
@@ -1553,9 +1720,9 @@ var CSS4 = `
1553
1720
  .vela-ot-btn:disabled { opacity: 0.35; cursor: default; }
1554
1721
  .vela-ot-empty { padding: 20px 10px; text-align: center; color: var(--vela-fg-muted); font-size: 12px; }
1555
1722
 
1556
- /* \u2500\u2500 drawing groups \u2500\u2500 */
1723
+ /* ── drawing groups ── */
1557
1724
  /* One top-level entry in a pane's drawing list: a lone drawing, or a whole group block. No
1558
- 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
1559
1726
  drop-into-group outline is drawn inside the box instead (see the drag-and-drop rules). */
1560
1727
  .vela-ot-unit { border-radius: 6px; }
1561
1728
  /* Adjacent picked rows merge into one contiguous highlight: the shared edge loses its rounding. */
@@ -1574,7 +1741,7 @@ var CSS4 = `
1574
1741
  color: var(--vela-fg-bright);
1575
1742
  font: inherit;
1576
1743
  }
1577
- /* 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
1578
1745
  stay in place and simply go dim until a drawing is selected. It stays put at the top of the
1579
1746
  list while the list scrolls under it, so they never scroll out of reach. */
1580
1747
  .vela-ot-selbar {
@@ -1593,7 +1760,7 @@ var CSS4 = `
1593
1760
  .vela-ot-selbar .vela-ot-btn { visibility: visible; width: 24px; height: 22px; }
1594
1761
  .vela-ot-selbar .vela-ot-btn[data-icon='group'] .vela-icon { width: 16px; height: 16px; font-size: 16px; }
1595
1762
 
1596
- /* \u2500\u2500 drag-and-drop \u2500\u2500 */
1763
+ /* ── drag-and-drop ── */
1597
1764
  .vela-ot-row[data-drag] { cursor: grab; }
1598
1765
  .vela-ot .vela-panel-body[data-dragging] .vela-ot-row[data-drag] { cursor: grabbing; }
1599
1766
  /* The row in flight fades: the ghost under the pointer is the thing being moved. */
@@ -1679,8 +1846,8 @@ var MenuBuilder = class {
1679
1846
  }
1680
1847
  };
1681
1848
  var ObjectTree = class extends SidePanel {
1682
- constructor(host, iconFor) {
1683
- super(host, "Object tree", "vela-ot");
1849
+ constructor(host, iconFor, i18n) {
1850
+ super(host, TEXT5.objectTree, "vela-ot", i18n ? { i18n } : {});
1684
1851
  this.iconFor = iconFor;
1685
1852
  this.chart = null;
1686
1853
  /** What the CHART has selected — every member of a multi-selection, mirrored as rows. */
@@ -1734,6 +1901,7 @@ var ObjectTree = class extends SidePanel {
1734
1901
  for (const ev of REFRESH_EVENTS) {
1735
1902
  this.unsubs.push(chart.on(ev, () => this.refresh()));
1736
1903
  }
1904
+ this.unsubs.push(chart.renderer.onConfigChanged(() => this.refresh()));
1737
1905
  this.unsubs.push(
1738
1906
  chart.on("drawing:selected", ({ ids }) => {
1739
1907
  this.selectedDrawings = new Set(ids);
@@ -1762,16 +1930,21 @@ var ObjectTree = class extends SidePanel {
1762
1930
  panes: chart.panes.list(),
1763
1931
  indicatorVisible: (id) => handle(id)?.visible !== false,
1764
1932
  handleTitle: (id) => handle(id)?.title,
1933
+ i18n: this.i18n,
1765
1934
  stackable,
1766
1935
  interleave: stackable && chart.drawings.supported && chart.renderer.capabilities.drawingDepth === true,
1767
1936
  zOrder: stackable ? chart.renderer.get("seriesOrder") ?? [] : [],
1768
1937
  candleZ: stackable ? Number(chart.renderer.get("candleZOrder")) || 0 : 0,
1769
- priceLabel: this.symbolName || "Price",
1938
+ priceLabel: this.symbolName || this.t(TEXT5.price),
1770
1939
  priceVisible: chart.renderer.get("candleVisible") !== false,
1771
1940
  drawings: chart.drawings.supported ? chart.drawings.all() : [],
1772
1941
  groups: this.groups
1773
1942
  };
1774
1943
  }
1944
+ relocalize() {
1945
+ super.relocalize();
1946
+ this.refresh();
1947
+ }
1775
1948
  /**
1776
1949
  * Repaint because something changed — unless the panel is mid-interaction. A live drag
1777
1950
  * resolves drops against the rows as currently rendered, and a rename lives in an input
@@ -1802,7 +1975,7 @@ var ObjectTree = class extends SidePanel {
1802
1975
  if (treeIsEmpty(panes)) {
1803
1976
  const empty = doc.createElement("div");
1804
1977
  empty.className = "vela-ot-empty";
1805
- empty.textContent = "Add an indicator or a drawing to populate the tree";
1978
+ empty.textContent = this.t(TEXT5.emptyHint);
1806
1979
  this.body.appendChild(empty);
1807
1980
  }
1808
1981
  }
@@ -1817,7 +1990,7 @@ var ObjectTree = class extends SidePanel {
1817
1990
  bar.appendChild(
1818
1991
  this.btn({
1819
1992
  icon: "group",
1820
- 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),
1821
1994
  disabled: !groupable,
1822
1995
  run: () => this.makeGroup(ids)
1823
1996
  })
@@ -1825,7 +1998,7 @@ var ObjectTree = class extends SidePanel {
1825
1998
  bar.appendChild(
1826
1999
  this.btn({
1827
2000
  icon: "clone",
1828
- title: ids.length === 0 ? "Duplicate the selected drawings" : "Duplicate",
2001
+ title: ids.length === 0 ? this.t(TEXT5.duplicateSelected) : this.t(TEXT5.duplicate),
1829
2002
  disabled: ids.length === 0,
1830
2003
  run: () => {
1831
2004
  for (const id of ids) this.cloneInto(pass, id);
@@ -1835,7 +2008,7 @@ var ObjectTree = class extends SidePanel {
1835
2008
  );
1836
2009
  const count = doc.createElement("span");
1837
2010
  count.className = "vela-ot-selcount";
1838
- count.textContent = ids.length > 1 ? `${ids.length} selected` : "";
2011
+ count.textContent = ids.length > 1 ? this.t(TEXT5.selected, { count: ids.length }) : "";
1839
2012
  bar.appendChild(count);
1840
2013
  return bar;
1841
2014
  }
@@ -1910,11 +2083,11 @@ var ObjectTree = class extends SidePanel {
1910
2083
  acts.appendChild(this.btn({ icon: icon2, title, run }));
1911
2084
  };
1912
2085
  if (pane.kind !== "price") {
1913
- if (pane.order > 1) op("arrow-up", "Move pane up", () => chart.panes.move(pane.id, "up"));
1914
- if (pane.order < pass.panes.length - 1) op("arrow-down", "Move pane down", () => chart.panes.move(pane.id, "down"));
1915
- 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));
1916
2089
  }
1917
- 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));
1918
2091
  return head;
1919
2092
  }
1920
2093
  rowEl(pass, pane, row) {
@@ -1922,11 +2095,11 @@ var ObjectTree = class extends SidePanel {
1922
2095
  const { chart } = pass;
1923
2096
  if (row.kind === "price") {
1924
2097
  const base = this.symbolName.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || this.symbolName;
1925
- 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);
1926
2099
  const el2 = this.row(icon2, row.label, row.visible, [
1927
2100
  {
1928
2101
  icon: row.visible ? "eye" : "eye-off",
1929
- title: row.visible ? "Hide" : "Show",
2102
+ title: row.visible ? this.t(TEXT5.hide) : this.t(TEXT5.show),
1930
2103
  engaged: !row.visible,
1931
2104
  run: () => {
1932
2105
  chart.renderer.set("candleVisible", !row.visible);
@@ -1938,7 +2111,7 @@ var ObjectTree = class extends SidePanel {
1938
2111
  el2.dataset.pane = pane.id;
1939
2112
  if (pass.stackable) {
1940
2113
  el2.dataset.drag = "1";
1941
- el2.title = "Drag to change what draws in front";
2114
+ el2.title = this.t(TEXT5.restackHint);
1942
2115
  }
1943
2116
  return el2;
1944
2117
  }
@@ -1950,7 +2123,7 @@ var ObjectTree = class extends SidePanel {
1950
2123
  [
1951
2124
  {
1952
2125
  icon: row.visible ? "eye" : "eye-off",
1953
- title: row.visible ? "Hide" : "Show",
2126
+ title: row.visible ? this.t(TEXT5.hide) : this.t(TEXT5.show),
1954
2127
  engaged: !row.visible,
1955
2128
  run: () => {
1956
2129
  handle?.setVisible(!row.visible);
@@ -1959,21 +2132,21 @@ var ObjectTree = class extends SidePanel {
1959
2132
  },
1960
2133
  {
1961
2134
  icon: "trash",
1962
- title: "Remove",
2135
+ title: this.t(TEXT5.remove),
1963
2136
  run: () => {
1964
2137
  handle?.remove();
1965
2138
  this.refresh();
1966
2139
  }
1967
2140
  }
1968
2141
  ],
1969
- row.ownScale ? "scale" : void 0
2142
+ row.ownScale ? this.t(TEXT5.scaleTag) : void 0
1970
2143
  );
1971
2144
  el.dataset.rowKind = "indicator";
1972
2145
  el.dataset.id = row.id;
1973
2146
  el.dataset.pane = pane.id;
1974
2147
  if (pass.repanable || pass.stackable) {
1975
2148
  el.dataset.drag = "1";
1976
- 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);
1977
2150
  }
1978
2151
  return el;
1979
2152
  }
@@ -1992,22 +2165,22 @@ var ObjectTree = class extends SidePanel {
1992
2165
  const el = this.row(iconEl(folded ? "chevron-right" : "chevron-down", doc), g.name, !allHidden, [
1993
2166
  {
1994
2167
  icon: allLocked ? "lock" : "unlock",
1995
- title: allLocked ? "Unlock all" : "Lock all",
2168
+ title: allLocked ? this.t(TEXT5.unlockAll) : this.t(TEXT5.lockAll),
1996
2169
  engaged: allLocked,
1997
2170
  run: () => this.setMembers(pass, ids, { locked: !allLocked })
1998
2171
  },
1999
2172
  {
2000
2173
  icon: allHidden ? "eye-off" : "eye",
2001
- title: allHidden ? "Show all" : "Hide all",
2174
+ title: allHidden ? this.t(TEXT5.showAll) : this.t(TEXT5.hideAll),
2002
2175
  engaged: allHidden,
2003
2176
  run: () => this.setMembers(pass, ids, { visible: allHidden })
2004
2177
  },
2005
- { 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) }
2006
2179
  ]);
2007
2180
  el.dataset.rowKind = "group";
2008
2181
  el.dataset.id = g.id;
2009
2182
  el.dataset.pane = pane.id;
2010
- 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 });
2011
2184
  if (this.renaming === g.id) {
2012
2185
  const name = el.querySelector(".vela-ot-name");
2013
2186
  const input = doc.createElement("input");
@@ -2041,7 +2214,7 @@ var ObjectTree = class extends SidePanel {
2041
2214
  drawRow(pass, d, inGroup = false) {
2042
2215
  const doc = this.el.ownerDocument;
2043
2216
  const { chart } = pass;
2044
- const markup = drawingMeta(d.type).icon;
2217
+ const markup = drawingMeta(d.type, this.i18n).icon;
2045
2218
  let icon2;
2046
2219
  if (markup) {
2047
2220
  icon2 = doc.createElement("span");
@@ -2051,10 +2224,10 @@ var ObjectTree = class extends SidePanel {
2051
2224
  } else {
2052
2225
  icon2 = iconEl("pen", doc);
2053
2226
  }
2054
- const el = this.row(icon2, drawingLabel(d), d.visible, [
2227
+ const el = this.row(icon2, drawingLabel(d, this.i18n), d.visible, [
2055
2228
  {
2056
2229
  icon: d.locked ? "lock" : "unlock",
2057
- title: d.locked ? "Unlock" : "Lock",
2230
+ title: d.locked ? this.t(TEXT5.unlock) : this.t(TEXT5.lock),
2058
2231
  engaged: d.locked,
2059
2232
  run: () => {
2060
2233
  chart.drawings.lock(d.id, !d.locked);
@@ -2063,7 +2236,7 @@ var ObjectTree = class extends SidePanel {
2063
2236
  },
2064
2237
  {
2065
2238
  icon: d.visible ? "eye" : "eye-off",
2066
- title: d.visible ? "Hide" : "Show",
2239
+ title: d.visible ? this.t(TEXT5.hide) : this.t(TEXT5.show),
2067
2240
  engaged: !d.visible,
2068
2241
  run: () => {
2069
2242
  chart.drawings.show(d.id, !d.visible);
@@ -2072,7 +2245,7 @@ var ObjectTree = class extends SidePanel {
2072
2245
  },
2073
2246
  {
2074
2247
  icon: "trash",
2075
- title: "Remove",
2248
+ title: this.t(TEXT5.remove),
2076
2249
  run: () => {
2077
2250
  chart.drawings.remove(d.id);
2078
2251
  this.refresh();
@@ -2083,7 +2256,7 @@ var ObjectTree = class extends SidePanel {
2083
2256
  el.dataset.id = d.id;
2084
2257
  el.dataset.pane = d.paneId;
2085
2258
  el.dataset.drag = "1";
2086
- el.title = "Drag to restack, or onto another pane to move it there";
2259
+ el.title = this.t(TEXT5.moveDrawingHint);
2087
2260
  if (inGroup) el.classList.add("vela-ot-subrow");
2088
2261
  if (this.selectedDrawings.has(d.id)) el.dataset.selected = "1";
2089
2262
  if (this.picked.has(d.id)) el.dataset.picked = "1";
@@ -2131,7 +2304,7 @@ var ObjectTree = class extends SidePanel {
2131
2304
  const t = doc.createElement("span");
2132
2305
  t.className = "vela-ot-tag";
2133
2306
  t.textContent = tag;
2134
- t.title = "Draws against its own price scale";
2307
+ t.title = this.t(TEXT5.ownScaleHint);
2135
2308
  el.appendChild(t);
2136
2309
  }
2137
2310
  const acts = doc.createElement("span");
@@ -2162,7 +2335,7 @@ var ObjectTree = class extends SidePanel {
2162
2335
  makeGroup(ids) {
2163
2336
  if (!canGroup(ids, this.groups)) return;
2164
2337
  this.groupSeq += 1;
2165
- 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] }];
2166
2339
  this.picked.clear();
2167
2340
  this.render();
2168
2341
  }
@@ -2511,20 +2684,20 @@ var ObjectTree = class extends SidePanel {
2511
2684
  const { chart } = pass;
2512
2685
  const visible = chart.renderer.get("candleVisible") !== false;
2513
2686
  const items = [
2514
- 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", () => {
2515
2688
  chart.renderer.set("candleVisible", !visible);
2516
2689
  this.refresh();
2517
2690
  })
2518
2691
  ];
2519
2692
  if (pass.stackable) {
2520
2693
  items.push(
2521
- b.entry("Bring to front", "arrow-up", () => {
2694
+ b.entry(this.t(TEXT5.bringToFront), "arrow-up", () => {
2522
2695
  chart.renderer.set("candleZOrder", this.stackBounds(pass, PRICE_PANE_ID).top + 1);
2523
2696
  this.refresh();
2524
2697
  }, true)
2525
2698
  );
2526
2699
  items.push(
2527
- b.entry("Send to back", "arrow-down", () => {
2700
+ b.entry(this.t(TEXT5.sendToBack), "arrow-down", () => {
2528
2701
  chart.renderer.set("candleZOrder", this.stackBounds(pass, PRICE_PANE_ID).bottom - 1);
2529
2702
  this.refresh();
2530
2703
  })
@@ -2548,24 +2721,24 @@ var ObjectTree = class extends SidePanel {
2548
2721
  const { chart } = pass;
2549
2722
  const handle = pass.handle(row.id);
2550
2723
  const items = [
2551
- 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", () => {
2552
2725
  handle?.setVisible(!row.visible);
2553
2726
  this.refresh();
2554
2727
  })
2555
2728
  ];
2556
2729
  if (chart.renderer.supportsIndicatorSettings) {
2557
- 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)));
2558
2731
  }
2559
2732
  if (chart.panes.supported) {
2560
2733
  const moves = this.moveItems(b, pass, row.id, paneId);
2561
- 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));
2562
2735
  }
2563
2736
  if (pass.stackable) {
2564
- items.push(b.entry("Bring to front", "arrow-up", () => chart.renderer.set("seriesOrder", { id: row.id, z: this.stackBounds(pass, paneId).top + 1 }), true));
2565
- 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 })));
2566
2739
  }
2567
2740
  items.push(
2568
- b.entry("Remove", "trash", () => {
2741
+ b.entry(this.t(TEXT5.remove), "trash", () => {
2569
2742
  handle?.remove();
2570
2743
  this.refresh();
2571
2744
  }, true)
@@ -2583,33 +2756,33 @@ var ObjectTree = class extends SidePanel {
2583
2756
  const current = pass.panes.find((p) => p.id === fromPane);
2584
2757
  const alone = current !== void 0 && current.kind !== "price" && paneRows(current).length <= 1;
2585
2758
  if (!alone) {
2586
- if (fromPane !== PRICE_PANE_ID) items.push(b.entry("New pane above", void 0, () => chart.panes.moveIndicator(id, { newPane: { before: fromPane } })));
2587
- 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 } })));
2588
2761
  }
2589
2762
  return items;
2590
2763
  }
2591
2764
  drawingMenu(b, pass, d) {
2592
2765
  const { chart } = pass;
2593
2766
  const items = [
2594
- 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", () => {
2595
2768
  chart.drawings.show(d.id, !d.visible);
2596
2769
  this.refresh();
2597
2770
  }),
2598
- 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", () => {
2599
2772
  chart.drawings.lock(d.id, !d.locked);
2600
2773
  this.refresh();
2601
2774
  }),
2602
- b.entry("Duplicate", "clone", () => {
2775
+ b.entry(this.t(TEXT5.duplicate), "clone", () => {
2603
2776
  this.cloneInto(pass, d.id);
2604
2777
  this.render();
2605
2778
  }),
2606
2779
  // Front/back clear the WHOLE stack on a shared-z renderer — over or under the
2607
2780
  // candles and every indicator, not just the other drawings.
2608
- b.entry("Bring to front", "arrow-up", () => chart.drawings.bringToFront(d.id), true),
2609
- 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))
2610
2783
  ];
2611
2784
  items.push(...this.groupingItems(b, d.id));
2612
- 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));
2613
2786
  return items;
2614
2787
  }
2615
2788
  /** The grouping half of a drawing's menu: what it can do about the bundle it is (or isn't)
@@ -2619,15 +2792,15 @@ var ObjectTree = class extends SidePanel {
2619
2792
  const mine = groupOf(this.groups, id);
2620
2793
  const pick = this.picked.has(id) && this.picked.size > 1 ? [...this.picked] : null;
2621
2794
  if (pick && canGroup(pick, this.groups)) {
2622
- 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));
2623
2796
  } else if (!pick && mine === null) {
2624
- 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));
2625
2798
  }
2626
2799
  const others = this.groups.filter((g) => g.id !== mine?.id);
2627
2800
  if (!pick && others.length > 0) {
2628
2801
  items.push(
2629
2802
  b.submenu(
2630
- mine === null ? "Add to group" : "Move to group",
2803
+ mine === null ? this.t(TEXT5.addToGroup) : this.t(TEXT5.moveToGroup),
2631
2804
  "folder-plus",
2632
2805
  others.map(
2633
2806
  (g) => b.entry(g.name, void 0, () => {
@@ -2641,7 +2814,7 @@ var ObjectTree = class extends SidePanel {
2641
2814
  }
2642
2815
  if (mine !== null) {
2643
2816
  items.push(
2644
- b.entry(`Remove from ${mine.name}`, "folder-minus", () => {
2817
+ b.entry(this.t(TEXT5.removeFromGroup, { group: mine.name }), "folder-minus", () => {
2645
2818
  this.groups = removeFromGroups(this.groups, [id]);
2646
2819
  this.render();
2647
2820
  }, items.length === 0)
@@ -2654,24 +2827,24 @@ var ObjectTree = class extends SidePanel {
2654
2827
  const members = pass.chart.drawings.all().filter((d) => g.ids.includes(d.id));
2655
2828
  const { allHidden, allLocked } = groupState(members);
2656
2829
  const items = [
2657
- b.entry("Rename\u2026", "pen", () => {
2830
+ b.entry(this.t(TEXT5.rename), "pen", () => {
2658
2831
  this.renaming = g.id;
2659
2832
  this.render();
2660
2833
  }),
2661
- b.entry(allHidden ? "Show all" : "Hide all", allHidden ? "eye" : "eye-off", () => this.setMembers(pass, ids, { visible: allHidden }), true),
2662
- b.entry(allLocked ? "Unlock all" : "Lock all", allLocked ? "unlock" : "lock", () => this.setMembers(pass, ids, { locked: !allLocked })),
2663
- 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", () => {
2664
2837
  this.picked = new Set(ids);
2665
2838
  this.selectOnChart(ids);
2666
2839
  this.render();
2667
2840
  })
2668
2841
  ];
2669
2842
  if (members.length > 0) {
2670
- items.push(b.entry("Bring all to front", "arrow-up", () => this.restackGroup(pass, g, "front")));
2671
- 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")));
2672
2845
  }
2673
- items.push(b.entry("Ungroup", "ungroup", () => this.removeGroup(pass, g.id, false), true));
2674
- 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)));
2675
2848
  return items;
2676
2849
  }
2677
2850
  /** Forget everything that refers to a drawing or group the chart no longer has — otherwise a
@@ -2689,6 +2862,21 @@ var ObjectTree = class extends SidePanel {
2689
2862
  };
2690
2863
 
2691
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;
2692
2880
  var STYLE_ID5 = "vela-widget-datawindow";
2693
2881
  var CSS5 = `
2694
2882
  .vela-dw-group {
@@ -2714,19 +2902,21 @@ var CSS5 = `
2714
2902
  }
2715
2903
  .vela-dw-row:hover { background: var(--vela-hover); }
2716
2904
  .vela-dw-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--vela-fg-bright); }
2717
- .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; }
2718
2907
  .vela-dw-empty { padding: 20px 10px; text-align: center; color: var(--vela-fg-muted); font-size: 12px; }
2719
2908
  `;
2720
2909
  var REFRESH_EVENTS2 = ["bar", "context:changed", "indicator:added", "indicator:removed", "indicator:visibility", "indicator:moved", "pane:changed", "market:changed", "history:complete"];
2721
- var EM_DASH = "\u2014";
2722
- function dataWindowSections(readout) {
2910
+ var EM_DASH = "—";
2911
+ function dataWindowSections(readout, i18n) {
2912
+ const t = (m) => textOf(i18n, m);
2723
2913
  const sections = [];
2724
2914
  if (readout.date || readout.time) {
2725
2915
  sections.push({
2726
- title: "Time",
2916
+ title: t(TEXT6.timeSection),
2727
2917
  lines: [
2728
- { label: "Date", value: readout.date || EM_DASH, color: "" },
2729
- { 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: "" }
2730
2920
  ]
2731
2921
  });
2732
2922
  }
@@ -2734,13 +2924,13 @@ function dataWindowSections(readout) {
2734
2924
  if (ohlc) {
2735
2925
  const color = ohlc.up ? "var(--vela-up)" : "var(--vela-down)";
2736
2926
  const lines = [
2737
- { label: "Open", value: ohlc.o, color },
2738
- { label: "High", value: ohlc.h, color },
2739
- { label: "Low", value: ohlc.l, color },
2740
- { 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 }
2741
2931
  ];
2742
- if (ohlc.vol !== void 0) lines.push({ label: "Volume", value: ohlc.vol, color });
2743
- 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 });
2744
2934
  }
2745
2935
  for (const group of readout.groups) {
2746
2936
  sections.push({ title: group.name, lines: group.rows.map((r) => ({ label: r.label, value: r.value, color: r.color })) });
@@ -2748,8 +2938,8 @@ function dataWindowSections(readout) {
2748
2938
  return sections;
2749
2939
  }
2750
2940
  var DataWindow = class extends SidePanel {
2751
- constructor(host) {
2752
- super(host, "Data window", "vela-dw");
2941
+ constructor(host, i18n) {
2942
+ super(host, TEXT6.title, "vela-dw", i18n ? { i18n } : {});
2753
2943
  this.chart = null;
2754
2944
  this.unsubs = [];
2755
2945
  injectStyles(STYLE_ID5, CSS5, host.ownerDocument);
@@ -2770,6 +2960,10 @@ var DataWindow = class extends SidePanel {
2770
2960
  this.detach();
2771
2961
  super.destroy();
2772
2962
  }
2963
+ relocalize() {
2964
+ super.relocalize();
2965
+ this.refresh();
2966
+ }
2773
2967
  detach() {
2774
2968
  for (const u of this.unsubs) u();
2775
2969
  this.unsubs = [];
@@ -2780,11 +2974,11 @@ var DataWindow = class extends SidePanel {
2780
2974
  const doc = this.el.ownerDocument;
2781
2975
  const readout = this.chart.renderer.dataWindowReadout();
2782
2976
  this.body.replaceChildren();
2783
- const sections = readout ? dataWindowSections(readout) : [];
2977
+ const sections = readout ? dataWindowSections(readout, this.i18n) : [];
2784
2978
  if (sections.length === 0) {
2785
2979
  const empty = doc.createElement("div");
2786
2980
  empty.className = "vela-dw-empty";
2787
- empty.textContent = readout ? "No data" : "This renderer provides no data readout.";
2981
+ empty.textContent = this.t(readout ? TEXT6.noData : TEXT6.noReadout);
2788
2982
  this.body.appendChild(empty);
2789
2983
  return;
2790
2984
  }
@@ -2801,6 +2995,7 @@ var DataWindow = class extends SidePanel {
2801
2995
  label.textContent = line.label;
2802
2996
  const value = doc.createElement("span");
2803
2997
  value.className = "vela-dw-value";
2998
+ value.dir = "ltr";
2804
2999
  value.textContent = line.value;
2805
3000
  if (line.color) value.style.color = line.color;
2806
3001
  row.append(label, value);
@@ -2846,7 +3041,8 @@ var PanelDock = class {
2846
3041
  minWidth: desc.minWidth,
2847
3042
  maxWidth: desc.maxWidth,
2848
3043
  overlay: desc.overlay,
2849
- maximizable: desc.maximizable
3044
+ maximizable: desc.maximizable,
3045
+ ...this.deps.i18n ? { i18n: this.deps.i18n } : {}
2850
3046
  });
2851
3047
  const entry = {
2852
3048
  id: desc.id,
@@ -2895,7 +3091,12 @@ var PanelDock = class {
2895
3091
  /** The docked panels in dock order — what a non-topbar chrome (the mobile
2896
3092
  * three-dots drawer) lists so every panel stays reachable there too. */
2897
3093
  list() {
2898
- 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();
2899
3100
  }
2900
3101
  /** The dock's persistable state, or null when there is nothing worth saving. */
2901
3102
  getState() {
@@ -2972,7 +3173,7 @@ var PanelDock = class {
2972
3173
  }
2973
3174
  /** Push the current toggle group to the chrome, pressed states included. */
2974
3175
  publish() {
2975
- const buttons = this.entries.map((e) => ({ id: e.id, title: e.title, icon: e.icon }));
3176
+ const buttons = this.list().slice();
2976
3177
  this.deps.chrome.setPanelButtons(buttons, (id) => this.toggle(id));
2977
3178
  for (const entry of this.entries) this.deps.chrome.setPanelActive(entry.id, entry.panel.open);
2978
3179
  }
@@ -3047,7 +3248,7 @@ var STYLE_ID6 = "vela-widget-symbolpicker";
3047
3248
  var CSS6 = `
3048
3249
  /* The dialog body is flush (a flex column): search + market tabs are a fixed head
3049
3250
  and the result list is the ONLY scroller, in both layouts. A sticky head over a
3050
- 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
3051
3252
  scroller's content box, so the head lands one padding below the top and covers
3052
3253
  the first row. */
3053
3254
  .vela-sp-head { flex: none; padding: var(--vela-space-4) var(--vela-space-4) 0; }
@@ -3075,7 +3276,7 @@ var CSS6 = `
3075
3276
  .vela-sp-input::placeholder { text-transform: none; }
3076
3277
  .vela-sp-tabs { display: flex; gap: 14px; margin: 12px 2px 0; border-bottom: 1px solid var(--vela-border); padding-bottom: 8px; }
3077
3278
  /* Mobile (fullscreen dialog): the asset-class strip scrolls sideways instead of
3078
- 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
3079
3280
  rest of the screen. The bottom inset (+ safe area) is the kit's: every mobile
3080
3281
  dialog body carries it, flush or not, so the list adds none of its own. */
3081
3282
  [data-layout='mobile'] .vela-sp-tabs { overflow-x: auto; scrollbar-width: none; gap: 6px; }
@@ -3142,7 +3343,7 @@ var CSS6 = `
3142
3343
  letter-spacing: 0.06em;
3143
3344
  text-transform: uppercase;
3144
3345
  }
3145
- /* 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. */
3146
3347
  .vela-sp-badge[data-p='binance'] { color: #f0b90b; } /* palette-exempt: venue brand mark */
3147
3348
  .vela-sp-badge[data-p='hyperliquid'] { color: #50d2c1; } /* palette-exempt: venue brand mark */
3148
3349
  .vela-sp-empty { padding: var(--vela-space-3); color: var(--vela-fg-muted); text-align: center; }
@@ -3162,6 +3363,30 @@ var CSS6 = `
3162
3363
  .vela-sp-expander:hover { background: var(--vela-surface-elev); color: var(--vela-fg); }
3163
3364
  .vela-sp-row[data-member] { padding-left: 34px; }
3164
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
+ };
3165
3390
  var PAGE = 100;
3166
3391
  var SymbolPicker = class {
3167
3392
  constructor(opts) {
@@ -3170,7 +3395,8 @@ var SymbolPicker = class {
3170
3395
  this.rows = [];
3171
3396
  this.highlighted = 0;
3172
3397
  this.seed = "";
3173
- this.activeTab = "All";
3398
+ this.activeTab = "all";
3399
+ this.tabButtons = [];
3174
3400
  this.visible = PAGE;
3175
3401
  /** The last filter pass returned fewer raw rows than asked — the pool is drained
3176
3402
  * (checked BEFORE folding: folding shortens pages without meaning exhaustion). */
@@ -3184,7 +3410,7 @@ var SymbolPicker = class {
3184
3410
  injectStyles(STYLE_ID6, CSS6, doc);
3185
3411
  this.input = doc.createElement("input");
3186
3412
  this.input.className = "vela-sp-input";
3187
- this.input.placeholder = "Search symbol\u2026";
3413
+ this.input.placeholder = textOf(opts.i18n, TEXT7.search);
3188
3414
  this.input.setAttribute("spellcheck", "false");
3189
3415
  const searchRow = doc.createElement("div");
3190
3416
  searchRow.className = "vela-sp-searchrow";
@@ -3193,13 +3419,15 @@ var SymbolPicker = class {
3193
3419
  this.tabs.className = "vela-sp-tabs";
3194
3420
  const head = doc.createElement("div");
3195
3421
  head.className = "vela-sp-head";
3196
- for (const t of ["All", "Stocks", "ETFs", "Crypto", "Futures", "Forex", "Commodities"]) {
3422
+ for (const tab of TABS) {
3197
3423
  const b = doc.createElement("button");
3198
3424
  b.className = "vela-sp-tab";
3199
- b.textContent = t;
3200
- 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";
3201
3429
  b.addEventListener("click", () => {
3202
- this.activeTab = t;
3430
+ this.activeTab = tab.id;
3203
3431
  for (const c of this.tabs.children) delete c.dataset.active;
3204
3432
  b.dataset.active = "1";
3205
3433
  this.refresh();
@@ -3216,13 +3444,14 @@ var SymbolPicker = class {
3216
3444
  this.grow();
3217
3445
  });
3218
3446
  this.dialog = new Dialog({
3219
- title: "Symbol Search",
3447
+ title: textOf(opts.i18n, TEXT7.title),
3220
3448
  host: opts.host,
3221
3449
  closeOnInteractOutside: true,
3222
3450
  flush: true,
3223
3451
  content: (body) => body.append(head, this.list),
3224
3452
  onOpenChange: (open) => {
3225
3453
  if (open) {
3454
+ this.relabel();
3226
3455
  this.input.value = this.seed.toUpperCase();
3227
3456
  this.seed = "";
3228
3457
  this.refresh();
@@ -3346,7 +3575,7 @@ var SymbolPicker = class {
3346
3575
  Promise.resolve([...raw]).then((copy) => hook(copy)).then((out) => {
3347
3576
  this.ranked = { key, result: dedupeSymbols(out) };
3348
3577
  }).catch((err) => {
3349
- console.warn("[vela] symbol ranking failed \u2014 pool order kept:", err);
3578
+ console.warn("[vela] symbol ranking failed — pool order kept:", err);
3350
3579
  this.ranked = { key, result: [...raw] };
3351
3580
  }).finally(() => {
3352
3581
  this.ranking = false;
@@ -3355,10 +3584,17 @@ var SymbolPicker = class {
3355
3584
  }
3356
3585
  return this.ranked?.result ?? raw;
3357
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
+ }
3358
3594
  computeRows() {
3359
- const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Futures: ["futures", "root"], Forex: ["forex"], Commodities: ["commodity"] };
3360
3595
  const all = this.pool();
3361
- 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");
3362
3598
  const filtered = filterSymbols(pool, this.input.value, this.visible, symbolRanking() ? false : TOP_TICKERS);
3363
3599
  this.exhausted = filtered.length < this.visible;
3364
3600
  const folded = foldGroups(filtered);
@@ -3386,7 +3622,7 @@ var SymbolPicker = class {
3386
3622
  if (!this.rows.length) {
3387
3623
  const empty = doc.createElement("div");
3388
3624
  empty.className = "vela-sp-empty";
3389
- 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);
3390
3626
  this.list.appendChild(empty);
3391
3627
  return;
3392
3628
  }
@@ -3422,7 +3658,7 @@ var SymbolPicker = class {
3422
3658
  row.dataset.group = "1";
3423
3659
  const expander = doc.createElement("button");
3424
3660
  expander.className = "vela-sp-expander";
3425
- expander.setAttribute("aria-label", "Show contracts");
3661
+ expander.setAttribute("aria-label", textOf(this.opts.i18n, TEXT7.showContracts));
3426
3662
  expander.appendChild(iconEl(this.expanded.has(groupKeyOf(s)) ? "chevron-down" : "chevron-right", doc));
3427
3663
  row.appendChild(expander);
3428
3664
  } else if (s.group != null && this.expanded.has(groupKeyOf(s))) row.dataset.member = "1";
@@ -3438,6 +3674,17 @@ var SymbolPicker = class {
3438
3674
  };
3439
3675
 
3440
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";
3441
3688
  var STYLE_ID7 = "vela-widget-indpicker";
3442
3689
  var CSS7 = `
3443
3690
  .vela-ip-searchrow {
@@ -3455,7 +3702,7 @@ var CSS7 = `
3455
3702
  .vela-ip-searchrow .vela-icon { color: var(--vela-fg-muted); }
3456
3703
  .vela-ip-search { flex: 1; background: transparent; color: var(--vela-fg); border: none; font-size: 14px; outline: none; }
3457
3704
  .vela-ip-list { max-height: 50vh; overflow: auto; min-width: 380px; }
3458
- /* 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 —
3459
3706
  and no height cap; the fullscreen body owns the scrolling. */
3460
3707
  [data-layout='mobile'] .vela-ip-list { min-width: 0; max-height: none; }
3461
3708
  .vela-ip-list::-webkit-scrollbar { width: 8px; }
@@ -3519,7 +3766,7 @@ var IndicatorPicker = class {
3519
3766
  injectStyles(STYLE_ID7, CSS7, doc);
3520
3767
  this.search = doc.createElement("input");
3521
3768
  this.search.className = "vela-ip-search";
3522
- this.search.placeholder = "Search\u2026";
3769
+ this.search.placeholder = this.t(TEXT8.search);
3523
3770
  this.search.spellcheck = false;
3524
3771
  this.search.addEventListener("input", () => this.refresh());
3525
3772
  const searchRow = doc.createElement("div");
@@ -3528,7 +3775,7 @@ var IndicatorPicker = class {
3528
3775
  this.list = doc.createElement("div");
3529
3776
  this.list.className = "vela-ip-list";
3530
3777
  this.dialog = new Dialog({
3531
- title: "Indicators",
3778
+ title: this.t(TEXT8.title),
3532
3779
  host: opts.host,
3533
3780
  draggable: true,
3534
3781
  closeOnInteractOutside: true,
@@ -3538,6 +3785,7 @@ var IndicatorPicker = class {
3538
3785
  this.isOpen = open;
3539
3786
  if (open) {
3540
3787
  this.search.value = "";
3788
+ this.relabel();
3541
3789
  this.refresh();
3542
3790
  if (!this.search.closest('[data-layout="mobile"]')) setTimeout(() => this.search.focus(), 0);
3543
3791
  }
@@ -3559,6 +3807,14 @@ var IndicatorPicker = class {
3559
3807
  open() {
3560
3808
  this.dialog.show();
3561
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
+ }
3562
3818
  /** Re-render the lists if the dialog is open (async catalog updates land late). */
3563
3819
  sync() {
3564
3820
  if (this.isOpen) this.refresh();
@@ -3571,11 +3827,21 @@ var IndicatorPicker = class {
3571
3827
  }
3572
3828
  refresh() {
3573
3829
  const doc = this.list.ownerDocument;
3574
- 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();
3575
3841
  const library = this.opts.library();
3576
3842
  const instances = this.opts.onChart();
3577
3843
  this.list.replaceChildren();
3578
- 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));
3579
3845
  const rowEl = (r, opts) => {
3580
3846
  const row = doc.createElement("div");
3581
3847
  row.className = "vela-ip-row";
@@ -3589,7 +3855,7 @@ var IndicatorPicker = class {
3589
3855
  if (r.beta) {
3590
3856
  const beta = doc.createElement("span");
3591
3857
  beta.className = "vela-ip-badge";
3592
- beta.textContent = "beta";
3858
+ beta.textContent = this.t(TEXT8.beta);
3593
3859
  row.appendChild(beta);
3594
3860
  }
3595
3861
  if (opts.instance !== void 0) {
@@ -3603,7 +3869,7 @@ var IndicatorPicker = class {
3603
3869
  const trash = doc.createElement("button");
3604
3870
  trash.className = "vela-ip-trash";
3605
3871
  trash.appendChild(iconEl("trash", doc));
3606
- trash.title = "Remove from chart";
3872
+ trash.title = this.t(TEXT8.remove);
3607
3873
  row.appendChild(trash);
3608
3874
  }
3609
3875
  return row;
@@ -3614,7 +3880,7 @@ var IndicatorPicker = class {
3614
3880
  card.className = "vela-ip-oncard";
3615
3881
  const title = doc.createElement("div");
3616
3882
  title.className = "vela-ip-group";
3617
- title.textContent = `On chart \xB7 ${onChart.length}`;
3883
+ title.textContent = this.t(TEXT8.onChart, { count: onChart.length });
3618
3884
  card.appendChild(title);
3619
3885
  for (const [r, i] of onChart) card.appendChild(rowEl(r, { instance: i }));
3620
3886
  this.list.appendChild(card);
@@ -3622,7 +3888,7 @@ var IndicatorPicker = class {
3622
3888
  const byCat = /* @__PURE__ */ new Map();
3623
3889
  library.forEach((r, i) => {
3624
3890
  if (!match(r)) return;
3625
- const cat = r.category ?? "General";
3891
+ const cat = r.category ?? GENERAL;
3626
3892
  const bucket = byCat.get(cat) ?? [];
3627
3893
  bucket.push([r, i]);
3628
3894
  byCat.set(cat, bucket);
@@ -3630,14 +3896,14 @@ var IndicatorPicker = class {
3630
3896
  for (const [cat, bucket] of byCat) {
3631
3897
  const title = doc.createElement("div");
3632
3898
  title.className = "vela-ip-group";
3633
- title.textContent = cat;
3899
+ title.textContent = cat === GENERAL ? this.t(TEXT8.general) : cat;
3634
3900
  this.list.appendChild(title);
3635
3901
  for (const [r, i] of bucket) this.list.appendChild(rowEl(r, { library: i }));
3636
3902
  }
3637
3903
  if (!onChart.length && byCat.size === 0) {
3638
3904
  const empty = doc.createElement("div");
3639
3905
  empty.className = "vela-ip-empty";
3640
- empty.textContent = library.length ? "No indicators match." : "No indicators in the manifest.";
3906
+ empty.textContent = this.t(library.length ? TEXT8.noMatch : TEXT8.empty);
3641
3907
  this.list.appendChild(empty);
3642
3908
  }
3643
3909
  }
@@ -3665,8 +3931,18 @@ var CSS8 = `
3665
3931
  .vela-tq-hint { margin-top: var(--vela-space-2); text-align: center; color: var(--vela-fg-muted); min-height: 1.2em; }
3666
3932
  .vela-tq-hint[data-invalid] { color: var(--vela-danger); }
3667
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
+ };
3668
3940
  var TimeframeQuick = class {
3941
+ t(text) {
3942
+ return textOf(this.i18n, text);
3943
+ }
3669
3944
  constructor(opts) {
3945
+ this.i18n = opts.i18n ?? null;
3670
3946
  const doc = (opts.host ?? document.body).ownerDocument;
3671
3947
  injectStyles(STYLE_ID8, CSS8, doc);
3672
3948
  this.input = doc.createElement("input");
@@ -3675,7 +3951,7 @@ var TimeframeQuick = class {
3675
3951
  this.hint = doc.createElement("div");
3676
3952
  this.hint.className = "vela-tq-hint";
3677
3953
  this.dialog = new Dialog({
3678
- title: "Change timeframe",
3954
+ title: this.t(TEXT9.title),
3679
3955
  host: opts.host,
3680
3956
  draggable: true,
3681
3957
  closeOnInteractOutside: true,
@@ -3697,7 +3973,12 @@ var TimeframeQuick = class {
3697
3973
  }
3698
3974
  });
3699
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
+ }
3700
3980
  open(seed = "") {
3981
+ this.relabel();
3701
3982
  this.dialog.show();
3702
3983
  this.input.value = seed.toUpperCase();
3703
3984
  this.renderHint();
@@ -3728,21 +4009,32 @@ var TimeframeQuick = class {
3728
4009
  }
3729
4010
  renderHint() {
3730
4011
  const raw = this.input.value.trim();
3731
- const parsed = parseTimeframe(raw);
4012
+ const parsed = parseTimeframe(raw, this.i18n);
3732
4013
  if (!raw) {
3733
- this.hint.textContent = "e.g. 15, 4H, D, 3M";
4014
+ this.hint.textContent = this.t(TEXT9.example);
3734
4015
  delete this.hint.dataset.invalid;
3735
4016
  } else if (parsed.valid) {
3736
4017
  this.hint.textContent = parsed.label ?? "";
3737
4018
  delete this.hint.dataset.invalid;
3738
4019
  } else {
3739
- this.hint.textContent = "Not a timeframe";
4020
+ this.hint.textContent = this.t(TEXT9.invalid);
3740
4021
  this.hint.dataset.invalid = "1";
3741
4022
  }
3742
4023
  }
3743
4024
  };
3744
4025
 
3745
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
+ };
3746
4038
  var STYLE_ID9 = "vela-widget-shortcuts";
3747
4039
  var CSS9 = `
3748
4040
  .vela-sh-cat {
@@ -3766,13 +4058,15 @@ var CSS9 = `
3766
4058
  .vela-sh-static { color: var(--vela-fg-muted); font-size: var(--vela-font-size-sm); margin-top: var(--vela-space-2); }
3767
4059
  `;
3768
4060
  var ShortcutsHelp = class {
3769
- 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) {
3770
4063
  this.keymap = keymap;
4064
+ this.i18n = i18n ?? englishTranslator;
3771
4065
  const doc = (host ?? document.body).ownerDocument;
3772
4066
  injectStyles(STYLE_ID9, CSS9, doc);
3773
4067
  this.list = doc.createElement("div");
3774
4068
  this.dialog = new Dialog({
3775
- title: "Keyboard shortcuts",
4069
+ title: this.i18n().t(TEXT10.title),
3776
4070
  host,
3777
4071
  closeOnInteractOutside: true,
3778
4072
  content: (body) => body.appendChild(this.list),
@@ -3793,6 +4087,9 @@ var ShortcutsHelp = class {
3793
4087
  }
3794
4088
  refresh() {
3795
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);
3796
4093
  this.list.replaceChildren();
3797
4094
  const addCategory = (text) => {
3798
4095
  const cat = doc.createElement("div");
@@ -3818,7 +4115,7 @@ var ShortcutsHelp = class {
3818
4115
  this.list.appendChild(row);
3819
4116
  };
3820
4117
  const byCategory = /* @__PURE__ */ new Map();
3821
- for (const b of this.keymap.bindings()) {
4118
+ for (const b of this.keymap.bindings(i18n)) {
3822
4119
  const bucket = byCategory.get(b.category) ?? [];
3823
4120
  bucket.push(b);
3824
4121
  byCategory.set(b.category, bucket);
@@ -3827,13 +4124,13 @@ var ShortcutsHelp = class {
3827
4124
  addCategory(category);
3828
4125
  for (const b of bindings) addRow(b.label, b.display);
3829
4126
  }
3830
- addCategory("Mouse");
3831
- addRow("Scroll through history", ["Shift+Scroll"]);
3832
- addRow("Measure from the press point", ["Shift+Click"]);
3833
- 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)]);
3834
4131
  const s = doc.createElement("div");
3835
4132
  s.className = "vela-sh-static";
3836
- 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);
3837
4134
  this.list.appendChild(s);
3838
4135
  }
3839
4136
  };
@@ -3973,6 +4270,20 @@ function sanitizeDrawingTools(raw) {
3973
4270
  if (typeof r.stay === "boolean") out.stay = r.stay;
3974
4271
  return Object.keys(out).length > 0 ? out : null;
3975
4272
  }
4273
+ function successorNative(entry) {
4274
+ const type = typeof entry === "string" ? entry : entry.type;
4275
+ if (type !== "linear-regression") return entry;
4276
+ const hidden = typeof entry === "object" && entry.hidden === true;
4277
+ const { mode, lineColor, ...rest } = typeof entry === "object" ? entry.inputs ?? {} : {};
4278
+ const slope = mode === "Slope";
4279
+ const inputs = slope ? { ...rest, ...lineColor !== void 0 ? { lineColor } : {} } : (
4280
+ // Saved deltas omit the curve's 100-bar default, which the LSMA's default is not.
4281
+ { length: 100, ...rest, ...lineColor !== void 0 ? { neutralColor: lineColor } : {} }
4282
+ );
4283
+ const next = slope ? "linear-regression-slope" : "lsma";
4284
+ const hasInputs = Object.keys(inputs).length > 0;
4285
+ return hasInputs || hidden ? { type: next, ...hasInputs ? { inputs } : {}, ...hidden ? { hidden } : {} } : next;
4286
+ }
3976
4287
  function sanitizeCell(raw) {
3977
4288
  if (raw == null || typeof raw !== "object") return null;
3978
4289
  const c = raw;
@@ -4012,7 +4323,7 @@ function sanitizeCell(raw) {
4012
4323
  return [inputs || hidden ? { type: e.type, ...inputs ? { inputs } : {}, ...hidden ? { hidden } : {} } : e.type];
4013
4324
  }
4014
4325
  return [];
4015
- }) : [];
4326
+ }).map(successorNative) : [];
4016
4327
  out.indicators = { manifest, natives };
4017
4328
  }
4018
4329
  const ext = sanitizeExt(c.ext);
@@ -4170,7 +4481,7 @@ function decimalsFor(ref) {
4170
4481
  return a >= 1 ? 2 : a >= 0.01 ? 4 : 6;
4171
4482
  }
4172
4483
  function fmtPrice(n, dp) {
4173
- if (n == null || !Number.isFinite(n)) return "\u2014";
4484
+ if (n == null || !Number.isFinite(n)) return "—";
4174
4485
  const d = dp ?? decimalsFor(n);
4175
4486
  return n.toLocaleString("en-US", { minimumFractionDigits: d, maximumFractionDigits: d });
4176
4487
  }
@@ -4181,13 +4492,25 @@ function fmtChange(open, close) {
4181
4492
  const sign = diff >= 0 ? "+" : "";
4182
4493
  return `${sign}${fmtPrice(diff, decimalsFor(close))} (${sign}${pct.toFixed(2)}%)`;
4183
4494
  }
4184
- function fmtChangePct(open, close) {
4185
- if (open == null || close == null || !Number.isFinite(open) || !Number.isFinite(close) || open === 0) return "";
4186
- const pct = (close - open) / open * 100;
4187
- return `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`;
4188
- }
4189
4495
 
4190
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
+ };
4191
4514
  function segmentVisibility(parts, chartHidden = false) {
4192
4515
  return { avatar: parts.logo, symbol: parts.name, meta: parts.name, market: parts.market, ohlc: parts.ohlc && !chartHidden, change: parts.change && !chartHidden, eye: chartHidden };
4193
4516
  }
@@ -4197,28 +4520,31 @@ var STATUSLINE_LADDER = [
4197
4520
  { stacked: true, level: "compact" },
4198
4521
  { stacked: true, level: "minimal" }
4199
4522
  ];
4200
- function readoutCells(level, readout) {
4523
+ function readoutCells(level, readout, i18n) {
4201
4524
  if (readout === "value") return [{ key: "close", label: "" }];
4525
+ const label = (key) => textOf(i18n, CELL_LABELS[key]);
4202
4526
  if (level === "full") {
4203
4527
  return [
4204
- { key: "open", label: "O" },
4205
- { key: "high", label: "H" },
4206
- { key: "low", label: "L" },
4207
- { 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") }
4208
4532
  ];
4209
4533
  }
4210
- return [{ key: "close", label: level === "compact" ? "C" : "" }];
4534
+ return [{ key: "close", label: level === "compact" ? label("close") : "" }];
4211
4535
  }
4212
- function statuslineMenuItems(parts, chartVisible) {
4536
+ function statuslineMenuItems(parts, chartVisible, i18n) {
4537
+ const t = (m) => textOf(i18n, m);
4213
4538
  return [
4214
- { id: "part:logo", label: "Symbol logo", checked: parts.logo },
4215
- { id: "part:name", label: "Symbol name", checked: parts.name },
4216
- { id: "part:market", label: "Market status", checked: parts.market },
4217
- { id: "part:ohlc", label: "OHLC values", checked: parts.ohlc },
4218
- { id: "part:change", label: "Bar change values", checked: parts.change },
4219
- { 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 }
4220
4545
  ];
4221
4546
  }
4547
+ var SHOW_CHART_LABEL = TEXT11.showChart;
4222
4548
 
4223
4549
  // src/widget/statusline.ts
4224
4550
  var STYLE_ID10 = "vela-widget-statusline";
@@ -4227,7 +4553,7 @@ var CSS10 = `
4227
4553
  position: absolute;
4228
4554
  top: var(--vela-space-2);
4229
4555
  /* Track the indicator legend's left edge: the renderer publishes its toolbar gutter
4230
- * 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 —
4231
4557
  * so the two columns stay aligned whether the toolbar is docked (44px), collapsed
4232
4558
  * (16px), or absent entirely (a workspace cell: 0). */
4233
4559
  left: calc(var(--vela-toolbar-gutter, 0px) + 10px);
@@ -4243,8 +4569,8 @@ var CSS10 = `
4243
4569
  color: var(--vela-fg);
4244
4570
  font-size: var(--vela-font-size-md);
4245
4571
  /* Same chip treatment as the indicator legend rows (InputsUI): a translucent wash of
4246
- * the chart background when idle \u2014 enough to keep the readout legible when candles
4247
- * 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
4248
4574
  * compensating negative margin (mirroring the legend rows) keeps the avatar's left
4249
4575
  * edge on the legend column's left edge (both at left:10px) while the chip itself
4250
4576
  * extends 7px further left, so both columns' chips share the same left edge. */
@@ -4256,12 +4582,12 @@ var CSS10 = `
4256
4582
  }
4257
4583
  /* Hovering opens the chip the same way a legend row opens: solid chart background
4258
4584
  * plus the same inset neutral outline the indicator rows wear (InputsUI's
4259
- * setRowHighlighted) \u2014 the two columns read as one family. */
4585
+ * setRowHighlighted) — the two columns read as one family. */
4260
4586
  .vela-statusline:hover { background: var(--vela-bg); box-shadow: inset 0 0 0 1px var(--vela-border); }
4261
- /* 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
4262
4588
  * the same 0.5 wash a hidden indicator's legend row wears. */
4263
4589
  .vela-statusline.vela-sl-chart-hidden { opacity: 0.5; }
4264
- /* 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
4265
4591
  * value readout (O/H/L/C + change, or the show-chart eye). In the widest layout they sit
4266
4592
  * side by side on one line; the STACKED layouts (fit() decides, see the ladder in
4267
4593
  * statusline-model) turn the chip into a column so the values drop under the symbol. */
@@ -4278,7 +4604,7 @@ var CSS10 = `
4278
4604
  * that follows the chip (see the stacked legend shift below). */
4279
4605
  row-gap: var(--vela-space-1);
4280
4606
  }
4281
- /* 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
4282
4608
  * never does, so its overflow is what fit() measures to keep descending the ladder. */
4283
4609
  .vela-statusline.vela-sl-stacked .vela-sl-identity { max-width: 100%; }
4284
4610
  .vela-statusline.vela-sl-stacked .vela-sl-meta {
@@ -4301,25 +4627,28 @@ var CSS10 = `
4301
4627
  }
4302
4628
  .vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
4303
4629
  .vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
4304
- /* 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
4305
4631
  * venue sits after it, not a full row gap away. */
4306
4632
  .vela-statusline .vela-sl-symbol + .vela-sl-meta { margin-left: calc(var(--vela-space-1) - var(--vela-space-2)); }
4307
- /* 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
4308
4634
  * via the kit tooltip); the session tint is applied per status in setMarketStatus. While
4309
4635
  * the chart replays past bars it wears the replay badge instead (the inverse chip). */
4310
4636
  .vela-statusline .vela-sl-market { align-self: center; display: inline-flex; }
4311
4637
  .vela-statusline .vela-sl-market > [hidden] { display: none !important; }
4312
4638
  .vela-statusline .vela-sl-replay-badge, .vela-statusline .vela-sl-replay-badge svg { display: block; width: 16px; height: 16px; }
4313
4639
  /* Tabular figures: every digit has one advance, so a readout whose text changes only in
4314
- * 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). */
4315
4641
  .vela-statusline .vela-sl-values { font-variant-numeric: tabular-nums; }
4316
4642
  .vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
4317
4643
  .vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
4318
- /* 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) —
4319
4648
  * these are the pre-ink fallbacks only. */
4320
4649
  .vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
4321
4650
  .vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
4322
- /* 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),
4323
4652
  * replacing the value readout it took away. Same footprint as a legend action button. */
4324
4653
  .vela-statusline .vela-sl-eye {
4325
4654
  align-self: center;
@@ -4338,18 +4667,18 @@ var CSS10 = `
4338
4667
  }
4339
4668
  .vela-statusline .vela-sl-eye:hover { color: var(--vela-fg); background: color-mix(in srgb, var(--vela-fg) 12%, transparent); }
4340
4669
  /* Stack the TOP pane's legend below the status line (lower study panes stay put).
4341
- * The renderer marks whichever legend sits at the plot's top edge \u2014 the price pane
4342
- * 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
4343
4672
  * with the status line whichever pane owns the top. The renderer sets the legend's
4344
- * inline top \u2014 shift with a transform, don't fight it. Scoped to hosts that actually
4345
- * 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
4346
4675
  * stylesheet is document-global, so a bare attribute selector here would shift every
4347
4676
  * chart on the page, including statusline-less ones. */
4348
4677
  .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
4349
- /* 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
4350
4679
  * host whenever the ladder lands on a stacked layout. */
4351
4680
  .vela-has-statusline.vela-sl-stacked-host [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
4352
- /* 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
4353
4682
  * of the readout fits, exactly as on a narrow desktop chart. */
4354
4683
  [data-layout='mobile'] .vela-statusline { font-size: var(--vela-font-size-sm); }
4355
4684
  [data-layout='mobile'] .vela-statusline .vela-sl-symbol { font-size: var(--vela-font-size-md); }
@@ -4358,14 +4687,16 @@ function baseOfTicker(ticker) {
4358
4687
  return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
4359
4688
  }
4360
4689
  var MARKET_LABELS = {
4361
- open: "Market Open",
4362
- pre: "Pre-Market",
4363
- post: "Post-Market",
4364
- extended: "Extended Hours",
4365
- closed: "Market Closed",
4366
- 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")
4367
4696
  };
4368
- 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 = "—";
4369
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>';
4370
4701
  var MARKET_INKS = {
4371
4702
  open: "var(--vela-up)",
@@ -4434,13 +4765,16 @@ var Statusline = class {
4434
4765
  this.fitKey = null;
4435
4766
  /** Web fonts landing after the last fit change every width without a resize. */
4436
4767
  this.onFontsLoaded = () => this.fit();
4768
+ /** The bound chart's translator (English until {@link onChart}). */
4769
+ this.i18n = null;
4770
+ this.meta = null;
4437
4771
  this.onContextMenu = (e) => {
4438
4772
  if (!this.menu || !this.menuHooks) return;
4439
4773
  e.preventDefault();
4440
4774
  e.stopPropagation();
4441
4775
  const chartVisible = this.menuHooks.chartVisible();
4442
4776
  this.setChartHidden(!chartVisible);
4443
- this.menu.setItems(statuslineMenuItems(this.parts, chartVisible));
4777
+ this.menu.setItems(statuslineMenuItems(this.parts, chartVisible, this.i18n));
4444
4778
  this.menu.openAt(e.clientX, e.clientY);
4445
4779
  };
4446
4780
  const doc = host.ownerDocument;
@@ -4460,7 +4794,7 @@ var Statusline = class {
4460
4794
  icon: "market-open",
4461
4795
  background: `color-mix(in srgb, ${MARKET_INKS.open} 20%, transparent)`,
4462
4796
  color: MARKET_INKS.open,
4463
- label: MARKET_LABELS.open,
4797
+ label: this.t(MARKET_LABELS.open),
4464
4798
  host
4465
4799
  });
4466
4800
  this.replayBadge = doc.createElement("span");
@@ -4474,11 +4808,12 @@ var Statusline = class {
4474
4808
  this.ohlcEl.className = "vela-sl-ohlc";
4475
4809
  this.changeEl = doc.createElement("span");
4476
4810
  this.changeEl.className = "vela-sl-change";
4811
+ this.changeEl.dir = "ltr";
4477
4812
  this.eyeEl = doc.createElement("button");
4478
4813
  this.eyeEl.type = "button";
4479
4814
  this.eyeEl.className = "vela-sl-eye";
4480
4815
  this.eyeEl.innerHTML = iconAt("eye-off", 14);
4481
- this.eyeEl.setAttribute("aria-label", "Show chart");
4816
+ this.eyeEl.setAttribute("aria-label", this.t(SHOW_CHART_LABEL));
4482
4817
  this.eyeEl.style.display = "none";
4483
4818
  this.eyeEl.addEventListener("click", (e) => {
4484
4819
  e.stopPropagation();
@@ -4498,11 +4833,14 @@ var Statusline = class {
4498
4833
  this.fitRO.observe(host);
4499
4834
  }
4500
4835
  doc.fonts?.addEventListener("loadingdone", this.onFontsLoaded);
4501
- this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
4502
- 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" });
4503
4838
  this.setMarketStatus("open");
4504
4839
  this.render();
4505
4840
  }
4841
+ t(text) {
4842
+ return textOf(this.i18n, text);
4843
+ }
4506
4844
  setSymbol(symbol) {
4507
4845
  const ticker = parseSymbol(symbol).ticker;
4508
4846
  this.symbolEl.textContent = ticker;
@@ -4587,7 +4925,17 @@ var Statusline = class {
4587
4925
  }
4588
4926
  /** The "· BINANCE · 1h" segment after the symbol — venue first, then resolution. */
4589
4927
  setMeta(timeframe, provider) {
4590
- 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;
4591
4939
  this.fit();
4592
4940
  }
4593
4941
  /** Dress the market badge for a session state: its icon, tinted circle, and the
@@ -4608,7 +4956,7 @@ var Statusline = class {
4608
4956
  this.replayBadge.hidden = !this.replaying;
4609
4957
  if (this.replaying) {
4610
4958
  this.marketEl.dataset.status = "replay";
4611
- this.marketTip.setContent(REPLAY_LABEL);
4959
+ this.marketTip.setContent(this.t(REPLAY_LABEL));
4612
4960
  return;
4613
4961
  }
4614
4962
  const status = this.marketStatus;
@@ -4618,9 +4966,9 @@ var Statusline = class {
4618
4966
  icon: `market-${status}`,
4619
4967
  background: `color-mix(in srgb, ${ink} 20%, transparent)`,
4620
4968
  color: ink,
4621
- label: MARKET_LABELS[status]
4969
+ label: this.t(MARKET_LABELS[status])
4622
4970
  });
4623
- this.marketTip.setContent(MARKET_LABELS[status]);
4971
+ this.marketTip.setContent(this.t(MARKET_LABELS[status]));
4624
4972
  }
4625
4973
  /** Show/hide one part — the settings dialog's Status line tab and the right-click
4626
4974
  * menu both drive these. 'name' owns the venue/timeframe meta too (see
@@ -4677,6 +5025,9 @@ var Statusline = class {
4677
5025
  /** (Re)bind to a chart instance — called after every widget rebuild. */
4678
5026
  onChart(chart) {
4679
5027
  this.detach();
5028
+ const relabel = this.i18n !== chart.i18n;
5029
+ this.i18n = chart.i18n;
5030
+ if (relabel) this.relocalize();
4680
5031
  this.lastBar = null;
4681
5032
  this.hoverBar = null;
4682
5033
  this.unsubs.push(
@@ -4752,18 +5103,29 @@ var Statusline = class {
4752
5103
  const doc = this.el.ownerDocument;
4753
5104
  const up = this.isUp ? this.isUp(bar) : bar.close >= bar.open;
4754
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)}%`;
4755
5109
  const cell = (k, v) => {
4756
5110
  const s = doc.createElement("span");
4757
5111
  if (k) s.append(`${k} `);
4758
5112
  const b = doc.createElement("b");
4759
- b.textContent = fmtPrice(v, dp);
5113
+ b.dir = "ltr";
5114
+ b.textContent = price(v);
4760
5115
  b.style.color = ink;
4761
5116
  s.appendChild(b);
4762
5117
  return s;
4763
5118
  };
4764
5119
  const { level } = this.layout;
4765
- this.ohlcEl.replaceChildren(...readoutCells(level, this.readout).map((c) => cell(c.label, bar[c.key])));
4766
- 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;
4767
5129
  this.changeEl.dataset.dir = up ? "up" : "down";
4768
5130
  this.changeEl.style.color = ink;
4769
5131
  }
@@ -4778,7 +5140,7 @@ var CSS11 = `
4778
5140
  .vela-watermark {
4779
5141
  position: absolute;
4780
5142
  /* Insets follow the renderer-published gutters AND the price-pane vertical
4781
- * 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
4782
5144
  * the study panes below, the drawings toolbar, or the price scale. */
4783
5145
  top: var(--vela-price-pane-top, 0px);
4784
5146
  bottom: var(--vela-price-pane-bottom, 0px);
@@ -4812,8 +5174,14 @@ function watermarkFontPx(availPx, textPxAtMax) {
4812
5174
  if (textPxAtMax <= 0) return MAX_FONT_PX;
4813
5175
  return Math.max(MIN_FONT_PX, Math.min(MAX_FONT_PX, Math.floor(MAX_FONT_PX * availPx * FILL / textPxAtMax)));
4814
5176
  }
5177
+ var TEXT12 = {
5178
+ replay: msg("widget.watermark.replay", "Replay"),
5179
+ mark: msg("widget.watermark.mark", "{symbol} · {timeframe}")
5180
+ };
4815
5181
  var Watermark = class {
4816
- constructor(host, symbol, timeframe) {
5182
+ /** `i18n` reads the chart's translator (absent: English). */
5183
+ constructor(host, symbol, timeframe, i18n = () => null) {
5184
+ this.i18n = i18n;
4817
5185
  this.resizeObserver = null;
4818
5186
  /** The host's visibility preference (the persisted watermark toggle). */
4819
5187
  this.shown = true;
@@ -4825,6 +5193,8 @@ var Watermark = class {
4825
5193
  * canvas, so the mark stays out of its way. Starts true: the FIRST `load:start`
4826
5194
  * fires during chart construction, before any subscriber can see it. */
4827
5195
  this.loading = true;
5196
+ this.symbol = "";
5197
+ this.timeframe = "";
4828
5198
  injectStyles(STYLE_ID11, CSS11, host.ownerDocument);
4829
5199
  this.el = host.ownerDocument.createElement("div");
4830
5200
  this.el.className = "vela-watermark";
@@ -4832,7 +5202,8 @@ var Watermark = class {
4832
5202
  this.text = host.ownerDocument.createElement("span");
4833
5203
  this.replayLine = host.ownerDocument.createElement("span");
4834
5204
  this.replayLine.className = "vela-watermark-replay";
4835
- 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);
4836
5207
  this.el.append(this.text, this.replayLine);
4837
5208
  host.appendChild(this.el);
4838
5209
  if (typeof ResizeObserver !== "undefined") {
@@ -4868,9 +5239,17 @@ var Watermark = class {
4868
5239
  this.el.style.display = (this.shown || replay) && !this.loading ? "" : "none";
4869
5240
  }
4870
5241
  update(symbol, timeframe) {
4871
- 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) }) : "";
4872
5246
  this.fit();
4873
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
+ }
4874
5253
  /** Measure the text at the cap and shrink it to the price pane's width. */
4875
5254
  fit() {
4876
5255
  if (this.el.clientWidth <= 0 || !this.text.textContent) return;
@@ -4887,7 +5266,7 @@ var Watermark = class {
4887
5266
  // src/widget/context-menu.ts
4888
5267
  var PRICE_AXIS_W = 60;
4889
5268
  var TIME_AXIS_H = 26;
4890
- var OFF_PLOT = { price: null, time: null, paneKind: null };
5269
+ var OFF_PLOT = { price: null, time: null, paneKind: null, paneId: null };
4891
5270
  var ChartContextMenu = class {
4892
5271
  constructor(host, cbs) {
4893
5272
  this.cbs = cbs;
@@ -4930,7 +5309,7 @@ var ChartContextMenu = class {
4930
5309
  this.chart = chart;
4931
5310
  this.crosshair = OFF_PLOT;
4932
5311
  this.offCrosshair = chart.renderer.onCrosshairMove((e) => {
4933
- this.crosshair = { price: e.price, time: e.time, paneKind: e.paneKind ?? null };
5312
+ this.crosshair = { price: e.price, time: e.time, paneKind: e.paneKind ?? null, paneId: e.paneId ?? null };
4934
5313
  });
4935
5314
  }
4936
5315
  destroy() {
@@ -4966,7 +5345,7 @@ var ChartContextMenu = class {
4966
5345
  }
4967
5346
  contributed(zone) {
4968
5347
  return widgetActions(`context:${zone}`, this.context()).map((a) => ({
4969
- 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 },
4970
5349
  order: a.order
4971
5350
  }));
4972
5351
  }
@@ -4974,6 +5353,7 @@ var ChartContextMenu = class {
4974
5353
  return composeMenu(zone, this.builtinItems(zone), this.contributed(zone));
4975
5354
  }
4976
5355
  builtinItems(zone) {
5356
+ const chart = this.chart;
4977
5357
  if (zone === "price-axis") {
4978
5358
  const pane = this.lastPane;
4979
5359
  return priceAxisItems({
@@ -4984,16 +5364,18 @@ var ChartContextMenu = class {
4984
5364
  priceLabel: this.flag("priceLabel"),
4985
5365
  countdown: this.flag("countdown"),
4986
5366
  priceLine: this.flag("currentPriceLine")
4987
- });
5367
+ }, this.chart?.i18n);
4988
5368
  }
4989
5369
  if (zone === "time-axis") {
4990
- 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);
4991
5371
  }
4992
- const chart = this.chart;
4993
- return bodyItems({
4994
- drawings: chart?.drawings.supported ? chart.drawings.all().length : 0,
4995
- indicators: chart?.indicators().length ?? 0
4996
- });
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
+ );
4997
5379
  }
4998
5380
  run(id) {
4999
5381
  const chart = this.chart;
@@ -5390,6 +5772,14 @@ var CONTROLS_BOTTOM_PX = TIME_AXIS_H2 + 12;
5390
5772
  var CLUSTER_H = 24;
5391
5773
  var CLUSTER_PILL = "rgba(0,0,0,0.65)";
5392
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
+ };
5393
5783
  var STYLE_ID12 = "vela-cell-controls";
5394
5784
  var CSS12 = `
5395
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;}
@@ -5450,17 +5840,17 @@ var CellControls = class {
5450
5840
  this.host.appendChild(this.root);
5451
5841
  this.refresh();
5452
5842
  }
5453
- /** 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). */
5454
5844
  refresh() {
5455
5845
  this.root.textContent = "";
5456
5846
  const multi = this.deps.multiCell();
5457
5847
  const maximized = multi && this.deps.isMaximized();
5458
5848
  if (multi && !maximized) this.root.appendChild(this.makeGrip());
5459
- this.root.appendChild(this.button("minus", "Zoom out", () => this.glider.zoom(ZOOM_OUT)));
5460
- 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)));
5461
5851
  if (multi) {
5462
5852
  this.root.appendChild(
5463
- 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(), {
5464
5854
  // The maximized state reads as an inverse chip (white-on-dark, dark-on-light),
5465
5855
  // the same active-state affordance as a collapsed pane's expand button.
5466
5856
  selected: maximized
@@ -5468,14 +5858,19 @@ var CellControls = class {
5468
5858
  );
5469
5859
  }
5470
5860
  this.root.appendChild(
5471
- this.button("reset", "Reset chart", () => {
5861
+ this.button("reset", TEXT13.resetChart, () => {
5472
5862
  this.glider.stop();
5473
5863
  this.deps.reset();
5474
5864
  })
5475
5865
  );
5476
5866
  }
5477
- 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 = {}) {
5478
5872
  const b = this.host.ownerDocument.createElement("button");
5873
+ const title = this.t(text);
5479
5874
  b.type = "button";
5480
5875
  b.title = title;
5481
5876
  b.setAttribute("aria-label", title);
@@ -5493,8 +5888,8 @@ var CellControls = class {
5493
5888
  makeGrip() {
5494
5889
  const b = this.host.ownerDocument.createElement("button");
5495
5890
  b.type = "button";
5496
- b.title = "Drag to move chart";
5497
- b.setAttribute("aria-label", "Drag to move chart");
5891
+ b.title = this.t(TEXT13.dragToMove);
5892
+ b.setAttribute("aria-label", b.title);
5498
5893
  b.className = "vela-cc-btn vela-cc-grip";
5499
5894
  b.innerHTML = icon("grip");
5500
5895
  b.addEventListener("pointerdown", (e) => this.onGripDown(b, e));
@@ -5637,6 +6032,28 @@ var WidgetHistory = class {
5637
6032
  };
5638
6033
 
5639
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
+ };
5640
6057
  function seedDefaults(opts) {
5641
6058
  return {
5642
6059
  symbol: opts.symbol,
@@ -5668,6 +6085,13 @@ function instanceDeltas(handle) {
5668
6085
  const props = inputDeltas(handle.props, handle.propValues());
5669
6086
  return inputs || props ? { ...inputs ? { inputs } : {}, ...props ? { props } : {} } : void 0;
5670
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
+ }
5671
6095
  var ChartCell = class {
5672
6096
  constructor(id, gridHost, seed, deps) {
5673
6097
  this.id = id;
@@ -5743,6 +6167,7 @@ var ChartCell = class {
5743
6167
  this.host,
5744
6168
  {
5745
6169
  ...deps.chartDefaults,
6170
+ ...deps.localization?.(),
5746
6171
  symbol,
5747
6172
  timeframe: seed.timeframe,
5748
6173
  bars: seed.bars,
@@ -5828,7 +6253,7 @@ var ChartCell = class {
5828
6253
  if (!this.indicatorValuesOn) this.inner.renderer.set("indicatorValues", false);
5829
6254
  this.watermarkOn = seed.watermark ?? deps.watermark;
5830
6255
  this.replayWatermarkOn = seed.replayWatermark ?? true;
5831
- 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;
5832
6257
  if (!this.watermarkOn) this.watermark?.setVisible(false);
5833
6258
  if (!this.replayWatermarkOn) this.watermark?.setReplayVisible(false);
5834
6259
  this.inner.on("replay:start", () => this.watermark?.setReplaying(true));
@@ -6056,12 +6481,10 @@ var ChartCell = class {
6056
6481
  pushSettingsSections() {
6057
6482
  const chart = this.inner;
6058
6483
  if (!chart) return;
6059
- const rth = "Regular hours (RTH)";
6060
- const eth = "Extended hours (ETH)";
6061
6484
  const shadeRows = this.sessionOvernightFlag ? [
6062
6485
  {
6063
6486
  kind: "color",
6064
- label: "Extended hours",
6487
+ label: TEXT14.extendedColor,
6065
6488
  id: "extended-color",
6066
6489
  get: () => this.sessionShadeColor("extendedColor"),
6067
6490
  set: (v) => this.setSessionShadeColor("extendedColor", v)
@@ -6069,43 +6492,46 @@ var ChartCell = class {
6069
6492
  ] : [
6070
6493
  {
6071
6494
  kind: "color",
6072
- label: "Pre-market",
6495
+ label: TEXT14.premarketColor,
6073
6496
  id: "premarket-color",
6074
6497
  get: () => this.sessionShadeColor("premarketColor"),
6075
6498
  set: (v) => this.setSessionShadeColor("premarketColor", v)
6076
6499
  },
6077
6500
  {
6078
6501
  kind: "color",
6079
- label: "Post-market",
6502
+ label: TEXT14.postmarketColor,
6080
6503
  id: "postmarket-color",
6081
6504
  get: () => this.sessionShadeColor("postmarketColor"),
6082
6505
  set: (v) => this.setSessionShadeColor("postmarketColor", v)
6083
6506
  }
6084
6507
  ];
6085
6508
  const sessionSection = {
6086
- title: "Trading session",
6509
+ title: TEXT14.tradingSession,
6087
6510
  id: "trading-session",
6088
6511
  placement: "symbol",
6089
6512
  rows: [
6090
6513
  {
6091
6514
  kind: "select",
6092
- label: "Session",
6515
+ label: TEXT14.session,
6093
6516
  id: "session",
6094
- options: [rth, eth],
6095
- get: () => this.session === "extended" ? eth : rth,
6096
- 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")
6097
6523
  },
6098
6524
  ...shadeRows
6099
6525
  ]
6100
6526
  };
6101
6527
  const advanced = {
6102
- title: "Advanced",
6528
+ title: TEXT14.advanced,
6103
6529
  id: "advanced",
6104
6530
  placement: "end",
6105
6531
  rows: [
6106
6532
  {
6107
6533
  kind: "select",
6108
- label: "Bars to fetch",
6534
+ label: TEXT14.barsToFetch,
6109
6535
  id: "bars",
6110
6536
  options: ["500", "1000", "2000", "5000", "10000", "20000", "50000", "60000", "80000", "100000"],
6111
6537
  get: () => String(this.state.bars ?? 1e3),
@@ -6118,15 +6544,15 @@ var ChartCell = class {
6118
6544
  ]
6119
6545
  };
6120
6546
  const timezoneSection = {
6121
- title: "Time zone",
6547
+ title: TEXT14.timeZone,
6122
6548
  id: "time-zone",
6123
6549
  placement: "symbol",
6124
6550
  rows: [
6125
6551
  {
6126
6552
  kind: "select",
6127
- label: "Time zone",
6553
+ label: TEXT14.timeZoneRow,
6128
6554
  id: "zone",
6129
- 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]),
6130
6556
  get: () => {
6131
6557
  const choice = this.deps.timezone();
6132
6558
  return isExchangeTimezone(choice) ? choice : normalizeTimezone(choice);
@@ -6136,20 +6562,20 @@ var ChartCell = class {
6136
6562
  ]
6137
6563
  };
6138
6564
  const watermarkSection = {
6139
- title: "Watermark",
6565
+ title: TEXT14.watermark,
6140
6566
  id: "watermark",
6141
6567
  placement: "symbol",
6142
6568
  rows: [
6143
6569
  {
6144
6570
  kind: "toggle",
6145
- label: "Symbol watermark",
6571
+ label: TEXT14.symbolWatermark,
6146
6572
  id: "visible",
6147
6573
  get: () => this.watermarkOn,
6148
6574
  set: (v) => this.setWatermarkVisible(v)
6149
6575
  },
6150
6576
  {
6151
6577
  kind: "toggle",
6152
- label: "Replay watermark",
6578
+ label: TEXT14.replayWatermark,
6153
6579
  id: "replay",
6154
6580
  get: () => this.replayWatermarkOn,
6155
6581
  set: (v) => this.setReplayWatermarkVisible(v)
@@ -6160,49 +6586,49 @@ var ChartCell = class {
6160
6586
  if (this.statusline) {
6161
6587
  const sl = this.statusline;
6162
6588
  sections.push({
6163
- title: "Status line",
6589
+ title: TEXT14.statusLine,
6164
6590
  id: "status-line",
6165
6591
  rows: [
6166
- { kind: "heading", label: "Status line", id: "parts" },
6592
+ { kind: "heading", label: TEXT14.statusLineParts, id: "parts" },
6167
6593
  {
6168
6594
  kind: "toggle",
6169
- label: "Symbol name",
6595
+ label: STATUSLINE_PART_LABELS.name,
6170
6596
  id: "name",
6171
6597
  get: () => sl.partVisible("name"),
6172
6598
  set: (v) => this.setStatuslinePart("name", v)
6173
6599
  },
6174
6600
  {
6175
6601
  kind: "toggle",
6176
- label: "Market status",
6602
+ label: STATUSLINE_PART_LABELS.market,
6177
6603
  id: "market",
6178
6604
  get: () => sl.partVisible("market"),
6179
6605
  set: (v) => this.setStatuslinePart("market", v)
6180
6606
  },
6181
6607
  {
6182
6608
  kind: "toggle",
6183
- label: "OHLC values",
6609
+ label: STATUSLINE_PART_LABELS.ohlc,
6184
6610
  id: "ohlc",
6185
6611
  get: () => sl.partVisible("ohlc"),
6186
6612
  set: (v) => this.setStatuslinePart("ohlc", v)
6187
6613
  },
6188
6614
  {
6189
6615
  kind: "toggle",
6190
- label: "Bar change values",
6616
+ label: STATUSLINE_PART_LABELS.change,
6191
6617
  id: "change",
6192
6618
  get: () => sl.partVisible("change"),
6193
6619
  set: (v) => this.setStatuslinePart("change", v)
6194
6620
  },
6195
- { kind: "heading", label: "Indicators", id: "indicators" },
6621
+ { kind: "heading", label: TEXT14.indicators, id: "indicators" },
6196
6622
  {
6197
6623
  kind: "toggle",
6198
- label: "Titles",
6624
+ label: TEXT14.indicatorTitles,
6199
6625
  id: "indicator-titles",
6200
6626
  get: () => this.indicatorTitlesOn,
6201
6627
  set: (v) => this.setIndicatorTitlesVisible(v)
6202
6628
  },
6203
6629
  {
6204
6630
  kind: "toggle",
6205
- label: "Values",
6631
+ label: TEXT14.indicatorValues,
6206
6632
  id: "indicator-values",
6207
6633
  get: () => this.indicatorValuesOn,
6208
6634
  set: (v) => this.setIndicatorValuesVisible(v)
@@ -6279,6 +6705,16 @@ var ChartCell = class {
6279
6705
  if (!this.inner) throw new Error(`[vela] cell "${this.id}" is destroyed`);
6280
6706
  return this.inner;
6281
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
+ }
6282
6718
  get symbol() {
6283
6719
  return this.state.symbol ?? "";
6284
6720
  }
@@ -6467,20 +6903,29 @@ var ChartCell = class {
6467
6903
  * index space the picker hands back, so `libraryRows` and `addFromLibrary` MUST both
6468
6904
  * read it — indexing the unsorted catalog on add would land on a different study. */
6469
6905
  supportedNatives() {
6470
- 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));
6471
6909
  }
6472
6910
  /** The picker's library rows: supported natives first (see {@link supportedNatives}),
6473
6911
  * then the manifest in the host's own order. */
6474
6912
  libraryRows() {
6475
6913
  return [
6476
- ...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
+ })),
6477
6922
  ...this.manifest.map((e) => ({ name: e.name, language: e.language, category: e.category }))
6478
6923
  ];
6479
6924
  }
6480
6925
  /** The picker's on-chart rows: native instances first, then live script instances. */
6481
6926
  onChartRows() {
6482
6927
  return [
6483
- ...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 })),
6484
6929
  ...this.instances.map((it) => ({ name: it.entry.name, language: it.entry.language }))
6485
6930
  ];
6486
6931
  }
@@ -6850,7 +7295,7 @@ var WorkspaceReplay = class {
6850
7295
  if (!this.session) return this;
6851
7296
  if (intervalMs !== void 0) {
6852
7297
  if (!(Number.isFinite(intervalMs) && intervalMs > 0)) {
6853
- 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`);
6854
7299
  return this;
6855
7300
  }
6856
7301
  this.intervalMs = intervalMs;
@@ -7078,27 +7523,37 @@ function monthSpan(timeframe) {
7078
7523
 
7079
7524
  // src/workspace/layouts.ts
7080
7525
  var registry = /* @__PURE__ */ new Map();
7526
+ var labels = /* @__PURE__ */ new Map();
7081
7527
  function registerLayout(def) {
7082
- 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);
7083
7530
  }
7084
7531
  function unregisterLayout(id) {
7085
7532
  registry.delete(id);
7533
+ labels.delete(id);
7534
+ }
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 };
7086
7540
  }
7087
- function layoutDefinition(id) {
7088
- return registry.get(id);
7541
+ function layoutDefinition(id, i18n) {
7542
+ const def = registry.get(id);
7543
+ return def && inLanguage(def, i18n);
7089
7544
  }
7090
- function layouts() {
7091
- return [...registry.values()];
7545
+ function layouts(i18n) {
7546
+ return [...registry.values()].map((def) => inLanguage(def, i18n));
7092
7547
  }
7093
7548
  function slots(n) {
7094
7549
  return Array.from({ length: n }, (_, i) => ({ id: `c${i + 1}` }));
7095
7550
  }
7096
7551
  function registerBuiltinLayouts() {
7097
- registerLayout({ id: "1", label: "Single", cols: [1], rows: [1], cells: slots(1) });
7098
- registerLayout({ id: "2h", label: "2 side by side", cols: [1, 1], rows: [1], cells: slots(2) });
7099
- registerLayout({ id: "2v", label: "2 stacked", cols: [1], rows: [1, 1], cells: slots(2) });
7100
- registerLayout({ id: "4", label: "4 grid", cols: [1, 1], rows: [1, 1], cells: slots(4) });
7101
- 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) });
7102
7557
  }
7103
7558
  var GRID_PICKER_MAX = 4;
7104
7559
  var GRID_BUILTIN_IDS = {
@@ -7108,17 +7563,20 @@ var GRID_BUILTIN_IDS = {
7108
7563
  "2x2": "4",
7109
7564
  "2x4": "8"
7110
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
+ }
7111
7570
  var clampTrack = (n) => Math.max(1, Math.min(GRID_PICKER_MAX, Math.round(n)));
7112
- function layoutForGrid(rows, cols) {
7571
+ function layoutForGrid(rows, cols, i18n) {
7113
7572
  const r = clampTrack(rows);
7114
7573
  const c = clampTrack(cols);
7115
7574
  const builtin = GRID_BUILTIN_IDS[`${r}x${c}`];
7116
7575
  const registered = builtin ? registry.get(builtin) : void 0;
7117
- if (registered) return registered;
7576
+ if (registered) return inLanguage(registered, i18n);
7118
7577
  return {
7119
7578
  id: `g${r}x${c}`,
7120
- // Width-first label — matches the picker's caption ("3 × 2" = 3 wide, 2 tall).
7121
- label: `${c} \xD7 ${r} grid`,
7579
+ label: textOf(i18n, gridLabel(r, c)),
7122
7580
  cols: Array.from({ length: c }, () => 1),
7123
7581
  rows: Array.from({ length: r }, () => 1),
7124
7582
  cells: slots(r * c)
@@ -7141,7 +7599,7 @@ function layoutShape(def) {
7141
7599
  function occupancyGrid(def) {
7142
7600
  if (def.areas) return def.areas.map((row) => row.trim().split(/\s+/));
7143
7601
  const cols = def.cols.length;
7144
- 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}`));
7145
7603
  }
7146
7604
  var GLYPH_MIN = 1.5;
7147
7605
  var GLYPH_SPAN = 13;
@@ -7432,8 +7890,8 @@ var TOOL_BINDINGS = [
7432
7890
  ["hline", "drawings.hline-cursor"],
7433
7891
  ["vline", "drawings.vline-cursor"]
7434
7892
  ];
7435
- function toolShortcutHints(keymap) {
7436
- 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]]));
7437
7895
  const out = {};
7438
7896
  for (const [type, id] of TOOL_BINDINGS) {
7439
7897
  const display = byId.get(id);
@@ -7531,10 +7989,10 @@ var CSS14 = `
7531
7989
  .vela-mb-item:active { background: var(--vela-hover); }
7532
7990
  .vela-mb-item .vela-icon { font-size: 18px; width: 18px; height: 18px; }
7533
7991
  /* A lit stop (the maximize toggle while something is isolated): the inverse
7534
- "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. */
7535
7993
  .vela-mb-item.vela-mb-on, .vela-mb-item.vela-mb-on:active { background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
7536
7994
  /* Left-aligned contributed actions get their own stops (the built-in indicators
7537
- 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. */
7538
7996
  .vela-mb-actions { display: contents; }
7539
7997
  .vela-mb-symbol {
7540
7998
  flex: 1.6 1 0;
@@ -7548,39 +8006,57 @@ var CSS14 = `
7548
8006
  text-align: center;
7549
8007
  }
7550
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
+ };
7551
8019
  var MobileBar = class {
7552
8020
  constructor(host, opts) {
8021
+ /** Each fixed stop's accessible name, re-read by {@link relocalize}. */
8022
+ this.labels = [];
8023
+ this.maximizeActive = false;
7553
8024
  this.opts = opts;
7554
8025
  const doc = host.ownerDocument;
7555
8026
  injectStyles(STYLE_ID14, CSS14, doc);
7556
8027
  this.el = doc.createElement("div");
7557
8028
  this.el.className = "vela-mobilebar";
8029
+ this.timeframe = opts.timeframe;
7558
8030
  const item = (cls, label, onClick, icon2) => {
7559
8031
  const b = doc.createElement("button");
7560
8032
  b.className = `vela-mb-item ${cls}`;
7561
- b.setAttribute("aria-label", label);
8033
+ b.setAttribute("aria-label", this.t(label));
8034
+ this.labels.push({ el: b, label });
7562
8035
  if (icon2) b.appendChild(iconEl(icon2, doc));
7563
8036
  b.addEventListener("click", onClick);
7564
8037
  return b;
7565
8038
  };
7566
- this.symbolEl = item("vela-mb-symbol", "Symbol search", opts.onSymbolClick);
8039
+ this.symbolEl = item("vela-mb-symbol", TEXT15.symbolSearch, opts.onSymbolClick);
7567
8040
  this.symbolEl.textContent = parseSymbol(opts.symbol).ticker;
7568
- this.tfEl = item("vela-mb-tf", "Timeframe", opts.onTimeframeClick);
7569
- 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);
7570
8043
  const onIndicators = opts.onIndicatorsClick;
7571
- const indicators = onIndicators ? item("vela-mb-indicators", "Indicators", onIndicators, "indicators") : null;
8044
+ const indicators = onIndicators ? item("vela-mb-indicators", TEXT15.indicators, onIndicators, "indicators") : null;
7572
8045
  this.actionsHost = doc.createElement("span");
7573
8046
  this.actionsHost.className = "vela-mb-actions";
7574
8047
  const onDrawings = opts.onDrawingsClick;
7575
- const drawings = onDrawings ? item("vela-mb-drawings", "Drawings", onDrawings, "pen") : null;
8048
+ const drawings = onDrawings ? item("vela-mb-drawings", TEXT15.drawings, onDrawings, "pen") : null;
7576
8049
  const onMaximize = opts.onMaximizeClick;
7577
- this.maxEl = onMaximize ? item("vela-mb-maximize", "Maximize chart", onMaximize, "maximize") : null;
7578
- const more = item("vela-mb-more", "More", opts.onMoreClick, "kebab");
7579
- 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");
7580
8053
  this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], ...this.maxEl ? [this.maxEl] : [], more, settings);
7581
8054
  host.appendChild(this.el);
7582
8055
  this.renderActions();
7583
8056
  }
8057
+ t(text) {
8058
+ return textOf(this.opts.i18n, text);
8059
+ }
7584
8060
  /** Re-project the contributed actions placed on the bar (left-aligned by default —
7585
8061
  * see `mobilePlacement`) as icon-only stops in the indicators slot (call after
7586
8062
  * registrations change). The rest are three-dots drawer rows. */
@@ -7593,9 +8069,10 @@ var MobileBar = class {
7593
8069
  for (const action of widgetActions("topbar", ctx).filter((a) => mobilePlacement(a) === "bar" && !builtin.has(a.id))) {
7594
8070
  const b = doc.createElement("button");
7595
8071
  b.className = "vela-mb-item";
7596
- b.setAttribute("aria-label", action.label);
8072
+ const label = textOf(ctx.i18n ?? this.opts.i18n, action.label);
8073
+ b.setAttribute("aria-label", label);
7597
8074
  if (action.icon) b.appendChild(iconEl(action.icon, doc));
7598
- else b.appendChild(doc.createTextNode(action.label));
8075
+ else b.appendChild(doc.createTextNode(label));
7599
8076
  b.addEventListener("click", () => {
7600
8077
  const c = this.opts.getContext?.();
7601
8078
  if (c) action.run(c);
@@ -7607,14 +8084,23 @@ var MobileBar = class {
7607
8084
  this.symbolEl.textContent = parseSymbol(symbol).ticker;
7608
8085
  }
7609
8086
  setTimeframe(tf) {
7610
- 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();
7611
8096
  }
7612
8097
  /** Light the maximize stop while something is isolated (a chart over the grid,
7613
8098
  * or a maximized pane inside the active chart) — inverse chip + restore glyph. */
7614
8099
  setMaximizeActive(on) {
7615
8100
  if (!this.maxEl) return;
8101
+ this.maximizeActive = on;
7616
8102
  this.maxEl.classList.toggle("vela-mb-on", on);
7617
- this.maxEl.setAttribute("aria-label", on ? "Restore layout" : "Maximize chart");
8103
+ this.maxEl.setAttribute("aria-label", this.t(on ? TEXT15.restoreLayout : TEXT15.maximizeChart));
7618
8104
  this.maxEl.replaceChildren(iconEl(on ? "restore" : "maximize", this.el.ownerDocument));
7619
8105
  }
7620
8106
  /** Show the maximize stop only while the grid holds more than one chart — on a
@@ -7680,23 +8166,28 @@ var CSS15 = `
7680
8166
  background: var(--vela-hover);
7681
8167
  }
7682
8168
  `;
8169
+ var TEXT16 = {
8170
+ dateRange: msg("widget.timeframeDrawer.dateRange", "Date Range"),
8171
+ timeframes: msg("widget.timeframeDrawer.timeframes", "Timeframes")
8172
+ };
7683
8173
  var TimeframeDrawer = class {
7684
8174
  constructor(opts) {
7685
8175
  this.opts = opts;
7686
8176
  this.rangeChips = /* @__PURE__ */ new Map();
7687
8177
  this.tfChips = /* @__PURE__ */ new Map();
8178
+ this.headings = [];
7688
8179
  const doc = opts.host.ownerDocument;
7689
8180
  injectStyles(STYLE_ID15, CSS15, doc);
7690
8181
  this.drawer = new Drawer({ host: opts.host, onOpenChange: opts.onOpenChange });
7691
8182
  const rangeHeading = doc.createElement("div");
7692
8183
  rangeHeading.className = "vela-tfd-heading";
7693
- rangeHeading.textContent = "Date Range";
8184
+ this.headings.push({ el: rangeHeading, text: TEXT16.dateRange });
7694
8185
  const ranges = doc.createElement("div");
7695
8186
  ranges.className = "vela-tfd-ranges";
7696
8187
  for (const preset of opts.ranges) {
7697
8188
  const chip = doc.createElement("button");
7698
8189
  chip.className = "vela-tfd-chip";
7699
- chip.textContent = preset.id;
8190
+ chip.textContent = rangeLabel(preset.id, opts.i18n);
7700
8191
  chip.addEventListener("click", () => {
7701
8192
  opts.onRange(preset);
7702
8193
  this.drawer.hide();
@@ -7706,13 +8197,13 @@ var TimeframeDrawer = class {
7706
8197
  }
7707
8198
  const tfHeading = doc.createElement("div");
7708
8199
  tfHeading.className = "vela-tfd-heading";
7709
- tfHeading.textContent = "Timeframes";
8200
+ this.headings.push({ el: tfHeading, text: TEXT16.timeframes });
7710
8201
  const grid = doc.createElement("div");
7711
8202
  grid.className = "vela-tfd-grid";
7712
8203
  for (const tf of opts.timeframes) {
7713
8204
  const chip = doc.createElement("button");
7714
8205
  chip.className = "vela-tfd-chip";
7715
- chip.textContent = timeframeLabel(tf);
8206
+ chip.textContent = timeframeLabel(tf, opts.i18n);
7716
8207
  chip.addEventListener("click", () => {
7717
8208
  opts.onTimeframe(tf);
7718
8209
  this.drawer.hide();
@@ -7721,8 +8212,17 @@ var TimeframeDrawer = class {
7721
8212
  grid.appendChild(chip);
7722
8213
  }
7723
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);
7724
8223
  }
7725
8224
  open() {
8225
+ this.relabel();
7726
8226
  const tf = this.opts.currentTimeframe();
7727
8227
  const range = this.opts.activeRange();
7728
8228
  for (const [id, chip] of this.rangeChips) {
@@ -7744,6 +8244,12 @@ var TimeframeDrawer = class {
7744
8244
  };
7745
8245
 
7746
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
+ };
7747
8253
  var STYLE_ID16 = "vela-widget-drawings-drawer";
7748
8254
  var CSS16 = `
7749
8255
  /* Search + tabs stay pinned while the tool list scrolls underneath. */
@@ -7777,7 +8283,7 @@ var CSS16 = `
7777
8283
  gap: 4px;
7778
8284
  overflow-x: auto;
7779
8285
  scrollbar-width: none;
7780
- /* 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
7781
8287
  (the drawer body is pan-y so everywhere ELSE a sideways move swipes the tabs). */
7782
8288
  touch-action: pan-x;
7783
8289
  padding: 0 2px 8px;
@@ -7850,7 +8356,7 @@ var DrawingsDrawer = class {
7850
8356
  injectStyles(STYLE_ID16, CSS16, doc);
7851
8357
  this.drawer = new Drawer({
7852
8358
  host: opts.host,
7853
- title: "Drawings",
8359
+ title: this.t(TEXT17.title),
7854
8360
  onOpenChange: opts.onOpenChange,
7855
8361
  // Swiping across the tool list pages through the group tabs.
7856
8362
  onSwipe: (dir) => this.stepGroup(dir === "left" ? 1 : -1)
@@ -7862,7 +8368,7 @@ var DrawingsDrawer = class {
7862
8368
  search.appendChild(iconEl("search", doc));
7863
8369
  this.input = doc.createElement("input");
7864
8370
  this.input.type = "text";
7865
- this.input.placeholder = "Search tools\u2026";
8371
+ this.input.placeholder = this.t(TEXT17.search);
7866
8372
  this.input.addEventListener("input", () => this.renderList());
7867
8373
  search.appendChild(this.input);
7868
8374
  this.tabs = doc.createElement("div");
@@ -7872,7 +8378,12 @@ var DrawingsDrawer = class {
7872
8378
  this.list.className = "vela-dd-list";
7873
8379
  this.drawer.body.append(sticky, this.list);
7874
8380
  }
8381
+ t(m, values) {
8382
+ return textOf(this.opts.i18n?.() ?? null, m, values);
8383
+ }
7875
8384
  open() {
8385
+ this.drawer.setTitle(this.t(TEXT17.title));
8386
+ this.input.placeholder = this.t(TEXT17.search);
7876
8387
  this.definition = this.opts.toolbar();
7877
8388
  if (!this.definition.groups.some((g) => g.id === this.activeGroup)) this.activeGroup = this.definition.groups[0]?.id ?? "";
7878
8389
  this.input.value = "";
@@ -7904,7 +8415,7 @@ var DrawingsDrawer = class {
7904
8415
  for (const group of this.definition.groups) {
7905
8416
  const tab = doc.createElement("button");
7906
8417
  tab.className = "vela-dd-tab";
7907
- tab.textContent = group.label;
8418
+ tab.textContent = this.t(group.label);
7908
8419
  if (group.id === this.activeGroup) tab.dataset.active = "1";
7909
8420
  tab.addEventListener("click", () => {
7910
8421
  this.activeGroup = group.id;
@@ -7918,7 +8429,15 @@ var DrawingsDrawer = class {
7918
8429
  renderList() {
7919
8430
  const doc = this.list.ownerDocument;
7920
8431
  this.list.replaceChildren();
7921
- 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());
7922
8441
  const active = this.opts.currentTool();
7923
8442
  const section = (label) => {
7924
8443
  const el = doc.createElement("div");
@@ -7935,7 +8454,7 @@ var DrawingsDrawer = class {
7935
8454
  glyph.innerHTML = tool.icon;
7936
8455
  const label = doc.createElement("span");
7937
8456
  label.className = "vela-dd-label";
7938
- label.textContent = tool.label;
8457
+ label.textContent = this.t(tool.label);
7939
8458
  const star = doc.createElement("button");
7940
8459
  star.className = "vela-dd-star";
7941
8460
  const paintStar = (on) => {
@@ -7944,7 +8463,7 @@ var DrawingsDrawer = class {
7944
8463
  else delete star.dataset.on;
7945
8464
  };
7946
8465
  paintStar(this.opts.isFavorite(tool.type));
7947
- star.setAttribute("aria-label", `Favorite ${tool.label}`);
8466
+ star.setAttribute("aria-label", this.t(TEXT17.favorite, { tool: this.t(tool.label) }));
7948
8467
  star.addEventListener("click", (e) => {
7949
8468
  e.stopPropagation();
7950
8469
  const on = !this.opts.isFavorite(tool.type);
@@ -7961,16 +8480,16 @@ var DrawingsDrawer = class {
7961
8480
  if (query) {
7962
8481
  let any = false;
7963
8482
  for (const group2 of this.definition.groups) {
7964
- 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));
7965
8484
  if (hits.length === 0) continue;
7966
8485
  any = true;
7967
- section(group2.label);
8486
+ section(this.t(group2.label));
7968
8487
  for (const tool of hits) row(tool);
7969
8488
  }
7970
8489
  if (!any) {
7971
8490
  const empty = doc.createElement("div");
7972
8491
  empty.className = "vela-dd-empty";
7973
- empty.textContent = "No tools match.";
8492
+ empty.textContent = this.t(TEXT17.noMatch);
7974
8493
  this.list.appendChild(empty);
7975
8494
  }
7976
8495
  return;
@@ -7979,7 +8498,7 @@ var DrawingsDrawer = class {
7979
8498
  if (!group) return;
7980
8499
  if (group.sections && group.sections.length > 0) {
7981
8500
  for (const s of group.sections) {
7982
- section(s.label);
8501
+ section(this.t(s.label));
7983
8502
  for (const tool of s.tools) row(tool);
7984
8503
  }
7985
8504
  } else {
@@ -7989,6 +8508,17 @@ var DrawingsDrawer = class {
7989
8508
  };
7990
8509
 
7991
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
+ };
7992
8522
  var STYLE_ID17 = "vela-widget-more-drawer";
7993
8523
  var CSS17 = `
7994
8524
  .vela-md-actions {
@@ -8079,6 +8609,9 @@ var MoreDrawer = class {
8079
8609
  destroy() {
8080
8610
  this.drawer.destroy();
8081
8611
  }
8612
+ t(text, values) {
8613
+ return textOf(this.opts.i18n, text, values);
8614
+ }
8082
8615
  show(view) {
8083
8616
  this.view = view;
8084
8617
  this.render();
@@ -8090,7 +8623,7 @@ var MoreDrawer = class {
8090
8623
  const back = doc.createElement("button");
8091
8624
  back.className = "vela-md-back";
8092
8625
  back.appendChild(iconEl("chevron-left", doc));
8093
- 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)));
8094
8627
  back.addEventListener("click", () => this.show("main"));
8095
8628
  this.drawer.body.appendChild(back);
8096
8629
  }
@@ -8130,25 +8663,25 @@ var MoreDrawer = class {
8130
8663
  b.className = "vela-md-action";
8131
8664
  b.disabled = !enabled;
8132
8665
  b.appendChild(iconEl(icon2, doc));
8133
- b.appendChild(doc.createTextNode(label));
8666
+ b.appendChild(doc.createTextNode(this.t(label)));
8134
8667
  b.addEventListener("click", () => {
8135
8668
  run();
8136
8669
  this.drawer.hide();
8137
8670
  });
8138
8671
  actions.appendChild(b);
8139
8672
  };
8140
- if (this.opts.onUndo) action("undo", "Undo", this.opts.canUndo(), this.opts.onUndo);
8141
- if (this.opts.onRedo) action("redo", "Redo", this.opts.canRedo(), this.opts.onRedo);
8142
- 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);
8143
8676
  if (actions.childElementCount > 0) this.drawer.body.appendChild(actions);
8144
8677
  const list = doc.createElement("div");
8145
8678
  list.className = "vela-md-list";
8146
8679
  const current = this.opts.priceStyles().find((s) => s.id === this.opts.priceStyle());
8147
- 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") }));
8148
8681
  if (this.opts.layout) {
8149
8682
  const shape = this.opts.layout.shape();
8150
- const value = shape ? `${shape.cols} \xD7 ${shape.rows}` : this.opts.layout.presets().find((p) => p.checked)?.label;
8151
- 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") }));
8152
8685
  }
8153
8686
  for (const act of this.opts.primaryActions?.() ?? []) list.appendChild(this.actionRow(doc, act));
8154
8687
  for (const panel of this.opts.panels()) {
@@ -8164,7 +8697,7 @@ var MoreDrawer = class {
8164
8697
  }
8165
8698
  if (this.opts.alerts) {
8166
8699
  const alertCount = this.opts.alerts().length;
8167
- 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") }));
8168
8701
  }
8169
8702
  for (const act of this.opts.actions()) list.appendChild(this.actionRow(doc, act));
8170
8703
  this.drawer.body.appendChild(list);
@@ -8204,7 +8737,7 @@ var MoreDrawer = class {
8204
8737
  list.className = "vela-md-list";
8205
8738
  const wrap = doc.createElement("div");
8206
8739
  wrap.className = "vela-md-gridwrap";
8207
- const { el: grid, squares } = layoutGridCanvas(doc);
8740
+ const { el: grid, squares } = layoutGridCanvas(doc, this.opts.i18n);
8208
8741
  paintLayoutGrid(squares, layout.shape());
8209
8742
  grid.addEventListener("click", (e) => {
8210
8743
  const sq = e.target?.closest?.(".vela-lp-sq");
@@ -8229,7 +8762,7 @@ var MoreDrawer = class {
8229
8762
  if (syncs.length > 0) {
8230
8763
  const heading = doc.createElement("div");
8231
8764
  heading.className = "vela-md-section";
8232
- heading.textContent = "Sync";
8765
+ heading.textContent = this.t(TEXT18.sync);
8233
8766
  list.appendChild(heading);
8234
8767
  for (const s of syncs) {
8235
8768
  list.appendChild(
@@ -8250,20 +8783,23 @@ var MoreDrawer = class {
8250
8783
  if (alerts.length === 0) {
8251
8784
  const empty = doc.createElement("div");
8252
8785
  empty.className = "vela-md-empty";
8253
- empty.textContent = "No alerts yet.";
8786
+ empty.textContent = this.t(TEXT18.noAlerts);
8254
8787
  this.drawer.body.appendChild(empty);
8255
8788
  return;
8256
8789
  }
8257
8790
  const list = doc.createElement("div");
8258
8791
  list.className = "vela-md-list";
8259
8792
  for (const a of alerts) {
8260
- 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 }));
8261
8794
  }
8262
8795
  this.drawer.body.appendChild(list);
8263
8796
  }
8264
8797
  };
8265
8798
 
8266
8799
  // src/widget/timezone-drawer.ts
8800
+ var TEXT19 = {
8801
+ title: msg("widget.timezoneDrawer.title", "Time zone")
8802
+ };
8267
8803
  var STYLE_ID18 = "vela-widget-timezone-drawer";
8268
8804
  var CSS18 = `
8269
8805
  .vela-tzd-list { padding: 2px 0 4px; }
@@ -8285,9 +8821,10 @@ var TimezoneDrawer = class {
8285
8821
  constructor(opts) {
8286
8822
  this.opts = opts;
8287
8823
  injectStyles(STYLE_ID18, CSS18, opts.host.ownerDocument);
8288
- 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 });
8289
8825
  }
8290
8826
  open() {
8827
+ this.drawer.setTitle(textOf(this.opts.i18n, TEXT19.title));
8291
8828
  this.render();
8292
8829
  this.drawer.show();
8293
8830
  }
@@ -8302,7 +8839,7 @@ var TimezoneDrawer = class {
8302
8839
  this.drawer.body.replaceChildren();
8303
8840
  const list = doc.createElement("div");
8304
8841
  list.className = "vela-tzd-list";
8305
- for (const tz of timezoneMenuRows(this.opts.timezone())) {
8842
+ for (const tz of timezoneMenuRows(this.opts.timezone(), this.opts.i18n)) {
8306
8843
  const row = doc.createElement("div");
8307
8844
  row.className = "vela-tzd-row";
8308
8845
  const label = doc.createElement("span");
@@ -8347,17 +8884,21 @@ var CSS19 = `
8347
8884
  color: var(--vela-fg-muted);
8348
8885
  }
8349
8886
  `;
8887
+ var TEXT20 = {
8888
+ title: msg("widget.priceScaleDrawer.title", "Price scale")
8889
+ };
8350
8890
  var PriceScaleDrawer = class {
8351
8891
  constructor(opts) {
8352
8892
  this.opts = opts;
8353
8893
  this.pane = null;
8354
8894
  injectStyles(STYLE_ID19, CSS19, opts.host.ownerDocument);
8355
- 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 });
8356
8896
  }
8357
8897
  open() {
8358
8898
  const chart = this.opts.chart();
8359
8899
  const panes = chart?.renderer.get("paneScales");
8360
8900
  this.pane = Array.isArray(panes) ? paneScaleAt(panes, this.opts.pressY()) : null;
8901
+ this.drawer.setTitle(textOf(chart?.i18n, TEXT20.title));
8361
8902
  this.render();
8362
8903
  this.drawer.show();
8363
8904
  }
@@ -8384,7 +8925,7 @@ var PriceScaleDrawer = class {
8384
8925
  priceLabel: this.flag("priceLabel"),
8385
8926
  countdown: this.flag("countdown"),
8386
8927
  priceLine: this.flag("currentPriceLine")
8387
- });
8928
+ }, chart.i18n);
8388
8929
  const list = doc.createElement("div");
8389
8930
  list.className = "vela-psd-list";
8390
8931
  const row = (label, opts) => {
@@ -8407,7 +8948,7 @@ var PriceScaleDrawer = class {
8407
8948
  };
8408
8949
  for (const item of items) {
8409
8950
  if (item.id === "labels" && item.submenu) {
8410
- section("Labels");
8951
+ section(item.label);
8411
8952
  for (const sub of item.submenu) {
8412
8953
  row(sub.label, {
8413
8954
  checked: sub.checked,
@@ -8421,7 +8962,7 @@ var PriceScaleDrawer = class {
8421
8962
  continue;
8422
8963
  }
8423
8964
  if (item.id === "levels" && item.submenu) {
8424
- section("Levels");
8965
+ section(item.label);
8425
8966
  for (const sub of item.submenu) {
8426
8967
  row(sub.label, {
8427
8968
  checked: sub.checked,
@@ -8487,6 +9028,12 @@ var PriceScaleDrawer = class {
8487
9028
  };
8488
9029
 
8489
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
+ };
8490
9037
  var STYLE_ID20 = "vela-widget-drawing-pill";
8491
9038
  var CSS20 = `
8492
9039
  .vela-drawpill { display: none; }
@@ -8536,17 +9083,24 @@ var CSS20 = `
8536
9083
  .vela-drawpill-btn .vela-icon { font-size: 17px; width: 17px; height: 17px; }
8537
9084
  .vela-drawpill-badge {
8538
9085
  position: absolute;
8539
- top: 2px;
8540
- right: 2px;
8541
- font-size: 8px;
8542
- font-weight: 700;
9086
+ top: 5px;
9087
+ right: 5px;
9088
+ display: none;
9089
+ align-items: flex-end;
9090
+ gap: 1px;
8543
9091
  color: var(--vela-accent);
8544
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; }
8545
9097
  `;
8546
9098
  var DrawingPill = class {
8547
9099
  constructor(host) {
8548
9100
  this.chart = null;
8549
9101
  this.chartSubs = [];
9102
+ /** Each button's accessible name, re-read in the chart's language on every sync. */
9103
+ this.labels = [];
8550
9104
  const doc = host.ownerDocument;
8551
9105
  injectStyles(STYLE_ID20, CSS20, doc);
8552
9106
  this.el = doc.createElement("div");
@@ -8557,32 +9111,38 @@ var DrawingPill = class {
8557
9111
  const btn = (icon2, label, onClick) => {
8558
9112
  const b = doc.createElement("button");
8559
9113
  b.className = "vela-drawpill-btn";
8560
- b.setAttribute("aria-label", label);
9114
+ b.setAttribute("aria-label", this.t(label));
9115
+ this.labels.push({ el: b, label });
8561
9116
  b.appendChild(iconEl(icon2, doc));
8562
9117
  b.addEventListener("click", onClick);
8563
9118
  return b;
8564
9119
  };
8565
- this.magnetBtn = btn("magnet", "Magnet snap", () => {
9120
+ this.magnetBtn = btn("magnet", TEXT21.magnet, () => {
8566
9121
  const order = ["off", "weak", "strong"];
8567
9122
  const cur = this.chart?.drawings.getSnapMode() ?? "off";
8568
9123
  this.chart?.drawings.setSnapMode(order[(order.indexOf(cur) + 1) % order.length]);
8569
9124
  });
8570
9125
  this.magnetBadge = doc.createElement("span");
8571
9126
  this.magnetBadge.className = "vela-drawpill-badge";
9127
+ this.magnetBadge.setAttribute("aria-hidden", "true");
9128
+ this.magnetBadge.append(doc.createElement("i"), doc.createElement("i"));
8572
9129
  this.magnetBtn.appendChild(this.magnetBadge);
8573
- 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, () => {
8574
9131
  this.chart?.drawings.setStayMode(!this.chart.drawings.getStayMode());
8575
9132
  });
8576
- this.eraserBtn = btn("eraser", "Eraser", () => {
9133
+ this.eraserBtn = btn("eraser", TEXT21.eraser, () => {
8577
9134
  this.chart?.drawings.setMode(this.chart.drawings.getMode() === "eraser" ? null : "eraser");
8578
9135
  });
8579
- const close = btn("close", "Exit drawing mode", () => {
9136
+ const close = btn("close", TEXT21.exit, () => {
8580
9137
  this.chart?.drawings.setTool(null);
8581
9138
  this.chart?.drawings.setMode(null);
8582
9139
  });
8583
9140
  this.el.append(this.toolGlyph, this.magnetBtn, this.stayBtn, this.eraserBtn, close);
8584
9141
  host.appendChild(this.el);
8585
9142
  }
9143
+ t(text) {
9144
+ return textOf(this.chart?.i18n, text);
9145
+ }
8586
9146
  /** Rebind to another chart (a rebuild, or the workspace's active cell changing) —
8587
9147
  * the previous chart's subscriptions are dropped so only one chart drives the pill. */
8588
9148
  onChart(chart) {
@@ -8596,7 +9156,12 @@ var DrawingPill = class {
8596
9156
  ];
8597
9157
  this.sync();
8598
9158
  }
9159
+ /** Re-read the labels in the chart's language. */
9160
+ relocalize() {
9161
+ this.sync();
9162
+ }
8599
9163
  sync() {
9164
+ for (const { el, label } of this.labels) el.setAttribute("aria-label", this.t(label));
8600
9165
  const drawings = this.chart?.drawings;
8601
9166
  if (!drawings || !drawings.supported) {
8602
9167
  this.el.hidden = true;
@@ -8609,7 +9174,8 @@ var DrawingPill = class {
8609
9174
  this.toolGlyph.innerHTML = tool !== null ? getDrawingType(tool)?.icon ?? "" : iconMarkup("eraser") ?? "";
8610
9175
  const snap = drawings.getSnapMode();
8611
9176
  this.magnetBtn.dataset.on = snap !== "off" ? "1" : "";
8612
- 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;
8613
9179
  this.stayBtn.dataset.on = drawings.getStayMode() ? "1" : "";
8614
9180
  this.eraserBtn.dataset.on = eraser ? "1" : "";
8615
9181
  }
@@ -8644,6 +9210,7 @@ function buildContext(host) {
8644
9210
  togglePanel: (id, open) => host.togglePanel(id, open),
8645
9211
  dockStrip: (el) => host.dockStrip(el),
8646
9212
  host: host.root,
9213
+ i18n: host.i18n,
8647
9214
  toast: (message, kind) => host.toast(message, kind),
8648
9215
  addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
8649
9216
  addNativeIndicator: (type) => host.active()?.addNative(type),
@@ -8723,7 +9290,7 @@ var CSS21 = `
8723
9290
  .vela-ws-grid { position: relative; flex: 1 1 auto; min-width: 0; display: grid; gap: ${GAP_PX}px; background: var(--vela-border-soft); }
8724
9291
  .vela-cell { background: var(--vela-bg); position: relative; }
8725
9292
  /* Active-cell highlight: an overlay ring ABOVE the chart's own canvas stack (a plain
8726
- 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
8727
9294
  multi-cell grids ([data-multi]): a single-cell layout always has an active cell,
8728
9295
  and ringing the only chart would just be noise. */
8729
9296
  .vela-ws-grid[data-multi='1'] .vela-cell[data-active='1']::after {
@@ -8740,11 +9307,11 @@ var CSS21 = `
8740
9307
  .vela-ws-splitter:hover::after { content: ''; position: absolute; background: var(--vela-separator-hover-line); }
8741
9308
  .vela-ws-splitter[data-axis='cols']:hover::after { left: calc(50% - 1px); top: 0; width: 2px; height: 100%; }
8742
9309
  .vela-ws-splitter[data-axis='rows']:hover::after { top: calc(50% - 1px); left: 0; height: 2px; width: 100%; }
8743
- /* 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
8744
9311
  drawings drawer + on-chart pill replace it (same policy as the widget's in-chart bar). */
8745
9312
  [data-layout='mobile'] .vela-ws-toolbar { display: none; }
8746
9313
  /* A maximized cell owns the whole grid: the splitter strips have no seams to grab and
8747
- 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. */
8748
9315
  .vela-ws-grid[data-maximized='1'] .vela-ws-splitter { display: none; }
8749
9316
  .vela-ws-grid[data-maximized='1'] .vela-cell[data-active='1']::after { display: none; }
8750
9317
  /* Drop-target preview while a cell's drag handle is held: a dashed ring + the same
@@ -8764,24 +9331,58 @@ function declaredOrder(cells) {
8764
9331
  const names = Object.keys(cells ?? {});
8765
9332
  for (const n of names) {
8766
9333
  if (/^\d+$/.test(n)) {
8767
- 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}"`);
8768
9335
  }
8769
9336
  }
8770
9337
  return names.filter((n) => !/^\d+$/.test(n));
8771
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;
8772
9372
  function nextAutoCellId(taken) {
8773
9373
  for (let i = 1; ; i += 1) {
8774
9374
  if (!taken.has(`c${i}`)) return `c${i}`;
8775
9375
  }
8776
9376
  }
8777
9377
  function resolveTyping(key, open) {
9378
+ const printable = [...key].length === 1;
8778
9379
  if (open.dialogs > 0) {
8779
- if (key.length !== 1) return null;
9380
+ if (!printable) return null;
8780
9381
  if (open.symbolSearch) return { target: "symbol", text: key.toUpperCase() };
8781
9382
  if (open.timeframeEntry) return { target: "timeframe", text: key.toUpperCase() };
8782
9383
  return null;
8783
9384
  }
8784
- 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() };
8785
9386
  if (/^[1-9]$/.test(key)) return { target: "timeframe", text: key };
8786
9387
  return null;
8787
9388
  }
@@ -8897,6 +9498,7 @@ var VelaWorkspace = class {
8897
9498
  const hostEl = typeof container === "string" ? document.querySelector(container) : container;
8898
9499
  if (!hostEl) throw new Error(`VelaWorkspace: container not found: ${String(container)}`);
8899
9500
  this.opts = opts;
9501
+ this.localeState = new LocaleState(pickLocalization(opts));
8900
9502
  this.replay = new WorkspaceReplay({
8901
9503
  cells: () => this.cells(),
8902
9504
  activeId: () => this.activeId,
@@ -8947,12 +9549,14 @@ var VelaWorkspace = class {
8947
9549
  this.root = doc.createElement("div");
8948
9550
  this.root.className = "vela-workspace";
8949
9551
  ensureUIHost(this.root, resolveTheme(opts.theme));
9552
+ this.applyRootAttributes();
8950
9553
  for (const [name, make] of Object.entries(opts.providers ?? {})) void this.feed.registerProvider(name, make());
8951
9554
  void this.feed.ready().then(() => {
8952
9555
  if (!this.destroyed) this.refreshRetention();
8953
9556
  });
8954
9557
  this.symbolPicker = new SymbolPicker({
8955
9558
  host: this.root,
9559
+ i18n: this.localeState,
8956
9560
  // Row icons come from each descriptor's OWNING provider (resolveSymbolIcon).
8957
9561
  iconFor: (d) => this.feed.symbolIconOf(d),
8958
9562
  onSelect: (ticker) => this.active.setSymbol(ticker),
@@ -8964,6 +9568,7 @@ var VelaWorkspace = class {
8964
9568
  this.symbolPicker.setSource(() => this.feed.symbols());
8965
9569
  this.indicatorPicker = opts.indicatorPicker !== false && !this.indicatorsOverride && topbarHas(this.topbarComp, "indicators") ? new IndicatorPicker({
8966
9570
  host: this.root,
9571
+ i18n: this.localeState,
8967
9572
  library: () => this.active.libraryRows(),
8968
9573
  onChart: () => this.active.onChartRows(),
8969
9574
  onAdd: (i) => this.active.addFromLibrary(i),
@@ -8976,11 +9581,13 @@ var VelaWorkspace = class {
8976
9581
  const picker = this.indicatorPicker;
8977
9582
  this.tfQuick = new TimeframeQuick({
8978
9583
  host: this.root,
9584
+ i18n: this.localeState,
8979
9585
  onApply: (tf) => this.setActiveTimeframe(tf),
8980
9586
  onOpenChange: (open) => this.trackDialog(open)
8981
9587
  });
8982
9588
  this.topbar = new Topbar(this.root, {
8983
9589
  symbol: "",
9590
+ i18n: this.localeState,
8984
9591
  // RAW option, not the resolved lists — the bar distinguishes a host-declared
8985
9592
  // side (list is law) from a default one (an override's `order` may flow).
8986
9593
  composition: opts.topbar,
@@ -9004,18 +9611,13 @@ var VelaWorkspace = class {
9004
9611
  // The picker composes dynamic layouts on its grid canvas; registered
9005
9612
  // presets the canvas cannot express (bespoke plugin areas) list as rows.
9006
9613
  shape: () => layoutShape(this.def),
9007
- 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 })),
9008
9615
  onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
9009
9616
  onSelectPreset: (id) => this.setLayout(id),
9010
9617
  // The SYNC switches reflect the simple all-cells form; flipping one
9011
9618
  // OVERRIDES a host-set group record with plain on/off (groups stay an
9012
9619
  // API-only shape).
9013
- syncs: () => [
9014
- { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
9015
- { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
9016
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
9017
- { id: "style", label: "Style", checked: this.syncOpts.style === true }
9018
- ],
9620
+ syncs: () => this.syncRows(),
9019
9621
  onToggleSync: (id) => {
9020
9622
  const kind = id;
9021
9623
  this.sync.set(kind, this.syncOpts[kind] ? false : true);
@@ -9026,7 +9628,7 @@ var VelaWorkspace = class {
9026
9628
  });
9027
9629
  const main = doc.createElement("div");
9028
9630
  main.className = "vela-ws-main";
9029
- const toolbar = buildToolbar(opts.drawings);
9631
+ const toolbar = buildToolbar(opts.drawings, this.localeState);
9030
9632
  this.drawingsEnabled = opts.drawings !== false;
9031
9633
  this.toolbarDef = toolbar.definition;
9032
9634
  let toolbarHost = null;
@@ -9041,12 +9643,13 @@ var VelaWorkspace = class {
9041
9643
  this.dock = new PanelDock(main, {
9042
9644
  chrome: this.topbar,
9043
9645
  context: () => this.context(),
9044
- changed: () => this.markStateDirty()
9646
+ changed: () => this.markStateDirty(),
9647
+ i18n: this.localeState
9045
9648
  });
9046
- this.objectTree = new ObjectTree(main, (sym) => this.feed.symbolIcon(sym));
9047
- this.dataWindow = new DataWindow(main);
9048
- this.dock.addBuiltIn({ id: "dataWindow", title: "Data window", icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
9049
- 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) });
9050
9653
  this.dock.refresh();
9051
9654
  if (boot?.panels) this.dock.applyState(boot.panels);
9052
9655
  this.root.appendChild(main);
@@ -9057,7 +9660,7 @@ var VelaWorkspace = class {
9057
9660
  const attribution = rendererDefaults().attribution;
9058
9661
  if (attribution !== false) {
9059
9662
  const background = resolveTheme(opts.theme).background;
9060
- 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);
9061
9664
  Object.assign(mark.style, {
9062
9665
  left: "calc(var(--vela-toolbar-gutter, 0px) + 12px)",
9063
9666
  bottom: `calc(var(--vela-bottom-gutter, ${TIME_AXIS_H3}px) + 10px)`,
@@ -9096,6 +9699,7 @@ var VelaWorkspace = class {
9096
9699
  },
9097
9700
  {
9098
9701
  dock: "static",
9702
+ i18n: () => this.localeState,
9099
9703
  // Drawings sync is a WORKSPACE link (same model as the layout
9100
9704
  // dropdown's switches) — the bar only hosts its toggle.
9101
9705
  onDrawingsSync: (on) => {
@@ -9108,6 +9712,7 @@ var VelaWorkspace = class {
9108
9712
  this.drawToolbar?.setVisible(true);
9109
9713
  this.drawToolbar?.setDrawingsSyncMode(!!this.syncOpts.drawings);
9110
9714
  this.bottombar = opts.bottombar !== false ? new Bottombar(this.root, {
9715
+ i18n: this.localeState,
9111
9716
  timezone: this.timezone,
9112
9717
  clock: this.clock,
9113
9718
  onRange: (preset) => {
@@ -9121,6 +9726,7 @@ var VelaWorkspace = class {
9121
9726
  onSettingsClick: () => this.active.chart.renderer.openSettings()
9122
9727
  }) : null;
9123
9728
  this.mobileBar = opts.bottombar !== false ? new MobileBar(this.root, {
9729
+ i18n: this.localeState,
9124
9730
  symbol: "",
9125
9731
  timeframe: "60",
9126
9732
  onSymbolClick: () => this.symbolPicker.open(),
@@ -9154,7 +9760,7 @@ var VelaWorkspace = class {
9154
9760
  this.keymap = new KeymapManager();
9155
9761
  this.keymap.attach(this.root);
9156
9762
  this.registerDefaultKeys();
9157
- this.drawToolbar?.setShortcuts(toolShortcutHints(this.keymap));
9763
+ this.drawToolbar?.setShortcuts(toolShortcutHints(this.keymap, this.localeState));
9158
9764
  this.root.addEventListener("keydown", this.onRootKeydown);
9159
9765
  this.root.tabIndex = -1;
9160
9766
  this.cellBackend = this.backendFor(this.def);
@@ -9278,6 +9884,61 @@ var VelaWorkspace = class {
9278
9884
  on(event, handler) {
9279
9885
  return this.events.on(event, handler);
9280
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
+ }
9281
9942
  /** The context handed to contributed actions/attachments (rebuilt per invocation). */
9282
9943
  context() {
9283
9944
  return buildContext({
@@ -9294,6 +9955,7 @@ var VelaWorkspace = class {
9294
9955
  };
9295
9956
  },
9296
9957
  root: this.root,
9958
+ i18n: this.localeState,
9297
9959
  toast: (message, kind) => this.toastHost.show(message, kind),
9298
9960
  stateDirty: () => this.markStateDirty(),
9299
9961
  replay: this.replay
@@ -9794,7 +10456,7 @@ var VelaWorkspace = class {
9794
10456
  resolveLayout(layout) {
9795
10457
  if (typeof layout !== "string") return layout;
9796
10458
  const def = ensureLayout(layout);
9797
- 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().`);
9798
10460
  return def;
9799
10461
  }
9800
10462
  backendFor(def) {
@@ -9922,6 +10584,7 @@ var VelaWorkspace = class {
9922
10584
  feed: this.feed,
9923
10585
  engines: this.opts.engines ?? {},
9924
10586
  chartDefaults: cellChartDefaults(this.opts),
10587
+ localization: () => this.localeState.options,
9925
10588
  theme,
9926
10589
  live: this.opts.live ?? false,
9927
10590
  volume: this.opts.volume ?? true,
@@ -9974,10 +10637,11 @@ var VelaWorkspace = class {
9974
10637
  chart.on("script:run", (run) => this.events.emit("script:run", { ...run, cell: cell.id }));
9975
10638
  chart.on("priceStyle:change", ({ from, to }) => this.events.emit("cell:priceStyle", { id: cell.id, from, to }));
9976
10639
  chart.on("alert", (alert) => {
9977
- const source = [parseSymbol(cell.symbol).ticker || cell.symbol, timeframeLabel(cell.timeframe), alert.indicator].filter(Boolean).join(" ");
9978
- 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 });
9979
10642
  if (this.alerts.length > this.alertCap) this.alerts.pop();
9980
- 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);
9981
10645
  this.topbar.setAlertCount(this.alerts.length);
9982
10646
  });
9983
10647
  chart.on("drawing:favorites", ({ favorites }) => {
@@ -10063,7 +10727,7 @@ var VelaWorkspace = class {
10063
10727
  if (kind === "crosshair") {
10064
10728
  for (const cell of this.cellsById.values()) cell.chart.renderer.setExternalCrosshair(null);
10065
10729
  if (setting && ![...this.cellsById.values()].some((c) => c.chart.renderer.supportsExternalCrosshair)) {
10066
- 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.");
10067
10731
  }
10068
10732
  if (this.topbar && this.def) this.topbar.setLayout(this.def.id);
10069
10733
  this.markStateDirty();
@@ -10446,7 +11110,8 @@ var VelaWorkspace = class {
10446
11110
  this.active.applyRange(preset);
10447
11111
  this.bottombar?.setActiveRange(preset.id);
10448
11112
  },
10449
- onOpenChange: (open) => this.trackDialog(open)
11113
+ onOpenChange: (open) => this.trackDialog(open),
11114
+ i18n: this.localeState
10450
11115
  }));
10451
11116
  this.tfDrawer.open();
10452
11117
  }
@@ -10459,7 +11124,8 @@ var VelaWorkspace = class {
10459
11124
  isFavorite: (type) => this.active.chart.drawings.isFavorite(type),
10460
11125
  onFavorite: (type, on) => this.active.chart.drawings.setFavorite(type, on),
10461
11126
  onSelect: (type) => this.active.chart.drawings.setTool(type),
10462
- onOpenChange: (open) => this.trackDialog(open)
11127
+ onOpenChange: (open) => this.trackDialog(open),
11128
+ i18n: () => this.localeState
10463
11129
  }));
10464
11130
  this.drawingsDrawer.open();
10465
11131
  }
@@ -10467,16 +11133,17 @@ var VelaWorkspace = class {
10467
11133
  const has = (id) => topbarHas(this.topbarComp, id);
10468
11134
  this.moreDrawer ?? (this.moreDrawer = new MoreDrawer({
10469
11135
  host: this.root,
11136
+ i18n: this.localeState,
10470
11137
  ...has("undo-redo") ? { onUndo: () => this.active.history.undo(), onRedo: () => this.active.history.redo() } : {},
10471
11138
  ...has("screenshot") ? { onScreenshot: this.screenshotOverride ? () => this.runOverride(this.screenshotOverride) : () => this.downloadScreenshot() } : {},
10472
11139
  canUndo: () => this.active.history.canUndo,
10473
11140
  canRedo: () => this.active.history.canRedo,
10474
- 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) })),
10475
11142
  priceStyle: () => this.active.priceStyle,
10476
11143
  onPriceStyle: (id) => this.active.setPriceStyle(id),
10477
11144
  panels: () => has("panels") ? [...this.dock.list()] : [],
10478
11145
  onTogglePanel: (id) => this.dock.toggle(id),
10479
- ...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 })) } : {},
10480
11147
  // Actions placed on the bottom bar (left-aligned by default) have their stop
10481
11148
  // there — only the menu-placed ones land in the drawer, or they would appear
10482
11149
  // twice: left (primary) ones with the primary rows, right ones at the end.
@@ -10490,15 +11157,10 @@ var VelaWorkspace = class {
10490
11157
  // single-chart mode and a composition without 'layout' omit it here too.
10491
11158
  layout: this.monoLayout || !has("layout") ? void 0 : {
10492
11159
  shape: () => layoutShape(this.def),
10493
- 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 })),
10494
11161
  onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
10495
11162
  onSelectPreset: (id) => this.setLayout(id),
10496
- syncs: () => [
10497
- { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
10498
- { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
10499
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
10500
- { id: "style", label: "Style", checked: this.syncOpts.style === true }
10501
- ],
11163
+ syncs: () => this.syncRows(),
10502
11164
  onToggleSync: (id) => {
10503
11165
  const kind = id;
10504
11166
  this.sync.set(kind, this.syncOpts[kind] ? false : true);
@@ -10512,14 +11174,28 @@ var VelaWorkspace = class {
10512
11174
  /** The contributed topbar actions the mobile menu lists, from one cluster. */
10513
11175
  drawerActions(cluster) {
10514
11176
  const builtin = new Set(TOPBAR_BUILTIN_IDS);
10515
- 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
+ ];
10516
11191
  }
10517
11192
  openTimezoneDrawer() {
10518
11193
  this.timezoneDrawer ?? (this.timezoneDrawer = new TimezoneDrawer({
10519
11194
  host: this.root,
10520
11195
  timezone: () => this.timezone,
10521
11196
  onTimezone: (zone) => this.setTimezone(zone),
10522
- onOpenChange: (open) => this.trackDialog(open)
11197
+ onOpenChange: (open) => this.trackDialog(open),
11198
+ i18n: this.localeState
10523
11199
  }));
10524
11200
  this.timezoneDrawer.open();
10525
11201
  }
@@ -10538,8 +11214,8 @@ var VelaWorkspace = class {
10538
11214
  this.alertsMenu?.destroy();
10539
11215
  const items = this.alerts.length ? this.alerts.map((a, i) => ({
10540
11216
  id: String(i),
10541
- label: `${a.source} \xB7 ${new Date(a.time).toLocaleTimeString()} \xB7 ${a.title}: ${a.message}`.slice(0, 80)
10542
- })) : [{ 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 }];
10543
11219
  this.alertsMenu = new Menu({
10544
11220
  host: this.root,
10545
11221
  items,
@@ -10574,29 +11250,29 @@ var VelaWorkspace = class {
10574
11250
  this.keymap.register({
10575
11251
  id: "chart.screenshot",
10576
11252
  keys: "mod+alt+s",
10577
- label: ov ? ov.label : "Download a screenshot of the layout",
10578
- category: "Chart",
11253
+ label: ov ? ov.label : TEXT22.screenshot,
11254
+ category: TEXT22.categoryChart,
10579
11255
  run: ov ? () => this.runOverride(ov) : () => this.downloadScreenshot()
10580
11256
  });
10581
11257
  }
10582
- this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: "Reset view (all history)", category: "Chart", run: () => this.active.chart.setVisibleRangePreset("ALL") });
10583
- 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")) });
10584
11260
  this.keymap.register({
10585
11261
  id: "chart.toggle-percent",
10586
11262
  keys: "alt+p",
10587
- label: "Toggle percent scale",
10588
- category: "Chart",
11263
+ label: TEXT22.togglePercent,
11264
+ category: TEXT22.categoryChart,
10589
11265
  run: () => {
10590
11266
  const mode = this.active.chart.renderer.get("scaleMode");
10591
11267
  this.active.chart.renderer.set("scaleMode", mode === "percent" ? "price" : "percent");
10592
11268
  }
10593
11269
  });
10594
- 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") });
10595
11271
  this.keymap.register({
10596
11272
  id: "drawings.hline-cursor",
10597
11273
  keys: "alt+h",
10598
- label: "Horizontal line at the cursor price",
10599
- category: "Drawings",
11274
+ label: TEXT22.hlineCursor,
11275
+ category: TEXT22.categoryDrawings,
10600
11276
  run: () => {
10601
11277
  const c = this.active;
10602
11278
  if (c.lastCrossTime != null && c.lastCrossPrice != null) c.chart.drawings.add("hline", { anchors: [{ time: c.lastCrossTime, price: c.lastCrossPrice }] });
@@ -10605,32 +11281,32 @@ var VelaWorkspace = class {
10605
11281
  this.keymap.register({
10606
11282
  id: "drawings.vline-cursor",
10607
11283
  keys: "alt+v",
10608
- label: "Vertical line at the cursor time",
10609
- category: "Drawings",
11284
+ label: TEXT22.vlineCursor,
11285
+ category: TEXT22.categoryDrawings,
10610
11286
  run: () => {
10611
11287
  const c = this.active;
10612
11288
  if (c.lastCrossTime != null && c.lastCrossPrice != null) c.chart.drawings.add("vline", { anchors: [{ time: c.lastCrossTime, price: c.lastCrossPrice }] });
10613
11289
  }
10614
11290
  });
10615
- this.keymap.register({ id: "history.undo", keys: ["mod+z"], label: "Undo (active chart)", category: "Edit", run: () => this.active.history.undo() });
10616
- this.keymap.register({ id: "history.redo", keys: ["mod+y", "mod+shift+z"], label: "Redo (active chart)", category: "Edit", run: () => this.active.history.redo() });
10617
- this.keymap.register({ id: "view.zoom-in", keys: "mod+arrowup", label: "Zoom in", category: "Chart", run: () => this.glider.zoom(ZOOM_IN) });
10618
- this.keymap.register({ id: "view.zoom-out", keys: "mod+arrowdown", label: "Zoom out", category: "Chart", run: () => this.glider.zoom(ZOOM_OUT) });
10619
- this.keymap.register({ id: "view.pan-left", keys: "mod+arrowleft", label: "Pan toward history", category: "Chart", run: () => this.active.chart.panBy(-PAN_FAST) });
10620
- 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) });
10621
11297
  const indOv = this.indicatorsOverride;
10622
11298
  if (indOv && topbarHas(this.topbarComp, "indicators")) {
10623
- 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) });
10624
11300
  } else if (this.indicatorPicker) {
10625
- 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() });
10626
11302
  }
10627
11303
  this.keymap.register({
10628
11304
  id: "help.shortcuts",
10629
11305
  keys: "?",
10630
- label: "Show this shortcuts panel",
10631
- category: "Help",
11306
+ label: TEXT22.showShortcuts,
11307
+ category: TEXT22.categoryHelp,
10632
11308
  run: () => {
10633
- 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));
10634
11310
  this.shortcutsHelp.open();
10635
11311
  }
10636
11312
  });
@@ -10639,6 +11315,7 @@ var VelaWorkspace = class {
10639
11315
  routeTyping(ev) {
10640
11316
  if (this.destroyed) return;
10641
11317
  if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
11318
+ if (ev.isComposing || ev.key === "Process") return;
10642
11319
  if (isEditableTarget(ev)) return;
10643
11320
  const hit = resolveTyping(ev.key, { dialogs: this.openDialogs, symbolSearch: this.symbolPicker.isOpen, timeframeEntry: this.tfQuick.isOpen });
10644
11321
  if (!hit) return;