@squadbase/vantage 0.6.2-dev1.54bde93 → 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 (87) hide show
  1. package/dist/{build-A23M4GCH.js → build-U7GO26HR.js} +3 -3
  2. package/dist/{build-A23M4GCH.js.map → build-U7GO26HR.js.map} +1 -1
  3. package/dist/{check-FBHT6W6J.js → check-MRVAIHG5.js} +3 -3
  4. package/dist/{check-FBHT6W6J.js.map → check-MRVAIHG5.js.map} +1 -1
  5. package/dist/{chunk-57GHR5LN.js → chunk-3RJEOP3J.js} +29 -18
  6. package/dist/chunk-3RJEOP3J.js.map +1 -0
  7. package/dist/{chunk-QOXXOFGL.js → chunk-77CISBMG.js} +994 -464
  8. package/dist/chunk-77CISBMG.js.map +1 -0
  9. package/dist/{chunk-JXFCSF3K.js → chunk-EG4PX726.js} +12 -7
  10. package/dist/chunk-EG4PX726.js.map +1 -0
  11. package/dist/{chunk-2N72FKLB.js → chunk-NZ5QIIG4.js} +33 -11
  12. package/dist/chunk-NZ5QIIG4.js.map +1 -0
  13. package/dist/{chunk-5C2SEDPH.js → chunk-PJ4SPXHS.js} +4 -4
  14. package/dist/{chunk-5C2SEDPH.js.map → chunk-PJ4SPXHS.js.map} +1 -1
  15. package/dist/{chunk-7S52M2SL.js → chunk-QNOX7WLZ.js} +3 -3
  16. package/dist/{chunk-7S52M2SL.js.map → chunk-QNOX7WLZ.js.map} +1 -1
  17. package/dist/{chunk-PDOBHMUQ.js → chunk-ROHNXP7J.js} +72 -3
  18. package/dist/chunk-ROHNXP7J.js.map +1 -0
  19. package/dist/{chunk-6OTTGAPO.js → chunk-TRMHNMMZ.js} +24 -7
  20. package/dist/chunk-TRMHNMMZ.js.map +1 -0
  21. package/dist/cli.js +4 -4
  22. package/dist/components/index.d.ts +49 -29
  23. package/dist/components/index.js +5 -5
  24. package/dist/{delta-text-CvwBGJ95.d.ts → delta-text-CbrOaW83.d.ts} +4 -2
  25. package/dist/{dev-H4I2JL63.js → dev-O42JFQ64.js} +3 -3
  26. package/dist/{dev-H4I2JL63.js.map → dev-O42JFQ64.js.map} +1 -1
  27. package/dist/doc/index.d.ts +14 -14
  28. package/dist/doc/index.js +198 -84
  29. package/dist/doc/index.js.map +1 -1
  30. package/dist/{doctor-B2O3GJDU.js → doctor-YD3XAAOT.js} +3 -3
  31. package/dist/{doctor-B2O3GJDU.js.map → doctor-YD3XAAOT.js.map} +1 -1
  32. package/dist/{format-U0cQl069.d.ts → format-DihQkL1N.d.ts} +17 -11
  33. package/dist/slide/index.d.ts +2 -2
  34. package/dist/slide/index.js +95 -44
  35. package/dist/slide/index.js.map +1 -1
  36. package/dist/table/index.js +6 -6
  37. package/dist/ui/index.d.ts +1 -1
  38. package/dist/ui/index.js +5 -5
  39. package/docs/en/agent-skills.md +1 -1
  40. package/docs/ja/agent-skills.md +1 -1
  41. package/package.json +2 -2
  42. package/registry/ui/app-shell.tsx +138 -1
  43. package/registry/ui/area-chart.tsx +344 -85
  44. package/registry/ui/attention-list.tsx +177 -15
  45. package/registry/ui/bar-chart.tsx +341 -85
  46. package/registry/ui/bar-list.tsx +332 -77
  47. package/registry/ui/box-plot-chart.tsx +405 -97
  48. package/registry/ui/category-bar.tsx +248 -40
  49. package/registry/ui/chart-frame.tsx +257 -46
  50. package/registry/ui/data-table.tsx +229 -37
  51. package/registry/ui/date-range-picker.tsx +190 -36
  52. package/registry/ui/delta.tsx +171 -12
  53. package/registry/ui/donut-chart.tsx +339 -84
  54. package/registry/ui/echart.tsx +142 -4
  55. package/registry/ui/filter-bar.tsx +179 -20
  56. package/registry/ui/funnel-steps.tsx +354 -80
  57. package/registry/ui/heatmap.tsx +293 -52
  58. package/registry/ui/histogram-chart.tsx +395 -97
  59. package/registry/ui/kpi-card.tsx +245 -44
  60. package/registry/ui/legend.tsx +330 -76
  61. package/registry/ui/line-chart.tsx +344 -85
  62. package/registry/ui/multi-select.tsx +159 -9
  63. package/registry/ui/page-shell.tsx +141 -3
  64. package/registry/ui/paired-chart.tsx +349 -90
  65. package/registry/ui/progress-circle.tsx +205 -39
  66. package/registry/ui/sample-data.tsx +153 -9
  67. package/registry/ui/scatter-chart.tsx +391 -100
  68. package/registry/ui/searchable-select.tsx +145 -7
  69. package/registry/ui/states.tsx +170 -13
  70. package/registry/ui/status-badge.tsx +193 -28
  71. package/registry/ui/tracker.tsx +205 -39
  72. package/registry/ui/trend-indicator.tsx +149 -6
  73. package/registry/ui/updated-at.tsx +170 -16
  74. package/registry/ui/waterfall-chart.tsx +415 -102
  75. package/skills/vantage-design/SKILL.md +25 -27
  76. package/skills/vantage-design/references/extract.md +10 -13
  77. package/skills/vantage-design/references/format.md +12 -173
  78. package/skills/vantage-design/references/presets.md +0 -10
  79. package/templates/AGENTS.md +29 -15
  80. package/templates/agents/page.md +1 -1
  81. package/templates/agents/slide.md +7 -1
  82. package/dist/chunk-2N72FKLB.js.map +0 -1
  83. package/dist/chunk-57GHR5LN.js.map +0 -1
  84. package/dist/chunk-6OTTGAPO.js.map +0 -1
  85. package/dist/chunk-JXFCSF3K.js.map +0 -1
  86. package/dist/chunk-PDOBHMUQ.js.map +0 -1
  87. package/dist/chunk-QOXXOFGL.js.map +0 -1
@@ -18,15 +18,148 @@ import { forwardRef, useMemo, type ForwardedRef, type ReactElement, type Ref } f
18
18
  import { ChartFrame } from "@squadbase/vantage/components"
19
19
  import { EChart } from "@squadbase/vantage/components"
20
20
  import type { ChartThreshold } from "@squadbase/vantage/components"
21
+ import { useSyncExternalStore } from "react"
21
22
  import { createContext, useCallback, useEffect, useRef, useState, type RefObject } from "react"
22
23
  import type { ChartTone } from "@squadbase/vantage/components"
23
- import { DEFAULT_LOCALE, EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
24
+ import { EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
24
25
  import type { ChartColor, ChartColors, ChartSeriesColor, ValueFormatter } from "@squadbase/vantage/components"
25
26
  import { type ReactNode } from "react"
26
27
  import type { EChartsOption } from "@squadbase/vantage/components"
27
28
  import { computeComparison, defaultChangeDisplay, formatChange, toneForValue, type ComparisonKind, type PeriodGrain } from "@squadbase/vantage/components"
29
+ import { DEFAULT_LOCALE } from "@squadbase/vantage/components"
28
30
  import type { ChartAnnotation, ChartCommonProps, ChartEventPayload, ChartForecast, ChartForecastRow, ChartHighlight, NumericKeys } from "@squadbase/vantage/components"
29
31
 
32
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
33
+
34
+ /**
35
+ * The display language of the parts and the defaults that follow it.
36
+ *
37
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
38
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
39
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
40
+ *
41
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
42
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
43
+ */
44
+
45
+ /** The language of the built-in wording: Japanese or English. */
46
+ type UiLanguage = "ja" | "en"
47
+
48
+ /** The locale used when `<html lang>` gives none (and on the server). */
49
+ const FALLBACK_LOCALE = "en-US"
50
+
51
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
52
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
53
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
54
+ }
55
+
56
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
57
+ function normalizeLocale(tag: string | null | undefined): string {
58
+ const value = tag?.trim()
59
+ if (!value) return FALLBACK_LOCALE
60
+ try {
61
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
62
+ } catch {
63
+ return FALLBACK_LOCALE
64
+ }
65
+ }
66
+
67
+ /**
68
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
69
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
70
+ */
71
+ function documentLocale(): string {
72
+ if (typeof document === "undefined") return FALLBACK_LOCALE
73
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
74
+ }
75
+
76
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
77
+ function resolveLocale(locale?: string | null): string {
78
+ return locale || documentLocale()
79
+ }
80
+
81
+ /** Picks the wording table of the locale's language. */
82
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
83
+ return table[uiLanguage(resolveLocale(locale))]
84
+ }
85
+
86
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
87
+ function defaultCurrency(locale?: string | null): string {
88
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
89
+ }
90
+
91
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
92
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
93
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
94
+ }
95
+
96
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
97
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
98
+
99
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
100
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
101
+
102
+ /**
103
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
104
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
105
+ * Monday for the rest.
106
+ */
107
+ function weekStartsOn(locale?: string | null): WeekDay {
108
+ const tag = normalizeLocale(resolveLocale(locale))
109
+ if (uiLanguage(tag) === "ja") return 1
110
+ try {
111
+ const info = new Intl.Locale(tag) as Intl.Locale & {
112
+ getWeekInfo?: () => { firstDay: number }
113
+ weekInfo?: { firstDay: number }
114
+ }
115
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
116
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
117
+ } catch {
118
+ // Fall through to the table below.
119
+ }
120
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
121
+ }
122
+
123
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
124
+
125
+ const listeners = new Set<() => void>()
126
+ let observer: MutationObserver | null = null
127
+
128
+ /** One observer on `<html lang>` for every subscribed part. */
129
+ function subscribe(onChange: () => void): () => void {
130
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
131
+ listeners.add(onChange)
132
+ if (!observer) {
133
+ observer = new MutationObserver(() => {
134
+ for (const listener of [...listeners]) listener()
135
+ })
136
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
137
+ }
138
+ return () => {
139
+ listeners.delete(onChange)
140
+ if (listeners.size === 0 && observer) {
141
+ observer.disconnect()
142
+ observer = null
143
+ }
144
+ }
145
+ }
146
+
147
+ const serverLocale = () => FALLBACK_LOCALE
148
+
149
+ /**
150
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
151
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
152
+ */
153
+ function useDocumentLocale(): string {
154
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
155
+ }
156
+
157
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
158
+ function useLocale(locale?: string | null): string {
159
+ const page = useDocumentLocale()
160
+ return locale || page
161
+ }
162
+
30
163
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
31
164
 
32
165
  /**
@@ -530,9 +663,10 @@ function resolveValueFormatter(
530
663
  function resolveAxisFormatter(
531
664
  valueFormatter: ValueFormatter | undefined,
532
665
  format: ValueFormat | undefined,
533
- locale: string = DEFAULT_LOCALE,
666
+ localeArg?: string,
534
667
  ): ValueFormatter {
535
668
  if (valueFormatter) return valueFormatter
669
+ const locale = resolveLocale(localeArg)
536
670
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
537
671
  if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
538
672
  const currency = "currency" in spec ? spec.currency : undefined
@@ -541,40 +675,75 @@ function resolveAxisFormatter(
541
675
  return (value) => formatValue(value, short, locale)
542
676
  }
543
677
 
544
- /** The unit words of a ja number (`1.2万` / `3.4億` / `1兆`) and their sizes, largest first. */
545
- const JA_UNITS = [
546
- { char: "兆", size: 1e12 },
547
- { char: "億", size: 1e8 },
548
- { char: "万", size: 1e4 },
549
- ] as const
678
+ interface CompactUnit {
679
+ token: string
680
+ size: number
681
+ }
682
+
683
+ const compactUnitCache = new Map<string, CompactUnit[] | null>()
684
+
685
+ /**
686
+ * The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
687
+ * `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
688
+ * word (`1,5 Mio.`), where a token cannot be swapped in place.
689
+ */
690
+ function compactUnits(locale: string): CompactUnit[] | null {
691
+ const cached = compactUnitCache.get(locale)
692
+ if (cached !== undefined) return cached
693
+ let units: CompactUnit[] | null = []
694
+ try {
695
+ const compact = new Intl.NumberFormat(locale, { notation: "compact" })
696
+ const seen = new Set<string>()
697
+ for (let exp = 3; exp <= 15; exp++) {
698
+ const parts = compact.formatToParts(10 ** exp)
699
+ const at = parts.findIndex((p) => p.type === "compact")
700
+ if (at === -1) continue
701
+ if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
702
+ units = null
703
+ break
704
+ }
705
+ const token = parts[at]!.value
706
+ if (seen.has(token)) continue
707
+ seen.add(token)
708
+ units.push({ token, size: 10 ** exp })
709
+ }
710
+ } catch {
711
+ units = null
712
+ }
713
+ const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
714
+ compactUnitCache.set(locale, sorted)
715
+ return sorted
716
+ }
550
717
 
551
- const JA_NUMBER_TOKEN_RE = /\d[\d,]*(?:\.\d+)?(?:兆|億|万)?/
718
+ const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
552
719
 
553
720
  /**
554
721
  * The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
555
- * that largest tick, so 万 and 億 never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`).
556
- * Only the ja short forms (`compact` / `number` / `currency`, no `valueFormatter`) have units to align;
557
- * any other format — and an axis whose largest tick is under 1万 — keeps `resolveAxisFormatter`'s output as it is.
722
+ * that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
723
+ * `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
724
+ * no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
725
+ * unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
558
726
  */
559
727
  function resolveAxisTickFormatter(
560
728
  valueFormatter: ValueFormatter | undefined,
561
729
  format: ValueFormat | undefined,
562
- locale: string = DEFAULT_LOCALE,
730
+ localeArg?: string,
563
731
  ): (top: number) => ValueFormatter {
732
+ const locale = resolveLocale(localeArg)
564
733
  const base = resolveAxisFormatter(valueFormatter, format, locale)
565
734
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
566
- const aligns =
567
- !valueFormatter && locale.toLowerCase().startsWith("ja") && (spec.type === "compact" || spec.type === "number" || spec.type === "currency")
568
- if (!aligns) return () => base
735
+ const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
736
+ if (!units) return () => base
569
737
  const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
738
+ const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
570
739
  return (top) => {
571
- const unit = JA_UNITS.find((u) => Math.abs(top) >= u.size)
740
+ const unit = units.find((u) => Math.abs(top) >= u.size)
572
741
  if (!unit) return base
573
742
  return (value) => {
574
743
  const text = base(value)
575
744
  if (value === 0 || !Number.isFinite(value)) return text
576
- const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.char}`
577
- return text.replace(JA_NUMBER_TOKEN_RE, () => scaled)
745
+ const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
746
+ return text.replace(numberToken, () => scaled)
578
747
  }
579
748
  }
580
749
  }
@@ -603,8 +772,20 @@ function clamp(value: number, min: number, max: number): number {
603
772
  /** How many categories a chart colours individually; the rest fold into「その他」. */
604
773
  const MAX_CHART_SERIES = 5
605
774
 
606
- /** The series name the folded categories get. */
607
- const OTHER_SERIES_LABEL = "その他"
775
+ /** The series name the folded categories get, by language. */
776
+ const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
777
+
778
+ /** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
779
+ function otherSeriesLabel(locale?: string): string {
780
+ return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
781
+ }
782
+
783
+ const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
784
+
785
+ /** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
786
+ function isOtherName(name: string | null | undefined): boolean {
787
+ return typeof name === "string" && OTHER_NAMES.has(name)
788
+ }
608
789
 
609
790
  interface AggregatedItems<Item extends { name: string; value: number }> {
610
791
  /** The `n` largest items, in their given order. All items when there are `n` or fewer. */
@@ -624,7 +805,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
624
805
  function aggregateTopN<Item extends { name: string; value: number }>(
625
806
  items: readonly Item[],
626
807
  n: number = MAX_CHART_SERIES,
627
- otherLabel: string = OTHER_SERIES_LABEL,
808
+ otherLabel: string = otherSeriesLabel(),
628
809
  ): AggregatedItems<Item> {
629
810
  if (items.length <= n) return { top: [...items], rest: [], other: null }
630
811
  const ranked = items
@@ -653,7 +834,7 @@ function aggregateSeries(
653
834
  data: readonly Record<string, unknown>[],
654
835
  categories: readonly string[],
655
836
  n: number = MAX_CHART_SERIES,
656
- otherLabel: string = OTHER_SERIES_LABEL,
837
+ otherLabel: string = otherSeriesLabel(),
657
838
  ): AggregatedSeries {
658
839
  const totals = categories.map((name) => ({
659
840
  name,
@@ -685,12 +866,14 @@ function aggregateSeriesKeeping(
685
866
  categories: readonly string[],
686
867
  keep: string | undefined,
687
868
  n: number = MAX_CHART_SERIES,
869
+ otherLabel: string = otherSeriesLabel(),
688
870
  ): AggregatedSeries {
689
- if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
871
+ if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
690
872
  const aggregated = aggregateSeries(
691
873
  data,
692
874
  categories.filter((c) => c !== keep),
693
875
  n,
876
+ otherLabel,
694
877
  )
695
878
  return { ...aggregated, categories: [...aggregated.categories, keep] }
696
879
  }
@@ -726,7 +909,7 @@ function seriesColorsWithComparison(
726
909
  * Colour per drawn series: the record's token for that category, else the
727
910
  * array's token at the category's position in `allCategories` (cycled), else
728
911
  * the token palette by drawn position. `otherCategory` — and a category the
729
- * data itself names「その他」(`OTHER_SERIES_LABEL`) — always gets `otherColor`.
912
+ * data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
730
913
  */
731
914
  function seriesColorsFor(
732
915
  categories: readonly string[],
@@ -738,7 +921,7 @@ function seriesColorsFor(
738
921
  const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
739
922
  const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
740
923
  return categories.map((category, i) => {
741
- if ((otherCategory !== null && category === otherCategory) || category === OTHER_SERIES_LABEL) return otherColor
924
+ if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
742
925
  const fromRecord = record?.[category]
743
926
  if (fromRecord) return fromRecord
744
927
  if (list && list.length > 0) {
@@ -785,7 +968,7 @@ interface DonutSlicesConfig {
785
968
  */
786
969
  function donutSlices(
787
970
  data: readonly Record<string, unknown>[],
788
- { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = OTHER_SERIES_LABEL }: DonutSlicesConfig,
971
+ { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
789
972
  ): DonutSlice[] {
790
973
  const items = data
791
974
  .map((row, i) => {
@@ -798,7 +981,8 @@ function donutSlices(
798
981
  const slices: DonutSlice[] = top.map((item, rank) => ({
799
982
  name: item.name,
800
983
  value: item.value,
801
- color: palette[rank % palette.length]!,
984
+ // A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
985
+ color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
802
986
  isOther: false,
803
987
  row: item.row,
804
988
  }))
@@ -871,7 +1055,7 @@ interface BarListRowsConfig extends BarListOptions {
871
1055
  */
872
1056
  function barListRows<Row extends object>(
873
1057
  data: readonly Row[],
874
- { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = OTHER_SERIES_LABEL, exclude }: BarListRowsConfig = {},
1058
+ { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
875
1059
  ): BarListRow<Row>[] {
876
1060
  const items = data.map((row): BarListRow<Row> => {
877
1061
  const cells = row as Record<string, unknown>
@@ -917,19 +1101,21 @@ const BAR_LIST_OTHER_TINT = 30
917
1101
 
918
1102
  /**
919
1103
  * The fill of each BarList row: all real rows in the same tint of `color`
920
- * (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
1104
+ * (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
921
1105
  * (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
922
1106
  * ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
923
1107
  * for that row. A tint, not the solid token, so the label printed over the bar
924
1108
  * keeps its contrast.
925
1109
  */
926
1110
  function barListFills(
927
- rows: readonly Pick<BarListRow, "isOther" | "color">[],
1111
+ rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
928
1112
  color: ChartColor,
929
1113
  otherColor: string,
930
1114
  ): string[] {
931
1115
  return rows.map((row) =>
932
- row.isOther ? tintCss(otherColor, BAR_LIST_OTHER_TINT) : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
1116
+ row.isOther || isOtherName(row.name)
1117
+ ? tintCss(otherColor, BAR_LIST_OTHER_TINT)
1118
+ : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
933
1119
  )
934
1120
  }
935
1121
 
@@ -1145,11 +1331,30 @@ interface ChartSummaryInput {
1145
1331
  format?: ValueFormat
1146
1332
  /** Wins over `format`. */
1147
1333
  valueFormatter?: ValueFormatter
1334
+ /** Language of the sentence. Default the page's locale (`<html lang>`). */
1335
+ locale?: string
1336
+ }
1337
+
1338
+ const SUMMARY_TEXT = {
1339
+ ja: {
1340
+ up: "、前の点より増加",
1341
+ down: "、前の点より減少",
1342
+ flat: "、前の点と横ばい",
1343
+ latest: (label: string, value: string) => `最新 ${label} は ${value}`,
1344
+ top: (name: string, share: string) => `最大は ${name}(${share})`,
1345
+ },
1346
+ en: {
1347
+ up: ", up from the previous point",
1348
+ down: ", down from the previous point",
1349
+ flat: ", flat from the previous point",
1350
+ latest: (label: string, value: string) => `Latest ${label}: ${value}`,
1351
+ top: (name: string, share: string) => `Largest: ${name} (${share})`,
1352
+ },
1148
1353
  }
1149
1354
 
1150
1355
  /**
1151
1356
  * One-sentence summary built from the rows, for `summary="auto"`:
1152
- * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」. The direction is
1357
+ * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
1153
1358
  * computed from the last two non-empty points of the first series — never
1154
1359
  * written by hand, so it cannot contradict the data. `undefined` when there is
1155
1360
  * no usable point.
@@ -1165,30 +1370,59 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
1165
1370
  }
1166
1371
  const latest = points[points.length - 1]
1167
1372
  if (!latest) return undefined
1168
- const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact")
1373
+ const locale = resolveLocale(input.locale)
1374
+ const t = SUMMARY_TEXT[uiLanguage(locale)]
1375
+ const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
1169
1376
  const prev = points[points.length - 2]
1170
- const direction = !prev ? "" : latest.value > prev.value ? "、前の点より増加" : latest.value < prev.value ? "、前の点より減少" : "、前の点と横ばい"
1377
+ const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
1171
1378
  const head = input.title ? `${input.title}: ` : ""
1172
- return `${head}最新 ${latest.label} は ${text(latest.value)}${direction}`
1379
+ return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
1173
1380
  }
1174
1381
 
1175
1382
  /**
1176
- * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
1177
- * (largest first). `undefined` when there is nothing to describe.
1383
+ * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
1384
+ * (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
1178
1385
  */
1179
1386
  function buildShareSummary(
1180
1387
  slices: readonly { name: string; value: number }[],
1181
1388
  title?: string,
1389
+ locale?: string,
1182
1390
  ): string | undefined {
1183
1391
  const total = slices.reduce((sum, s) => sum + s.value, 0)
1184
1392
  const top = slices[0]
1185
1393
  if (!top || total <= 0) return undefined
1186
1394
  const share = `${Math.round((top.value / total) * 1000) / 10}%`
1187
- return `${title ? `${title}: ` : ""}最大は ${top.name}(${share})`
1395
+ return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
1188
1396
  }
1189
1397
 
1190
1398
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-options.ts (internal to @squadbase/vantage) ---------- */
1191
1399
 
1400
+ /** The built-in words of the option builders, by language. */
1401
+ const CHART_TEXT = {
1402
+ ja: {
1403
+ target: "目標",
1404
+ forecast: "予測",
1405
+ total: "合計",
1406
+ provisional: "速報",
1407
+ noComparison: "比較なし",
1408
+ share: (share: string) => `(${share})`,
1409
+ targetRate: (rate: string) => `(達成率 ${rate})`,
1410
+ provisionalTitle: (title: string, label: string) => `${title}(${label})`,
1411
+ },
1412
+ en: {
1413
+ target: "Target",
1414
+ forecast: "Forecast",
1415
+ total: "Total",
1416
+ provisional: "Preliminary",
1417
+ noComparison: "No comparison",
1418
+ share: (share: string) => ` (${share})`,
1419
+ targetRate: (rate: string) => ` (${rate} achieved)`,
1420
+ provisionalTitle: (title: string, label: string) => `${title} (${label})`,
1421
+ },
1422
+ } satisfies Record<UiLanguage, unknown>
1423
+
1424
+ const chartText = (locale?: string) => CHART_TEXT[uiLanguage(resolveLocale(locale))]
1425
+
1192
1426
  type CartesianChartKind = "area" | "bar" | "line"
1193
1427
  type CartesianStackType = "default" | "stacked" | "percent"
1194
1428
  type CartesianCurveType = "linear" | "smooth" | "step"
@@ -1373,7 +1607,7 @@ interface CartesianChartConfig {
1373
1607
  * all ticks read in one unit (¥0.5億 / ¥1億). Default: `axisFormatter` for every tick.
1374
1608
  */
1375
1609
  axisTicks?: (top: number) => (value: number) => string
1376
- /** Locale of the time-axis labels. Default `"ja-JP"`. */
1610
+ /** Locale of the time-axis labels and the built-in words (total, target, forecast). Default the page's (`<html lang>`). */
1377
1611
  locale?: string
1378
1612
  type?: CartesianStackType
1379
1613
  /** Bar only. `"vertical"` lays the categories down the y axis (horizontal bars). */
@@ -1611,11 +1845,12 @@ function timeAxisLabelStep(values: readonly unknown[], maxLabels: number = TIME_
1611
1845
  */
1612
1846
  function timeAxisLabels(
1613
1847
  values: readonly unknown[],
1614
- locale: string = DEFAULT_LOCALE,
1848
+ localeArg?: string,
1615
1849
  shown: (i: number) => boolean = () => true,
1616
1850
  ): string[] | null {
1617
1851
  const parsed = parseTimeIndex(values)
1618
1852
  if (!parsed) return null
1853
+ const locale = resolveLocale(localeArg)
1619
1854
  const { granularity, times } = parsed
1620
1855
  let prevYear: number | null = null
1621
1856
  return times.map((t, i) => {
@@ -1638,9 +1873,10 @@ function timeAxisLabels(
1638
1873
  }
1639
1874
 
1640
1875
  /** The tooltip titles for time-like x values (`2025年9月`, `2025年9月1日`), or `null` when they are not dates. */
1641
- function timeTooltipLabels(values: readonly unknown[], locale: string = DEFAULT_LOCALE): string[] | null {
1876
+ function timeTooltipLabels(values: readonly unknown[], localeArg?: string): string[] | null {
1642
1877
  const parsed = parseTimeIndex(values)
1643
1878
  if (!parsed) return null
1879
+ const locale = resolveLocale(localeArg)
1644
1880
  const options: Intl.DateTimeFormatOptions =
1645
1881
  parsed.granularity === "year"
1646
1882
  ? { year: "numeric" }
@@ -1780,7 +2016,9 @@ function tooltipHtml(
1780
2016
  rows: readonly TooltipRow[],
1781
2017
  valueFormatter: (value: number) => string,
1782
2018
  notes: readonly string[] = [],
2019
+ locale?: string,
1783
2020
  ): string {
2021
+ const t = chartText(locale)
1784
2022
  const lines = rows.map(
1785
2023
  (row) =>
1786
2024
  `<div style="display:flex;align-items:center;gap:8px;line-height:20px">` +
@@ -1789,7 +2027,7 @@ function tooltipHtml(
1789
2027
  `<span style="font-weight:600;font-variant-numeric:tabular-nums;margin-left:12px">${
1790
2028
  row.value === null ? EMPTY_VALUE : escapeHtml(valueFormatter(row.value))
1791
2029
  }${
1792
- row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">(${escapeHtml(formatShare(row.share))})</span>`
2030
+ row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">${escapeHtml(t.share(formatShare(row.share)))}</span>`
1793
2031
  }</span></div>` +
1794
2032
  (row.note === undefined
1795
2033
  ? ""
@@ -1803,8 +2041,10 @@ function tooltipHtml(
1803
2041
 
1804
2042
  /* ------------------------------------------------- per-bar targets / notes */
1805
2043
 
1806
- /** Default name of the per-bar target marker (legend, tooltip). */
1807
- const BAR_TARGET_LABEL = "目標"
2044
+ /** Default name of the per-bar target marker (legend, tooltip) in `locale`: "Target" / 「目標」. Default the page's locale. */
2045
+ function barTargetLabel(locale?: string): string {
2046
+ return chartText(locale).target
2047
+ }
1808
2048
 
1809
2049
  /** How far (px) the target marker reaches past each side of its bar. */
1810
2050
  const TARGET_MARKER_OVERHANG = 4
@@ -1855,16 +2095,17 @@ function achievementRate(value: number | null | undefined, target: number | null
1855
2095
  return (value / target) * 100
1856
2096
  }
1857
2097
 
1858
- /** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」. `null` without a target. */
2098
+ /** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」 / "Target $12M (85.3% achieved)". `null` without a target. */
1859
2099
  function barTargetNote(
1860
2100
  label: string,
1861
2101
  value: number | null,
1862
2102
  target: number | null,
1863
2103
  valueFormatter: (value: number) => string,
2104
+ locale?: string,
1864
2105
  ): string | null {
1865
2106
  if (target === null) return null
1866
2107
  const rate = achievementRate(value, target)
1867
- return `${label} ${valueFormatter(target)}${rate === null ? "" : `(達成率 ${formatShare(rate)})`}`
2108
+ return `${label} ${valueFormatter(target)}${rate === null ? "" : chartText(locale).targetRate(formatShare(rate))}`
1868
2109
  }
1869
2110
 
1870
2111
  /** Half the width of a bar in category-axis units, from `barCategoryGap` (`"20%"`; a px gap or none → ECharts' 20%). */
@@ -1956,11 +2197,12 @@ const TARGET_LEGEND_SERIES_ID = "__target-legend"
1956
2197
  /** A flat tick (8×8 legend box, aspect kept): the per-bar target's legend icon. */
1957
2198
  const TARGET_LEGEND_ICON = "path://M0,0 L100,0 L100,24 L0,24 Z"
1958
2199
 
1959
- const DEFAULT_FORECAST_LABEL = "予測"
1960
-
1961
- /** 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. */
1962
- function forecastBoundaryText(label: string = DEFAULT_FORECAST_LABEL): string {
1963
- return `${label} →`
2200
+ /**
2201
+ * 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.
2202
+ * Without a `label`, "Forecast" / 「予測」 in `locale` (default the page's).
2203
+ */
2204
+ function forecastBoundaryText(label?: string, locale?: string): string {
2205
+ return `${label ?? chartText(locale).forecast} →`
1964
2206
  }
1965
2207
 
1966
2208
  /** A comparison series: a thin, muted, solid line with no markers (60% of `--muted-foreground`). */
@@ -2000,8 +2242,10 @@ interface AxisExtent {
2000
2242
  max: number
2001
2243
  }
2002
2244
 
2003
- /** The label of a stack's total (highlight and tooltip). */
2004
- const STACK_TOTAL_LABEL = "合計"
2245
+ /** The label of a stack's total (highlight and tooltip) in `locale`: "Total" / 「合計」. Default the page's locale. */
2246
+ function stackTotalLabel(locale?: string): string {
2247
+ return chartText(locale).total
2248
+ }
2005
2249
 
2006
2250
  /** Per point, the sum of the stacked series; `null` where every series is missing. */
2007
2251
  function stackTotalsOf(values: readonly (readonly (number | null)[])[]): (number | null)[] {
@@ -2123,8 +2367,8 @@ function indexGrain(values: readonly unknown[]): PeriodGrain | null {
2123
2367
 
2124
2368
  /**
2125
2369
  * The tooltip line under the main series' row when a comparison series is drawn: the arrow, the signed change and the
2126
- * comparison's name, as `Delta` writes it ("↑ +12.4% 前年同月比"; a minus is U+2212). "比較なし 前年同月比" when
2127
- * either value is missing or the baseline is 0.
2370
+ * comparison's name, as `Delta` writes it ("↑ +12.4% vs. same month last year" / "↑ +12.4% 前年同月比"; a minus is U+2212).
2371
+ * "No comparison vs. same month last year" / "比較なし 前年同月比" when either value is missing or the baseline is 0.
2128
2372
  */
2129
2373
  function comparisonNote(
2130
2374
  current: number | null,
@@ -2132,16 +2376,18 @@ function comparisonNote(
2132
2376
  comparison: Pick<CartesianComparisonConfig, "vs" | "label">,
2133
2377
  grain?: PeriodGrain,
2134
2378
  format?: ValueFormat,
2135
- locale: string = DEFAULT_LOCALE,
2379
+ localeArg?: string,
2136
2380
  ): string {
2381
+ const locale = resolveLocale(localeArg)
2137
2382
  const result = computeComparison(
2138
2383
  current,
2139
2384
  comparison.vs === "custom"
2140
2385
  ? { previous, vs: "custom", label: comparison.label ?? "", grain }
2141
2386
  : { previous, vs: comparison.vs, label: comparison.label, grain },
2387
+ { locale },
2142
2388
  )
2143
2389
  const text = formatChange(result, defaultChangeDisplay(format), locale, format)
2144
- if (text === null) return `比較なし ${result.label}`
2390
+ if (text === null) return `${chartText(locale).noComparison} ${result.label}`
2145
2391
  const arrow = result.direction === "up" ? "↑" : result.direction === "down" ? "↓" : "→"
2146
2392
  return `${arrow} ${text.replace(/^-/, "−")} ${result.label}`
2147
2393
  }
@@ -2184,7 +2430,6 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2184
2430
  toneColors,
2185
2431
  valueFormatter,
2186
2432
  axisFormatter = valueFormatter,
2187
- locale = DEFAULT_LOCALE,
2188
2433
  type = "default",
2189
2434
  layout = "horizontal",
2190
2435
  curveType = "linear",
@@ -2221,7 +2466,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2221
2466
  chartOptions,
2222
2467
  reserveActions = false,
2223
2468
  } = cfg
2224
- const provisionalLabel = cfg.provisionalLabel ?? DEFAULT_PROVISIONAL_LABEL
2469
+ const locale = resolveLocale(cfg.locale)
2470
+ const text = chartText(locale)
2471
+ const provisionalLabel = cfg.provisionalLabel ?? text.provisional
2225
2472
 
2226
2473
  const stacked = kind !== "line" && type !== "default"
2227
2474
  const percent = kind !== "line" && type === "percent"
@@ -2370,9 +2617,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2370
2617
  // `barOptions.targetKey` (a target per bar) takes over from the single `target` line.
2371
2618
  const perBarTarget = kind === "bar" && Boolean(targetKey)
2372
2619
  const hasTarget = !perBarTarget && target !== undefined && Number.isFinite(target)
2373
- const targetText = hasTarget ? (targetLabel ?? `目標 ${axisFormatter(target)}`) : ""
2620
+ const targetText = hasTarget ? (targetLabel ?? `${text.target} ${axisFormatter(target)}`) : ""
2374
2621
  const highlightText = (value: number) =>
2375
- shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${STACK_TOTAL_LABEL} ` : ""}${axisFormatter(value)}`
2622
+ shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${text.total} ` : ""}${axisFormatter(value)}`
2376
2623
  /** Whether `p`'s label sits within the target's value band and under the target label (at the right end). */
2377
2624
  const nearTargetLabel = (p: HighlightPoint, side: "under" | "over") => {
2378
2625
  if (!hasTarget || !plotWidth || kind === "bar" || labels.length < 2) return false
@@ -2398,7 +2645,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2398
2645
  // One series (or a stack, against its total) can be compared to a target; grouped series and 100% stacks cannot.
2399
2646
  const barTargets: (number | null)[] | null =
2400
2647
  perBarTarget && targetKey && !percent && (stacked || categories.length === 1) ? barTargetValues(data, targetKey) : null
2401
- const barTargetName = targetLabel ?? BAR_TARGET_LABEL
2648
+ const barTargetName = targetLabel ?? text.target
2402
2649
  const barTargetColor = labelColor ?? guideColor
2403
2650
 
2404
2651
  const markPointFor = (c: number, color: string | undefined) =>
@@ -2612,7 +2859,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2612
2859
  }
2613
2860
 
2614
2861
  /* -- forecast: dashed continuation (+ optional band) of one series */
2615
- const forecastLabel = forecast?.label ?? DEFAULT_FORECAST_LABEL
2862
+ const forecastLabel = forecast?.label ?? text.forecast
2616
2863
  const forecastBandAlpha = isDarkSurface(labelBackground) ? FORECAST_BAND_ALPHA_DARK : FORECAST_BAND_ALPHA_LIGHT
2617
2864
  const forecastValues: (number | null)[] = []
2618
2865
  if (forecast) {
@@ -3088,7 +3335,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
3088
3335
  })
3089
3336
  }
3090
3337
  if (stackTotals && i <= lastActual && rows.length > 1) {
3091
- rows.push({ name: STACK_TOTAL_LABEL, color: "transparent", value: stackTotals[i] ?? null })
3338
+ rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
3092
3339
  }
3093
3340
  const provisional = provStart !== -1 && i >= provStart && i <= lastActual
3094
3341
  if (provisional) {
@@ -3104,19 +3351,19 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
3104
3351
  ...noteFor(at),
3105
3352
  })
3106
3353
  })
3107
- if (stackTotals && rows.length > 1) rows.push({ name: STACK_TOTAL_LABEL, color: "transparent", value: stackTotals[i] ?? null })
3354
+ if (stackTotals && rows.length > 1) rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
3108
3355
  }
3109
3356
  const title = tooltipTitles[i] ?? labels[i] ?? ""
3110
3357
  const lines: string[] = []
3111
3358
  if (barTargets && i <= lastActual) {
3112
3359
  const total = stackTotals ? (stackTotals[i] ?? null) : (values[0]?.[i] ?? null)
3113
- const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter)
3360
+ const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter, locale)
3114
3361
  if (note) lines.push(note)
3115
3362
  }
3116
3363
  for (const note of notes) {
3117
3364
  if (note.to === undefined ? note.from === i : note.from <= i && i <= note.to) lines.push(note.label)
3118
3365
  }
3119
- return tooltipHtml(provisional ? `${title}(${provisionalLabel})` : title, rows, valueFormatter, lines)
3366
+ return tooltipHtml(provisional ? text.provisionalTitle(title, provisionalLabel) : title, rows, valueFormatter, lines, locale)
3120
3367
  },
3121
3368
  },
3122
3369
  xAxis: horizontalBars ? valueAxis : categoryAxis,
@@ -3208,8 +3455,6 @@ interface DonutOptionSlice {
3208
3455
  /** How many of the largest slices get an outside label (「自然検索 41%」) by default. */
3209
3456
  const DONUT_OUTSIDE_LABELS = 2
3210
3457
 
3211
- const DEFAULT_TOTAL_LABEL = "合計"
3212
-
3213
3458
  const sliceValue = (value: number) => (Number.isFinite(value) && value > 0 ? value : 0)
3214
3459
 
3215
3460
  /** A slice's share of `total` as text: whole percent, `<1%` for a sliver, `0%` for nothing. */
@@ -3242,15 +3487,16 @@ function donutLegendItems(slices: readonly DonutOptionSlice[]): DonutLegendItem[
3242
3487
  }
3243
3488
 
3244
3489
  /**
3245
- * The default center label: what is totalled (「合計」), the formatted total
3490
+ * The default center label: what is totalled (「合計」 / "Total" in `locale`, default the page's), the formatted total
3246
3491
  * (`amount`, the big line), the unit (a small line under it) and both as one
3247
3492
  * string (`value`, 「9.2万 セッション」).
3248
3493
  */
3249
3494
  function donutCenterText(
3250
3495
  total: number,
3251
3496
  valueFormatter: (value: number) => string,
3252
- { totalLabel = DEFAULT_TOTAL_LABEL, unit }: { totalLabel?: string; unit?: string } = {},
3497
+ { totalLabel, unit, locale }: { totalLabel?: string; unit?: string; locale?: string } = {},
3253
3498
  ): { label: string; value: string; amount: string; unit?: string } {
3499
+ totalLabel ??= chartText(locale).total
3254
3500
  const amount = valueFormatter(total)
3255
3501
  return { label: totalLabel, value: unit ? `${amount} ${unit}` : amount, amount, ...(unit ? { unit } : {}) }
3256
3502
  }
@@ -3478,8 +3724,10 @@ function hasLabel(labels: Readonly<Record<string, string>> | undefined, key: str
3478
3724
 
3479
3725
  /* ------------------------------------------------------------ provisional */
3480
3726
 
3481
- /** The default label for points that are not final yet. */
3482
- const DEFAULT_PROVISIONAL_LABEL = "速報"
3727
+ /** The default label for points that are not final yet, in `locale`: "Preliminary" / 「速報」. Default the page's locale. */
3728
+ function defaultProvisionalLabel(locale?: string): string {
3729
+ return chartText(locale).provisional
3730
+ }
3483
3731
 
3484
3732
  /**
3485
3733
  * Position of the first provisional point: the first entry of `indexValues`
@@ -3531,6 +3779,11 @@ export interface BarChartProps<T extends Record<string, unknown> = Record<string
3531
3779
  barCategoryGap?: string | number
3532
3780
  }
3533
3781
 
3782
+ const TEXT = {
3783
+ ja: { item: "項目", chart: (categories: readonly string[]) => `${categories.join("・")}の棒グラフ` },
3784
+ en: { item: "Item", chart: (categories: readonly string[]) => `Bar chart of ${categories.join(", ")}` },
3785
+ }
3786
+
3534
3787
  /** Rows not given yet (a draft fills props in steps): draw the empty state, keep the same array between renders. */
3535
3788
  const NO_ROWS: never[] = []
3536
3789
 
@@ -3583,12 +3836,14 @@ function BarChartImpl<T extends Record<string, unknown>>(
3583
3836
  ) {
3584
3837
  // Highlight labels are backed by the surface the chart sits on (a card, the page).
3585
3838
  const { ref: frameRef, surface, width: plotWidth, tokens } = useChartSurface(ref)
3839
+ const locale = useLocale()
3840
+ const t = TEXT[uiLanguage(locale)]
3586
3841
  // Ticks and on-plot labels read short (¥1億); the tooltip reads the value as `format` says.
3587
- const tooltipFormatter = useMemo(() => resolveValueFormatter(valueFormatter, format), [valueFormatter, format])
3588
- const axisFormatter = useMemo(() => resolveAxisFormatter(valueFormatter, format), [valueFormatter, format])
3842
+ const tooltipFormatter = useMemo(() => resolveValueFormatter(valueFormatter, format, locale), [valueFormatter, format, locale])
3843
+ const axisFormatter = useMemo(() => resolveAxisFormatter(valueFormatter, format, locale), [valueFormatter, format, locale])
3589
3844
  // Ticks share one unit — the largest tick's (¥0.5億 / ¥1億).
3590
- const axisTicks = useMemo(() => resolveAxisTickFormatter(valueFormatter, format), [valueFormatter, format])
3591
- const series = useMemo(() => aggregateSeries(data, categories), [data, categories])
3845
+ const axisTicks = useMemo(() => resolveAxisTickFormatter(valueFormatter, format, locale), [valueFormatter, format, locale])
3846
+ const series = useMemo(() => aggregateSeries(data, categories, undefined, otherSeriesLabel(locale)), [data, categories, locale])
3592
3847
  const palette = useMemo(
3593
3848
  () =>
3594
3849
  resolveChartColors(
@@ -3650,24 +3905,25 @@ function BarChartImpl<T extends Record<string, unknown>>(
3650
3905
  plotWidth,
3651
3906
  plotHeight: typeof height === "number" ? height : undefined,
3652
3907
  chartOptions,
3908
+ locale,
3653
3909
  }),
3654
3910
  [
3655
3911
  series, index, palette, labels, thresholds, toneColors, tooltipFormatter, axisFormatter, axisTicks, stackType, barLayout, gap, showZeroValues,
3656
3912
  showLegend, showTooltip, showGridLines, showXAxis, showYAxis, startEndOnly, yAxisWidth,
3657
3913
  maxValue, allowDecimals, xAxisLabel, yAxisLabel, active,
3658
- provisionalFrom, provisionalLabel, actions, highlight, target, targetLabel, barOptions?.targetKey, barOptions?.highlight, annotations, height, tokens, surface, plotWidth, chartOptions,
3914
+ provisionalFrom, provisionalLabel, actions, highlight, target, targetLabel, barOptions?.targetKey, barOptions?.highlight, annotations, height, tokens, surface, plotWidth, chartOptions, locale,
3659
3915
  ],
3660
3916
  )
3661
3917
  // The screen-reader table: exact values (tooltip format), dates read as dates, 「その他」 as its own column.
3662
3918
  const dataTable = useMemo(() => {
3663
- const table = chartDataTable(labelRows(series.data, index, labels), index, series.categories, tooltipFormatter, labels, "項目")
3664
- const times = timeTooltipLabels(series.data.map((row) => row[index]))
3919
+ const table = chartDataTable(labelRows(series.data, index, labels), index, series.categories, tooltipFormatter, labels, t.item)
3920
+ const times = timeTooltipLabels(series.data.map((row) => row[index]), locale)
3665
3921
  return times ? { ...table, rows: table.rows.map((row, i) => [times[i] ?? row[0] ?? "", ...row.slice(1)]) } : table
3666
- }, [series, index, tooltipFormatter, labels])
3667
- const label = ariaLabel ?? `${categories.join("・")}の棒グラフ`
3922
+ }, [series, index, tooltipFormatter, labels, t, locale])
3923
+ const label = ariaLabel ?? t.chart(categories)
3668
3924
  const resolvedSummary = useMemo(
3669
- () => (summary === "auto" ? buildChartSummary({ data, index, categories, title, format, valueFormatter }) : summary),
3670
- [summary, data, index, categories, title, format, valueFormatter],
3925
+ () => (summary === "auto" ? buildChartSummary({ data, index, categories, title, format, valueFormatter, locale }) : summary),
3926
+ [summary, data, index, categories, title, format, valueFormatter, locale],
3671
3927
  )
3672
3928
  return (
3673
3929
  <ChartFrame