Localize provider usage UI

This commit is contained in:
Michael Wu
2026-10-03 05:07:05 +09:00
parent d831c7bf33
commit eb2eabe91e
13 changed files with 460 additions and 28 deletions
+1
View File
@@ -91,3 +91,4 @@ Within a migrated surface, do not leave mixed-language neighboring labels when t
- Batch 4Z expanded the supported locale set to the six UN official languages: Arabic, Chinese, English, French, Russian, and Spanish. Arabic, French, Russian, and Spanish now have full client-owned UI resource coverage, with key parity, fallback-ratio, and interpolation-placeholder tests guarding the generated translations. Arabic does not enable RTL layout direction in this batch.
- Batch 5A added Brazilian Portuguese (`pt-BR`) resource coverage, language selector labels, i18next registration, and system-locale mapping for `pt-BR` and bare `pt`. Non-Brazilian Portuguese regional locales remain unsupported until a matching resource is added.
- Batch 5B added Korean (`ko`) resource coverage, language selector labels, i18next registration, and system-locale mapping for `ko` and Korean regional locales.
- Batch 5C migrated provider-usage chrome across the Usage settings tab and context-meter tooltip: loading/error/empty states, refresh actions, usage/remaining labels, availability statuses, reset/run-out timing, and updated-at text. Provider names, plan labels, usage-window labels, balance/detail labels, source labels, and raw provider errors remain runtime values.
+37
View File
@@ -1950,6 +1950,43 @@ export const ar: TranslationResources = {
saveAccessibility: "حفظ تعليق المراجعة",
},
},
providerUsage: {
title: "استخدام الخطة",
refresh: "تحديث",
refreshing: "جارٍ التحديث...",
loading: "جارٍ تحميل بيانات الاستخدام...",
empty: "لا توجد بيانات استخدام",
errorTitle: "تعذّر تحميل بيانات الاستخدام",
hostUnavailable: "اتصل بهذا المضيف لعرض استخدام المزوّد",
hostUpgradeRequired: "حدّث المضيف لعرض استخدام المزوّد",
clientUnavailable: "اتصال المضيف غير جاهز",
retry: "حاول مرة أخرى",
tooltipLoading: "جارٍ تحميل استخدام الخطة...",
status: {
error: "خطأ",
unavailable: "غير متاح",
},
values: {
used: "تم استخدام {{percentage}}",
remaining: "متبقٍ {{amount}}",
},
duration: {
days: "{{value}} يوم",
hours: "{{value}} ساعة",
minutes: "{{value}} دقيقة",
},
timing: {
resettingNow: "تتم إعادة التعيين الآن",
resetsIn: "تتم إعادة التعيين خلال {{duration}}",
runsOutNow: "ينفد الآن",
runsOutIn: "ينفد خلال {{duration}}",
justNow: "الآن",
daysAgo: "قبل {{value}} يومًا",
hoursAgo: "قبل {{value}} ساعة",
minutesAgo: "قبل {{value}} دقيقة",
updated: "تم التحديث {{relativeTime}}",
},
},
settings: {
title: "إعدادات",
loading: "جارٍ تحميل الإعدادات...",
+37
View File
@@ -1975,6 +1975,43 @@ export const en = {
saveAccessibility: "Save review comment",
},
},
providerUsage: {
title: "Plan usage",
refresh: "Refresh",
refreshing: "Refreshing...",
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",
clientUnavailable: "Host connection is not ready",
retry: "Try again",
tooltipLoading: "Loading plan usage...",
status: {
error: "Error",
unavailable: "Unavailable",
},
values: {
used: "{{percentage}} used",
remaining: "{{amount}} left",
},
duration: {
days: "{{value}}d",
hours: "{{value}}h",
minutes: "{{value}}m",
},
timing: {
resettingNow: "resetting now",
resetsIn: "resets {{duration}}",
runsOutNow: "runs out now",
runsOutIn: "runs out {{duration}}",
justNow: "just now",
daysAgo: "{{value}}d ago",
hoursAgo: "{{value}}h ago",
minutesAgo: "{{value}}m ago",
updated: "Updated {{relativeTime}}",
},
},
settings: {
title: "Settings",
loading: "Loading settings...",
+37
View File
@@ -1998,6 +1998,43 @@ export const es: TranslationResources = {
saveAccessibility: "Guardar comentario de revisión",
},
},
providerUsage: {
title: "Uso del plan",
refresh: "Actualizar",
refreshing: "Actualizando...",
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",
clientUnavailable: "La conexión con el host no está lista",
retry: "Intentar de nuevo",
tooltipLoading: "Cargando uso del plan...",
status: {
error: "Error",
unavailable: "No disponible",
},
values: {
used: "Usado: {{percentage}}",
remaining: "Quedan {{amount}}",
},
duration: {
days: "{{value}} d",
hours: "{{value}} h",
minutes: "{{value}} min",
},
timing: {
resettingNow: "se restablece ahora",
resetsIn: "se restablece en {{duration}}",
runsOutNow: "se agota ahora",
runsOutIn: "se agota en {{duration}}",
justNow: "ahora mismo",
daysAgo: "hace {{value}} d",
hoursAgo: "hace {{value}} h",
minutesAgo: "hace {{value}} min",
updated: "Actualizado {{relativeTime}}",
},
},
settings: {
title: "Ajustes",
loading: "Cargando configuración...",
+37
View File
@@ -2003,6 +2003,43 @@ export const fr: TranslationResources = {
saveAccessibility: "Enregistrer le commentaire de l'avis",
},
},
providerUsage: {
title: "Utilisation du forfait",
refresh: "Actualiser",
refreshing: "Actualisation...",
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",
clientUnavailable: "La connexion à l’hôte n’est pas prête",
retry: "Réessayer",
tooltipLoading: "Chargement de l’utilisation du forfait...",
status: {
error: "Erreur",
unavailable: "Indisponible",
},
values: {
used: "Utilisé : {{percentage}}",
remaining: "Il reste {{amount}}",
},
duration: {
days: "{{value}} j",
hours: "{{value}} h",
minutes: "{{value}} min",
},
timing: {
resettingNow: "réinitialisation en cours",
resetsIn: "se réinitialise dans {{duration}}",
runsOutNow: "s’épuise maintenant",
runsOutIn: "s’épuise dans {{duration}}",
justNow: "à l’instant",
daysAgo: "il y a {{value}} j",
hoursAgo: "il y a {{value}} h",
minutesAgo: "il y a {{value}} min",
updated: "Mis à jour {{relativeTime}}",
},
},
settings: {
title: "Paramètres",
loading: "Chargement des paramètres...",
+37
View File
@@ -1970,6 +1970,43 @@ export const ja: TranslationResources = {
saveAccessibility: "レビューコメントを保存",
},
},
providerUsage: {
title: "プラン使用量",
refresh: "更新",
refreshing: "更新中...",
loading: "使用状況を読み込み中...",
empty: "使用状況データがありません",
errorTitle: "使用状況を読み込めません",
hostUnavailable: "このホストに接続してプロバイダーの使用状況を確認してください",
hostUpgradeRequired: "ホストを更新してプロバイダーの使用状況を確認してください",
clientUnavailable: "ホスト接続の準備ができていません",
retry: "再試行",
tooltipLoading: "プラン使用状況を読み込み中...",
status: {
error: "エラー",
unavailable: "利用不可",
},
values: {
used: "{{percentage}} 使用済み",
remaining: "残り {{amount}}",
},
duration: {
days: "{{value}}日",
hours: "{{value}}時間",
minutes: "{{value}}分",
},
timing: {
resettingNow: "まもなくリセット",
resetsIn: "{{duration}}後にリセット",
runsOutNow: "まもなく上限に到達",
runsOutIn: "{{duration}}後に上限に到達",
justNow: "たった今",
daysAgo: "{{value}}日前",
hoursAgo: "{{value}}時間前",
minutesAgo: "{{value}}分前",
updated: "更新: {{relativeTime}}",
},
},
settings: {
title: "設定",
loading: "設定を読み込み中...",
+37
View File
@@ -1983,6 +1983,43 @@ export const ptBR: TranslationResources = {
saveAccessibility: "Salvar comentário de revisão",
},
},
providerUsage: {
title: "Uso do plano",
refresh: "Atualizar",
refreshing: "Atualizando...",
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",
clientUnavailable: "A conexão com o host não está pronta",
retry: "Tentar novamente",
tooltipLoading: "Carregando uso do plano...",
status: {
error: "Erro",
unavailable: "Indisponível",
},
values: {
used: "{{percentage}} usado",
remaining: "{{amount}} restantes",
},
duration: {
days: "{{value}} d",
hours: "{{value}} h",
minutes: "{{value}} min",
},
timing: {
resettingNow: "redefinindo agora",
resetsIn: "redefine em {{duration}}",
runsOutNow: "esgota agora",
runsOutIn: "esgota em {{duration}}",
justNow: "agora mesmo",
daysAgo: "há {{value}} d",
hoursAgo: "há {{value}} h",
minutesAgo: "há {{value}} min",
updated: "Atualizado {{relativeTime}}",
},
},
settings: {
title: "Configurações",
loading: "Carregando configurações...",
+37
View File
@@ -1983,6 +1983,43 @@ export const ru: TranslationResources = {
saveAccessibility: "Сохранить комментарий к ревью",
},
},
providerUsage: {
title: "Использование плана",
refresh: "Обновить",
refreshing: "Обновление...",
loading: "Загрузка данных об использовании...",
empty: "Нет данных об использовании",
errorTitle: "Не удалось загрузить данные об использовании",
hostUnavailable: "Подключитесь к этому хосту, чтобы увидеть использование провайдера",
hostUpgradeRequired: "Обновите хост, чтобы увидеть использование провайдера",
clientUnavailable: "Соединение с хостом не готово",
retry: "Повторить",
tooltipLoading: "Загрузка использования плана...",
status: {
error: "Ошибка",
unavailable: "Недоступно",
},
values: {
used: "Использовано: {{percentage}}",
remaining: "Осталось: {{amount}}",
},
duration: {
days: "{{value}} дн.",
hours: "{{value}} ч",
minutes: "{{value}} мин",
},
timing: {
resettingNow: "сброс сейчас",
resetsIn: "сброс через {{duration}}",
runsOutNow: "закончится сейчас",
runsOutIn: "закончится через {{duration}}",
justNow: "только что",
daysAgo: "{{value}} дн. назад",
hoursAgo: "{{value}} ч назад",
minutesAgo: "{{value}} мин назад",
updated: "Обновлено {{relativeTime}}",
},
},
settings: {
title: "Настройки",
loading: "Загрузка настроек...",
+37
View File
@@ -1928,6 +1928,43 @@ export const zhCN: TranslationResources = {
saveAccessibility: "保存 review 评论",
},
},
providerUsage: {
title: "套餐用量",
refresh: "刷新",
refreshing: "刷新中...",
loading: "正在加载用量...",
empty: "暂无用量数据",
errorTitle: "无法加载用量",
hostUnavailable: "连接到此主机以查看提供商用量",
hostUpgradeRequired: "更新主机以查看提供商用量",
clientUnavailable: "主机连接尚未就绪",
retry: "重试",
tooltipLoading: "正在加载套餐用量...",
status: {
error: "错误",
unavailable: "不可用",
},
values: {
used: "已使用 {{percentage}}",
remaining: "剩余 {{amount}}",
},
duration: {
days: "{{value}}天",
hours: "{{value}}小时",
minutes: "{{value}}分钟",
},
timing: {
resettingNow: "正在重置",
resetsIn: "将在 {{duration}}后重置",
runsOutNow: "即将用尽",
runsOutIn: "将在 {{duration}}后用尽",
justNow: "刚刚",
daysAgo: "{{value}}天前",
hoursAgo: "{{value}}小时前",
minutesAgo: "{{value}}分钟前",
updated: "更新于 {{relativeTime}}",
},
},
settings: {
title: "设置",
loading: "正在加载设置...",
+14 -5
View File
@@ -1,4 +1,5 @@
import { useMemo } from "react";
import type { TFunction } from "i18next";
import { useTranslation } from "react-i18next";
import { Text, View, type StyleProp, type ViewStyle } from "react-native";
import { StyleSheet } from "react-native-unistyles";
@@ -10,7 +11,11 @@ interface ResolvedBalance {
usedPct: number | null;
}
function resolveBalance(balance: UsageBalance, locale: string): ResolvedBalance {
function resolveBalance(
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) {
@@ -20,7 +25,10 @@ function resolveBalance(balance: UsageBalance, locale: string): ResolvedBalance
return { amountText: `${usedText} / ${format(limit)}`, usedPct };
}
if (remaining != null) {
return { amountText: `${format(remaining)} left`, usedPct: null };
return {
amountText: t("providerUsage.values.remaining", { amount: format(remaining) }),
usedPct: null,
};
}
if (used != null) {
return { amountText: format(used), usedPct: null };
@@ -42,8 +50,8 @@ function fillToneStyle(tone: UsageTone) {
}
export function UsageBalanceBar({ balance }: { balance: UsageBalance }) {
const { i18n } = useTranslation();
const { amountText, usedPct } = resolveBalance(balance, i18n.language);
const { t, i18n } = useTranslation();
const { amountText, usedPct } = resolveBalance(balance, t, i18n.resolvedLanguage);
const tone = balance.tone ?? "default";
const resetLabel = formatResetLabel(balance.resetsAt);
@@ -58,7 +66,7 @@ export function UsageBalanceBar({ balance }: { balance: UsageBalance }) {
<Text style={styles.label} numberOfLines={1}>
{balance.label}
</Text>
<Text style={styles.value}>
<Text style={styles.value} numberOfLines={1} ellipsizeMode="tail">
{amountText}
{resetLabel ? <Text style={styles.reset}>{` · ${resetLabel}`}</Text> : null}
</Text>
@@ -88,6 +96,7 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.sm,
},
value: {
flexShrink: 1,
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.medium,
+26 -2
View File
@@ -1,7 +1,17 @@
import { describe, expect, it } from "vitest";
import { formatAmount } from "./format";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { i18n } from "@/i18n/i18next";
import { formatAgo, formatAmount, formatPct, formatResetLabel, formatRunsOutLabel } from "./format";
const NOW = Date.parse("2026-07-19T00:00:00.000Z");
describe("formatAmount", () => {
beforeEach(async () => {
await i18n.changeLanguage("en");
});
afterEach(async () => {
await i18n.changeLanguage("en");
});
it("groups thousands in the app's language", () => {
expect(formatAmount(12345, "credits", "en")).toBe("12,345");
expect(formatAmount(12345, "requests", "en")).toBe("12,345");
@@ -12,4 +22,18 @@ describe("formatAmount", () => {
expect(formatAmount(1234.5, "usd", "en")).toBe("$1,234.50");
expect(formatAmount(1234.5, "usd", "fr").replace(/\s/g, " ")).toBe("1 234,50 $US");
});
it("localizes usage timing without patching the global clock", async () => {
const twoHoursFromNow = new Date(NOW + 2 * 60 * 60 * 1000).toISOString();
const threeDaysAgo = new Date(NOW - 3 * 24 * 60 * 60 * 1000).toISOString();
expect(formatPct(7, "en")).toBe("7%");
expect(formatResetLabel(twoHoursFromNow, NOW)).toBe("resets in 2h");
expect(formatRunsOutLabel(twoHoursFromNow, NOW)).toBe("runs out in 2h");
await i18n.changeLanguage("ja");
expect(formatResetLabel(twoHoursFromNow, NOW)).toBe("2時間後にリセット");
expect(formatRunsOutLabel(twoHoursFromNow, NOW)).toBe("2時間後に上限に到達");
expect(formatAgo(threeDaysAgo, NOW)).toBe("3日前");
});
});
+109 -16
View File
@@ -1,46 +1,139 @@
import { formatTokenCount } from "@/components/context-window-meter.utils";
import { i18n } from "@/i18n/i18next";
import type { UsageDisplayAs } from "./preferences";
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",
credits: "providerUsage.labels.credits",
monthly_credits: "providerUsage.labels.monthlyCredits",
} as const;
export function formatProviderUsageLabel(id: string, fallback: string): string {
const key = providerUsageLabelKeys[id as keyof typeof providerUsageLabelKeys];
return key ? i18n.t(key) : fallback;
}
export function clampPct(value: number): number {
return Math.max(0, Math.min(100, value));
}
export function formatPct(value: number): string {
return `${Math.round(clampPct(value))}%`;
export function formatPct(value: number, locale?: string): string {
return new Intl.NumberFormat(locale, {
style: "percent",
maximumFractionDigits: 0,
}).format(clampPct(value) / 100);
}
/** "31%" of the window used, or "69% left" of it. */
export function formatDisplayPct(value: number, displayAs: UsageDisplayAs): string {
return displayAs === "used" ? formatPct(value) : `${formatPct(value)} left`;
/** A localized percentage that says whether it is used or remaining. */
export function formatDisplayPct(value: number, displayAs: UsageDisplayAs, locale?: string): string {
const percentage = formatPct(value, locale);
return displayAs === "used"
? i18n.t("providerUsage.values.used", { percentage })
: i18n.t("providerUsage.values.remaining", { amount: percentage });
}
function relativeDuration(iso: string): string | null {
const diffMs = new Date(iso).getTime() - Date.now();
type RelativeDuration = { unit: "now" } | { unit: "minutes" | "hours" | "days"; count: number };
function formatCount(value: number): string {
return new Intl.NumberFormat(i18n.resolvedLanguage).format(value);
}
function relativeDuration(iso: string, now: number): RelativeDuration | null {
const diffMs = new Date(iso).getTime() - now;
if (!Number.isFinite(diffMs)) return null;
if (diffMs <= 0) return "now";
if (diffMs <= 0) return { unit: "now" };
const diffMinutes = Math.floor(diffMs / 60_000);
const diffHours = Math.floor(diffMinutes / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffDays > 0) return `${diffDays}d`;
if (diffHours > 0) return `${diffHours}h`;
return `${diffMinutes}m`;
if (diffDays > 0) return { unit: "days", count: diffDays };
if (diffHours > 0) return { unit: "hours", count: diffHours };
return { unit: "minutes", count: diffMinutes };
}
export function formatResetLabel(iso: string | null | undefined): string | null {
function formatRelativeDuration(duration: RelativeDuration): string | null {
switch (duration.unit) {
case "now":
return null;
case "days":
return i18n.t("providerUsage.duration.days", {
count: duration.count,
value: formatCount(duration.count),
});
case "hours":
return i18n.t("providerUsage.duration.hours", {
count: duration.count,
value: formatCount(duration.count),
});
case "minutes":
return i18n.t("providerUsage.duration.minutes", {
count: duration.count,
value: formatCount(duration.count),
});
}
}
export function formatResetLabel(iso: string | null | undefined, now = Date.now()): string | null {
if (!iso) return null;
const rel = relativeDuration(iso);
const rel = relativeDuration(iso, now);
if (!rel) return null;
return rel === "now" ? "resetting now" : `resets ${rel}`;
if (rel.unit === "now") return i18n.t("providerUsage.timing.resettingNow");
const duration = formatRelativeDuration(rel);
return duration ? i18n.t("providerUsage.timing.resetsIn", { duration }) : null;
}
export function formatRunsOutLabel(
iso: string | null | undefined,
now = Date.now(),
): string | null {
if (!iso) return null;
const rel = relativeDuration(iso, now);
if (!rel) return null;
if (rel.unit === "now") return i18n.t("providerUsage.timing.runsOutNow");
const duration = formatRelativeDuration(rel);
return duration ? i18n.t("providerUsage.timing.runsOutIn", { duration }) : null;
}
export function formatAgo(iso: string | null | undefined, now = Date.now()): string | null {
if (!iso) return null;
const diffMs = now - new Date(iso).getTime();
if (!Number.isFinite(diffMs)) return null;
if (diffMs < 60_000) return i18n.t("providerUsage.timing.justNow");
const diffMinutes = Math.floor(diffMs / 60_000);
const diffHours = Math.floor(diffMinutes / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffDays > 0) {
return i18n.t("providerUsage.timing.daysAgo", {
count: diffDays,
value: formatCount(diffDays),
});
}
if (diffHours > 0) {
return i18n.t("providerUsage.timing.hoursAgo", {
count: diffHours,
value: formatCount(diffHours),
});
}
return i18n.t("providerUsage.timing.minutesAgo", {
count: diffMinutes,
value: formatCount(diffMinutes),
});
}
/** A balance amount as the app's language writes it: "$1,234.50", "12,345". */
export function formatAmount(value: number, unit: UsageBalanceUnit, locale: string): string {
export function formatAmount(value: number, unit: UsageBalanceUnit, locale?: string): string {
switch (unit) {
case "usd":
return new Intl.NumberFormat(locale, { style: "currency", currency: "USD" }).format(value);
case "tokens":
return formatTokenCount(value);
return new Intl.NumberFormat(locale, {
notation: "compact",
maximumFractionDigits: 1,
}).format(value);
default:
return new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(value);
}
+14 -5
View File
@@ -1,10 +1,16 @@
import { Pin } from "lucide-react-native";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Pressable, Text, View, type StyleProp, type ViewStyle } from "react-native";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { usageCopy } from "./copy";
import { formatDisplayPct, formatResetLabel } from "./format";
import {
formatDisplayPct,
formatProviderUsageLabel,
formatResetLabel,
formatRunsOutLabel,
} from "./format";
import { UsageMeter } from "./meter";
import { displayPercent, usageWindowRowLabel } from "./model";
import type { UsageDisplayAs } from "./preferences";
@@ -34,15 +40,17 @@ export function UsageWindowBar({
pinLabel: string;
pinTestID: string;
}) {
const { i18n } = useTranslation();
const shownPct = displayPercent(window, displayAs);
const tone = windowTone(window);
const isAtRisk = window.runsOutAt != null && window.shortfallPct != null;
const trailing = isAtRisk
? `runs out ${formatResetLabel(window.runsOutAt)?.replace("resets ", "") ?? ""}`.trim()
? formatRunsOutLabel(window.runsOutAt)
: formatResetLabel(window.resetsAt);
const value = shownPct != null ? formatDisplayPct(shownPct, displayAs) : "—";
const value =
shownPct != null ? formatDisplayPct(shownPct, displayAs, i18n.resolvedLanguage) : "—";
const accessibilityState = useMemo(() => ({ checked: pinned }), [pinned]);
// The whole row pins the window to the sidebar Usage item. Pinned or not, it keeps the same
@@ -60,7 +68,7 @@ export function UsageWindowBar({
{({ hovered }: { hovered?: boolean }) => (
<WindowRowContent
highlight={highlightStyle(pinned, Boolean(hovered))}
label={window.label}
label={formatProviderUsageLabel(window.id, window.label)}
value={value}
trailing={trailing}
isAtRisk={isAtRisk}
@@ -101,7 +109,7 @@ function WindowRowContent({
<Text style={styles.label} numberOfLines={1}>
{label}
</Text>
<Text style={styles.value}>
<Text style={styles.value} numberOfLines={1} ellipsizeMode="tail">
{value}
{trailing ? (
<Text style={isAtRisk ? styles.atRisk : styles.reset}>{` · ${trailing}`}</Text>
@@ -186,6 +194,7 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.sm,
},
value: {
flexShrink: 1,
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.medium,