mirror of
https://github.com/phantomptr/ps5upload.git
synced 2026-10-21 19:33:08 -05:00
Installed apps: localize the "last seen" label (today / yesterday / N days ago) (#454)
formatLastSeen returned hard-coded English words ("today", "yesterday",
"N days ago") that were then substituted into the translated line
installed_playtime_line, so every non-English UI showed e.g. Spanish
"visto por ultima vez yesterday".
Format the day count with Intl.RelativeTimeFormat in the app language
(numeric: "auto"), so 0 / 1 days read as today / yesterday and their
equivalents in each locale (hoy / ayer / hace 5 dias, heute / gestern, ...).
This covers every shipped locale without new i18n keys.
- formatLastSeen takes an optional `locale` (default "en"), so existing
callers and the English output are unchanged. An invalid tag falls back
to the previous English strings.
- AppCard passes the active language from useLangStore.
- Tests: Spanish and German labels, and the invalid-tag fallback.
The "never" label (a title with play time but no last-seen timestamp) is
still English; it has no Intl equivalent and is left out of this change.
Not run locally: npm run validate / vitest / tsc / eslint (no Node toolchain
on the machine used). Intl outputs were checked in a browser console.
This commit is contained in:
1 parent
01afdf8926
commit
16e48cb243
3 files changed
+34
-7
No files matched your search
@@ -83,7 +83,7 @@ import { useMakeWay } from "../../lib/useMakeWay";
|
||||
import { pushNotification } from "../../state/notifications";
|
||||
import { withConsolePrefix } from "../../state/roster";
|
||||
import { audit } from "../../state/auditLog";
|
||||
import { useTr } from "../../state/lang";
|
||||
import { useLangStore, useTr } from "../../state/lang";
|
||||
import { transferAddr, mgmtAddr, hostOf } from "../../lib/addr";
|
||||
import { useImageRetry } from "../../lib/useImageRetry";
|
||||
import { transferScreenBusy } from "../../lib/ps5Transfers";
|
||||
@@ -362,6 +362,7 @@ export const AppCard = memo(function AppCard({
|
||||
* bookkeeping around a cold start doesn't run for a foreground nudge. */
|
||||
}) {
|
||||
const tr = useTr();
|
||||
const lang = useLangStore((s) => s.lang);
|
||||
const navigate = useNavigate();
|
||||
const canPlay = !title.system;
|
||||
// On-console folder this title lives in — only folder/disc kinds carry a
|
||||
@@ -493,7 +494,7 @@ export const AppCard = memo(function AppCard({
|
||||
{playSeconds && playSeconds > 0
|
||||
? tr(
|
||||
"installed_playtime_line",
|
||||
{ time: formatPlayTime(playSeconds), seen: formatLastSeen(lastSeenMs) },
|
||||
{ time: formatPlayTime(playSeconds), seen: formatLastSeen(lastSeenMs, undefined, lang) },
|
||||
`${formatPlayTime(playSeconds)} · last seen ${formatLastSeen(lastSeenMs)}`,
|
||||
)
|
||||
: tr("installed_playtime_never", undefined, "not seen playing")}
|
||||
|
||||
@@ -153,6 +153,17 @@ describe("formatLastSeen", () => {
|
||||
expect(formatLastSeen(now - DAY, now)).toBe("yesterday");
|
||||
expect(formatLastSeen(now - 5 * DAY, now)).toBe("5 days ago");
|
||||
});
|
||||
it("localizes the label to the app language", () => {
|
||||
expect(formatLastSeen(now, now, "es")).toBe("hoy");
|
||||
expect(formatLastSeen(now - DAY, now, "es")).toBe("ayer");
|
||||
expect(formatLastSeen(now - 5 * DAY, now, "es")).toBe("hace 5 días");
|
||||
expect(formatLastSeen(now - DAY, now, "de")).toBe("gestern");
|
||||
});
|
||||
it("falls back to English for an unknown locale tag", () => {
|
||||
expect(formatLastSeen(now, now, "not a locale")).toBe("today");
|
||||
expect(formatLastSeen(now - DAY, now, "not a locale")).toBe("yesterday");
|
||||
expect(formatLastSeen(now - 5 * DAY, now, "not a locale")).toBe("5 days ago");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatPlayTime", () => {
|
||||
|
||||
@@ -239,11 +239,26 @@ export function daysSinceLastSeen(
|
||||
* "N days ago". Drives the Installed-Apps not-played surface (#116). The
|
||||
* honest phrasing is "seen" — it's when ps5upload last observed the title
|
||||
* running, not Sony's console-side last-played (which the app can't read
|
||||
* cheaply). `now` injectable for tests. */
|
||||
export function formatLastSeen(ms: number | undefined, now = Date.now()): string {
|
||||
* cheaply). `now` injectable for tests. `locale` is the app language (a BCP-47
|
||||
* tag); it defaults to English so callers without one stay deterministic. */
|
||||
export function formatLastSeen(
|
||||
ms: number | undefined,
|
||||
now = Date.now(),
|
||||
locale = "en",
|
||||
): string {
|
||||
const d = daysSinceLastSeen(ms, now);
|
||||
if (d === null) return "never";
|
||||
if (d === 0) return "today";
|
||||
if (d === 1) return "yesterday";
|
||||
return `${d} days ago`;
|
||||
try {
|
||||
// `numeric: "auto"` turns 0 and -1 days into "today" / "yesterday" (and
|
||||
// their equivalents in other languages); older reads "N days ago".
|
||||
return new Intl.RelativeTimeFormat(locale, { numeric: "auto" }).format(
|
||||
d === 0 ? 0 : -d,
|
||||
"day",
|
||||
);
|
||||
} catch {
|
||||
// Unknown locale tag: fall back to English.
|
||||
if (d === 0) return "today";
|
||||
if (d === 1) return "yesterday";
|
||||
return `${d} days ago`;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user