@celestia-island/hikari 0.52.1 → 0.53.0
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.
- package/package.json +1 -1
- package/src/i18n/context.ts +7 -0
- package/src/index.ts +3 -0
- package/src/utils/format.test.ts +45 -1
- package/src/utils/format.ts +39 -4
package/package.json
CHANGED
package/src/i18n/context.ts
CHANGED
|
@@ -54,6 +54,13 @@ localeCache.set("en", enFallback);
|
|
|
54
54
|
// every language without knowing which locale is currently active.
|
|
55
55
|
let mergedMessages: Record<string, Messages> = {};
|
|
56
56
|
|
|
57
|
+
/** The app-selected hikari locale ("en" until setLocale runs). Date and
|
|
58
|
+
* time formatters consume this so display follows the user's chosen
|
|
59
|
+
* language instead of the browser's. */
|
|
60
|
+
export function activeLocale(): string {
|
|
61
|
+
return state.locale;
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
export async function setLocale(locale: string): Promise<void> {
|
|
58
65
|
if (!localeCache.has(locale)) {
|
|
59
66
|
localeCache.set(locale, buildLocaleMessages(locale));
|
package/src/index.ts
CHANGED
|
@@ -268,6 +268,7 @@ export {
|
|
|
268
268
|
export {
|
|
269
269
|
useI18n,
|
|
270
270
|
setLocale,
|
|
271
|
+
activeLocale,
|
|
271
272
|
mergeMessages,
|
|
272
273
|
} from "./i18n/context";
|
|
273
274
|
|
|
@@ -455,6 +456,8 @@ export {
|
|
|
455
456
|
formatPriceUsd,
|
|
456
457
|
formatRelativeTime,
|
|
457
458
|
formatDateTime,
|
|
459
|
+
formatDate,
|
|
460
|
+
formatTime,
|
|
458
461
|
formatMs,
|
|
459
462
|
type RelativeTimeT,
|
|
460
463
|
} from "./utils/format";
|
package/src/utils/format.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
|
-
import { formatRelativeTime, type RelativeTimeT } from "./format";
|
|
3
|
+
import { formatDate, formatDateTime, formatRelativeTime, formatTime, type RelativeTimeT } from "./format";
|
|
4
4
|
|
|
5
5
|
const MIN = 60_000;
|
|
6
6
|
const HOUR = 3_600_000;
|
|
@@ -114,3 +114,47 @@ describe("formatRelativeTime", () => {
|
|
|
114
114
|
expect(calls).toHaveLength(0);
|
|
115
115
|
});
|
|
116
116
|
});
|
|
117
|
+
|
|
118
|
+
// ── Locale awareness (2026-09-15) ────────────────────────────────────
|
|
119
|
+
// Every locale-sensitive formatter must follow the app-selected hikari
|
|
120
|
+
// locale (setLocale), not the browser default: a zh-Hans app on an en
|
|
121
|
+
// browser used to render dates as "9/13/2026" next to zh words.
|
|
122
|
+
|
|
123
|
+
describe("locale-aware date formatting", () => {
|
|
124
|
+
it("formatDate composes the locale's full day label", async () => {
|
|
125
|
+
const { setLocale } = await import("../i18n/context");
|
|
126
|
+
const d = new Date(2026, 8, 13);
|
|
127
|
+
await setLocale("zh-Hans");
|
|
128
|
+
expect(formatDate(d, { month: "short", day: "numeric" })).toBe("9月13日");
|
|
129
|
+
await setLocale("en");
|
|
130
|
+
expect(formatDate(d, { month: "short", day: "numeric" })).toBe("Sep 13");
|
|
131
|
+
// Whole-date default: no opts = the locale's own date format.
|
|
132
|
+
expect(formatDate(d)).toContain("2026");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("formatTime follows the locale's clock convention", async () => {
|
|
136
|
+
const { setLocale } = await import("../i18n/context");
|
|
137
|
+
const d = new Date(2026, 8, 13, 15, 24);
|
|
138
|
+
await setLocale("en");
|
|
139
|
+
expect(formatTime(d)).toMatch(/3:24/);
|
|
140
|
+
await setLocale("zh-Hans");
|
|
141
|
+
expect(formatTime(d)).toMatch(/15:24/);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("formatDateTime renders through the app locale", async () => {
|
|
145
|
+
const { setLocale } = await import("../i18n/context");
|
|
146
|
+
await setLocale("ja");
|
|
147
|
+
const out = formatDateTime(new Date(2026, 8, 13, 15, 24));
|
|
148
|
+
expect(out).toMatch(/2026/);
|
|
149
|
+
// ja month rendering carries the 月 particle from the ja locale data.
|
|
150
|
+
expect(out).toMatch(/9月13日|9\/13/);
|
|
151
|
+
await setLocale("en");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("formatDate/formatTime return empty for missing or invalid input", () => {
|
|
155
|
+
expect(formatDate("")).toBe("");
|
|
156
|
+
expect(formatDate("nope")).toBe("");
|
|
157
|
+
expect(formatTime(0)).toBe("");
|
|
158
|
+
expect(formatTime("junk")).toBe("");
|
|
159
|
+
});
|
|
160
|
+
});
|
package/src/utils/format.ts
CHANGED
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
* Consolidates the hand-rolled copies previously scattered across
|
|
6
6
|
* arona (formatDate/formatUptime/formatNumber) and shittim-chest
|
|
7
7
|
* (formatTokenCount/formatMediaTime).
|
|
8
|
-
|
|
8
|
+
*
|
|
9
|
+
* Every locale-sensitive formatter resolves its locale through the
|
|
10
|
+
* hikari i18n state (activeLocale()), NOT the browser default: the app
|
|
11
|
+
* selects its language explicitly (setLocale on switch), and dates that
|
|
12
|
+
* follow the browser while every word around them follows the app read
|
|
13
|
+
* as mixed-language output (2026-09-15 user report: "9月 13" headings
|
|
14
|
+
* under a zh-Hans app on an en browser). */
|
|
15
|
+
import { activeLocale } from "../i18n/context";
|
|
9
16
|
|
|
10
17
|
/** "1234" -> "1.2k", "2500000" -> "2.5M". */
|
|
11
18
|
export function formatTokenCount(n: number): string {
|
|
@@ -75,14 +82,15 @@ export function formatRelativeTime(
|
|
|
75
82
|
t?.("common.time.weeksAgo", "{n} w ago", { n: weeks }) ?? `${weeks}w ago`
|
|
76
83
|
);
|
|
77
84
|
}
|
|
78
|
-
return d.toLocaleDateString();
|
|
85
|
+
return d.toLocaleDateString(activeLocale());
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
// Media timestamps ("m:ss") already live on the media-player kit — one
|
|
82
89
|
// definition, re-exported so `../utils/format` is the single import site.
|
|
83
90
|
export { formatMediaTime } from "../components/HkMediaControlBar";
|
|
84
91
|
|
|
85
|
-
/** Absolute timestamp formatting with a shared locale-aware renderer.
|
|
92
|
+
/** Absolute timestamp formatting with a shared locale-aware renderer.
|
|
93
|
+
* Follows the app-selected hikari locale (see activeLocale). */
|
|
86
94
|
export function formatDateTime(
|
|
87
95
|
input: string | number | Date,
|
|
88
96
|
opts?: { dateStyle?: "short" | "medium" | "long"; timeStyle?: "short" | "medium" },
|
|
@@ -90,12 +98,39 @@ export function formatDateTime(
|
|
|
90
98
|
if (!input) return "";
|
|
91
99
|
const d = input instanceof Date ? input : new Date(input);
|
|
92
100
|
if (isNaN(d.getTime())) return "";
|
|
93
|
-
return d.toLocaleString(
|
|
101
|
+
return d.toLocaleString(activeLocale(), {
|
|
94
102
|
dateStyle: opts?.dateStyle ?? "medium",
|
|
95
103
|
timeStyle: opts?.timeStyle ?? "short",
|
|
96
104
|
});
|
|
97
105
|
}
|
|
98
106
|
|
|
107
|
+
/** Date-only rendering (no time), following the app-selected hikari
|
|
108
|
+
* locale. `opts` is the raw Intl.DateTimeFormatOptions passthrough —
|
|
109
|
+
* omit it for the locale's whole-date default, or scope to a slice
|
|
110
|
+
* (e.g. { month: "short", day: "numeric" } for day-group headings). */
|
|
111
|
+
export function formatDate(
|
|
112
|
+
input: string | number | Date,
|
|
113
|
+
opts?: Intl.DateTimeFormatOptions,
|
|
114
|
+
): string {
|
|
115
|
+
if (!input) return "";
|
|
116
|
+
const d = input instanceof Date ? input : new Date(input);
|
|
117
|
+
if (isNaN(d.getTime())) return "";
|
|
118
|
+
return d.toLocaleDateString(activeLocale(), opts);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Time-only rendering (no date), following the app-selected hikari
|
|
122
|
+
* locale. `opts` is the raw Intl.DateTimeFormatOptions passthrough —
|
|
123
|
+
* omit it for the locale's default (en "3:24 PM", zh "15:24"). */
|
|
124
|
+
export function formatTime(
|
|
125
|
+
input: string | number | Date,
|
|
126
|
+
opts?: Intl.DateTimeFormatOptions,
|
|
127
|
+
): string {
|
|
128
|
+
if (!input) return "";
|
|
129
|
+
const d = input instanceof Date ? input : new Date(input);
|
|
130
|
+
if (isNaN(d.getTime())) return "";
|
|
131
|
+
return d.toLocaleTimeString(activeLocale(), opts);
|
|
132
|
+
}
|
|
133
|
+
|
|
99
134
|
/** Milliseconds for latency/duration displays: sub-second keeps one
|
|
100
135
|
* decimal, >=1s switches to whole seconds (and beyond to minutes).
|
|
101
136
|
* Thousands-grouped for the rare large value. */
|