mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 01:32:40 +00:00
fix(app): complete provider usage localization
This commit is contained in:
@@ -2050,8 +2050,8 @@ export const ar: TranslationResources = {
|
||||
loading: "جارٍ تحميل بيانات الاستخدام...",
|
||||
empty: "لا توجد بيانات استخدام",
|
||||
errorTitle: "تعذّر تحميل بيانات الاستخدام",
|
||||
hostUnavailable: "اتصل بهذا المضيف لعرض استخدام المزوّد",
|
||||
hostUpgradeRequired: "حدّث المضيف لعرض استخدام المزوّد",
|
||||
hostUnavailable: "اتصل بـ{{host}} لعرض استخدام المزوّد",
|
||||
hostUpgradeRequired: "حدّث {{host}} لعرض استخدام المزوّد",
|
||||
clientUnavailable: "اتصال المضيف غير جاهز",
|
||||
retry: "حاول مرة أخرى",
|
||||
tooltipLoading: "جارٍ تحميل استخدام الخطة...",
|
||||
|
||||
@@ -2075,8 +2075,8 @@ export const en = {
|
||||
loading: "Loading usage...",
|
||||
empty: "No usage data",
|
||||
errorTitle: "Unable to load usage",
|
||||
hostUnavailable: "Connect to this host to see provider usage",
|
||||
hostUpgradeRequired: "Update the host to see provider usage",
|
||||
hostUnavailable: "Connect to {{host}} to see provider usage",
|
||||
hostUpgradeRequired: "Update {{host}} to see provider usage",
|
||||
clientUnavailable: "Host connection is not ready",
|
||||
retry: "Try again",
|
||||
tooltipLoading: "Loading plan usage...",
|
||||
|
||||
@@ -2098,8 +2098,8 @@ export const es: TranslationResources = {
|
||||
loading: "Cargando uso...",
|
||||
empty: "No hay datos de uso",
|
||||
errorTitle: "No se pudo cargar el uso",
|
||||
hostUnavailable: "Conéctate a este host para ver el uso del proveedor",
|
||||
hostUpgradeRequired: "Actualiza el host para ver el uso del proveedor",
|
||||
hostUnavailable: "Conéctate a {{host}} para ver el uso del proveedor",
|
||||
hostUpgradeRequired: "Actualiza {{host}} para ver el uso del proveedor",
|
||||
clientUnavailable: "La conexión con el host no está lista",
|
||||
retry: "Intentar de nuevo",
|
||||
tooltipLoading: "Cargando uso del plan...",
|
||||
|
||||
@@ -2097,8 +2097,8 @@ export const fr: TranslationResources = {
|
||||
loading: "Chargement de l’utilisation...",
|
||||
empty: "Aucune donnée d’utilisation",
|
||||
errorTitle: "Impossible de charger l’utilisation",
|
||||
hostUnavailable: "Connectez-vous à cet hôte pour voir l’utilisation du fournisseur",
|
||||
hostUpgradeRequired: "Mettez à jour l’hôte pour voir l’utilisation du fournisseur",
|
||||
hostUnavailable: "Connectez-vous à {{host}} pour voir l’utilisation du fournisseur",
|
||||
hostUpgradeRequired: "Mettez à jour {{host}} pour voir l’utilisation du fournisseur",
|
||||
clientUnavailable: "La connexion à l’hôte n’est pas prête",
|
||||
retry: "Réessayer",
|
||||
tooltipLoading: "Chargement de l’utilisation du forfait...",
|
||||
|
||||
@@ -2071,8 +2071,8 @@ export const ja: TranslationResources = {
|
||||
loading: "使用状況を読み込み中...",
|
||||
empty: "使用状況データがありません",
|
||||
errorTitle: "使用状況を読み込めません",
|
||||
hostUnavailable: "このホストに接続してプロバイダーの使用状況を確認してください",
|
||||
hostUpgradeRequired: "ホストを更新してプロバイダーの使用状況を確認してください",
|
||||
hostUnavailable: "{{host}} に接続してプロバイダーの使用状況を確認してください",
|
||||
hostUpgradeRequired: "{{host}} を更新してプロバイダーの使用状況を確認してください",
|
||||
clientUnavailable: "ホスト接続の準備ができていません",
|
||||
retry: "再試行",
|
||||
tooltipLoading: "プラン使用状況を読み込み中...",
|
||||
|
||||
@@ -2060,8 +2060,8 @@ export const ko: TranslationResources = {
|
||||
loading: "사용량 불러오는 중...",
|
||||
empty: "사용량 데이터가 없습니다",
|
||||
errorTitle: "사용량을 불러올 수 없습니다",
|
||||
hostUnavailable: "이 호스트에 연결하여 제공업체 사용량을 확인하세요",
|
||||
hostUpgradeRequired: "호스트를 업데이트하여 제공업체 사용량을 확인하세요",
|
||||
hostUnavailable: "{{host}}에 연결하여 제공업체 사용량을 확인하세요",
|
||||
hostUpgradeRequired: "{{host}}를 업데이트하여 제공업체 사용량을 확인하세요",
|
||||
clientUnavailable: "호스트 연결이 준비되지 않았습니다",
|
||||
retry: "다시 시도",
|
||||
tooltipLoading: "플랜 사용량 불러오는 중...",
|
||||
|
||||
@@ -2083,8 +2083,8 @@ export const ptBR: TranslationResources = {
|
||||
loading: "Carregando uso...",
|
||||
empty: "Sem dados de uso",
|
||||
errorTitle: "Não foi possível carregar o uso",
|
||||
hostUnavailable: "Conecte-se a este host para ver o uso do provedor",
|
||||
hostUpgradeRequired: "Atualize o host para ver o uso do provedor",
|
||||
hostUnavailable: "Conecte-se a {{host}} para ver o uso do provedor",
|
||||
hostUpgradeRequired: "Atualize {{host}} para ver o uso do provedor",
|
||||
clientUnavailable: "A conexão com o host não está pronta",
|
||||
retry: "Tentar novamente",
|
||||
tooltipLoading: "Carregando uso do plano...",
|
||||
|
||||
@@ -2083,8 +2083,8 @@ export const ru: TranslationResources = {
|
||||
loading: "Загрузка данных об использовании...",
|
||||
empty: "Нет данных об использовании",
|
||||
errorTitle: "Не удалось загрузить данные об использовании",
|
||||
hostUnavailable: "Подключитесь к этому хосту, чтобы увидеть использование провайдера",
|
||||
hostUpgradeRequired: "Обновите хост, чтобы увидеть использование провайдера",
|
||||
hostUnavailable: "Подключитесь к {{host}}, чтобы увидеть использование провайдера",
|
||||
hostUpgradeRequired: "Обновите {{host}}, чтобы увидеть использование провайдера",
|
||||
clientUnavailable: "Соединение с хостом не готово",
|
||||
retry: "Повторить",
|
||||
tooltipLoading: "Загрузка использования плана...",
|
||||
|
||||
@@ -2028,8 +2028,8 @@ export const zhCN: TranslationResources = {
|
||||
loading: "正在加载用量...",
|
||||
empty: "暂无用量数据",
|
||||
errorTitle: "无法加载用量",
|
||||
hostUnavailable: "连接到此主机以查看提供商用量",
|
||||
hostUpgradeRequired: "更新主机以查看提供商用量",
|
||||
hostUnavailable: "连接到 {{host}} 以查看提供商用量",
|
||||
hostUpgradeRequired: "更新 {{host}} 以查看提供商用量",
|
||||
clientUnavailable: "主机连接尚未就绪",
|
||||
retry: "重试",
|
||||
tooltipLoading: "正在加载套餐用量...",
|
||||
|
||||
@@ -12,7 +12,15 @@ interface ResolvedBalance {
|
||||
usedPct: number | null;
|
||||
}
|
||||
|
||||
function resolveBalance(balance: UsageBalance, t: TFunction, locale?: string): ResolvedBalance {
|
||||
function resolveBalance({
|
||||
balance,
|
||||
t,
|
||||
locale,
|
||||
}: {
|
||||
balance: UsageBalance;
|
||||
t: TFunction;
|
||||
locale?: string;
|
||||
}): ResolvedBalance {
|
||||
const { used, remaining, limit, unit } = balance;
|
||||
const format = (value: number) => formatAmount(value, unit, locale);
|
||||
if (limit != null && limit > 0) {
|
||||
@@ -48,8 +56,8 @@ function fillToneStyle(tone: UsageTone) {
|
||||
|
||||
export function UsageBalanceBar({ balance }: { balance: UsageBalance }) {
|
||||
const { t, i18n } = useTranslation();
|
||||
useRelativeTimeTick(balance.resetsAt != null);
|
||||
const { amountText, usedPct } = resolveBalance(balance, t, i18n.resolvedLanguage);
|
||||
useRelativeTimeTick({ enabled: balance.resetsAt != null });
|
||||
const { amountText, usedPct } = resolveBalance({ balance, t, locale: i18n.resolvedLanguage });
|
||||
const tone = balance.tone ?? "default";
|
||||
const resetLabel = formatResetLabel(balance.resetsAt);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { RotateCw } from "lucide-react-native";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Text, View, type StyleProp, type TextStyle } from "react-native";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import {
|
||||
@@ -12,16 +13,17 @@ import { ToolbarButton, paneContentToolbarIconSize } from "@/components/ui/pane-
|
||||
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";
|
||||
import { formatUsageFreshness, type UsageRefresh } from "./model";
|
||||
import { formatAgo } from "./format";
|
||||
import type { UsageRefresh } from "./model";
|
||||
import { useReportRefresh } from "./queries";
|
||||
import { UsageSourceIcon } from "./source-icon";
|
||||
import type { UsageReport, UsageReportEntry, UsageWindow } from "./types";
|
||||
import { UsageWindowBar } from "./window-bar";
|
||||
import { useRelativeTimeTick } from "./use-relative-time-tick";
|
||||
|
||||
function statusText(report: UsageReport): string | null {
|
||||
if (report.status === "available") return null;
|
||||
@@ -276,10 +278,13 @@ function UsageFreshness({
|
||||
style: StyleProp<TextStyle>;
|
||||
testID: string;
|
||||
}) {
|
||||
const elapsed = useCompactTimeAgo(new Date(fetchedAt));
|
||||
const { t } = useTranslation();
|
||||
useRelativeTimeTick({ enabled: true });
|
||||
const relativeTime = formatAgo(fetchedAt);
|
||||
if (!relativeTime) return null;
|
||||
return (
|
||||
<Text style={style} numberOfLines={1} testID={testID}>
|
||||
{formatUsageFreshness(elapsed)}
|
||||
{t("providerUsage.timing.updated", { relativeTime })}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ export const usageCopy = {
|
||||
return i18n.t("providerUsage.refreshing");
|
||||
},
|
||||
refreshFailed: "Unable to refresh usage",
|
||||
updated: "Updated",
|
||||
get loading() {
|
||||
return i18n.t("providerUsage.loading");
|
||||
},
|
||||
@@ -43,9 +42,11 @@ export const usageCopy = {
|
||||
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",
|
||||
hostUnavailable: (host: string) => i18n.t("providerUsage.hostUnavailable", { host }),
|
||||
hostUpgradeRequired: (host: string) => i18n.t("providerUsage.hostUpgradeRequired", { host }),
|
||||
get clientUnavailable() {
|
||||
return i18n.t("providerUsage.clientUnavailable");
|
||||
},
|
||||
get retry() {
|
||||
return i18n.t("providerUsage.retry");
|
||||
},
|
||||
|
||||
@@ -82,6 +82,12 @@ describe("formatAmount", () => {
|
||||
expect(formatProviderUsageLabel("five_hour", "Session")).toBe("세션");
|
||||
expect(formatProviderUsageLabel("five_hour", "5-hour limit")).toBe("5-hour limit");
|
||||
expect(formatProviderUsageLabel("monthly", "Monthly")).toBe("월간");
|
||||
expect(formatProviderUsageLabel("code_review:weekly", "Code review · Weekly")).toBe(
|
||||
"코드 검토 · 주간",
|
||||
);
|
||||
expect(formatProviderUsageLabel("code_review:five_hour", "Code review · 5-hour")).toBe(
|
||||
"코드 검토 · 5-hour",
|
||||
);
|
||||
expect(formatProviderUsageLabel("credits", "Credits")).toBe("크레딧");
|
||||
expect(formatProviderUsageLabel("custom_limit", "Custom limit")).toBe("Custom limit");
|
||||
});
|
||||
|
||||
@@ -12,6 +12,17 @@ const providerUsageLabelKeys = {
|
||||
} as const;
|
||||
|
||||
export function formatProviderUsageLabel(id: string, fallback: string): string {
|
||||
if (id.startsWith("code_review:")) {
|
||||
const periodId = id.slice("code_review:".length);
|
||||
const separator = " · ";
|
||||
const periodFallback = fallback.includes(separator)
|
||||
? fallback.slice(fallback.lastIndexOf(separator) + separator.length)
|
||||
: fallback;
|
||||
return `${i18n.t(providerUsageLabelKeys.code_review)} · ${formatProviderUsageLabel(
|
||||
periodId,
|
||||
periodFallback,
|
||||
)}`;
|
||||
}
|
||||
const key =
|
||||
providerUsageLabelKeys[id as keyof typeof providerUsageLabelKeys] ??
|
||||
// Claude exposes its session window with a five-hour ID. Other sources use that same ID for
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { i18n } from "@/i18n/i18next";
|
||||
import {
|
||||
displayPercent,
|
||||
formatUsageFreshness,
|
||||
resolveAgentUsageView,
|
||||
replaceReport,
|
||||
resolveUsageRefresh,
|
||||
@@ -50,7 +50,7 @@ describe("resolveUsageView", () => {
|
||||
supportsUsage: false,
|
||||
query: ready([entry({})]),
|
||||
}),
|
||||
).toEqual({ kind: "unavailable", message: "Update Laptop to see usage" });
|
||||
).toEqual({ kind: "unavailable", message: "Update Laptop to see provider usage" });
|
||||
});
|
||||
|
||||
it("asks for a connection before anything else", () => {
|
||||
@@ -61,7 +61,19 @@ describe("resolveUsageView", () => {
|
||||
supportsUsage: false,
|
||||
query: undefined,
|
||||
}),
|
||||
).toEqual({ kind: "unavailable", message: "Connect to Laptop to see usage" });
|
||||
).toEqual({ kind: "unavailable", message: "Connect to Laptop to see provider usage" });
|
||||
});
|
||||
|
||||
it("localizes unavailable host messages while retaining the host name", async () => {
|
||||
await i18n.changeLanguage("ko");
|
||||
expect(
|
||||
resolveUsageView({
|
||||
hostLabel: "Laptop",
|
||||
isConnected: false,
|
||||
supportsUsage: false,
|
||||
query: undefined,
|
||||
}),
|
||||
).toEqual({ kind: "unavailable", message: "Laptop에 연결하여 제공업체 사용량을 확인하세요" });
|
||||
});
|
||||
|
||||
it("moves from loading to ready to error", () => {
|
||||
@@ -79,6 +91,10 @@ describe("resolveUsageView", () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await i18n.changeLanguage("en");
|
||||
});
|
||||
|
||||
const hosts: UsageHost[] = [
|
||||
{ serverId: "offline", label: "Offline", isConnected: false, supportsUsage: true },
|
||||
{ serverId: "old", label: "Old", isConnected: true, supportsUsage: false },
|
||||
@@ -184,14 +200,6 @@ describe("displayPercent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatUsageFreshness", () => {
|
||||
it("says when the report was fetched", () => {
|
||||
expect(formatUsageFreshness("now")).toBe("Updated just now");
|
||||
expect(formatUsageFreshness("3m")).toBe("Updated 3m ago");
|
||||
expect(formatUsageFreshness("Jan 15")).toBe("Updated Jan 15");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveUsageRefresh", () => {
|
||||
it("is pending while a refresh runs, even after an earlier failure", () => {
|
||||
expect(resolveUsageRefresh({ isPending: true, error: new Error("boom") })).toBe("pending");
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { formatCompactTimeAgoAsProse } from "@/utils/time";
|
||||
import { usageCopy } from "./copy";
|
||||
import type { UsageDisplayAs } from "./preferences";
|
||||
import type { UsageReportEntry, UsageView, UsageWindow } from "./types";
|
||||
@@ -31,11 +30,6 @@ export function usageWindowRowLabel(input: {
|
||||
return `${input.pinLabel}, ${summary}`;
|
||||
}
|
||||
|
||||
/** When a report was fetched, from its compact relative time: "Updated 3m ago". */
|
||||
export function formatUsageFreshness(compactTimeAgo: string): string {
|
||||
return `${usageCopy.updated} ${formatCompactTimeAgoAsProse(compactTimeAgo)}`;
|
||||
}
|
||||
|
||||
/** A user-requested refresh of one report. The previous report stays on screen throughout. */
|
||||
export type UsageRefresh = "idle" | "pending" | "failed";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useReducer } from "react";
|
||||
import { subscribeToRelativeTimeTick } from "@/utils/relative-time-ticker";
|
||||
|
||||
/** Re-renders a mounted usage label when its relative time can change. */
|
||||
export function useRelativeTimeTick(enabled: boolean): void {
|
||||
export function useRelativeTimeTick({ enabled }: { enabled: boolean }): void {
|
||||
const [, tick] = useReducer((value: number) => value + 1, 0);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -50,7 +50,7 @@ export function UsageWindowBar({
|
||||
|
||||
const isAtRisk = window.runsOutAt != null && window.shortfallPct != null;
|
||||
const relativeTime = isAtRisk ? window.runsOutAt : window.resetsAt;
|
||||
useRelativeTimeTick(relativeTime != null);
|
||||
useRelativeTimeTick({ enabled: relativeTime != null });
|
||||
const trailing = isAtRisk
|
||||
? formatRunsOutLabel(window.runsOutAt)
|
||||
: formatResetLabel(window.resetsAt);
|
||||
|
||||
Reference in New Issue
Block a user