Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/desktop-tauri/scripts/proof-scenarios.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@
{ "screenshot": "tray-kpi-long-titles", "clip": "panel" }
]
},
{
"name": "tray-uk-regen-countdown",
"query": "window=main&fixture=uk-regen-countdown",
"size": [310, 1032],
"steps": [
{ "click": { "selector": ".provider-grid__item", "label": "Claude" } },
{ "expectText": ["Регенерує через 2 дн. 3 год"] },
{ "expectNoText": ["Regenerates in", "Regenerates now"] },
{ "screenshot": "tray-uk-regen-countdown", "clip": "panel" }
]
},
{
"name": "tray-errors",
"query": "window=main&fixture=errors",
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src/i18n/keyGroups/providerText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const PROVIDER_TEXT_LOCALE_KEYS = [
"ResetsInHoursOnly",
"ResetsInDaysOnly",
"RegeneratesCountdown",
"RegeneratesNow",
"CostTitleClawRouterSpend",
"CostTitleClawRouterMonthlyBudget",
"ResetsAtTime",
Expand Down
14 changes: 11 additions & 3 deletions apps/desktop-tauri/src/lib/metricRowModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ describe("metricResetText", () => {
[45 * MINUTE, false, "Regenerates in 45m"],
[2 * HOUR + 5 * MINUTE, true, "Regenerates in 2h 5m"],
[DAY + 3 * HOUR, true, "Regenerates in 1d 3h"],
[2 * DAY + 3 * HOUR, true, "Regenerates in 2d 3h"],
[2 * DAY, true, "Regenerates in 2d"],
[500, true, "Regenerates now"],
])("counts a regen lane %d ms down to %j (relative %s)", (ms, relative, expected) => {
Expand All @@ -128,12 +129,19 @@ describe("metricResetText", () => {
expect(metricResetText(snap, relative, NOW, en)).toBe(expected);
});

it("uses the Mac translation of Regenerates around the English countdown", () => {
// The countdown is a bare localized duration; each template supplies its own
// preposition, so no locale inherits the English "in " prefix.
it.each([
["uk-UA.ftl", 45 * MINUTE, "Регенерує через 45 хв"],
["uk-UA.ftl", 2 * DAY + 3 * HOUR, "Регенерує через 2 дн. 3 год"],
["uk-UA.ftl", 500, "Регенерує зараз"],
["ja-JP.ftl", 45 * MINUTE, "45 分後に再生成"],
])("localizes the regen countdown in %s for %d ms", (file, ms, expected) => {
const snap = rateWindow({
resetsAt: new Date(NOW + 45 * MINUTE).toISOString(),
resetsAt: new Date(NOW + ms).toISOString(),
resetRegenerates: true,
});
expect(metricResetText(snap, true, NOW, fromFtl("ja-JP.ftl"))).toBe("in 45m に再生成");
expect(metricResetText(snap, true, NOW, fromFtl(file))?.replace(/\u202f/g, " ")).toBe(expected);
});
});

Expand Down
22 changes: 8 additions & 14 deletions apps/desktop-tauri/src/lib/metricRowModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,20 +183,14 @@ function absoluteResetDescription(target: Date, now: Date, t: Translate): string
}

/**
* Upstream `UsageFormatter.resetCountdownDescription`: the unlocalized "in 2d 3h"
* countdown that `syntheticRegenDetail` puts inside "Regenerates %@", always relative.
* Upstream `syntheticRegenDetail`: "Regenerates %@" around the countdown, always
* relative. The bare localized duration fills `RegeneratesCountdown`, which owns
* its own preposition per locale; zero remaining uses the whole-sentence
* `RegeneratesNow`.
*/
function regenCountdown(seconds: number): string {
const parts = countdownParts(seconds);
if (!parts) return "now";
const { days, hours, minutes } = parts;
if (days > 0) {
if (hours > 0) return `in ${days}d ${hours}h`;
if (minutes > 0) return `in ${days}d ${minutes}m`;
return `in ${days}d`;
}
if (hours > 0) return minutes > 0 ? `in ${hours}h ${minutes}m` : `in ${hours}h`;
return `in ${minutes}m`;
function regenText(seconds: number, t: Translate): string {
const duration = formatCountdown(seconds, DURATION_KEYS, t);
return duration === null ? t("RegeneratesNow") : fill(t("RegeneratesCountdown"), duration);
}

export function metricResetText(
Expand All @@ -210,7 +204,7 @@ export function metricResetText(
return normalizeResetDescription(resetDescriptionFallback(snap), t);
}
if (snap.resetRegenerates) {
return fill(t("RegeneratesCountdown"), regenCountdown((target - now) / 1000));
return regenText((target - now) / 1000, t);
}
if (!relative) {
return fill(t("ResetsAtLabel"), absoluteResetDescription(new Date(target), new Date(now), t));
Expand Down
33 changes: 33 additions & 0 deletions apps/desktop-tauri/src/proof/fixtures/scenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
SpendModelRow,
TokenAccountBridge,
} from "../../types/bridge";
import { ukLocaleOverrides } from "../locale";
import {
PROOF_PROVIDER_CATALOG,
baseSettings,
Expand Down Expand Up @@ -51,6 +52,7 @@ export const PROOF_SCENARIO_NAMES = [
"claude-reset-credits",
"claude-reset-credits-hidden",
"kpi-long-titles",
"uk-regen-countdown",
"claudeplatform",
"cookie-no-browsers",
"vercel",
Expand Down Expand Up @@ -1093,6 +1095,36 @@ function kpiLongTitlesScenario(now: Date): ProofScenario {
};
}

/**
* Default data with Claude's session window marked as regenerating, shown under
* the shipped uk-UA strings. The reset is 2 days 3 hours 30 minutes away; the
* countdown rounds up to whole minutes, so it reads "2 дн. 3 год" (uk-UA) and
* "Regenerates in 2d 3h" (en-US) for the first few minutes after load.
*/
function ukRegenCountdownScenario(now: Date): ProofScenario {
const base = defaultScenario(now);
const resetsAt = isoAfter(now, 2 * DAY_MINUTES + 3 * 60 + 30);
return {
...base,
name: "uk-regen-countdown",
localeOverrides: ukLocaleOverrides([
"RegeneratesCountdown",
"RegeneratesNow",
"DurationDaysHours",
"DurationDaysMinutes",
"DurationDays",
"DurationHoursMinutes",
"DurationHours",
"DurationMinutes",
]),
providers: base.providers.map((snap) => {
if (snap.providerId !== "claude") return snap;
const primary = { ...snap.primary, resetsAt, resetRegenerates: true };
return { ...snap, primary, selectedMetric: primary };
}),
};
}

function claudePlatformScenario(now: Date): ProofScenario {
return {
name: "claudeplatform",
Expand Down Expand Up @@ -1241,6 +1273,7 @@ const BUILDERS: Record<ProofScenarioName, (now: Date) => ProofScenario> = {
"claude-reset-credits": (now) => claudeResetCreditsScenario(now, false),
"claude-reset-credits-hidden": (now) => claudeResetCreditsScenario(now, true),
"kpi-long-titles": kpiLongTitlesScenario,
"uk-regen-countdown": ukRegenCountdownScenario,
claudeplatform: claudePlatformScenario,
"cookie-no-browsers": cookieNoBrowsersScenario,
vercel: vercelScenario,
Expand Down
16 changes: 16 additions & 0 deletions apps/desktop-tauri/src/proof/locale.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import englishFtl from "../../../../rust/src/locale/en-US.ftl?raw";
import ukFtl from "../../../../rust/src/locale/uk-UA.ftl?raw";
import { ALL_LOCALE_KEYS } from "../i18n/keys";
import type { LanguageOption, LocaleStrings } from "../types/bridge";

Expand All @@ -12,6 +13,21 @@ export function parseFtlEntries(text: string): Record<string, string> {
return entries;
}

/**
* The shipped uk-UA strings for `keys`, parsed the same way as the English file.
* A scenario passes the result as `localeOverrides` to show one localized surface
* while the rest of the proof stays English. Keys missing from uk-UA are omitted.
*/
export function ukLocaleOverrides(keys: readonly string[]): Record<string, string> {
const parsed = parseFtlEntries(ukFtl);
const overrides: Record<string, string> = {};
for (const key of keys) {
const value = parsed[key];
if (value !== undefined) overrides[key] = value;
}
return overrides;
}

let cached: LocaleStrings | null = null;

/**
Expand Down
1 change: 1 addition & 0 deletions rust/src/locale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1780,6 +1780,7 @@ locale_keys! {
ResetsInHoursOnly,
ResetsInDaysOnly,
RegeneratesCountdown,
RegeneratesNow,
CostTitleClawRouterSpend,
CostTitleClawRouterMonthlyBudget,
ResetsAtTime,
Expand Down
3 changes: 2 additions & 1 deletion rust/src/locale/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1365,7 +1365,8 @@ WindowLabelDays = { "{}" }-day
ResetsAtLabel = Resets { "{}" }
ResetsInHoursOnly = Resets in { "{}" }h
ResetsInDaysOnly = Resets in { "{}" }d
RegeneratesCountdown = Regenerates { "{}" }
RegeneratesCountdown = Regenerates in { "{}" }
RegeneratesNow = Regenerates now
CostTitleClawRouterSpend = ClawRouter spend
CostTitleClawRouterMonthlyBudget = Monthly budget
ResetsAtTime = Resets at { "{}" }
Expand Down
3 changes: 2 additions & 1 deletion rust/src/locale/es-MX.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1362,7 +1362,8 @@ WindowLabelDays = { "{}" } días
ResetsAtLabel = Reinicia { "{}" }
ResetsInHoursOnly = Reinicia en { "{}" }h
ResetsInDaysOnly = Reinicia en { "{}" }d
RegeneratesCountdown = Se regenera { "{}" }
RegeneratesCountdown = Se regenera en { "{}" }
RegeneratesNow = Se regenera ahora
CostTitleClawRouterSpend = ClawRouter spend
CostTitleClawRouterMonthlyBudget = Monthly budget
ResetsAtTime = Reinicia a las { "{}" }
Expand Down
3 changes: 2 additions & 1 deletion rust/src/locale/ja-JP.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1363,7 +1363,8 @@ WindowLabelDays = { "{}" } 日間
ResetsAtLabel = リセット { "{}" }
ResetsInHoursOnly = リセットまで { "{}" }時間
ResetsInDaysOnly = リセットまで { "{}" }日
RegeneratesCountdown = { "{}" } に再生成
RegeneratesCountdown = { "{}" }後に再生成
RegeneratesNow = 今再生成
CostTitleClawRouterSpend = ClawRouter spend
CostTitleClawRouterMonthlyBudget = Monthly budget
ResetsAtTime = { "{}" } にリセット
Expand Down
3 changes: 2 additions & 1 deletion rust/src/locale/ko-KR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1362,7 +1362,8 @@ WindowLabelDays = { "{}" }일
ResetsAtLabel = 초기화 { "{}" }
ResetsInHoursOnly = 초기화까지 { "{}" }시간
ResetsInDaysOnly = 초기화까지 { "{}" }일
RegeneratesCountdown = { "{}" }에 재생성
RegeneratesCountdown = { "{}" } 후 재생성
RegeneratesNow = 지금 재생성
CostTitleClawRouterSpend = ClawRouter spend
CostTitleClawRouterMonthlyBudget = Monthly budget
ResetsAtTime = { "{}" }에 초기화
Expand Down
3 changes: 2 additions & 1 deletion rust/src/locale/pt-BR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1367,7 +1367,8 @@ WindowLabelDays = { "{}" } dias
ResetsAtLabel = Renova { "{}" }
ResetsInHoursOnly = Renova em { "{}" }h
ResetsInDaysOnly = Renova em { "{}" }d
RegeneratesCountdown = Regenera { "{}" }
RegeneratesCountdown = Regenera em { "{}" }
RegeneratesNow = Regenera agora
CostTitleClawRouterSpend = ClawRouter spend
CostTitleClawRouterMonthlyBudget = Monthly budget
ResetsAtTime = Renova às { "{}" }
Expand Down
3 changes: 2 additions & 1 deletion rust/src/locale/ru-RU.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1363,7 +1363,8 @@ WindowLabelDays = { "{}" } дн.
ResetsAtLabel = Сброс { "{}" }
ResetsInHoursOnly = Сброс через { "{}" } ч
ResetsInDaysOnly = Сброс через { "{}" } д
RegeneratesCountdown = Пополняется { "{}" }
RegeneratesCountdown = Пополняется через { "{}" }
RegeneratesNow = Пополняется сейчас
CostTitleClawRouterSpend = ClawRouter spend
CostTitleClawRouterMonthlyBudget = Monthly budget
ResetsAtTime = Сброс в { "{}" }
Expand Down
1 change: 1 addition & 0 deletions rust/src/locale/tr-TR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1364,6 +1364,7 @@ ResetsAtLabel = Sıfırlanma: { "{}" }
ResetsInHoursOnly = Sıfırlanmaya { "{}" } sa
ResetsInDaysOnly = Sıfırlanmaya { "{}" } g
RegeneratesCountdown = Yenilenme: { "{}" }
RegeneratesNow = Yenilenme: şimdi
CostTitleClawRouterSpend = ClawRouter spend
CostTitleClawRouterMonthlyBudget = Monthly budget
ResetsAtTime = { "{}" } saatinde sıfırlanır
Expand Down
31 changes: 16 additions & 15 deletions rust/src/locale/uk-UA.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,10 @@ PaceLastsUntilReset = Вистачить до скидання
PaceDailyTargetPercent = ≈ { "{}" }% на день, вистачить до скидання
PaceDailyTargetCredits = ≈ { "{}" } кредитів на день, вистачить до скидання
PaceDailyTargetRequests = ≈ { "{}" } запитів на день, вистачить до скидання
PaceRunsOutIn = Закінчується за { "{}" }
PaceRunsOutIn = Закінчується через { "{}" }
PaceRunsOutNow = Зараз закінчується
PaceProjectedEmptyIn = Передбачається порожній у { "{}" }
PaceProjectedEmptyNow = Зараз проектується порожнім
PaceProjectedEmptyIn = За прогнозом, вичерпається через { "{}" }
PaceProjectedEmptyNow = За прогнозом, ліміт вичерпано
UsageBarQuotaWarnings = Попередження про квоту
UsageBarWorkDays = Щотижневі робочі дні
MetricResetsIn = Скинеться через
Expand Down Expand Up @@ -515,8 +515,8 @@ TokenActivityLess = Менше
TokenActivityMore = Більше
TokenActivityEachColumnWeek = Кожен стовпець = 1 тиждень
TokenActivityRunningTotal = Накопичений підсумок
TokenActivityDaily = Щодня
TokenActivityWeekly = Щотижня
TokenActivityDaily = Денний
TokenActivityWeekly = Тижневий
TokenActivityCumulative = Наростаючим підсумком
TokenActivityView = Вигляд
TokenActivityEarlier = Раніша активність
Expand Down Expand Up @@ -945,15 +945,15 @@ CredsBrowseAction = Огляд…
CredsGeminiCliLabel = Gemini CLI
CredsGeminiCliHelperPrefix = Використовує облікові дані OAuth із
CredsGeminiCliSetupAction = Налаштувати Gemini CLI
CredsGeminiCliSetupHelp = Установіть Gemini CLI та виконайте `gemini auth login`, щоб увійти.
CredsGeminiCliSetupHelp = Встановіть Gemini CLI та виконайте `gemini auth login`, щоб увійти.
CredsVertexAiLabel = Google Cloud
CredsVertexAiHelperPrefix = Використовує облікові дані Google Cloud із
CredsVertexAiSetupAction = Налаштувати вхід у Google Cloud
CredsVertexAiSetupHelp = Виконайте `gcloud auth application-default login`, щоб створити облікові дані.
CredsJetBrainsLabel = JetBrains IDE
CredsJetBrainsHelperDetectedPrefix = Використовується виявлена конфігурація IDE:
CredsJetBrainsHelperCustomPrefix = Використовується власний базовий шлях IDE
CredsJetBrainsHelperMissing = Установіть IDE JetBrains з увімкненим AI Assistant, потім оновіть CodexBar.
CredsJetBrainsHelperMissing = Встановіть IDE JetBrains з увімкненим AI Assistant, потім оновіть CodexBar.
CredsJetBrainsCustomPathLabel = Власний шлях
CredsJetBrainsCustomPathPlaceholder = %APPDATA%/JetBrains/IntelliJIdea...
CredsJetBrainsSelectLabel = Виберіть IDE JetBrains для відстеження.
Expand Down Expand Up @@ -1303,7 +1303,7 @@ RegionZaiChinaMainland = Материковий Китай (BigModel)
UsageItemUnavailableTitle = { "{}" } (недоступно)
UsageItemFallbackTitle = Елемент використання
WindowLabelQuota = Квота
WindowLabelDaily = Щодня
WindowLabelDaily = Денний
WindowLabelBudget = Бюджет
WindowLabelWeeklyQuota = Тижнева квота
WindowLabelMonthlyQuota = Місячна квота
Expand Down Expand Up @@ -1361,7 +1361,8 @@ WindowLabelDays = { "{}" } дн.
ResetsAtLabel = Скидання { "{}" }
ResetsInHoursOnly = Скинеться через { "{}" } год
ResetsInDaysOnly = Скинеться через { "{}" } дн.
RegeneratesCountdown = Регенерує { "{}" }
RegeneratesCountdown = Регенерує через { "{}" }
RegeneratesNow = Регенерує зараз
CostTitleClawRouterSpend = ClawRouter spend
CostTitleClawRouterMonthlyBudget = Monthly budget
ResetsAtTime = Скинеться о { "{}" }
Expand Down Expand Up @@ -1432,7 +1433,7 @@ ProviderTextNoBrandBudgetQuotaReported = Квоту бюджету { "{}" } не
ProviderTextNoCachedBrandQuota = Немає збережених даних про квоту { "{}" }
ProviderTextNoActiveBrandTier = Немає активного ліміту тарифу { "{}" }
IntlLocale = uk-UA
ProviderLabelMonthly = Щомісяця
ProviderLabelMonthly = Місячний
ProviderLabelHourly = Щогодини
ProviderLabelQuarterly = Щокварталу
ProviderLabelYearly = Щороку
Expand Down Expand Up @@ -1653,10 +1654,10 @@ ProviderTextExpiresPrefix = спливає { "{}" }
ProviderTextRenewsIn = Поновлення через { "{}" }
ProviderTextExpiresIn = Спливає через { "{}" }
ProviderTextCycleEndsIn = Цикл закінчиться через { "{}" }
DurationDaysHours = { "{}" } д { "{}" } год
DurationDaysMinutes = { "{}" } д { "{}" } хв
DurationDaysHours = { "{}" } дн. { "{}" } год
DurationDaysMinutes = { "{}" } дн. { "{}" } хв
DurationHoursMinutes = { "{}" } год { "{}" } хв
DurationDays = { "{}" } д
DurationDays = { "{}" } дн.
DurationHours = { "{}" } год
DurationMinutes = { "{}" } хв
DurationMonths = { "{}" } міс.
Expand Down Expand Up @@ -1708,8 +1709,8 @@ UsageSpendLocalHistory = Локальна орієнтовна історія
UsageSpendEstimatedSpend = Орієнтовні витрати
UsageSpendTrackedTokens = Враховані токени
UsageSpendSources = Джерела
UsageSpendTokenInput = Вхід
UsageSpendTokenOutput = Вихід
UsageSpendTokenInput = Вхідні
UsageSpendTokenOutput = Вихідні
UsageSpendTokenCacheRead = Читання кешу
UsageSpendTokenCacheWrite = Запис у кеш
UsageSpendTokenReasoning = Міркування
Expand Down
1 change: 1 addition & 0 deletions rust/src/locale/zh-CN.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1364,6 +1364,7 @@ ResetsAtLabel = 重置 { "{}" }
ResetsInHoursOnly = { "{}" } 小时后重置
ResetsInDaysOnly = { "{}" } 天后重置
RegeneratesCountdown = { "{}" }后恢复
RegeneratesNow = 现在恢复
CostTitleClawRouterSpend = ClawRouter spend
CostTitleClawRouterMonthlyBudget = Monthly budget
ResetsAtTime = { "{}" } 重置
Expand Down
1 change: 1 addition & 0 deletions rust/src/locale/zh-TW.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1364,6 +1364,7 @@ ResetsAtLabel = 重置 { "{}" }
ResetsInHoursOnly = { "{}" } 小時後重置
ResetsInDaysOnly = { "{}" } 天後重置
RegeneratesCountdown = { "{}" }後恢復
RegeneratesNow = 現在恢復
CostTitleClawRouterSpend = ClawRouter spend
CostTitleClawRouterMonthlyBudget = Monthly budget
ResetsAtTime = { "{}" } 重置
Expand Down