@squadbase/vantage 0.6.2-dev1.48b34e1 → 0.6.2-dev1.51e401a

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 (71) hide show
  1. package/dist/{chunk-57GHR5LN.js → chunk-3RJEOP3J.js} +29 -18
  2. package/dist/chunk-3RJEOP3J.js.map +1 -0
  3. package/dist/{chunk-QOXXOFGL.js → chunk-77CISBMG.js} +994 -464
  4. package/dist/chunk-77CISBMG.js.map +1 -0
  5. package/dist/{chunk-JXFCSF3K.js → chunk-EG4PX726.js} +12 -7
  6. package/dist/chunk-EG4PX726.js.map +1 -0
  7. package/dist/{chunk-2N72FKLB.js → chunk-NZ5QIIG4.js} +33 -11
  8. package/dist/chunk-NZ5QIIG4.js.map +1 -0
  9. package/dist/{chunk-7S52M2SL.js → chunk-QNOX7WLZ.js} +3 -3
  10. package/dist/{chunk-7S52M2SL.js.map → chunk-QNOX7WLZ.js.map} +1 -1
  11. package/dist/{chunk-PDOBHMUQ.js → chunk-ROHNXP7J.js} +72 -3
  12. package/dist/chunk-ROHNXP7J.js.map +1 -0
  13. package/dist/{chunk-6OTTGAPO.js → chunk-TRMHNMMZ.js} +24 -7
  14. package/dist/chunk-TRMHNMMZ.js.map +1 -0
  15. package/dist/components/index.d.ts +49 -29
  16. package/dist/components/index.js +5 -5
  17. package/dist/{delta-text-CvwBGJ95.d.ts → delta-text-CbrOaW83.d.ts} +4 -2
  18. package/dist/doc/index.d.ts +14 -14
  19. package/dist/doc/index.js +198 -84
  20. package/dist/doc/index.js.map +1 -1
  21. package/dist/{format-U0cQl069.d.ts → format-DihQkL1N.d.ts} +17 -11
  22. package/dist/slide/index.d.ts +2 -2
  23. package/dist/slide/index.js +95 -44
  24. package/dist/slide/index.js.map +1 -1
  25. package/dist/table/index.js +6 -6
  26. package/dist/ui/index.d.ts +1 -1
  27. package/dist/ui/index.js +5 -5
  28. package/package.json +2 -2
  29. package/registry/ui/app-shell.tsx +138 -1
  30. package/registry/ui/area-chart.tsx +344 -85
  31. package/registry/ui/attention-list.tsx +177 -15
  32. package/registry/ui/bar-chart.tsx +341 -85
  33. package/registry/ui/bar-list.tsx +332 -77
  34. package/registry/ui/box-plot-chart.tsx +405 -97
  35. package/registry/ui/category-bar.tsx +248 -40
  36. package/registry/ui/chart-frame.tsx +257 -46
  37. package/registry/ui/data-table.tsx +229 -37
  38. package/registry/ui/date-range-picker.tsx +190 -36
  39. package/registry/ui/delta.tsx +171 -12
  40. package/registry/ui/donut-chart.tsx +339 -84
  41. package/registry/ui/echart.tsx +142 -4
  42. package/registry/ui/filter-bar.tsx +179 -20
  43. package/registry/ui/funnel-steps.tsx +354 -80
  44. package/registry/ui/heatmap.tsx +293 -52
  45. package/registry/ui/histogram-chart.tsx +395 -97
  46. package/registry/ui/kpi-card.tsx +245 -44
  47. package/registry/ui/legend.tsx +330 -76
  48. package/registry/ui/line-chart.tsx +344 -85
  49. package/registry/ui/multi-select.tsx +159 -9
  50. package/registry/ui/page-shell.tsx +141 -3
  51. package/registry/ui/paired-chart.tsx +349 -90
  52. package/registry/ui/progress-circle.tsx +205 -39
  53. package/registry/ui/sample-data.tsx +153 -9
  54. package/registry/ui/scatter-chart.tsx +391 -100
  55. package/registry/ui/searchable-select.tsx +145 -7
  56. package/registry/ui/states.tsx +170 -13
  57. package/registry/ui/status-badge.tsx +193 -28
  58. package/registry/ui/tracker.tsx +205 -39
  59. package/registry/ui/trend-indicator.tsx +149 -6
  60. package/registry/ui/updated-at.tsx +170 -16
  61. package/registry/ui/waterfall-chart.tsx +415 -102
  62. package/skills/vantage-add-feature/SKILL.md +5 -0
  63. package/templates/AGENTS.md +10 -8
  64. package/templates/agents/api.md +7 -0
  65. package/templates/agents/page.md +36 -0
  66. package/dist/chunk-2N72FKLB.js.map +0 -1
  67. package/dist/chunk-57GHR5LN.js.map +0 -1
  68. package/dist/chunk-6OTTGAPO.js.map +0 -1
  69. package/dist/chunk-JXFCSF3K.js.map +0 -1
  70. package/dist/chunk-PDOBHMUQ.js.map +0 -1
  71. package/dist/chunk-QOXXOFGL.js.map +0 -1
@@ -31,15 +31,148 @@ import { cn } from "@squadbase/vantage/ui"
31
31
  import { ChartFrame } from "@squadbase/vantage/components"
32
32
  import { toneForValue } from "@squadbase/vantage/components"
33
33
  import type { ChartColor, ChartCommonProps, ChartSeriesColor, ChartThreshold } from "@squadbase/vantage/components"
34
+ import { useSyncExternalStore } from "react"
34
35
  import { createContext, useCallback, useRef, type RefObject } from "react"
35
36
  import type { ChartTone } from "@squadbase/vantage/components"
36
- import { DEFAULT_LOCALE, EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
37
+ import { EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
37
38
  import type { ChartColors, ValueFormatter } from "@squadbase/vantage/components"
38
39
  import { useMemo, type ReactNode } from "react"
39
40
  import type { EChartsOption } from "@squadbase/vantage/components"
40
41
  import { computeComparison, defaultChangeDisplay, formatChange, type ComparisonKind, type PeriodGrain } from "@squadbase/vantage/components"
42
+ import { DEFAULT_LOCALE } from "@squadbase/vantage/components"
41
43
  import type { ChartAnnotation, ChartEventPayload, ChartForecast, ChartForecastRow, ChartHighlight, NumericKeys } from "@squadbase/vantage/components"
42
44
 
45
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
46
+
47
+ /**
48
+ * The display language of the parts and the defaults that follow it.
49
+ *
50
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
51
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
52
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
53
+ *
54
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
55
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
56
+ */
57
+
58
+ /** The language of the built-in wording: Japanese or English. */
59
+ type UiLanguage = "ja" | "en"
60
+
61
+ /** The locale used when `<html lang>` gives none (and on the server). */
62
+ const FALLBACK_LOCALE = "en-US"
63
+
64
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
65
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
66
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
67
+ }
68
+
69
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
70
+ function normalizeLocale(tag: string | null | undefined): string {
71
+ const value = tag?.trim()
72
+ if (!value) return FALLBACK_LOCALE
73
+ try {
74
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
75
+ } catch {
76
+ return FALLBACK_LOCALE
77
+ }
78
+ }
79
+
80
+ /**
81
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
82
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
83
+ */
84
+ function documentLocale(): string {
85
+ if (typeof document === "undefined") return FALLBACK_LOCALE
86
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
87
+ }
88
+
89
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
90
+ function resolveLocale(locale?: string | null): string {
91
+ return locale || documentLocale()
92
+ }
93
+
94
+ /** Picks the wording table of the locale's language. */
95
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
96
+ return table[uiLanguage(resolveLocale(locale))]
97
+ }
98
+
99
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
100
+ function defaultCurrency(locale?: string | null): string {
101
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
102
+ }
103
+
104
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
105
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
106
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
107
+ }
108
+
109
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
110
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
111
+
112
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
113
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
114
+
115
+ /**
116
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
117
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
118
+ * Monday for the rest.
119
+ */
120
+ function weekStartsOn(locale?: string | null): WeekDay {
121
+ const tag = normalizeLocale(resolveLocale(locale))
122
+ if (uiLanguage(tag) === "ja") return 1
123
+ try {
124
+ const info = new Intl.Locale(tag) as Intl.Locale & {
125
+ getWeekInfo?: () => { firstDay: number }
126
+ weekInfo?: { firstDay: number }
127
+ }
128
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
129
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
130
+ } catch {
131
+ // Fall through to the table below.
132
+ }
133
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
134
+ }
135
+
136
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
137
+
138
+ const listeners = new Set<() => void>()
139
+ let observer: MutationObserver | null = null
140
+
141
+ /** One observer on `<html lang>` for every subscribed part. */
142
+ function subscribe(onChange: () => void): () => void {
143
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
144
+ listeners.add(onChange)
145
+ if (!observer) {
146
+ observer = new MutationObserver(() => {
147
+ for (const listener of [...listeners]) listener()
148
+ })
149
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
150
+ }
151
+ return () => {
152
+ listeners.delete(onChange)
153
+ if (listeners.size === 0 && observer) {
154
+ observer.disconnect()
155
+ observer = null
156
+ }
157
+ }
158
+ }
159
+
160
+ const serverLocale = () => FALLBACK_LOCALE
161
+
162
+ /**
163
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
164
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
165
+ */
166
+ function useDocumentLocale(): string {
167
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
168
+ }
169
+
170
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
171
+ function useLocale(locale?: string | null): string {
172
+ const page = useDocumentLocale()
173
+ return locale || page
174
+ }
175
+
43
176
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-theme.ts (internal to @squadbase/vantage) ---------- */
44
177
 
45
178
  /**
@@ -543,9 +676,10 @@ function resolveValueFormatter(
543
676
  function resolveAxisFormatter(
544
677
  valueFormatter: ValueFormatter | undefined,
545
678
  format: ValueFormat | undefined,
546
- locale: string = DEFAULT_LOCALE,
679
+ localeArg?: string,
547
680
  ): ValueFormatter {
548
681
  if (valueFormatter) return valueFormatter
682
+ const locale = resolveLocale(localeArg)
549
683
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
550
684
  if (spec.type !== "compact" && spec.type !== "number" && spec.type !== "currency") return toValueFormatter(format, locale)
551
685
  const currency = "currency" in spec ? spec.currency : undefined
@@ -554,40 +688,75 @@ function resolveAxisFormatter(
554
688
  return (value) => formatValue(value, short, locale)
555
689
  }
556
690
 
557
- /** The unit words of a ja number (`1.2万` / `3.4億` / `1兆`) and their sizes, largest first. */
558
- const JA_UNITS = [
559
- { char: "兆", size: 1e12 },
560
- { char: "億", size: 1e8 },
561
- { char: "万", size: 1e4 },
562
- ] as const
691
+ interface CompactUnit {
692
+ token: string
693
+ size: number
694
+ }
695
+
696
+ const compactUnitCache = new Map<string, CompactUnit[] | null>()
697
+
698
+ /**
699
+ * The compact-notation unit words of a locale and their sizes, largest first: `兆` / `億` / `万` in ja,
700
+ * `T` / `B` / `M` / `K` in en. `null` when the locale's compact form puts anything between the number and its
701
+ * word (`1,5 Mio.`), where a token cannot be swapped in place.
702
+ */
703
+ function compactUnits(locale: string): CompactUnit[] | null {
704
+ const cached = compactUnitCache.get(locale)
705
+ if (cached !== undefined) return cached
706
+ let units: CompactUnit[] | null = []
707
+ try {
708
+ const compact = new Intl.NumberFormat(locale, { notation: "compact" })
709
+ const seen = new Set<string>()
710
+ for (let exp = 3; exp <= 15; exp++) {
711
+ const parts = compact.formatToParts(10 ** exp)
712
+ const at = parts.findIndex((p) => p.type === "compact")
713
+ if (at === -1) continue
714
+ if (parts[at - 1]?.type !== "integer" || at !== parts.length - 1) {
715
+ units = null
716
+ break
717
+ }
718
+ const token = parts[at]!.value
719
+ if (seen.has(token)) continue
720
+ seen.add(token)
721
+ units.push({ token, size: 10 ** exp })
722
+ }
723
+ } catch {
724
+ units = null
725
+ }
726
+ const sorted = units && units.length > 0 ? units.sort((a, b) => b.size - a.size) : null
727
+ compactUnitCache.set(locale, sorted)
728
+ return sorted
729
+ }
563
730
 
564
- const JA_NUMBER_TOKEN_RE = /\d[\d,]*(?:\.\d+)?(?:兆|億|万)?/
731
+ const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
565
732
 
566
733
  /**
567
734
  * The value-axis tick formatter for an axis whose largest tick is `top`: every tick reads in the unit of
568
- * that largest tick, so 万 and 億 never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`).
569
- * Only the ja short forms (`compact` / `number` / `currency`, no `valueFormatter`) have units to align;
570
- * any other format — and an axis whose largest tick is under 1万 — keeps `resolveAxisFormatter`'s output as it is.
735
+ * that largest tick, so units never mix on one axis (`¥0.5億` / `¥1億` / `¥1.5億`, not `¥5,000万` / `¥1億`;
736
+ * `$0.5M` / `$1M` / `$1.5M`, not `$500K` / `$1M`). Only the short forms (`compact` / `number` / `currency`,
737
+ * no `valueFormatter`) have units to align; any other format — an axis whose largest tick is under the smallest
738
+ * unit (1万 / 1K), or a locale whose compact form cannot be aligned in place — keeps `resolveAxisFormatter`'s output as it is.
571
739
  */
572
740
  function resolveAxisTickFormatter(
573
741
  valueFormatter: ValueFormatter | undefined,
574
742
  format: ValueFormat | undefined,
575
- locale: string = DEFAULT_LOCALE,
743
+ localeArg?: string,
576
744
  ): (top: number) => ValueFormatter {
745
+ const locale = resolveLocale(localeArg)
577
746
  const base = resolveAxisFormatter(valueFormatter, format, locale)
578
747
  const spec = format === undefined ? { type: "compact" as const } : typeof format === "string" ? { type: format } : format
579
- const aligns =
580
- !valueFormatter && locale.toLowerCase().startsWith("ja") && (spec.type === "compact" || spec.type === "number" || spec.type === "currency")
581
- if (!aligns) return () => base
748
+ const units = !valueFormatter && (spec.type === "compact" || spec.type === "number" || spec.type === "currency") ? compactUnits(locale) : null
749
+ if (!units) return () => base
582
750
  const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 3 })
751
+ const numberToken = new RegExp(`\\d[\\d,]*(?:\\.\\d+)?(?:${units.map((u) => escapeRegExp(u.token)).join("|")})?`)
583
752
  return (top) => {
584
- const unit = JA_UNITS.find((u) => Math.abs(top) >= u.size)
753
+ const unit = units.find((u) => Math.abs(top) >= u.size)
585
754
  if (!unit) return base
586
755
  return (value) => {
587
756
  const text = base(value)
588
757
  if (value === 0 || !Number.isFinite(value)) return text
589
- const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.char}`
590
- return text.replace(JA_NUMBER_TOKEN_RE, () => scaled)
758
+ const scaled = `${number.format(Math.abs(value) / unit.size)}${unit.token}`
759
+ return text.replace(numberToken, () => scaled)
591
760
  }
592
761
  }
593
762
  }
@@ -616,8 +785,20 @@ function clamp(value: number, min: number, max: number): number {
616
785
  /** How many categories a chart colours individually; the rest fold into「その他」. */
617
786
  const MAX_CHART_SERIES = 5
618
787
 
619
- /** The series name the folded categories get. */
620
- const OTHER_SERIES_LABEL = "その他"
788
+ /** The series name the folded categories get, by language. */
789
+ const OTHER_SERIES_LABELS: Record<UiLanguage, string> = { ja: "その他", en: "Other" }
790
+
791
+ /** The series name the folded categories get in `locale` ("Other" / "その他"). Default the page's locale (`<html lang>`). */
792
+ function otherSeriesLabel(locale?: string): string {
793
+ return OTHER_SERIES_LABELS[uiLanguage(resolveLocale(locale))]
794
+ }
795
+
796
+ const OTHER_NAMES = new Set(Object.values(OTHER_SERIES_LABELS))
797
+
798
+ /** Whether a category / row is the "other" bucket by name (「その他」 or "Other", whatever the page language). */
799
+ function isOtherName(name: string | null | undefined): boolean {
800
+ return typeof name === "string" && OTHER_NAMES.has(name)
801
+ }
621
802
 
622
803
  interface AggregatedItems<Item extends { name: string; value: number }> {
623
804
  /** The `n` largest items, in their given order. All items when there are `n` or fewer. */
@@ -637,7 +818,7 @@ interface AggregatedItems<Item extends { name: string; value: number }> {
637
818
  function aggregateTopN<Item extends { name: string; value: number }>(
638
819
  items: readonly Item[],
639
820
  n: number = MAX_CHART_SERIES,
640
- otherLabel: string = OTHER_SERIES_LABEL,
821
+ otherLabel: string = otherSeriesLabel(),
641
822
  ): AggregatedItems<Item> {
642
823
  if (items.length <= n) return { top: [...items], rest: [], other: null }
643
824
  const ranked = items
@@ -666,7 +847,7 @@ function aggregateSeries(
666
847
  data: readonly Record<string, unknown>[],
667
848
  categories: readonly string[],
668
849
  n: number = MAX_CHART_SERIES,
669
- otherLabel: string = OTHER_SERIES_LABEL,
850
+ otherLabel: string = otherSeriesLabel(),
670
851
  ): AggregatedSeries {
671
852
  const totals = categories.map((name) => ({
672
853
  name,
@@ -698,12 +879,14 @@ function aggregateSeriesKeeping(
698
879
  categories: readonly string[],
699
880
  keep: string | undefined,
700
881
  n: number = MAX_CHART_SERIES,
882
+ otherLabel: string = otherSeriesLabel(),
701
883
  ): AggregatedSeries {
702
- if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n)
884
+ if (keep === undefined || !categories.includes(keep)) return aggregateSeries(data, categories, n, otherLabel)
703
885
  const aggregated = aggregateSeries(
704
886
  data,
705
887
  categories.filter((c) => c !== keep),
706
888
  n,
889
+ otherLabel,
707
890
  )
708
891
  return { ...aggregated, categories: [...aggregated.categories, keep] }
709
892
  }
@@ -739,7 +922,7 @@ function seriesColorsWithComparison(
739
922
  * Colour per drawn series: the record's token for that category, else the
740
923
  * array's token at the category's position in `allCategories` (cycled), else
741
924
  * the token palette by drawn position. `otherCategory` — and a category the
742
- * data itself names「その他」(`OTHER_SERIES_LABEL`) — always gets `otherColor`.
925
+ * data itself names「その他」/ "Other" (`OTHER_SERIES_LABELS`) — always gets `otherColor`.
743
926
  */
744
927
  function seriesColorsFor(
745
928
  categories: readonly string[],
@@ -751,7 +934,7 @@ function seriesColorsFor(
751
934
  const list = Array.isArray(colors) ? (colors as ChartSeriesColor[]) : undefined
752
935
  const record = !Array.isArray(colors) && colors ? (colors as Partial<Record<string, ChartSeriesColor>>) : undefined
753
936
  return categories.map((category, i) => {
754
- if ((otherCategory !== null && category === otherCategory) || category === OTHER_SERIES_LABEL) return otherColor
937
+ if ((otherCategory !== null && category === otherCategory) || isOtherName(category)) return otherColor
755
938
  const fromRecord = record?.[category]
756
939
  if (fromRecord) return fromRecord
757
940
  if (list && list.length > 0) {
@@ -798,7 +981,7 @@ interface DonutSlicesConfig {
798
981
  */
799
982
  function donutSlices(
800
983
  data: readonly Record<string, unknown>[],
801
- { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = OTHER_SERIES_LABEL }: DonutSlicesConfig,
984
+ { index, value, colors, maxSlices = MAX_CHART_SERIES, otherColor, otherLabel = otherSeriesLabel() }: DonutSlicesConfig,
802
985
  ): DonutSlice[] {
803
986
  const items = data
804
987
  .map((row, i) => {
@@ -811,7 +994,8 @@ function donutSlices(
811
994
  const slices: DonutSlice[] = top.map((item, rank) => ({
812
995
  name: item.name,
813
996
  value: item.value,
814
- color: palette[rank % palette.length]!,
997
+ // A row the data itself names「その他」/ "Other" is the same remainder: grey, like the folded slice.
998
+ color: isOtherName(item.name) ? otherColor : palette[rank % palette.length]!,
815
999
  isOther: false,
816
1000
  row: item.row,
817
1001
  }))
@@ -884,7 +1068,7 @@ interface BarListRowsConfig extends BarListOptions {
884
1068
  */
885
1069
  function barListRows<Row extends object>(
886
1070
  data: readonly Row[],
887
- { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = OTHER_SERIES_LABEL, exclude }: BarListRowsConfig = {},
1071
+ { index = "name", category = "value", sortOrder = "descending", maxItems = MAX_CHART_SERIES, otherLabel = otherSeriesLabel(), exclude }: BarListRowsConfig = {},
888
1072
  ): BarListRow<Row>[] {
889
1073
  const items = data.map((row): BarListRow<Row> => {
890
1074
  const cells = row as Record<string, unknown>
@@ -930,19 +1114,21 @@ const BAR_LIST_OTHER_TINT = 30
930
1114
 
931
1115
  /**
932
1116
  * The fill of each BarList row: all real rows in the same tint of `color`
933
- * (`BAR_LIST_TINT`), the folded「その他」row in a tint of `otherColor`
1117
+ * (`BAR_LIST_TINT`), the folded「その他」row (and a row the data names「その他」/ "Other") in a tint of `otherColor`
934
1118
  * (`BAR_LIST_OTHER_TINT`) — the palest fill, a remainder never outweighs a
935
1119
  * ranked row. A legacy per-row `color` (or a threshold tone) replaces `color`
936
1120
  * for that row. A tint, not the solid token, so the label printed over the bar
937
1121
  * keeps its contrast.
938
1122
  */
939
1123
  function barListFills(
940
- rows: readonly Pick<BarListRow, "isOther" | "color">[],
1124
+ rows: readonly (Pick<BarListRow, "isOther" | "color"> & { name?: string })[],
941
1125
  color: ChartColor,
942
1126
  otherColor: string,
943
1127
  ): string[] {
944
1128
  return rows.map((row) =>
945
- row.isOther ? tintCss(otherColor, BAR_LIST_OTHER_TINT) : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
1129
+ row.isOther || isOtherName(row.name)
1130
+ ? tintCss(otherColor, BAR_LIST_OTHER_TINT)
1131
+ : tintCss(chartColorToCss(row.color ?? color), BAR_LIST_TINT),
946
1132
  )
947
1133
  }
948
1134
 
@@ -1158,11 +1344,30 @@ interface ChartSummaryInput {
1158
1344
  format?: ValueFormat
1159
1345
  /** Wins over `format`. */
1160
1346
  valueFormatter?: ValueFormatter
1347
+ /** Language of the sentence. Default the page's locale (`<html lang>`). */
1348
+ locale?: string
1349
+ }
1350
+
1351
+ const SUMMARY_TEXT = {
1352
+ ja: {
1353
+ up: "、前の点より増加",
1354
+ down: "、前の点より減少",
1355
+ flat: "、前の点と横ばい",
1356
+ latest: (label: string, value: string) => `最新 ${label} は ${value}`,
1357
+ top: (name: string, share: string) => `最大は ${name}(${share})`,
1358
+ },
1359
+ en: {
1360
+ up: ", up from the previous point",
1361
+ down: ", down from the previous point",
1362
+ flat: ", flat from the previous point",
1363
+ latest: (label: string, value: string) => `Latest ${label}: ${value}`,
1364
+ top: (name: string, share: string) => `Largest: ${name} (${share})`,
1365
+ },
1161
1366
  }
1162
1367
 
1163
1368
  /**
1164
1369
  * One-sentence summary built from the rows, for `summary="auto"`:
1165
- * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」. The direction is
1370
+ * 「<title>: 最新 <index> は <値>、前の点より増加/減少/横ばい」("<title>: Latest <index>: <value>, up from the previous point"). The direction is
1166
1371
  * computed from the last two non-empty points of the first series — never
1167
1372
  * written by hand, so it cannot contradict the data. `undefined` when there is
1168
1373
  * no usable point.
@@ -1178,30 +1383,59 @@ function buildChartSummary(input: ChartSummaryInput): string | undefined {
1178
1383
  }
1179
1384
  const latest = points[points.length - 1]
1180
1385
  if (!latest) return undefined
1181
- const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact")
1386
+ const locale = resolveLocale(input.locale)
1387
+ const t = SUMMARY_TEXT[uiLanguage(locale)]
1388
+ const text = input.valueFormatter ?? toValueFormatter(input.format ?? "compact", locale)
1182
1389
  const prev = points[points.length - 2]
1183
- const direction = !prev ? "" : latest.value > prev.value ? "、前の点より増加" : latest.value < prev.value ? "、前の点より減少" : "、前の点と横ばい"
1390
+ const direction = !prev ? "" : latest.value > prev.value ? t.up : latest.value < prev.value ? t.down : t.flat
1184
1391
  const head = input.title ? `${input.title}: ` : ""
1185
- return `${head}最新 ${latest.label} は ${text(latest.value)}${direction}`
1392
+ return `${head}${t.latest(latest.label, text(latest.value))}${direction}`
1186
1393
  }
1187
1394
 
1188
1395
  /**
1189
- * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」 from the slices
1190
- * (largest first). `undefined` when there is nothing to describe.
1396
+ * `summary="auto"` for a donut: 「<title>: 最大は <名前>(<構成比>)」("<title>: Largest: <name> (<share>)") from the slices
1397
+ * (largest first). `undefined` when there is nothing to describe. `locale` picks the language; default the page's.
1191
1398
  */
1192
1399
  function buildShareSummary(
1193
1400
  slices: readonly { name: string; value: number }[],
1194
1401
  title?: string,
1402
+ locale?: string,
1195
1403
  ): string | undefined {
1196
1404
  const total = slices.reduce((sum, s) => sum + s.value, 0)
1197
1405
  const top = slices[0]
1198
1406
  if (!top || total <= 0) return undefined
1199
1407
  const share = `${Math.round((top.value / total) * 1000) / 10}%`
1200
- return `${title ? `${title}: ` : ""}最大は ${top.name}(${share})`
1408
+ return `${title ? `${title}: ` : ""}${SUMMARY_TEXT[uiLanguage(resolveLocale(locale))].top(top.name, share)}`
1201
1409
  }
1202
1410
 
1203
1411
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-options.ts (internal to @squadbase/vantage) ---------- */
1204
1412
 
1413
+ /** The built-in words of the option builders, by language. */
1414
+ const CHART_TEXT = {
1415
+ ja: {
1416
+ target: "目標",
1417
+ forecast: "予測",
1418
+ total: "合計",
1419
+ provisional: "速報",
1420
+ noComparison: "比較なし",
1421
+ share: (share: string) => `(${share})`,
1422
+ targetRate: (rate: string) => `(達成率 ${rate})`,
1423
+ provisionalTitle: (title: string, label: string) => `${title}(${label})`,
1424
+ },
1425
+ en: {
1426
+ target: "Target",
1427
+ forecast: "Forecast",
1428
+ total: "Total",
1429
+ provisional: "Preliminary",
1430
+ noComparison: "No comparison",
1431
+ share: (share: string) => ` (${share})`,
1432
+ targetRate: (rate: string) => ` (${rate} achieved)`,
1433
+ provisionalTitle: (title: string, label: string) => `${title} (${label})`,
1434
+ },
1435
+ } satisfies Record<UiLanguage, unknown>
1436
+
1437
+ const chartText = (locale?: string) => CHART_TEXT[uiLanguage(resolveLocale(locale))]
1438
+
1205
1439
  type CartesianChartKind = "area" | "bar" | "line"
1206
1440
  type CartesianStackType = "default" | "stacked" | "percent"
1207
1441
  type CartesianCurveType = "linear" | "smooth" | "step"
@@ -1386,7 +1620,7 @@ interface CartesianChartConfig {
1386
1620
  * all ticks read in one unit (¥0.5億 / ¥1億). Default: `axisFormatter` for every tick.
1387
1621
  */
1388
1622
  axisTicks?: (top: number) => (value: number) => string
1389
- /** Locale of the time-axis labels. Default `"ja-JP"`. */
1623
+ /** Locale of the time-axis labels and the built-in words (total, target, forecast). Default the page's (`<html lang>`). */
1390
1624
  locale?: string
1391
1625
  type?: CartesianStackType
1392
1626
  /** Bar only. `"vertical"` lays the categories down the y axis (horizontal bars). */
@@ -1624,11 +1858,12 @@ function timeAxisLabelStep(values: readonly unknown[], maxLabels: number = TIME_
1624
1858
  */
1625
1859
  function timeAxisLabels(
1626
1860
  values: readonly unknown[],
1627
- locale: string = DEFAULT_LOCALE,
1861
+ localeArg?: string,
1628
1862
  shown: (i: number) => boolean = () => true,
1629
1863
  ): string[] | null {
1630
1864
  const parsed = parseTimeIndex(values)
1631
1865
  if (!parsed) return null
1866
+ const locale = resolveLocale(localeArg)
1632
1867
  const { granularity, times } = parsed
1633
1868
  let prevYear: number | null = null
1634
1869
  return times.map((t, i) => {
@@ -1651,9 +1886,10 @@ function timeAxisLabels(
1651
1886
  }
1652
1887
 
1653
1888
  /** The tooltip titles for time-like x values (`2025年9月`, `2025年9月1日`), or `null` when they are not dates. */
1654
- function timeTooltipLabels(values: readonly unknown[], locale: string = DEFAULT_LOCALE): string[] | null {
1889
+ function timeTooltipLabels(values: readonly unknown[], localeArg?: string): string[] | null {
1655
1890
  const parsed = parseTimeIndex(values)
1656
1891
  if (!parsed) return null
1892
+ const locale = resolveLocale(localeArg)
1657
1893
  const options: Intl.DateTimeFormatOptions =
1658
1894
  parsed.granularity === "year"
1659
1895
  ? { year: "numeric" }
@@ -1793,7 +2029,9 @@ function tooltipHtml(
1793
2029
  rows: readonly TooltipRow[],
1794
2030
  valueFormatter: (value: number) => string,
1795
2031
  notes: readonly string[] = [],
2032
+ locale?: string,
1796
2033
  ): string {
2034
+ const t = chartText(locale)
1797
2035
  const lines = rows.map(
1798
2036
  (row) =>
1799
2037
  `<div style="display:flex;align-items:center;gap:8px;line-height:20px">` +
@@ -1802,7 +2040,7 @@ function tooltipHtml(
1802
2040
  `<span style="font-weight:600;font-variant-numeric:tabular-nums;margin-left:12px">${
1803
2041
  row.value === null ? EMPTY_VALUE : escapeHtml(valueFormatter(row.value))
1804
2042
  }${
1805
- row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">(${escapeHtml(formatShare(row.share))})</span>`
2043
+ row.share === undefined ? "" : `<span style="opacity:.6;margin-left:4px">${escapeHtml(t.share(formatShare(row.share)))}</span>`
1806
2044
  }</span></div>` +
1807
2045
  (row.note === undefined
1808
2046
  ? ""
@@ -1816,8 +2054,10 @@ function tooltipHtml(
1816
2054
 
1817
2055
  /* ------------------------------------------------- per-bar targets / notes */
1818
2056
 
1819
- /** Default name of the per-bar target marker (legend, tooltip). */
1820
- const BAR_TARGET_LABEL = "目標"
2057
+ /** Default name of the per-bar target marker (legend, tooltip) in `locale`: "Target" / 「目標」. Default the page's locale. */
2058
+ function barTargetLabel(locale?: string): string {
2059
+ return chartText(locale).target
2060
+ }
1821
2061
 
1822
2062
  /** How far (px) the target marker reaches past each side of its bar. */
1823
2063
  const TARGET_MARKER_OVERHANG = 4
@@ -1868,16 +2108,17 @@ function achievementRate(value: number | null | undefined, target: number | null
1868
2108
  return (value / target) * 100
1869
2109
  }
1870
2110
 
1871
- /** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」. `null` without a target. */
2111
+ /** The tooltip line of a bar with a target: 「目標 ¥1,200万(達成率 85.3%)」 / "Target $12M (85.3% achieved)". `null` without a target. */
1872
2112
  function barTargetNote(
1873
2113
  label: string,
1874
2114
  value: number | null,
1875
2115
  target: number | null,
1876
2116
  valueFormatter: (value: number) => string,
2117
+ locale?: string,
1877
2118
  ): string | null {
1878
2119
  if (target === null) return null
1879
2120
  const rate = achievementRate(value, target)
1880
- return `${label} ${valueFormatter(target)}${rate === null ? "" : `(達成率 ${formatShare(rate)})`}`
2121
+ return `${label} ${valueFormatter(target)}${rate === null ? "" : chartText(locale).targetRate(formatShare(rate))}`
1881
2122
  }
1882
2123
 
1883
2124
  /** Half the width of a bar in category-axis units, from `barCategoryGap` (`"20%"`; a px gap or none → ECharts' 20%). */
@@ -1969,11 +2210,12 @@ const TARGET_LEGEND_SERIES_ID = "__target-legend"
1969
2210
  /** A flat tick (8×8 legend box, aspect kept): the per-bar target's legend icon. */
1970
2211
  const TARGET_LEGEND_ICON = "path://M0,0 L100,0 L100,24 L0,24 Z"
1971
2212
 
1972
- const DEFAULT_FORECAST_LABEL = "予測"
1973
-
1974
- /** 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. */
1975
- function forecastBoundaryText(label: string = DEFAULT_FORECAST_LABEL): string {
1976
- return `${label} →`
2213
+ /**
2214
+ * 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.
2215
+ * Without a `label`, "Forecast" / 「予測」 in `locale` (default the page's).
2216
+ */
2217
+ function forecastBoundaryText(label?: string, locale?: string): string {
2218
+ return `${label ?? chartText(locale).forecast} →`
1977
2219
  }
1978
2220
 
1979
2221
  /** A comparison series: a thin, muted, solid line with no markers (60% of `--muted-foreground`). */
@@ -2013,8 +2255,10 @@ interface AxisExtent {
2013
2255
  max: number
2014
2256
  }
2015
2257
 
2016
- /** The label of a stack's total (highlight and tooltip). */
2017
- const STACK_TOTAL_LABEL = "合計"
2258
+ /** The label of a stack's total (highlight and tooltip) in `locale`: "Total" / 「合計」. Default the page's locale. */
2259
+ function stackTotalLabel(locale?: string): string {
2260
+ return chartText(locale).total
2261
+ }
2018
2262
 
2019
2263
  /** Per point, the sum of the stacked series; `null` where every series is missing. */
2020
2264
  function stackTotalsOf(values: readonly (readonly (number | null)[])[]): (number | null)[] {
@@ -2136,8 +2380,8 @@ function indexGrain(values: readonly unknown[]): PeriodGrain | null {
2136
2380
 
2137
2381
  /**
2138
2382
  * The tooltip line under the main series' row when a comparison series is drawn: the arrow, the signed change and the
2139
- * comparison's name, as `Delta` writes it ("↑ +12.4% 前年同月比"; a minus is U+2212). "比較なし 前年同月比" when
2140
- * either value is missing or the baseline is 0.
2383
+ * comparison's name, as `Delta` writes it ("↑ +12.4% vs. same month last year" / "↑ +12.4% 前年同月比"; a minus is U+2212).
2384
+ * "No comparison vs. same month last year" / "比較なし 前年同月比" when either value is missing or the baseline is 0.
2141
2385
  */
2142
2386
  function comparisonNote(
2143
2387
  current: number | null,
@@ -2145,16 +2389,18 @@ function comparisonNote(
2145
2389
  comparison: Pick<CartesianComparisonConfig, "vs" | "label">,
2146
2390
  grain?: PeriodGrain,
2147
2391
  format?: ValueFormat,
2148
- locale: string = DEFAULT_LOCALE,
2392
+ localeArg?: string,
2149
2393
  ): string {
2394
+ const locale = resolveLocale(localeArg)
2150
2395
  const result = computeComparison(
2151
2396
  current,
2152
2397
  comparison.vs === "custom"
2153
2398
  ? { previous, vs: "custom", label: comparison.label ?? "", grain }
2154
2399
  : { previous, vs: comparison.vs, label: comparison.label, grain },
2400
+ { locale },
2155
2401
  )
2156
2402
  const text = formatChange(result, defaultChangeDisplay(format), locale, format)
2157
- if (text === null) return `比較なし ${result.label}`
2403
+ if (text === null) return `${chartText(locale).noComparison} ${result.label}`
2158
2404
  const arrow = result.direction === "up" ? "↑" : result.direction === "down" ? "↓" : "→"
2159
2405
  return `${arrow} ${text.replace(/^-/, "−")} ${result.label}`
2160
2406
  }
@@ -2197,7 +2443,6 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2197
2443
  toneColors,
2198
2444
  valueFormatter,
2199
2445
  axisFormatter = valueFormatter,
2200
- locale = DEFAULT_LOCALE,
2201
2446
  type = "default",
2202
2447
  layout = "horizontal",
2203
2448
  curveType = "linear",
@@ -2234,7 +2479,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2234
2479
  chartOptions,
2235
2480
  reserveActions = false,
2236
2481
  } = cfg
2237
- const provisionalLabel = cfg.provisionalLabel ?? DEFAULT_PROVISIONAL_LABEL
2482
+ const locale = resolveLocale(cfg.locale)
2483
+ const text = chartText(locale)
2484
+ const provisionalLabel = cfg.provisionalLabel ?? text.provisional
2238
2485
 
2239
2486
  const stacked = kind !== "line" && type !== "default"
2240
2487
  const percent = kind !== "line" && type === "percent"
@@ -2383,9 +2630,9 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2383
2630
  // `barOptions.targetKey` (a target per bar) takes over from the single `target` line.
2384
2631
  const perBarTarget = kind === "bar" && Boolean(targetKey)
2385
2632
  const hasTarget = !perBarTarget && target !== undefined && Number.isFinite(target)
2386
- const targetText = hasTarget ? (targetLabel ?? `目標 ${axisFormatter(target)}`) : ""
2633
+ const targetText = hasTarget ? (targetLabel ?? `${text.target} ${axisFormatter(target)}`) : ""
2387
2634
  const highlightText = (value: number) =>
2388
- shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${STACK_TOTAL_LABEL} ` : ""}${axisFormatter(value)}`
2635
+ shareHighlight ? `${Math.round(value)}%` : `${stackTotals ? `${text.total} ` : ""}${axisFormatter(value)}`
2389
2636
  /** Whether `p`'s label sits within the target's value band and under the target label (at the right end). */
2390
2637
  const nearTargetLabel = (p: HighlightPoint, side: "under" | "over") => {
2391
2638
  if (!hasTarget || !plotWidth || kind === "bar" || labels.length < 2) return false
@@ -2411,7 +2658,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2411
2658
  // One series (or a stack, against its total) can be compared to a target; grouped series and 100% stacks cannot.
2412
2659
  const barTargets: (number | null)[] | null =
2413
2660
  perBarTarget && targetKey && !percent && (stacked || categories.length === 1) ? barTargetValues(data, targetKey) : null
2414
- const barTargetName = targetLabel ?? BAR_TARGET_LABEL
2661
+ const barTargetName = targetLabel ?? text.target
2415
2662
  const barTargetColor = labelColor ?? guideColor
2416
2663
 
2417
2664
  const markPointFor = (c: number, color: string | undefined) =>
@@ -2625,7 +2872,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2625
2872
  }
2626
2873
 
2627
2874
  /* -- forecast: dashed continuation (+ optional band) of one series */
2628
- const forecastLabel = forecast?.label ?? DEFAULT_FORECAST_LABEL
2875
+ const forecastLabel = forecast?.label ?? text.forecast
2629
2876
  const forecastBandAlpha = isDarkSurface(labelBackground) ? FORECAST_BAND_ALPHA_DARK : FORECAST_BAND_ALPHA_LIGHT
2630
2877
  const forecastValues: (number | null)[] = []
2631
2878
  if (forecast) {
@@ -3101,7 +3348,7 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
3101
3348
  })
3102
3349
  }
3103
3350
  if (stackTotals && i <= lastActual && rows.length > 1) {
3104
- rows.push({ name: STACK_TOTAL_LABEL, color: "transparent", value: stackTotals[i] ?? null })
3351
+ rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
3105
3352
  }
3106
3353
  const provisional = provStart !== -1 && i >= provStart && i <= lastActual
3107
3354
  if (provisional) {
@@ -3117,19 +3364,19 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
3117
3364
  ...noteFor(at),
3118
3365
  })
3119
3366
  })
3120
- if (stackTotals && rows.length > 1) rows.push({ name: STACK_TOTAL_LABEL, color: "transparent", value: stackTotals[i] ?? null })
3367
+ if (stackTotals && rows.length > 1) rows.push({ name: text.total, color: "transparent", value: stackTotals[i] ?? null })
3121
3368
  }
3122
3369
  const title = tooltipTitles[i] ?? labels[i] ?? ""
3123
3370
  const lines: string[] = []
3124
3371
  if (barTargets && i <= lastActual) {
3125
3372
  const total = stackTotals ? (stackTotals[i] ?? null) : (values[0]?.[i] ?? null)
3126
- const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter)
3373
+ const note = barTargetNote(barTargetName, total, barTargets[i] ?? null, valueFormatter, locale)
3127
3374
  if (note) lines.push(note)
3128
3375
  }
3129
3376
  for (const note of notes) {
3130
3377
  if (note.to === undefined ? note.from === i : note.from <= i && i <= note.to) lines.push(note.label)
3131
3378
  }
3132
- return tooltipHtml(provisional ? `${title}(${provisionalLabel})` : title, rows, valueFormatter, lines)
3379
+ return tooltipHtml(provisional ? text.provisionalTitle(title, provisionalLabel) : title, rows, valueFormatter, lines, locale)
3133
3380
  },
3134
3381
  },
3135
3382
  xAxis: horizontalBars ? valueAxis : categoryAxis,
@@ -3221,8 +3468,6 @@ interface DonutOptionSlice {
3221
3468
  /** How many of the largest slices get an outside label (「自然検索 41%」) by default. */
3222
3469
  const DONUT_OUTSIDE_LABELS = 2
3223
3470
 
3224
- const DEFAULT_TOTAL_LABEL = "合計"
3225
-
3226
3471
  const sliceValue = (value: number) => (Number.isFinite(value) && value > 0 ? value : 0)
3227
3472
 
3228
3473
  /** A slice's share of `total` as text: whole percent, `<1%` for a sliver, `0%` for nothing. */
@@ -3255,15 +3500,16 @@ function donutLegendItems(slices: readonly DonutOptionSlice[]): DonutLegendItem[
3255
3500
  }
3256
3501
 
3257
3502
  /**
3258
- * The default center label: what is totalled (「合計」), the formatted total
3503
+ * The default center label: what is totalled (「合計」 / "Total" in `locale`, default the page's), the formatted total
3259
3504
  * (`amount`, the big line), the unit (a small line under it) and both as one
3260
3505
  * string (`value`, 「9.2万 セッション」).
3261
3506
  */
3262
3507
  function donutCenterText(
3263
3508
  total: number,
3264
3509
  valueFormatter: (value: number) => string,
3265
- { totalLabel = DEFAULT_TOTAL_LABEL, unit }: { totalLabel?: string; unit?: string } = {},
3510
+ { totalLabel, unit, locale }: { totalLabel?: string; unit?: string; locale?: string } = {},
3266
3511
  ): { label: string; value: string; amount: string; unit?: string } {
3512
+ totalLabel ??= chartText(locale).total
3267
3513
  const amount = valueFormatter(total)
3268
3514
  return { label: totalLabel, value: unit ? `${amount} ${unit}` : amount, amount, ...(unit ? { unit } : {}) }
3269
3515
  }
@@ -3491,8 +3737,10 @@ function hasLabel(labels: Readonly<Record<string, string>> | undefined, key: str
3491
3737
 
3492
3738
  /* ------------------------------------------------------------ provisional */
3493
3739
 
3494
- /** The default label for points that are not final yet. */
3495
- const DEFAULT_PROVISIONAL_LABEL = "速報"
3740
+ /** The default label for points that are not final yet, in `locale`: "Preliminary" / 「速報」. Default the page's locale. */
3741
+ function defaultProvisionalLabel(locale?: string): string {
3742
+ return chartText(locale).provisional
3743
+ }
3496
3744
 
3497
3745
  /**
3498
3746
  * Position of the first provisional point: the first entry of `indexValues`
@@ -3572,6 +3820,11 @@ export interface BarListProps<T extends object = BarListItem>
3572
3820
  ariaLabel?: string
3573
3821
  }
3574
3822
 
3823
+ const TEXT = {
3824
+ ja: { item: "項目", value: "値" },
3825
+ en: { item: "Item", value: "Value" },
3826
+ }
3827
+
3575
3828
  /** Row height (h-8) + gap (space-y-1.5): the skeleton / empty height for `n` rows. */
3576
3829
  const listHeight = (n: number) => n * 32 + Math.max(0, n - 1) * 6
3577
3830
 
@@ -3614,17 +3867,19 @@ function BarListImpl<T extends object>(
3614
3867
  return () => cancelAnimationFrame(frame)
3615
3868
  }, [showAnimation])
3616
3869
 
3870
+ const locale = useLocale()
3871
+ const t = TEXT[uiLanguage(locale)]
3617
3872
  const maxItems = barListOptions?.maxItems ?? MAX_CHART_SERIES
3618
3873
  const token = Array.isArray(colors) ? colors[0] : colors
3619
3874
  const barColor = token ?? color ?? "chart-1"
3620
3875
 
3621
- const formatter = resolveValueFormatter(valueFormatter ?? formatValue, format)
3876
+ const formatter = resolveValueFormatter(valueFormatter ?? formatValue, format, locale)
3622
3877
  const baseRows = barListRows(data, {
3623
3878
  index,
3624
3879
  category: categories?.[0],
3625
3880
  sortOrder: barListOptions?.sortOrder ?? sortOrder ?? "descending",
3626
3881
  maxItems,
3627
- otherLabel: barListOptions?.otherLabel,
3882
+ otherLabel: barListOptions?.otherLabel ?? otherSeriesLabel(locale),
3628
3883
  exclude: barListOptions?.exclude,
3629
3884
  })
3630
3885
  const rows = baseRows.map((row) => {
@@ -3643,7 +3898,7 @@ function BarListImpl<T extends object>(
3643
3898
  "name",
3644
3899
  ["value"],
3645
3900
  formatter,
3646
- { name: (index && labels?.[index]) || "項目", value: (categories?.[0] && labels?.[categories[0]]) || "値" },
3901
+ { name: (index && labels?.[index]) || t.item, value: (categories?.[0] && labels?.[categories[0]]) || t.value },
3647
3902
  )
3648
3903
 
3649
3904
  const list = (