@squadbase/vantage 0.6.2-dev1.54bde93 → 0.6.2-dev1.56da87f

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 (89) 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-add-feature/SKILL.md +5 -0
  76. package/skills/vantage-design/SKILL.md +25 -27
  77. package/skills/vantage-design/references/extract.md +10 -13
  78. package/skills/vantage-design/references/format.md +12 -173
  79. package/skills/vantage-design/references/presets.md +0 -10
  80. package/templates/AGENTS.md +29 -15
  81. package/templates/agents/api.md +7 -0
  82. package/templates/agents/page.md +37 -1
  83. package/templates/agents/slide.md +7 -1
  84. package/dist/chunk-2N72FKLB.js.map +0 -1
  85. package/dist/chunk-57GHR5LN.js.map +0 -1
  86. package/dist/chunk-6OTTGAPO.js.map +0 -1
  87. package/dist/chunk-JXFCSF3K.js.map +0 -1
  88. package/dist/chunk-PDOBHMUQ.js.map +0 -1
  89. package/dist/chunk-QOXXOFGL.js.map +0 -1
@@ -15,6 +15,153 @@ import { cva } from "class-variance-authority"
15
15
 
16
16
  import { cn } from "@squadbase/vantage/ui"
17
17
  import type { TrendDirection } from "@squadbase/vantage/components"
18
+ import { useSyncExternalStore } from "react"
19
+
20
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
21
+
22
+ /**
23
+ * The display language of the parts and the defaults that follow it.
24
+ *
25
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
26
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
27
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
28
+ *
29
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
30
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
31
+ */
32
+
33
+ /** The language of the built-in wording: Japanese or English. */
34
+ type UiLanguage = "ja" | "en"
35
+
36
+ /** The locale used when `<html lang>` gives none (and on the server). */
37
+ const FALLBACK_LOCALE = "en-US"
38
+
39
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
40
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
41
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
42
+ }
43
+
44
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
45
+ function normalizeLocale(tag: string | null | undefined): string {
46
+ const value = tag?.trim()
47
+ if (!value) return FALLBACK_LOCALE
48
+ try {
49
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
50
+ } catch {
51
+ return FALLBACK_LOCALE
52
+ }
53
+ }
54
+
55
+ /**
56
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
57
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
58
+ */
59
+ function documentLocale(): string {
60
+ if (typeof document === "undefined") return FALLBACK_LOCALE
61
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
62
+ }
63
+
64
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
65
+ function resolveLocale(locale?: string | null): string {
66
+ return locale || documentLocale()
67
+ }
68
+
69
+ /** Picks the wording table of the locale's language. */
70
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
71
+ return table[uiLanguage(resolveLocale(locale))]
72
+ }
73
+
74
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
75
+ function defaultCurrency(locale?: string | null): string {
76
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
77
+ }
78
+
79
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
80
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
81
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
82
+ }
83
+
84
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
85
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
86
+
87
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
88
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
89
+
90
+ /**
91
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
92
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
93
+ * Monday for the rest.
94
+ */
95
+ function weekStartsOn(locale?: string | null): WeekDay {
96
+ const tag = normalizeLocale(resolveLocale(locale))
97
+ if (uiLanguage(tag) === "ja") return 1
98
+ try {
99
+ const info = new Intl.Locale(tag) as Intl.Locale & {
100
+ getWeekInfo?: () => { firstDay: number }
101
+ weekInfo?: { firstDay: number }
102
+ }
103
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
104
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
105
+ } catch {
106
+ // Fall through to the table below.
107
+ }
108
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
109
+ }
110
+
111
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
112
+
113
+ const listeners = new Set<() => void>()
114
+ let observer: MutationObserver | null = null
115
+
116
+ /** One observer on `<html lang>` for every subscribed part. */
117
+ function subscribe(onChange: () => void): () => void {
118
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
119
+ listeners.add(onChange)
120
+ if (!observer) {
121
+ observer = new MutationObserver(() => {
122
+ for (const listener of [...listeners]) listener()
123
+ })
124
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
125
+ }
126
+ return () => {
127
+ listeners.delete(onChange)
128
+ if (listeners.size === 0 && observer) {
129
+ observer.disconnect()
130
+ observer = null
131
+ }
132
+ }
133
+ }
134
+
135
+ const serverLocale = () => FALLBACK_LOCALE
136
+
137
+ /**
138
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
139
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
140
+ */
141
+ function useDocumentLocale(): string {
142
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
143
+ }
144
+
145
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
146
+ function useLocale(locale?: string | null): string {
147
+ const page = useDocumentLocale()
148
+ return locale || page
149
+ }
150
+
151
+ /* ---------- end of inlined code ---------- */
152
+
153
+ const TEXT = {
154
+ ja: {
155
+ flat: (value: number) => `変化なし ${value}%`,
156
+ up: (value: number) => `${value}% 増加`,
157
+ down: (value: number) => `${value}% 減少`,
158
+ },
159
+ en: {
160
+ flat: (value: number) => `No change ${value}%`,
161
+ up: (value: number) => `Up ${value}%`,
162
+ down: (value: number) => `Down ${value}%`,
163
+ },
164
+ }
18
165
 
19
166
  const trendIndicatorVariants = cva("inline-flex items-center gap-0.5 text-sm font-medium", {
20
167
  variants: {
@@ -51,12 +198,8 @@ export const TrendIndicator = forwardRef<HTMLSpanElement, TrendIndicatorProps>(
51
198
 
52
199
  const Icon = direction === "up" ? ArrowUp : direction === "down" ? ArrowDown : Minus
53
200
 
54
- const ariaLabel =
55
- direction === "neutral"
56
- ? `変化なし ${value}%`
57
- : direction === "up"
58
- ? `${value}% 増加`
59
- : `${value}% 減少`
201
+ const t = TEXT[uiLanguage(useLocale())]
202
+ const ariaLabel = direction === "neutral" ? t.flat(value) : direction === "up" ? t.up(value) : t.down(value)
60
203
 
61
204
  return (
62
205
  <span
@@ -7,8 +7,9 @@
7
7
  * instant, with the absolute time and zone ("2026/10/20 16:41 JST") in both a
8
8
  * tooltip and the `title` attribute.
9
9
  *
10
- * `variant="relative"` (default) reads "23 分前に更新"; `"absolute-first"` reads
11
- * "16:41 更新(23 分前)" (the date is added when it is not today).
10
+ * `variant="relative"` (default) reads "Updated 23 minutes ago" (ja: "23 分前に更新"); `"absolute-first"` reads
11
+ * "Updated 16:41 (23 minutes ago)" (ja: "16:41 更新(23 分前)") (the date is added when it is not today).
12
+ * The language and date format follow the page (`<html lang>`).
12
13
  *
13
14
  * Hydration-safe: the first render (server and client) is an empty `<time>`
14
15
  * carrying only the ISO `dateTime`, because the text depends on the clock and
@@ -24,9 +25,156 @@ import { EMPTY_VALUE, formatDate } from "@squadbase/vantage/components"
24
25
  import { formatRelativeTime } from "@squadbase/vantage/components"
25
26
  import { cn } from "@squadbase/vantage/ui"
26
27
  import { Tooltip, TooltipContent, TooltipTrigger } from "@squadbase/vantage/ui"
28
+ import { useSyncExternalStore } from "react"
29
+
30
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
31
+
32
+ /**
33
+ * The display language of the parts and the defaults that follow it.
34
+ *
35
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
36
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
37
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
38
+ *
39
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
40
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
41
+ */
42
+
43
+ /** The language of the built-in wording: Japanese or English. */
44
+ type UiLanguage = "ja" | "en"
45
+
46
+ /** The locale used when `<html lang>` gives none (and on the server). */
47
+ const FALLBACK_LOCALE = "en-US"
48
+
49
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
50
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
51
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
52
+ }
53
+
54
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
55
+ function normalizeLocale(tag: string | null | undefined): string {
56
+ const value = tag?.trim()
57
+ if (!value) return FALLBACK_LOCALE
58
+ try {
59
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
60
+ } catch {
61
+ return FALLBACK_LOCALE
62
+ }
63
+ }
64
+
65
+ /**
66
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
67
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
68
+ */
69
+ function documentLocale(): string {
70
+ if (typeof document === "undefined") return FALLBACK_LOCALE
71
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
72
+ }
73
+
74
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
75
+ function resolveLocale(locale?: string | null): string {
76
+ return locale || documentLocale()
77
+ }
78
+
79
+ /** Picks the wording table of the locale's language. */
80
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
81
+ return table[uiLanguage(resolveLocale(locale))]
82
+ }
83
+
84
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
85
+ function defaultCurrency(locale?: string | null): string {
86
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
87
+ }
88
+
89
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
90
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
91
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
92
+ }
93
+
94
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
95
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
96
+
97
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
98
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
99
+
100
+ /**
101
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
102
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
103
+ * Monday for the rest.
104
+ */
105
+ function weekStartsOn(locale?: string | null): WeekDay {
106
+ const tag = normalizeLocale(resolveLocale(locale))
107
+ if (uiLanguage(tag) === "ja") return 1
108
+ try {
109
+ const info = new Intl.Locale(tag) as Intl.Locale & {
110
+ getWeekInfo?: () => { firstDay: number }
111
+ weekInfo?: { firstDay: number }
112
+ }
113
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
114
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
115
+ } catch {
116
+ // Fall through to the table below.
117
+ }
118
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
119
+ }
120
+
121
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
122
+
123
+ const listeners = new Set<() => void>()
124
+ let observer: MutationObserver | null = null
125
+
126
+ /** One observer on `<html lang>` for every subscribed part. */
127
+ function subscribe(onChange: () => void): () => void {
128
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
129
+ listeners.add(onChange)
130
+ if (!observer) {
131
+ observer = new MutationObserver(() => {
132
+ for (const listener of [...listeners]) listener()
133
+ })
134
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
135
+ }
136
+ return () => {
137
+ listeners.delete(onChange)
138
+ if (listeners.size === 0 && observer) {
139
+ observer.disconnect()
140
+ observer = null
141
+ }
142
+ }
143
+ }
144
+
145
+ const serverLocale = () => FALLBACK_LOCALE
146
+
147
+ /**
148
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
149
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
150
+ */
151
+ function useDocumentLocale(): string {
152
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
153
+ }
154
+
155
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
156
+ function useLocale(locale?: string | null): string {
157
+ const page = useDocumentLocale()
158
+ return locale || page
159
+ }
160
+
161
+ /* ---------- end of inlined code ---------- */
27
162
 
28
163
  const REFRESH_MS = 60_000
29
164
 
165
+ const TEXT = {
166
+ ja: {
167
+ justNow: "たった今",
168
+ relative: (relative: string) => `${relative}に更新`,
169
+ absoluteFirst: (shown: string, relative: string) => `${shown} 更新(${relative})`,
170
+ },
171
+ en: {
172
+ justNow: "just now",
173
+ relative: (relative: string) => `Updated ${relative}`,
174
+ absoluteFirst: (shown: string, relative: string) => `Updated ${shown} (${relative})`,
175
+ },
176
+ }
177
+
30
178
  export interface UpdatedAtProps {
31
179
  /** When the data was last updated. */
32
180
  at: Date | string | number
@@ -44,35 +192,41 @@ export interface UpdatedAtProps {
44
192
  const toMs = (value: Date | string | number) =>
45
193
  value instanceof Date ? value.getTime() : new Date(value).getTime()
46
194
 
47
- function relative(at: number, now: number): string {
48
- if (now - at < REFRESH_MS && now - at > -REFRESH_MS) return "たった今"
49
- return formatRelativeTime(at, { now })
195
+ function relative(at: number, now: number, locale: string): string {
196
+ if (now - at < REFRESH_MS && now - at > -REFRESH_MS) return TEXT[uiLanguage(locale)].justNow
197
+ return formatRelativeTime(at, { now, locale })
50
198
  }
51
199
 
52
200
  /** "2026/10/20 16:41 JST" */
53
- function absolute(at: number, timeZone?: string): string | undefined {
201
+ function absolute(at: number, locale: string, timeZone?: string): string | undefined {
54
202
  if (!Number.isFinite(at)) return undefined
55
- const zone = new Intl.DateTimeFormat("ja-JP", { timeZone, timeZoneName: "short" })
203
+ const zone = new Intl.DateTimeFormat(locale, { timeZone, timeZoneName: "short" })
56
204
  .formatToParts(at)
57
205
  .find((part) => part.type === "timeZoneName")?.value
58
- const text = formatDate(at, "datetime", "ja-JP", { timeZone })
206
+ const text = formatDate(at, "datetime", locale, { timeZone })
59
207
  return zone ? `${text} ${zone}` : text
60
208
  }
61
209
 
62
- function label(at: number, now: number, variant: "relative" | "absolute-first", timeZone?: string): string {
210
+ function label(at: number, now: number, variant: "relative" | "absolute-first", locale: string, timeZone?: string): string {
63
211
  if (!Number.isFinite(at) || !Number.isFinite(now)) return EMPTY_VALUE
212
+ const t = TEXT[uiLanguage(locale)]
64
213
  if (variant === "relative") {
65
- return now - at < REFRESH_MS && now - at > -REFRESH_MS ? "たった今" : `${relative(at, now)}に更新`
214
+ return now - at < REFRESH_MS && now - at > -REFRESH_MS
215
+ ? uiLanguage(locale) === "ja"
216
+ ? t.justNow
217
+ : t.relative(t.justNow)
218
+ : t.relative(relative(at, now, locale))
66
219
  }
67
- const today = formatDate(at, "date", "ja-JP", { timeZone }) === formatDate(now, "date", "ja-JP", { timeZone })
68
- const when = formatDate(at, "datetime", "ja-JP", { timeZone })
69
- // "2026/10/20 16:41" -> time only when it is today.
220
+ const today = formatDate(at, "date", locale, { timeZone }) === formatDate(now, "date", locale, { timeZone })
221
+ const when = formatDate(at, "datetime", locale, { timeZone })
222
+ // "2026/10/20 16:41" -> time only when it is today (the time is always the last 5 characters: hourCycle h23).
70
223
  const shown = today ? when.slice(-5) : when
71
- return `${shown} 更新(${relative(at, now)})`
224
+ return t.absoluteFirst(shown, relative(at, now, locale))
72
225
  }
73
226
 
74
227
  export const UpdatedAt = forwardRef<HTMLTimeElement, UpdatedAtProps>(
75
228
  ({ at, variant = "relative", timeZone, prefix, now, className }, ref) => {
229
+ const locale = useLocale()
76
230
  const atMs = toMs(at)
77
231
  const [current, setCurrent] = useState<number | null>(null)
78
232
  const fixed = now === undefined ? undefined : toMs(now as Date | number)
@@ -87,9 +241,9 @@ export const UpdatedAt = forwardRef<HTMLTimeElement, UpdatedAtProps>(
87
241
  const clock = fixed ?? current
88
242
  const ready = clock !== null
89
243
  const dateTime = Number.isFinite(atMs) ? new Date(atMs).toISOString() : undefined
90
- const full = ready ? absolute(atMs, timeZone) : undefined
244
+ const full = ready ? absolute(atMs, locale, timeZone) : undefined
91
245
  const classes = cn("text-sm text-muted-foreground tabular-nums", className)
92
- const text = ready ? `${prefix ?? ""}${label(atMs, clock, variant, timeZone)}` : null
246
+ const text = ready ? `${prefix ?? ""}${label(atMs, clock, variant, locale, timeZone)}` : null
93
247
 
94
248
  if (!full) {
95
249
  return (