fix(app): address usage localization review

This commit is contained in:
Michael Wu
2026-10-06 05:34:49 +09:00
parent 4874b3cf96
commit 6fcd93bb5a
6 changed files with 53 additions and 26 deletions
@@ -4,6 +4,7 @@ import { gotoAppShell, openSettings } from "../support/helpers/app";
import { getServerId } from "../support/helpers/server-id";
import { openSettingsHostSection } from "../support/helpers/settings";
import {
expectUsageBalanceInsideCard,
installUsageReportsFixture,
type UsageReportsFixture,
} from "../support/helpers/usage-reports";
@@ -163,18 +164,7 @@ test.describe("usage settings", () => {
await openSettings(page);
await openSettingsHostSection(page, serverId, "usage");
const card = page.getByTestId("usage-card");
const value = page.getByTestId("provider-usage-balance-credits-value");
const reset = page.getByTestId("provider-usage-balance-credits-reset");
await expect(value).toBeVisible({ timeout: 10_000 });
await expect(reset).toHaveText(/resets \d+d/);
await expect(reset).toBeVisible();
const [cardBox, valueBox] = await Promise.all([card.boundingBox(), value.boundingBox()]);
expect(cardBox).not.toBeNull();
expect(valueBox).not.toBeNull();
expect((valueBox?.x ?? 0) + (valueBox?.width ?? 0)).toBeLessThanOrEqual(
(cardBox?.x ?? 0) + (cardBox?.width ?? 0),
);
await expectUsageBalanceInsideCard(page, "credits");
});
test("asks to update a host without usage support and never calls it", async ({ page }) => {
@@ -1,4 +1,4 @@
import type { Page } from "@playwright/test";
import { expect, type Page } from "@playwright/test";
import type { UsageReportEntry } from "@getpaseo/protocol/messages";
import { daemonWsRoutePattern, wsRoutePatternForPort } from "./daemon-port";
@@ -13,6 +13,21 @@ export interface UsageReportsFixture {
waitForListRequests(count: number): Promise<void>;
}
export async function expectUsageBalanceInsideCard(page: Page, balanceId: string): Promise<void> {
const card = page.getByTestId("usage-card");
const value = page.getByTestId(`provider-usage-balance-${balanceId}-value`);
const reset = page.getByTestId(`provider-usage-balance-${balanceId}-reset`);
await expect(value).toBeVisible({ timeout: 10_000 });
await expect(reset).toHaveText(/resets \d+d/);
await expect(reset).toBeVisible();
const [cardBox, valueBox] = await Promise.all([card.boundingBox(), value.boundingBox()]);
expect(cardBox).not.toBeNull();
expect(valueBox).not.toBeNull();
expect((valueBox?.x ?? 0) + (valueBox?.width ?? 0)).toBeLessThanOrEqual(
(cardBox?.x ?? 0) + (cardBox?.width ?? 0),
);
}
interface UsageReportsFixtureOptions {
/**
* Successive `usage.list_reports` responses; the last one repeats. `{ error }` fails that
+4 -1
View File
@@ -13,6 +13,7 @@ import { StatusBadge } from "@/components/ui/status-badge";
import { useIsCompactFormFactor } from "@/constants/layout";
import { isNative } from "@/constants/platform";
import { useCompactTimeAgo } from "@/hooks/use-time-ago";
import { i18n } from "@/i18n/i18next";
import { UsageBalanceBar } from "./balance-bar";
import { usageCopy } from "./copy";
import type { UsageDisplay } from "./display";
@@ -24,7 +25,9 @@ import { UsageWindowBar } from "./window-bar";
function statusText(report: UsageReport): string | null {
if (report.status === "available") return null;
return report.status === "error" ? "Error" : "Unavailable";
return report.status === "error"
? i18n.t("providerUsage.status.error")
: i18n.t("providerUsage.status.unavailable");
}
function reportContent(report: UsageReport) {
+28 -9
View File
@@ -1,4 +1,5 @@
import type { UsageProblem } from "@getpaseo/protocol/messages";
import { i18n } from "@/i18n/i18next";
import { formatCompactTimeAgo, formatCompactTimeAgoAsProse } from "@/utils/time";
// User-facing copy for the usage surfaces, kept in one file so localization is a
@@ -13,23 +14,41 @@ export const usageCopy = {
const ago = formatCompactTimeAgoAsProse(formatCompactTimeAgo(new Date(problem.expiresAt), now));
return `Login expired ${ago}. ${remedy}`;
},
title: "Usage",
planUsage: "Plan usage",
get title() {
return i18n.t("providerUsage.title");
},
get planUsage() {
return i18n.t("providerUsage.title");
},
options: "Settings",
refresh: "Refresh",
refreshAll: "Refresh all",
refreshing: "Refreshing...",
get refresh() {
return i18n.t("providerUsage.refresh");
},
get refreshAll() {
return i18n.t("providerUsage.refresh");
},
get refreshing() {
return i18n.t("providerUsage.refreshing");
},
refreshFailed: "Unable to refresh usage",
updated: "Updated",
loading: "Loading usage...",
empty: "No usage data",
get loading() {
return i18n.t("providerUsage.loading");
},
get empty() {
return i18n.t("providerUsage.empty");
},
noHosts: "No connected hosts",
errorTitle: "Unable to load usage",
get errorTitle() {
return i18n.t("providerUsage.errorTitle");
},
agentError: (reason: string) => `Unable to load usage: ${reason}`,
hostUnavailable: (host: string) => `Connect to ${host} to see usage`,
hostUpgradeRequired: (host: string) => `Update ${host} to see usage`,
clientUnavailable: "Host connection is not ready",
retry: "Try again",
get retry() {
return i18n.t("providerUsage.retry");
},
pin: "Pin",
unpin: "Unpin",
displayAs: "Percentages",
+1 -1
View File
@@ -79,7 +79,7 @@ describe("formatAmount", () => {
await i18n.changeLanguage("ko");
expect(formatProviderUsageLabel("session", "Session")).toBe("세션");
expect(formatProviderUsageLabel("five_hour", "Session")).toBe("세션");
expect(formatProviderUsageLabel("five_hour", "5-hour limit")).toBe("5-hour limit");
expect(formatProviderUsageLabel("credits", "Credits")).toBe("크레딧");
expect(formatProviderUsageLabel("custom_limit", "Custom limit")).toBe("Custom limit");
});
+2 -2
View File
@@ -4,7 +4,6 @@ import type { UsageBalanceUnit } from "./types";
const providerUsageLabelKeys = {
session: "providerUsage.labels.session",
five_hour: "providerUsage.labels.session",
weekly: "providerUsage.labels.weekly",
monthly: "providerUsage.labels.monthly",
code_review: "providerUsage.labels.codeReview",
@@ -22,7 +21,8 @@ export function clampPct(value: number): number {
}
export function formatPct(value: number, locale?: string): string {
return new Intl.NumberFormat(locale, {
const resolvedLocale = locale ?? i18n.resolvedLanguage;
return new Intl.NumberFormat(resolvedLocale, {
style: "percent",
maximumFractionDigits: 0,
}).format(clampPct(value) / 100);