@luxalgo/vela 0.8.5 → 0.9.1

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