@squadbase/vantage 0.6.2-dev1.5aae04e → 0.6.2-dev1.5c79ef5

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 (228) hide show
  1. package/dist/{build-X5NVCQKT.js → build-U7GO26HR.js} +7 -7
  2. package/dist/{build-X5NVCQKT.js.map → build-U7GO26HR.js.map} +1 -1
  3. package/dist/{check-ZBIH422T.js → check-MRVAIHG5.js} +5 -5
  4. package/dist/{check-ZBIH422T.js.map → check-MRVAIHG5.js.map} +1 -1
  5. package/dist/chunk-2IAPIOPS.js +152 -0
  6. package/dist/chunk-2IAPIOPS.js.map +1 -0
  7. package/dist/chunk-3RJEOP3J.js +174 -0
  8. package/dist/chunk-3RJEOP3J.js.map +1 -0
  9. package/dist/{chunk-HKSVYMYJ.js → chunk-77CISBMG.js} +1049 -1387
  10. package/dist/chunk-77CISBMG.js.map +1 -0
  11. package/dist/{chunk-V3LF7BE7.js → chunk-ANZLVSEP.js} +4 -5
  12. package/dist/chunk-ANZLVSEP.js.map +1 -0
  13. package/dist/{chunk-XQUSGIDG.js → chunk-CKWPK4RO.js} +10 -3
  14. package/dist/chunk-CKWPK4RO.js.map +1 -0
  15. package/dist/chunk-EG4PX726.js +678 -0
  16. package/dist/chunk-EG4PX726.js.map +1 -0
  17. package/dist/{chunk-2N72FKLB.js → chunk-NZ5QIIG4.js} +33 -11
  18. package/dist/chunk-NZ5QIIG4.js.map +1 -0
  19. package/dist/{chunk-7DZGJQ3P.js → chunk-PJ4SPXHS.js} +23 -6
  20. package/dist/chunk-PJ4SPXHS.js.map +1 -0
  21. package/dist/{chunk-XPQSTZUU.js → chunk-QNOX7WLZ.js} +5 -4
  22. package/dist/chunk-QNOX7WLZ.js.map +1 -0
  23. package/dist/{chunk-PDOBHMUQ.js → chunk-ROHNXP7J.js} +72 -3
  24. package/dist/chunk-ROHNXP7J.js.map +1 -0
  25. package/dist/{chunk-CEXAQQ5W.js → chunk-S5MDTHH7.js} +180 -18
  26. package/dist/chunk-S5MDTHH7.js.map +1 -0
  27. package/dist/{chunk-2ICNHUG6.js → chunk-SDJTTMLK.js} +64 -7
  28. package/dist/chunk-SDJTTMLK.js.map +1 -0
  29. package/dist/{chunk-WAMZWZJB.js → chunk-SHZM3EXB.js} +19 -8
  30. package/dist/chunk-SHZM3EXB.js.map +1 -0
  31. package/dist/chunk-TRMHNMMZ.js +61 -0
  32. package/dist/chunk-TRMHNMMZ.js.map +1 -0
  33. package/dist/{chunk-JKLLFVEU.js → chunk-UWSQUHX6.js} +4 -99
  34. package/dist/chunk-UWSQUHX6.js.map +1 -0
  35. package/dist/cli.js +6 -6
  36. package/dist/client/index.d.ts +2 -0
  37. package/dist/client/index.js +8 -2
  38. package/dist/client/index.js.map +1 -1
  39. package/dist/components/index.d.ts +57 -273
  40. package/dist/components/index.js +6 -3
  41. package/dist/delta-text-CbrOaW83.d.ts +17 -0
  42. package/dist/{dev-4XEDTELV.js → dev-O42JFQ64.js} +7 -7
  43. package/dist/{dev-4XEDTELV.js.map → dev-O42JFQ64.js.map} +1 -1
  44. package/dist/doc/index.d.ts +215 -2
  45. package/dist/doc/index.js +422 -4
  46. package/dist/doc/index.js.map +1 -1
  47. package/dist/{doctor-5E2VEZPK.js → doctor-YD3XAAOT.js} +5 -5
  48. package/dist/{doctor-5E2VEZPK.js.map → doctor-YD3XAAOT.js.map} +1 -1
  49. package/dist/format-DihQkL1N.d.ts +249 -0
  50. package/dist/query/index.js +2 -2
  51. package/dist/{routes-3SGAFDMC.js → routes-PVKNT777.js} +4 -4
  52. package/dist/{routes-3SGAFDMC.js.map → routes-PVKNT777.js.map} +1 -1
  53. package/dist/slide/index.d.ts +272 -1
  54. package/dist/slide/index.js +1023 -2
  55. package/dist/slide/index.js.map +1 -1
  56. package/dist/table/index.js +9 -6
  57. package/dist/table/index.js.map +1 -1
  58. package/dist/ui/index.d.ts +5 -2
  59. package/dist/ui/index.js +6 -6
  60. package/dist/ui/index.js.map +1 -1
  61. package/dist/{upgrade-JHXZBGQS.js → upgrade-6DT7ADRP.js} +4 -4
  62. package/dist/{upgrade-JHXZBGQS.js.map → upgrade-6DT7ADRP.js.map} +1 -1
  63. package/dist/vite/index.js +3 -3
  64. package/docs/en/agent-skills.md +1 -1
  65. package/docs/en/changelog.md +2 -0
  66. package/docs/en/components.md +13 -2
  67. package/docs/en/doc/delta-text.md +40 -0
  68. package/docs/en/doc/doc-action-items.md +49 -0
  69. package/docs/en/doc/doc-callout.md +38 -0
  70. package/docs/en/doc/doc-data-table.md +60 -0
  71. package/docs/en/doc/doc-executive-summary.md +64 -0
  72. package/docs/en/doc/doc-figure.md +56 -0
  73. package/docs/en/doc/doc-key-findings.md +61 -0
  74. package/docs/en/doc/doc-kpi-scorecard.md +80 -0
  75. package/docs/en/doc/doc-notes-and-sources.md +43 -0
  76. package/docs/en/doc/doc-report-header.md +46 -0
  77. package/docs/en/pages-and-metadata.md +15 -0
  78. package/docs/en/parts/area-chart.md +99 -0
  79. package/docs/en/parts/bar-chart.md +91 -0
  80. package/docs/en/parts/bar-list.md +50 -0
  81. package/docs/en/parts/category-bar.md +37 -0
  82. package/docs/en/parts/chart-frame.md +39 -0
  83. package/docs/en/parts/delta.md +4 -3
  84. package/docs/en/parts/donut-chart.md +72 -0
  85. package/docs/en/parts/funnel-steps.md +2 -1
  86. package/docs/en/parts/heatmap.md +53 -0
  87. package/docs/en/parts/kpi-card.md +2 -1
  88. package/docs/en/parts/legend.md +35 -0
  89. package/docs/en/parts/line-chart.md +94 -0
  90. package/docs/en/parts/progress-circle.md +33 -0
  91. package/docs/en/parts/sample-data.md +39 -0
  92. package/docs/en/parts/scatter-chart.md +56 -0
  93. package/docs/en/parts/tracker.md +36 -0
  94. package/docs/en/parts/updated-at.md +30 -0
  95. package/docs/en/parts/waterfall-chart.md +56 -0
  96. package/docs/en/slide/slide-chart-full.md +48 -0
  97. package/docs/en/slide/slide-chart-insight.md +52 -0
  98. package/docs/en/slide/slide-columns.md +46 -0
  99. package/docs/en/slide/slide-comparison.md +68 -0
  100. package/docs/en/slide/slide-cover.md +39 -0
  101. package/docs/en/slide/slide-exec-summary.md +47 -0
  102. package/docs/en/slide/slide-heading.md +38 -0
  103. package/docs/en/slide/slide-kpi-grid.md +55 -0
  104. package/docs/en/slide/slide-kpi-hero.md +58 -0
  105. package/docs/en/slide/slide-next-actions.md +49 -0
  106. package/docs/en/slide/slide-scorecard-table.md +54 -0
  107. package/docs/en/slide/slide-section.md +44 -0
  108. package/docs/en/slide/slide-source-line.md +34 -0
  109. package/docs/en/ui/alert-dialog.md +42 -0
  110. package/docs/en/ui/avatar.md +31 -0
  111. package/docs/en/ui/combobox.md +74 -0
  112. package/docs/en/ui/drawer.md +41 -0
  113. package/docs/en/ui/empty.md +1 -1
  114. package/docs/en/ui/error-state.md +4 -3
  115. package/docs/en/ui/field.md +42 -0
  116. package/docs/en/ui/hover-card.md +21 -0
  117. package/docs/en/ui/kbd.md +17 -0
  118. package/docs/en/ui/loading.md +1 -1
  119. package/docs/en/ui/pagination.md +36 -0
  120. package/docs/en/ui/radio-group.md +21 -0
  121. package/docs/en/ui/toast.md +44 -0
  122. package/docs/en/ui/toggle-group.md +4 -3
  123. package/docs/index.json +857 -77
  124. package/docs/ja/agent-skills.md +1 -1
  125. package/docs/ja/changelog.md +2 -0
  126. package/docs/ja/components.md +12 -1
  127. package/docs/ja/doc/delta-text.md +40 -0
  128. package/docs/ja/doc/doc-action-items.md +49 -0
  129. package/docs/ja/doc/doc-callout.md +38 -0
  130. package/docs/ja/doc/doc-data-table.md +60 -0
  131. package/docs/ja/doc/doc-executive-summary.md +64 -0
  132. package/docs/ja/doc/doc-figure.md +56 -0
  133. package/docs/ja/doc/doc-key-findings.md +61 -0
  134. package/docs/ja/doc/doc-kpi-scorecard.md +80 -0
  135. package/docs/ja/doc/doc-notes-and-sources.md +43 -0
  136. package/docs/ja/doc/doc-report-header.md +46 -0
  137. package/docs/ja/page-types/slide.md +1 -0
  138. package/docs/ja/pages-and-metadata.md +15 -0
  139. package/docs/ja/parts/area-chart.md +49 -25
  140. package/docs/ja/parts/bar-chart.md +50 -24
  141. package/docs/ja/parts/bar-list.md +31 -8
  142. package/docs/ja/parts/chart-frame.md +36 -0
  143. package/docs/ja/parts/delta.md +5 -4
  144. package/docs/ja/parts/donut-chart.md +45 -26
  145. package/docs/ja/parts/funnel-steps.md +27 -8
  146. package/docs/ja/parts/heatmap.md +45 -0
  147. package/docs/ja/parts/kpi-card.md +2 -1
  148. package/docs/ja/parts/legend.md +36 -0
  149. package/docs/ja/parts/line-chart.md +48 -22
  150. package/docs/ja/parts/sample-data.md +61 -0
  151. package/docs/ja/parts/scatter-chart.md +69 -0
  152. package/docs/ja/parts/updated-at.md +27 -0
  153. package/docs/ja/parts/waterfall-chart.md +61 -0
  154. package/docs/ja/slide/slide-chart-full.md +48 -0
  155. package/docs/ja/slide/slide-chart-insight.md +52 -0
  156. package/docs/ja/slide/slide-columns.md +46 -0
  157. package/docs/ja/slide/slide-comparison.md +68 -0
  158. package/docs/ja/slide/slide-cover.md +39 -0
  159. package/docs/ja/slide/slide-exec-summary.md +47 -0
  160. package/docs/ja/slide/slide-heading.md +38 -0
  161. package/docs/ja/slide/slide-kpi-grid.md +55 -0
  162. package/docs/ja/slide/slide-kpi-hero.md +58 -0
  163. package/docs/ja/slide/slide-next-actions.md +49 -0
  164. package/docs/ja/slide/slide-scorecard-table.md +54 -0
  165. package/docs/ja/slide/slide-section.md +44 -0
  166. package/docs/ja/slide/slide-source-line.md +34 -0
  167. package/docs/ja/ui/empty.md +1 -1
  168. package/docs/ja/ui/error-state.md +4 -3
  169. package/docs/ja/ui/loading.md +1 -1
  170. package/docs/ja/ui/toggle-group.md +4 -3
  171. package/package.json +2 -2
  172. package/registry/ui/app-shell.tsx +138 -1
  173. package/registry/ui/area-chart.tsx +389 -104
  174. package/registry/ui/attention-list.tsx +177 -15
  175. package/registry/ui/bar-chart.tsx +386 -104
  176. package/registry/ui/bar-list.tsx +376 -94
  177. package/registry/ui/box-plot-chart.tsx +450 -116
  178. package/registry/ui/category-bar.tsx +292 -57
  179. package/registry/ui/chart-frame.tsx +301 -63
  180. package/registry/ui/data-table.tsx +229 -37
  181. package/registry/ui/date-range-picker.tsx +190 -36
  182. package/registry/ui/delta.tsx +181 -19
  183. package/registry/ui/donut-chart.tsx +385 -103
  184. package/registry/ui/echart.tsx +313 -58
  185. package/registry/ui/filter-bar.tsx +179 -20
  186. package/registry/ui/funnel-steps.tsx +354 -80
  187. package/registry/ui/heatmap.tsx +338 -71
  188. package/registry/ui/histogram-chart.tsx +440 -116
  189. package/registry/ui/kpi-card.tsx +254 -48
  190. package/registry/ui/legend.tsx +374 -93
  191. package/registry/ui/line-chart.tsx +389 -104
  192. package/registry/ui/multi-select.tsx +159 -9
  193. package/registry/ui/page-shell.tsx +148 -10
  194. package/registry/ui/paired-chart.tsx +394 -109
  195. package/registry/ui/progress-circle.tsx +205 -39
  196. package/registry/ui/sample-data.tsx +154 -10
  197. package/registry/ui/scatter-chart.tsx +436 -119
  198. package/registry/ui/searchable-select.tsx +145 -7
  199. package/registry/ui/slider.tsx +1 -1
  200. package/registry/ui/states.tsx +170 -13
  201. package/registry/ui/status-badge.tsx +201 -36
  202. package/registry/ui/tooltip.tsx +6 -2
  203. package/registry/ui/tracker.tsx +214 -42
  204. package/registry/ui/trend-indicator.tsx +151 -8
  205. package/registry/ui/updated-at.tsx +170 -16
  206. package/registry/ui/waterfall-chart.tsx +460 -121
  207. package/skills/vantage-design/SKILL.md +54 -54
  208. package/skills/vantage-design/references/extract.md +26 -27
  209. package/skills/vantage-design/references/format.md +12 -172
  210. package/skills/vantage-design/references/presets.md +50 -110
  211. package/skills/vantage-pitfalls/SKILL.md +5 -0
  212. package/skills/vantage-slide/SKILL.md +54 -36
  213. package/templates/AGENTS.md +32 -16
  214. package/templates/agents/doc.md +48 -0
  215. package/templates/agents/page.md +23 -2
  216. package/templates/agents/slide.md +77 -3
  217. package/theme.css +23 -19
  218. package/dist/chunk-2ICNHUG6.js.map +0 -1
  219. package/dist/chunk-2N72FKLB.js.map +0 -1
  220. package/dist/chunk-7DZGJQ3P.js.map +0 -1
  221. package/dist/chunk-CEXAQQ5W.js.map +0 -1
  222. package/dist/chunk-HKSVYMYJ.js.map +0 -1
  223. package/dist/chunk-JKLLFVEU.js.map +0 -1
  224. package/dist/chunk-PDOBHMUQ.js.map +0 -1
  225. package/dist/chunk-V3LF7BE7.js.map +0 -1
  226. package/dist/chunk-WAMZWZJB.js.map +0 -1
  227. package/dist/chunk-XPQSTZUU.js.map +0 -1
  228. package/dist/chunk-XQUSGIDG.js.map +0 -1
@@ -32,13 +32,146 @@ import { cn } from "@squadbase/vantage/ui"
32
32
  import { ChartFrame } from "@squadbase/vantage/components"
33
33
  import { formatPoints, type ValueFormat } from "@squadbase/vantage/components"
34
34
  import type { ChartColorToken, ChartCommonProps } from "@squadbase/vantage/components"
35
- import { DEFAULT_LOCALE, EMPTY_VALUE, formatValue, toValueFormatter } from "@squadbase/vantage/components"
35
+ import { useSyncExternalStore } from "react"
36
+ import { EMPTY_VALUE, formatValue, toValueFormatter } from "@squadbase/vantage/components"
36
37
  import type { ChartColor, ChartColors, ChartSeriesColor, ChartTone, ValueFormatter } from "@squadbase/vantage/components"
37
38
  import { useMemo, useRef, type ReactNode } from "react"
38
39
  import type { EChartsOption } from "@squadbase/vantage/components"
39
40
  import { computeComparison, defaultChangeDisplay, formatChange, toneForValue, type ComparisonKind, type PeriodGrain } from "@squadbase/vantage/components"
41
+ import { DEFAULT_LOCALE } from "@squadbase/vantage/components"
40
42
  import type { ChartThreshold, ChartAnnotation, ChartEventPayload, ChartForecast, ChartForecastRow, ChartHighlight, NumericKeys } from "@squadbase/vantage/components"
41
43
 
44
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
45
+
46
+ /**
47
+ * The display language of the parts and the defaults that follow it.
48
+ *
49
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
50
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
51
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
52
+ *
53
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
54
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
55
+ */
56
+
57
+ /** The language of the built-in wording: Japanese or English. */
58
+ type UiLanguage = "ja" | "en"
59
+
60
+ /** The locale used when `<html lang>` gives none (and on the server). */
61
+ const FALLBACK_LOCALE = "en-US"
62
+
63
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
64
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
65
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
66
+ }
67
+
68
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
69
+ function normalizeLocale(tag: string | null | undefined): string {
70
+ const value = tag?.trim()
71
+ if (!value) return FALLBACK_LOCALE
72
+ try {
73
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
74
+ } catch {
75
+ return FALLBACK_LOCALE
76
+ }
77
+ }
78
+
79
+ /**
80
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
81
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
82
+ */
83
+ function documentLocale(): string {
84
+ if (typeof document === "undefined") return FALLBACK_LOCALE
85
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
86
+ }
87
+
88
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
89
+ function resolveLocale(locale?: string | null): string {
90
+ return locale || documentLocale()
91
+ }
92
+
93
+ /** Picks the wording table of the locale's language. */
94
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
95
+ return table[uiLanguage(resolveLocale(locale))]
96
+ }
97
+
98
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
99
+ function defaultCurrency(locale?: string | null): string {
100
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
101
+ }
102
+
103
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
104
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
105
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
106
+ }
107
+
108
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
109
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
110
+
111
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
112
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
113
+
114
+ /**
115
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
116
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
117
+ * Monday for the rest.
118
+ */
119
+ function weekStartsOn(locale?: string | null): WeekDay {
120
+ const tag = normalizeLocale(resolveLocale(locale))
121
+ if (uiLanguage(tag) === "ja") return 1
122
+ try {
123
+ const info = new Intl.Locale(tag) as Intl.Locale & {
124
+ getWeekInfo?: () => { firstDay: number }
125
+ weekInfo?: { firstDay: number }
126
+ }
127
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
128
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
129
+ } catch {
130
+ // Fall through to the table below.
131
+ }
132
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
133
+ }
134
+
135
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
136
+
137
+ const listeners = new Set<() => void>()
138
+ let observer: MutationObserver | null = null
139
+
140
+ /** One observer on `<html lang>` for every subscribed part. */
141
+ function subscribe(onChange: () => void): () => void {
142
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
143
+ listeners.add(onChange)
144
+ if (!observer) {
145
+ observer = new MutationObserver(() => {
146
+ for (const listener of [...listeners]) listener()
147
+ })
148
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
149
+ }
150
+ return () => {
151
+ listeners.delete(onChange)
152
+ if (listeners.size === 0 && observer) {
153
+ observer.disconnect()
154
+ observer = null
155
+ }
156
+ }
157
+ }
158
+
159
+ const serverLocale = () => FALLBACK_LOCALE
160
+
161
+ /**
162
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
163
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
164
+ */
165
+ function useDocumentLocale(): string {
166
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
167
+ }
168
+
169
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
170
+ function useLocale(locale?: string | null): string {
171
+ const page = useDocumentLocale()
172
+ return locale || page
173
+ }
174
+
42
175
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
43
176
 
44
177
  const ROOT_PROP_KEYS = new Set(["id", "style", "role", "tabIndex", "lang", "hidden"])
@@ -146,9 +279,10 @@ function resolveValueFormatter(
146
279
  function resolveAxisFormatter(
147
280
  valueFormatter: ValueFormatter | undefined,
148
281
  format: ValueFormat | undefined,
149
- locale: string = DEFAULT_LOCALE,
282
+ localeArg?: string,
150
283
  ): ValueFormatter {
151
284
  if (valueFormatter) return valueFormatter
285
+ const locale = resolveLocale(localeArg)
152
286
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
153
287
  if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
154
288
  const currency = "currency" in spec ? spec.currency : undefined
@@ -157,40 +291,75 @@ function resolveAxisFormatter(
157
291
  return (value) => formatValue(value, short, locale)
158
292
  }
159
293
 
160
- /** The unit words of a ja number (`1.2万` / `3.4億` / `1兆`) and their sizes, largest first. */
161
- const JA_UNITS = [
162
- { char: "兆", size: 1e12 },
163
- { char: "億", size: 1e8 },
164
- { char: "万", size: 1e4 },
165
- ] as const
294
+ interface CompactUnit {
295
+ token: string
296
+ size: number
297
+ }
298
+
299
+ const compactUnitCache = new Map<string, CompactUnit[] | null>()
300
+
301
+ /**
302
+ * The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
303
+ * `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
304
+ * word (`1,5 Mio.`), where a token cannot be swapped in place.
305
+ */
306
+ function compactUnits(locale: string): CompactUnit[] | null {
307
+ const cached = compactUnitCache.get(locale)
308
+ if (cached !== undefined) return cached
309
+ let units: CompactUnit[] | null = []
310
+ try {
311
+ const compact = new Intl.NumberFormat(locale, { notation: "compact" })
312
+ const seen = new Set<string>()
313
+ for (let exp = 3; exp <= 15; exp++) {
314
+ const parts = compact.formatToParts(10 ** exp)
315
+ const at = parts.findIndex((p) => p.type === "compact")
316
+ if (at === -1) continue
317
+ if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
318
+ units = null
319
+ break
320
+ }
321
+ const token = parts[at]!.value
322
+ if (seen.has(token)) continue
323
+ seen.add(token)
324
+ units.push({ token, size: 10 ** exp })
325
+ }
326
+ } catch {
327
+ units = null
328
+ }
329
+ const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
330
+ compactUnitCache.set(locale, sorted)
331
+ return sorted
332
+ }
166
333
 
167
- const JA_NUMBER_TOKEN_RE = /\d[\d,]*(?:\.\d+)?(?:兆|億|万)?/
334
+ const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
168
335
 
169
336
  /**
170
337
  * The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
171
- * that largest tick, so 万 and 億 never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`).
172
- * Only the ja short forms (`compact` / `number` / `currency`, no `valueFormatter`) have units to align;
173
- * any other format — and an axis whose largest tick is under 1万 — keeps `resolveAxisFormatter`'s output as it is.
338
+ * that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
339
+ * `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
340
+ * no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
341
+ * unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
174
342
  */
175
343
  function resolveAxisTickFormatter(
176
344
  valueFormatter: ValueFormatter | undefined,
177
345
  format: ValueFormat | undefined,
178
- locale: string = DEFAULT_LOCALE,
346
+ localeArg?: string,
179
347
  ): (top: number) => ValueFormatter {
348
+ const locale = resolveLocale(localeArg)
180
349
  const base = resolveAxisFormatter(valueFormatter, format, locale)
181
350
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
182
- const aligns =
183
- !valueFormatter && locale.toLowerCase().startsWith("ja") && (spec.type === "compact" || spec.type === "number" || spec.type === "currency")
184
- if (!aligns) return () => base
351
+ const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
352
+ if (!units) return () => base
185
353
  const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
354
+ const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
186
355
  return (top) => {
187
- const unit = JA_UNITS.find((u) => Math.abs(top) >= u.size)
356
+ const unit = units.find((u) => Math.abs(top) >= u.size)
188
357
  if (!unit) return base
189
358
  return (value) => {
190
359
  const text = base(value)
191
360
  if (value === 0 || !Number.isFinite(value)) return text
192
- const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.char}`
193
- return text.replace(JA_NUMBER_TOKEN_RE, () => scaled)
361
+ const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
362
+ return text.replace(numberToken, () => scaled)
194
363
  }
195
364
  }
196
365
  }
@@ -219,8 +388,20 @@ function clamp(value: number, min: number, max: number): number {
219
388
  /** How many categories a chart colours individually; the rest fold into「その他」. */
220
389
  const MAX_CHART_SERIES = 5
221
390
 
222
- /** The series name the folded categories get. */
223
- const OTHER_SERIES_LABEL = "その他"
391
+ /** The series name the folded categories get, by language. */
392
+ const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
393
+
394
+ /** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
395
+ function otherSeriesLabel(locale?: string): string {
396
+ return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
397
+ }
398
+
399
+ const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
400
+
401
+ /** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
402
+ function isOtherName(name: string | null | undefined): boolean {
403
+ return typeof name === "string" && OTHER_NAMES.has(name)
404
+ }
224
405
 
225
406
  interface AggregatedItems<Item extends { name: string; value: number }> {
226
407
  /** The `n` largest items, in their given order. All items when there are `n` or fewer. */
@@ -240,7 +421,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
240
421
  function aggregateTopN<Item extends { name: string; value: number }>(
241
422
  items: readonly Item[],
242
423
  n: number = MAX_CHART_SERIES,
243
- otherLabel: string = OTHER_SERIES_LABEL,
424
+ otherLabel: string = otherSeriesLabel(),
244
425
  ): AggregatedItems<Item> {
245
426
  if (items.length <= n) return { top: [...items], rest: [], other: null }
246
427
  const ranked = items
@@ -269,7 +450,7 @@ function aggregateSeries(
269
450
  data: readonly Record<string, unknown>[],
270
451
  categories: readonly string[],
271
452
  n: number = MAX_CHART_SERIES,
272
- otherLabel: string = OTHER_SERIES_LABEL,
453
+ otherLabel: string = otherSeriesLabel(),
273
454
  ): AggregatedSeries {
274
455
  const totals = categories.map((name) => ({
275
456
  name,
@@ -301,12 +482,14 @@ function aggregateSeriesKeeping(
301
482
  categories: readonly string[],
302
483
  keep: string | undefined,
303
484
  n: number = MAX_CHART_SERIES,
485
+ otherLabel: string = otherSeriesLabel(),
304
486
  ): AggregatedSeries {
305
- if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
487
+ if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
306
488
  const aggregated = aggregateSeries(
307
489
  data,
308
490
  categories.filter((c) => c !== keep),
309
491
  n,
492
+ otherLabel,
310
493
  )
311
494
  return { ...aggregated, categories: [...aggregated.categories, keep] }
312
495
  }
@@ -342,7 +525,7 @@ function seriesColorsWithComparison(
342
525
  * Colour per drawn series: the record's token for that category, else the
343
526
  * array's token at the category's position in `allCategories` (cycled), else
344
527
  * the token palette by drawn position. `otherCategory` — and a category the
345
- * data itself names「その他」(`OTHER_SERIES_LABEL`) — always gets `otherColor`.
528
+ * data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
346
529
  */
347
530
  function seriesColorsFor(
348
531
  categories: readonly string[],
@@ -354,7 +537,7 @@ function seriesColorsFor(
354
537
  const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
355
538
  const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
356
539
  return categories.map((category, i) => {
357
- if ((otherCategory !== null && category === otherCategory) || category === OTHER_SERIES_LABEL) return otherColor
540
+ if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
358
541
  const fromRecord = record?.[category]
359
542
  if (fromRecord) return fromRecord
360
543
  if (list && list.length > 0) {
@@ -401,7 +584,7 @@ interface DonutSlicesConfig {
401
584
  */
402
585
  function donutSlices(
403
586
  data: readonly Record<string, unknown>[],
404
- { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = OTHER_SERIES_LABEL }: DonutSlicesConfig,
587
+ { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
405
588
  ): DonutSlice[] {
406
589
  const items = data
407
590
  .map((row, i) => {
@@ -414,7 +597,8 @@ function donutSlices(
414
597
  const slices: DonutSlice[] = top.map((item, rank) => ({
415
598
  name: item.name,
416
599
  value: item.value,
417
- color: palette[rank % palette.length]!,
600
+ // A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
601
+ color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
418
602
  isOther: false,
419
603
  row: item.row,
420
604
  }))
@@ -487,7 +671,7 @@ interface BarListRowsConfig extends BarListOptions {
487
671
  */
488
672
  function barListRows<Row extends object>(
489
673
  data: readonly Row[],
490
- { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = OTHER_SERIES_LABEL, exclude }: BarListRowsConfig = {},
674
+ { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
491
675
  ): BarListRow<Row>[] {
492
676
  const items = data.map((row): BarListRow<Row> => {
493
677
  const cells = row as Record<string, unknown>
@@ -533,19 +717,21 @@ const BAR_LIST_OTHER_TINT = 30
533
717
 
534
718
  /**
535
719
  * The fill of each BarList row: all real rows in the same tint of `color`
536
- * (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
720
+ * (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
537
721
  * (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
538
722
  * ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
539
723
  * for that row. A tint, not the solid token, so the label printed over the bar
540
724
  * keeps its contrast.
541
725
  */
542
726
  function barListFills(
543
- rows: readonly Pick<BarListRow, "isOther" | "color">[],
727
+ rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
544
728
  color: ChartColor,
545
729
  otherColor: string,
546
730
  ): string[] {
547
731
  return rows.map((row) =>
548
- row.isOther ? tintCss(otherColor, BAR_LIST_OTHER_TINT) : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
732
+ row.isOther || isOtherName(row.name)
733
+ ? tintCss(otherColor, BAR_LIST_OTHER_TINT)
734
+ : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
549
735
  )
550
736
  }
551
737
 
@@ -761,11 +947,30 @@ interface ChartSummaryInput {
761
947
  format?: ValueFormat
762
948
  /** Wins over `format`. */
763
949
  valueFormatter?: ValueFormatter
950
+ /** Language of the sentence. Default the page's locale (`<html lang>`). */
951
+ locale?: string
952
+ }
953
+
954
+ const SUMMARY_TEXT = {
955
+ ja: {
956
+ up: "、前の点より増加",
957
+ down: "、前の点より減少",
958
+ flat: "、前の点と横ばい",
959
+ latest: (label: string, value: string) => `最新 ${label} は ${value}`,
960
+ top: (name: string, share: string) => `最大は ${name}(${share})`,
961
+ },
962
+ en: {
963
+ up: ", up from the previous point",
964
+ down: ", down from the previous point",
965
+ flat: ", flat from the previous point",
966
+ latest: (label: string, value: string) => `Latest ${label}: ${value}`,
967
+ top: (name: string, share: string) => `Largest: ${name} (${share})`,
968
+ },
764
969
  }
765
970
 
766
971
  /**
767
972
  * One-sentence summary built from the rows, for `summary="auto"`:
768
- * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」. The direction is
973
+ * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
769
974
  * computed from the last two non-empty points of the first series — never
770
975
  * written by hand, so it cannot contradict the data. `undefined` when there is
771
976
  * no usable point.
@@ -781,30 +986,59 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
781
986
  }
782
987
  const latest = points[points.length - 1]
783
988
  if (!latest) return undefined
784
- const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact")
989
+ const locale = resolveLocale(input.locale)
990
+ const t = SUMMARY_TEXT[uiLanguage(locale)]
991
+ const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
785
992
  const prev = points[points.length - 2]
786
- const direction = !prev ? "" : latest.value > prev.value ? "、前の点より増加" : latest.value < prev.value ? "、前の点より減少" : "、前の点と横ばい"
993
+ const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
787
994
  const head = input.title ? `${input.title}: ` : ""
788
- return `${head}最新 ${latest.label} は ${text(latest.value)}${direction}`
995
+ return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
789
996
  }
790
997
 
791
998
  /**
792
- * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
793
- * (largest first). `undefined` when there is nothing to describe.
999
+ * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
1000
+ * (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
794
1001
  */
795
1002
  function buildShareSummary(
796
1003
  slices: readonly { name: string; value: number }[],
797
1004
  title?: string,
1005
+ locale?: string,
798
1006
  ): string | undefined {
799
1007
  const total = slices.reduce((sum, s) => sum + s.value, 0)
800
1008
  const top = slices[0]
801
1009
  if (!top || total <= 0) return undefined
802
1010
  const share = `${Math.round((top.value / total) * 1000) / 10}%`
803
- return `${title ? `${title}: ` : ""}最大は ${top.name}(${share})`
1011
+ return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
804
1012
  }
805
1013
 
806
1014
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-options.ts (internal to @squadbase/vantage) ---------- */
807
1015
 
1016
+ /** The built-in words of the option builders, by language. */
1017
+ const CHART_TEXT = {
1018
+ ja: {
1019
+ target: "目標",
1020
+ forecast: "予測",
1021
+ total: "合計",
1022
+ provisional: "速報",
1023
+ noComparison: "比較なし",
1024
+ share: (share: string) => `(${share})`,
1025
+ targetRate: (rate: string) => `(達成率 ${rate})`,
1026
+ provisionalTitle: (title: string, label: string) => `${title}(${label})`,
1027
+ },
1028
+ en: {
1029
+ target: "Target",
1030
+ forecast: "Forecast",
1031
+ total: "Total",
1032
+ provisional: "Preliminary",
1033
+ noComparison: "No comparison",
1034
+ share: (share: string) => ` (${share})`,
1035
+ targetRate: (rate: string) => ` (${rate} achieved)`,
1036
+ provisionalTitle: (title: string, label: string) => `${title} (${label})`,
1037
+ },
1038
+ } satisfies Record<UiLanguage, unknown>
1039
+
1040
+ const chartText = (locale?: string) => CHART_TEXT[uiLanguage(resolveLocale(locale))]
1041
+
808
1042
  type CartesianChartKind = "area" | "bar" | "line"
809
1043
  type CartesianStackType = "default" | "stacked" | "percent"
810
1044
  type CartesianCurveType = "linear" | "smooth" | "step"
@@ -989,7 +1223,7 @@ interface CartesianChartConfig {
989
1223
  * all ticks read in one unit (¥0.5億 / ¥1億). Default: `axisFormatter` for every tick.
990
1224
  */
991
1225
  axisTicks?: (top: number) => (value: number) => string
992
- /** Locale of the time-axis labels. Default `"ja-JP"`. */
1226
+ /** Locale of the time-axis labels and the built-in words (total, target, forecast). Default the page's (`<html lang>`). */
993
1227
  locale?: string
994
1228
  type?: CartesianStackType
995
1229
  /** Bar only. `"vertical"` lays the categories down the y axis (horizontal bars). */
@@ -1227,11 +1461,12 @@ function timeAxisLabelStep(values: readonly unknown[], maxLabels: number = TIME_
1227
1461
  */
1228
1462
  function timeAxisLabels(
1229
1463
  values: readonly unknown[],
1230
- locale: string = DEFAULT_LOCALE,
1464
+ localeArg?: string,
1231
1465
  shown: (i: number) => boolean = () => true,
1232
1466
  ): string[] | null {
1233
1467
  const parsed = parseTimeIndex(values)
1234
1468
  if (!parsed) return null
1469
+ const locale = resolveLocale(localeArg)
1235
1470
  const { granularity, times } = parsed
1236
1471
  let prevYear: number | null = null
1237
1472
  return times.map((t, i) => {
@@ -1254,9 +1489,10 @@ function timeAxisLabels(
1254
1489
  }
1255
1490
 
1256
1491
  /** The tooltip titles for time-like x values (`2025年9月`, `2025年9月1日`), or `null` when they are not dates. */
1257
- function timeTooltipLabels(values: readonly unknown[], locale: string = DEFAULT_LOCALE): string[] | null {
1492
+ function timeTooltipLabels(values: readonly unknown[], localeArg?: string): string[] | null {
1258
1493
  const parsed = parseTimeIndex(values)
1259
1494
  if (!parsed) return null
1495
+ const locale = resolveLocale(localeArg)
1260
1496
  const options: Intl.DateTimeFormatOptions =
1261
1497
  parsed.granularity === "year"
1262
1498
  ? { year: "numeric" }
@@ -1396,7 +1632,9 @@ function tooltipHtml(
1396
1632
  rows: readonly TooltipRow[],
1397
1633
  valueFormatter: (value: number) => string,
1398
1634
  notes: readonly string[] = [],
1635
+ locale?: string,
1399
1636
  ): string {
1637
+ const t = chartText(locale)
1400
1638
  const lines = rows.map(
1401
1639
  (row) =>
1402
1640
  `<div style="display:flex;align-items:center;gap:8px;line-height:20px">` +
@@ -1405,7 +1643,7 @@ function tooltipHtml(
1405
1643
  `<span style="font-weight:600;font-variant-numeric:tabular-nums;margin-left:12px">${
1406
1644
  row.value === null ? EMPTY_VALUE : escapeHtml(valueFormatter(row.value))
1407
1645
  }${
1408
- row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">(${escapeHtml(formatShare(row.share))})</span>`
1646
+ row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">${escapeHtml(t.share(formatShare(row.share)))}</span>`
1409
1647
  }</span></div>` +
1410
1648
  (row.note === undefined
1411
1649
  ? ""
@@ -1419,8 +1657,10 @@ function tooltipHtml(
1419
1657
 
1420
1658
  /* ------------------------------------------------- per-bar targets / notes */
1421
1659
 
1422
- /** Default name of the per-bar target marker (legend, tooltip). */
1423
- const BAR_TARGET_LABEL = "目標"
1660
+ /** Default name of the per-bar target marker (legend, tooltip) in `locale`: "Target" / 「目標」. Default the page's locale. */
1661
+ function barTargetLabel(locale?: string): string {
1662
+ return chartText(locale).target
1663
+ }
1424
1664
 
1425
1665
  /** How far (px) the target marker reaches past each side of its bar. */
1426
1666
  const TARGET_MARKER_OVERHANG = 4
@@ -1471,16 +1711,17 @@ function achievementRate(value: number | null | undefined, target: number | null
1471
1711
  return (value / target) * 100
1472
1712
  }
1473
1713
 
1474
- /** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」. `null` without a target. */
1714
+ /** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」 / "Target $12M (85.3% achieved)". `null` without a target. */
1475
1715
  function barTargetNote(
1476
1716
  label: string,
1477
1717
  value: number | null,
1478
1718
  target: number | null,
1479
1719
  valueFormatter: (value: number) => string,
1720
+ locale?: string,
1480
1721
  ): string | null {
1481
1722
  if (target === null) return null
1482
1723
  const rate = achievementRate(value, target)
1483
- return `${label} ${valueFormatter(target)}${rate === null ? "" : `(達成率 ${formatShare(rate)})`}`
1724
+ return `${label} ${valueFormatter(target)}${rate === null ? "" : chartText(locale).targetRate(formatShare(rate))}`
1484
1725
  }
1485
1726
 
1486
1727
  /** Half the width of a bar in category-axis units, from `barCategoryGap` (`"20%"`; a px gap or none → ECharts' 20%). */
@@ -1572,11 +1813,12 @@ const TARGET_LEGEND_SERIES_ID = "__target-legend"
1572
1813
  /** A flat tick (8×8 legend box, aspect kept): the per-bar target's legend icon. */
1573
1814
  const TARGET_LEGEND_ICON = "path://M0,0 L100,0 L100,24 L0,24 Z"
1574
1815
 
1575
- const DEFAULT_FORECAST_LABEL = "予測"
1576
-
1577
- /** The label at the top of a forecast's boundary line: the forecast's name (as in the legend) and an arrow toward the forecast side. */
1578
- function forecastBoundaryText(label: string = DEFAULT_FORECAST_LABEL): string {
1579
- return `${label} →`
1816
+ /**
1817
+ * The label at the top of a forecast's boundary line: the forecast's name (as in the legend) and an arrow toward the forecast side.
1818
+ * Without a `label`, "Forecast" / 「予測」 in `locale` (default the page's).
1819
+ */
1820
+ function forecastBoundaryText(label?: string, locale?: string): string {
1821
+ return `${label ?? chartText(locale).forecast} →`
1580
1822
  }
1581
1823
 
1582
1824
  /** A comparison series: a thin, muted, solid line with no markers (60% of `--muted-foreground`). */
@@ -1616,8 +1858,10 @@ interface AxisExtent {
1616
1858
  max: number
1617
1859
  }
1618
1860
 
1619
- /** The label of a stack's total (highlight and tooltip). */
1620
- const STACK_TOTAL_LABEL = "合計"
1861
+ /** The label of a stack's total (highlight and tooltip) in `locale`: "Total" / 「合計」. Default the page's locale. */
1862
+ function stackTotalLabel(locale?: string): string {
1863
+ return chartText(locale).total
1864
+ }
1621
1865
 
1622
1866
  /** Per point, the sum of the stacked series; `null` where every series is missing. */
1623
1867
  function stackTotalsOf(values: readonly (readonly (number | null)[])[]): (number | null)[] {
@@ -1739,8 +1983,8 @@ function indexGrain(values: readonly unknown[]): PeriodGrain | null {
1739
1983
 
1740
1984
  /**
1741
1985
  * The tooltip line under the main series' row when a comparison series is drawn: the arrow, the signed change and the
1742
- * comparison's name, as `Delta` writes it ("↑ +12.4% 前年同月比"; a minus is U+2212). "比較なし 前年同月比" when
1743
- * either value is missing or the baseline is 0.
1986
+ * comparison's name, as `Delta` writes it ("↑ +12.4% vs. same month last year" / "↑ +12.4% 前年同月比"; a minus is U+2212).
1987
+ * "No comparison vs. same month last year" / "比較なし 前年同月比" when either value is missing or the baseline is 0.
1744
1988
  */
1745
1989
  function comparisonNote(
1746
1990
  current: number | null,
@@ -1748,16 +1992,18 @@ function comparisonNote(
1748
1992
  comparison: Pick<CartesianComparisonConfig, "vs" | "label">,
1749
1993
  grain?: PeriodGrain,
1750
1994
  format?: ValueFormat,
1751
- locale: string = DEFAULT_LOCALE,
1995
+ localeArg?: string,
1752
1996
  ): string {
1997
+ const locale = resolveLocale(localeArg)
1753
1998
  const result = computeComparison(
1754
1999
  current,
1755
2000
  comparison.vs === "custom"
1756
2001
  ? { previous, vs: "custom", label: comparison.label ?? "", grain }
1757
2002
  : { previous, vs: comparison.vs, label: comparison.label, grain },
2003
+ { locale },
1758
2004
  )
1759
2005
  const text = formatChange(result, defaultChangeDisplay(format), locale, format)
1760
- if (text === null) return `比較なし ${result.label}`
2006
+ if (text === null) return `${chartText(locale).noComparison} ${result.label}`
1761
2007
  const arrow = result.direction === "up" ? "↑" : result.direction === "down" ? "↓" : "→"
1762
2008
  return `${arrow} ${text.replace(/^-/, "−")} ${result.label}`
1763
2009
  }
@@ -1800,7 +2046,6 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
1800
2046
  toneColors,
1801
2047
  valueFormatter,
1802
2048
  axisFormatter = valueFormatter,
1803
- locale = DEFAULT_LOCALE,
1804
2049
  type = "default",
1805
2050
  layout = "horizontal",
1806
2051
  curveType = "linear",
@@ -1837,7 +2082,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
1837
2082
  chartOptions,
1838
2083
  reserveActions = false,
1839
2084
  } = cfg
1840
- const provisionalLabel = cfg.provisionalLabel ?? DEFAULT_PROVISIONAL_LABEL
2085
+ const locale = resolveLocale(cfg.locale)
2086
+ const text = chartText(locale)
2087
+ const provisionalLabel = cfg.provisionalLabel ?? text.provisional
1841
2088
 
1842
2089
  const stacked = kind !== "line" && type !== "default"
1843
2090
  const percent = kind !== "line" && type === "percent"
@@ -1986,9 +2233,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
1986
2233
  // `barOptions.targetKey` (a target per bar) takes over from the single `target` line.
1987
2234
  const perBarTarget = kind === "bar" && Boolean(targetKey)
1988
2235
  const hasTarget = !perBarTarget && target !== undefined && Number.isFinite(target)
1989
- const targetText = hasTarget ? (targetLabel ?? `目標 ${axisFormatter(target)}`) : ""
2236
+ const targetText = hasTarget ? (targetLabel ?? `${text.target} ${axisFormatter(target)}`) : ""
1990
2237
  const highlightText = (value: number) =>
1991
- shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${STACK_TOTAL_LABEL} ` : ""}${axisFormatter(value)}`
2238
+ shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${text.total} ` : ""}${axisFormatter(value)}`
1992
2239
  /** Whether `p`'s label sits within the target's value band and under the target label (at the right end). */
1993
2240
  const nearTargetLabel = (p: HighlightPoint, side: "under" | "over") => {
1994
2241
  if (!hasTarget || !plotWidth || kind === "bar" || labels.length < 2) return false
@@ -2014,7 +2261,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2014
2261
  // One series (or a stack, against its total) can be compared to a target; grouped series and 100% stacks cannot.
2015
2262
  const barTargets: (number | null)[] | null =
2016
2263
  perBarTarget && targetKey && !percent && (stacked || categories.length === 1) ? barTargetValues(data, targetKey) : null
2017
- const barTargetName = targetLabel ?? BAR_TARGET_LABEL
2264
+ const barTargetName = targetLabel ?? text.target
2018
2265
  const barTargetColor = labelColor ?? guideColor
2019
2266
 
2020
2267
  const markPointFor = (c: number, color: string | undefined) =>
@@ -2228,7 +2475,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2228
2475
  }
2229
2476
 
2230
2477
  /* -- forecast: dashed continuation (+ optional band) of one series */
2231
- const forecastLabel = forecast?.label ?? DEFAULT_FORECAST_LABEL
2478
+ const forecastLabel = forecast?.label ?? text.forecast
2232
2479
  const forecastBandAlpha = isDarkSurface(labelBackground) ? FORECAST_BAND_ALPHA_DARK : FORECAST_BAND_ALPHA_LIGHT
2233
2480
  const forecastValues: (number | null)[] = []
2234
2481
  if (forecast) {
@@ -2704,7 +2951,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2704
2951
  })
2705
2952
  }
2706
2953
  if (stackTotals && i <= lastActual && rows.length > 1) {
2707
- rows.push({ name: STACK_TOTAL_LABEL, color: "transparent", value: stackTotals[i] ?? null })
2954
+ rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
2708
2955
  }
2709
2956
  const provisional = provStart !== -1 && i >= provStart && i <= lastActual
2710
2957
  if (provisional) {
@@ -2720,19 +2967,19 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2720
2967
  ...noteFor(at),
2721
2968
  })
2722
2969
  })
2723
- if (stackTotals && rows.length > 1) rows.push({ name: STACK_TOTAL_LABEL, color: "transparent", value: stackTotals[i] ?? null })
2970
+ if (stackTotals && rows.length > 1) rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
2724
2971
  }
2725
2972
  const title = tooltipTitles[i] ?? labels[i] ?? ""
2726
2973
  const lines: string[] = []
2727
2974
  if (barTargets && i <= lastActual) {
2728
2975
  const total = stackTotals ? (stackTotals[i] ?? null) : (values[0]?.[i] ?? null)
2729
- const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter)
2976
+ const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter, locale)
2730
2977
  if (note) lines.push(note)
2731
2978
  }
2732
2979
  for (const note of notes) {
2733
2980
  if (note.to === undefined ? note.from === i : note.from <= i && i <= note.to) lines.push(note.label)
2734
2981
  }
2735
- return tooltipHtml(provisional ? `${title}(${provisionalLabel})` : title, rows, valueFormatter, lines)
2982
+ return tooltipHtml(provisional ? text.provisionalTitle(title, provisionalLabel) : title, rows, valueFormatter, lines, locale)
2736
2983
  },
2737
2984
  },
2738
2985
  xAxis: horizontalBars ? valueAxis : categoryAxis,
@@ -2824,8 +3071,6 @@ interface DonutOptionSlice {
2824
3071
  /** How many of the largest slices get an outside label (「自然検索 41%」) by default. */
2825
3072
  const DONUT_OUTSIDE_LABELS = 2
2826
3073
 
2827
- const DEFAULT_TOTAL_LABEL = "合計"
2828
-
2829
3074
  const sliceValue = (value: number) => (Number.isFinite(value) && value > 0 ? value : 0)
2830
3075
 
2831
3076
  /** A slice's share of `total` as text: whole percent, `<1%` for a sliver, `0%` for nothing. */
@@ -2858,15 +3103,16 @@ function donutLegendItems(slices: readonly DonutOptionSlice[]): DonutLegendItem[
2858
3103
  }
2859
3104
 
2860
3105
  /**
2861
- * The default center label: what is totalled (「合計」), the formatted total
3106
+ * The default center label: what is totalled (「合計」 / "Total" in `locale`, default the page's), the formatted total
2862
3107
  * (`amount`, the big line), the unit (a small line under it) and both as one
2863
3108
  * string (`value`, 「9.2万 セッション」).
2864
3109
  */
2865
3110
  function donutCenterText(
2866
3111
  total: number,
2867
3112
  valueFormatter: (value: number) => string,
2868
- { totalLabel = DEFAULT_TOTAL_LABEL, unit }: { totalLabel?: string; unit?: string } = {},
3113
+ { totalLabel, unit, locale }: { totalLabel?: string; unit?: string; locale?: string } = {},
2869
3114
  ): { label: string; value: string; amount: string; unit?: string } {
3115
+ totalLabel ??= chartText(locale).total
2870
3116
  const amount = valueFormatter(total)
2871
3117
  return { label: totalLabel, value: unit ? `${amount} ${unit}` : amount, amount, ...(unit ? { unit } : {}) }
2872
3118
  }
@@ -3094,8 +3340,10 @@ function hasLabel(labels: Readonly<Record<string, string>> | undefined, key: str
3094
3340
 
3095
3341
  /* ------------------------------------------------------------ provisional */
3096
3342
 
3097
- /** The default label for points that are not final yet. */
3098
- const DEFAULT_PROVISIONAL_LABEL = "速報"
3343
+ /** The default label for points that are not final yet, in `locale`: "Preliminary" / 「速報」. Default the page's locale. */
3344
+ function defaultProvisionalLabel(locale?: string): string {
3345
+ return chartText(locale).provisional
3346
+ }
3099
3347
 
3100
3348
  /**
3101
3349
  * Position of the first provisional point: the first entry of `indexValues`
@@ -3272,8 +3520,28 @@ export function resolveFunnelFormatter(
3272
3520
  valueFormatter: ((value: number) => string) | undefined,
3273
3521
  formatValue: ((value: number) => string) | undefined,
3274
3522
  format?: ValueFormat,
3523
+ locale?: string,
3275
3524
  ): (value: number) => string {
3276
- return resolveValueFormatter(valueFormatter ?? formatValue, format)
3525
+ return resolveValueFormatter(valueFormatter ?? formatValue, format, locale)
3526
+ }
3527
+
3528
+ const TEXT = {
3529
+ ja: {
3530
+ step: "ステップ",
3531
+ value: "値",
3532
+ previous: "前期",
3533
+ previousHeader: "前期",
3534
+ change: (label: string, points: string) => `(${label}比 ${points})`,
3535
+ funnel: "ファネル",
3536
+ },
3537
+ en: {
3538
+ step: "Step",
3539
+ value: "Value",
3540
+ previous: "prior period",
3541
+ previousHeader: "Prior period",
3542
+ change: (label: string, points: string) => `(vs. ${label} ${points})`,
3543
+ funnel: "Funnel",
3544
+ },
3277
3545
  }
3278
3546
 
3279
3547
  /**
@@ -3397,7 +3665,9 @@ function FunnelStepsImpl<T extends object>(
3397
3665
  ) {
3398
3666
  const [mounted, setMounted] = useState(false)
3399
3667
  const isHorizontal = direction === "horizontal"
3400
- const format = resolveFunnelFormatter(valueFormatter, formatValue, valueFormat)
3668
+ const locale = useLocale()
3669
+ const t = TEXT[uiLanguage(locale)]
3670
+ const format = resolveFunnelFormatter(valueFormatter, formatValue, valueFormat, locale)
3401
3671
 
3402
3672
  useEffect(() => {
3403
3673
  if (animate) setMounted(true)
@@ -3412,7 +3682,7 @@ function FunnelStepsImpl<T extends object>(
3412
3682
  const values = steps.map((s) => s.value)
3413
3683
  // `comparison` puts the rates on screen too.
3414
3684
  const previous = comparison?.values
3415
- const comparisonLabel = comparison?.label ?? "前期"
3685
+ const comparisonLabel = comparison?.label ?? t.previous
3416
3686
  const showRates = showConversion || previous !== undefined
3417
3687
  // Only with the rates on screen: the step behind the weakest rate (or, with `comparison`,
3418
3688
  // the rate that fell the most) is the one to look at.
@@ -3422,7 +3692,11 @@ function FunnelStepsImpl<T extends object>(
3422
3692
  "label",
3423
3693
  previous ? ["value", "previous"] : ["value"],
3424
3694
  format,
3425
- { label: labels?.[index] ?? "ステップ", value: (categories?.[0] && labels?.[categories[0]]) || "値", previous: comparisonLabel },
3695
+ {
3696
+ label: labels?.[index] ?? t.step,
3697
+ value: (categories?.[0] && labels?.[categories[0]]) || t.value,
3698
+ previous: comparison?.label ?? t.previousHeader,
3699
+ },
3426
3700
  )
3427
3701
 
3428
3702
  const list = (
@@ -3527,7 +3801,7 @@ function FunnelStepsImpl<T extends object>(
3527
3801
  → {computeConversionRate(steps[index + 1]!.value, step.value)}
3528
3802
  {previous && (
3529
3803
  <span data-slot="funnel-change" className="ml-1 tabular-nums">
3530
- ({comparisonLabel}比 {formatPoints(conversionChange(values, previous, index + 1))})
3804
+ {t.change(comparisonLabel, formatPoints(conversionChange(values, previous, index + 1), locale))}
3531
3805
  </span>
3532
3806
  )}
3533
3807
  </span>
@@ -3550,7 +3824,7 @@ function FunnelStepsImpl<T extends object>(
3550
3824
  // Loading / empty keep a fixed height; the steps themselves are as tall as they are.
3551
3825
  height={ready ? "auto" : FRAME_HEIGHT[size ?? "md"]}
3552
3826
  variant="list"
3553
- ariaLabel={ariaLabel ?? "ファネル"}
3827
+ ariaLabel={ariaLabel ?? t.funnel}
3554
3828
  plotRole="group"
3555
3829
  dataTable={dataTable}
3556
3830
  className={className}