diff --git a/apps/desktop-tauri/scripts/proof-scenarios.json b/apps/desktop-tauri/scripts/proof-scenarios.json index 6fb06fb46a..22bf65ce04 100644 --- a/apps/desktop-tauri/scripts/proof-scenarios.json +++ b/apps/desktop-tauri/scripts/proof-scenarios.json @@ -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", diff --git a/apps/desktop-tauri/src/i18n/keyGroups/providerText.ts b/apps/desktop-tauri/src/i18n/keyGroups/providerText.ts index e7f0c6dd74..26f94bd6c4 100644 --- a/apps/desktop-tauri/src/i18n/keyGroups/providerText.ts +++ b/apps/desktop-tauri/src/i18n/keyGroups/providerText.ts @@ -69,6 +69,7 @@ export const PROVIDER_TEXT_LOCALE_KEYS = [ "ResetsInHoursOnly", "ResetsInDaysOnly", "RegeneratesCountdown", + "RegeneratesNow", "CostTitleClawRouterSpend", "CostTitleClawRouterMonthlyBudget", "ResetsAtTime", diff --git a/apps/desktop-tauri/src/lib/metricRowModel.test.ts b/apps/desktop-tauri/src/lib/metricRowModel.test.ts index 2efb0e291a..f3df2c19ac 100644 --- a/apps/desktop-tauri/src/lib/metricRowModel.test.ts +++ b/apps/desktop-tauri/src/lib/metricRowModel.test.ts @@ -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) => { @@ -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); }); }); diff --git a/apps/desktop-tauri/src/lib/metricRowModel.ts b/apps/desktop-tauri/src/lib/metricRowModel.ts index f5a306085d..c938e3b5b9 100644 --- a/apps/desktop-tauri/src/lib/metricRowModel.ts +++ b/apps/desktop-tauri/src/lib/metricRowModel.ts @@ -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( @@ -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)); diff --git a/apps/desktop-tauri/src/proof/fixtures/scenarios.ts b/apps/desktop-tauri/src/proof/fixtures/scenarios.ts index 5edb4afec6..19ab226ccf 100644 --- a/apps/desktop-tauri/src/proof/fixtures/scenarios.ts +++ b/apps/desktop-tauri/src/proof/fixtures/scenarios.ts @@ -19,6 +19,7 @@ import type { SpendModelRow, TokenAccountBridge, } from "../../types/bridge"; +import { ukLocaleOverrides } from "../locale"; import { PROOF_PROVIDER_CATALOG, baseSettings, @@ -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", @@ -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", @@ -1241,6 +1273,7 @@ const BUILDERS: Record 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, diff --git a/apps/desktop-tauri/src/proof/locale.ts b/apps/desktop-tauri/src/proof/locale.ts index 69db82173f..fbc835b39b 100644 --- a/apps/desktop-tauri/src/proof/locale.ts +++ b/apps/desktop-tauri/src/proof/locale.ts @@ -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"; @@ -12,6 +13,21 @@ export function parseFtlEntries(text: string): Record { 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 { + const parsed = parseFtlEntries(ukFtl); + const overrides: Record = {}; + for (const key of keys) { + const value = parsed[key]; + if (value !== undefined) overrides[key] = value; + } + return overrides; +} + let cached: LocaleStrings | null = null; /** diff --git a/rust/src/locale.rs b/rust/src/locale.rs index 05cc7c9786..393d78963b 100644 --- a/rust/src/locale.rs +++ b/rust/src/locale.rs @@ -1780,6 +1780,7 @@ locale_keys! { ResetsInHoursOnly, ResetsInDaysOnly, RegeneratesCountdown, + RegeneratesNow, CostTitleClawRouterSpend, CostTitleClawRouterMonthlyBudget, ResetsAtTime, diff --git a/rust/src/locale/en-US.ftl b/rust/src/locale/en-US.ftl index 3fe8cfb1a0..47df937032 100644 --- a/rust/src/locale/en-US.ftl +++ b/rust/src/locale/en-US.ftl @@ -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 { "{}" } diff --git a/rust/src/locale/es-MX.ftl b/rust/src/locale/es-MX.ftl index 6839302318..aef1d26633 100644 --- a/rust/src/locale/es-MX.ftl +++ b/rust/src/locale/es-MX.ftl @@ -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 { "{}" } diff --git a/rust/src/locale/ja-JP.ftl b/rust/src/locale/ja-JP.ftl index c517c6665f..ee77d4e037 100644 --- a/rust/src/locale/ja-JP.ftl +++ b/rust/src/locale/ja-JP.ftl @@ -1363,7 +1363,8 @@ WindowLabelDays = { "{}" } 日間 ResetsAtLabel = リセット { "{}" } ResetsInHoursOnly = リセットまで { "{}" }時間 ResetsInDaysOnly = リセットまで { "{}" }日 -RegeneratesCountdown = { "{}" } に再生成 +RegeneratesCountdown = { "{}" }後に再生成 +RegeneratesNow = 今再生成 CostTitleClawRouterSpend = ClawRouter spend CostTitleClawRouterMonthlyBudget = Monthly budget ResetsAtTime = { "{}" } にリセット diff --git a/rust/src/locale/ko-KR.ftl b/rust/src/locale/ko-KR.ftl index 5fe14aebdc..ed5cfb561f 100644 --- a/rust/src/locale/ko-KR.ftl +++ b/rust/src/locale/ko-KR.ftl @@ -1362,7 +1362,8 @@ WindowLabelDays = { "{}" }일 ResetsAtLabel = 초기화 { "{}" } ResetsInHoursOnly = 초기화까지 { "{}" }시간 ResetsInDaysOnly = 초기화까지 { "{}" }일 -RegeneratesCountdown = { "{}" }에 재생성 +RegeneratesCountdown = { "{}" } 후 재생성 +RegeneratesNow = 지금 재생성 CostTitleClawRouterSpend = ClawRouter spend CostTitleClawRouterMonthlyBudget = Monthly budget ResetsAtTime = { "{}" }에 초기화 diff --git a/rust/src/locale/pt-BR.ftl b/rust/src/locale/pt-BR.ftl index 8f7e531cf2..66746fc18d 100644 --- a/rust/src/locale/pt-BR.ftl +++ b/rust/src/locale/pt-BR.ftl @@ -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 { "{}" } diff --git a/rust/src/locale/ru-RU.ftl b/rust/src/locale/ru-RU.ftl index e647b3a822..21e71ac030 100644 --- a/rust/src/locale/ru-RU.ftl +++ b/rust/src/locale/ru-RU.ftl @@ -1363,7 +1363,8 @@ WindowLabelDays = { "{}" } дн. ResetsAtLabel = Сброс { "{}" } ResetsInHoursOnly = Сброс через { "{}" } ч ResetsInDaysOnly = Сброс через { "{}" } д -RegeneratesCountdown = Пополняется { "{}" } +RegeneratesCountdown = Пополняется через { "{}" } +RegeneratesNow = Пополняется сейчас CostTitleClawRouterSpend = ClawRouter spend CostTitleClawRouterMonthlyBudget = Monthly budget ResetsAtTime = Сброс в { "{}" } diff --git a/rust/src/locale/tr-TR.ftl b/rust/src/locale/tr-TR.ftl index ea9f3a6644..480de316fd 100644 --- a/rust/src/locale/tr-TR.ftl +++ b/rust/src/locale/tr-TR.ftl @@ -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 diff --git a/rust/src/locale/uk-UA.ftl b/rust/src/locale/uk-UA.ftl index 17ed6274f2..be8db9eeb6 100644 --- a/rust/src/locale/uk-UA.ftl +++ b/rust/src/locale/uk-UA.ftl @@ -244,10 +244,10 @@ PaceLastsUntilReset = Вистачить до скидання PaceDailyTargetPercent = ≈ { "{}" }% на день, вистачить до скидання PaceDailyTargetCredits = ≈ { "{}" } кредитів на день, вистачить до скидання PaceDailyTargetRequests = ≈ { "{}" } запитів на день, вистачить до скидання -PaceRunsOutIn = Закінчується за { "{}" } +PaceRunsOutIn = Закінчується через { "{}" } PaceRunsOutNow = Зараз закінчується -PaceProjectedEmptyIn = Передбачається порожній у { "{}" } -PaceProjectedEmptyNow = Зараз проектується порожнім +PaceProjectedEmptyIn = За прогнозом, вичерпається через { "{}" } +PaceProjectedEmptyNow = За прогнозом, ліміт вичерпано UsageBarQuotaWarnings = Попередження про квоту UsageBarWorkDays = Щотижневі робочі дні MetricResetsIn = Скинеться через @@ -515,8 +515,8 @@ TokenActivityLess = Менше TokenActivityMore = Більше TokenActivityEachColumnWeek = Кожен стовпець = 1 тиждень TokenActivityRunningTotal = Накопичений підсумок -TokenActivityDaily = Щодня -TokenActivityWeekly = Щотижня +TokenActivityDaily = Денний +TokenActivityWeekly = Тижневий TokenActivityCumulative = Наростаючим підсумком TokenActivityView = Вигляд TokenActivityEarlier = Раніша активність @@ -945,7 +945,7 @@ 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 @@ -953,7 +953,7 @@ CredsVertexAiSetupHelp = Виконайте `gcloud auth application-default log 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 для відстеження. @@ -1303,7 +1303,7 @@ RegionZaiChinaMainland = Материковий Китай (BigModel) UsageItemUnavailableTitle = { "{}" } (недоступно) UsageItemFallbackTitle = Елемент використання WindowLabelQuota = Квота -WindowLabelDaily = Щодня +WindowLabelDaily = Денний WindowLabelBudget = Бюджет WindowLabelWeeklyQuota = Тижнева квота WindowLabelMonthlyQuota = Місячна квота @@ -1361,7 +1361,8 @@ WindowLabelDays = { "{}" } дн. ResetsAtLabel = Скидання { "{}" } ResetsInHoursOnly = Скинеться через { "{}" } год ResetsInDaysOnly = Скинеться через { "{}" } дн. -RegeneratesCountdown = Регенерує { "{}" } +RegeneratesCountdown = Регенерує через { "{}" } +RegeneratesNow = Регенерує зараз CostTitleClawRouterSpend = ClawRouter spend CostTitleClawRouterMonthlyBudget = Monthly budget ResetsAtTime = Скинеться о { "{}" } @@ -1432,7 +1433,7 @@ ProviderTextNoBrandBudgetQuotaReported = Квоту бюджету { "{}" } не ProviderTextNoCachedBrandQuota = Немає збережених даних про квоту { "{}" } ProviderTextNoActiveBrandTier = Немає активного ліміту тарифу { "{}" } IntlLocale = uk-UA -ProviderLabelMonthly = Щомісяця +ProviderLabelMonthly = Місячний ProviderLabelHourly = Щогодини ProviderLabelQuarterly = Щокварталу ProviderLabelYearly = Щороку @@ -1653,10 +1654,10 @@ ProviderTextExpiresPrefix = спливає { "{}" } ProviderTextRenewsIn = Поновлення через { "{}" } ProviderTextExpiresIn = Спливає через { "{}" } ProviderTextCycleEndsIn = Цикл закінчиться через { "{}" } -DurationDaysHours = { "{}" } д { "{}" } год -DurationDaysMinutes = { "{}" } д { "{}" } хв +DurationDaysHours = { "{}" } дн. { "{}" } год +DurationDaysMinutes = { "{}" } дн. { "{}" } хв DurationHoursMinutes = { "{}" } год { "{}" } хв -DurationDays = { "{}" } д +DurationDays = { "{}" } дн. DurationHours = { "{}" } год DurationMinutes = { "{}" } хв DurationMonths = { "{}" } міс. @@ -1708,8 +1709,8 @@ UsageSpendLocalHistory = Локальна орієнтовна історія UsageSpendEstimatedSpend = Орієнтовні витрати UsageSpendTrackedTokens = Враховані токени UsageSpendSources = Джерела -UsageSpendTokenInput = Вхід -UsageSpendTokenOutput = Вихід +UsageSpendTokenInput = Вхідні +UsageSpendTokenOutput = Вихідні UsageSpendTokenCacheRead = Читання кешу UsageSpendTokenCacheWrite = Запис у кеш UsageSpendTokenReasoning = Міркування diff --git a/rust/src/locale/zh-CN.ftl b/rust/src/locale/zh-CN.ftl index 22b1ea1657..e94a7c6fbf 100644 --- a/rust/src/locale/zh-CN.ftl +++ b/rust/src/locale/zh-CN.ftl @@ -1364,6 +1364,7 @@ ResetsAtLabel = 重置 { "{}" } ResetsInHoursOnly = { "{}" } 小时后重置 ResetsInDaysOnly = { "{}" } 天后重置 RegeneratesCountdown = { "{}" }后恢复 +RegeneratesNow = 现在恢复 CostTitleClawRouterSpend = ClawRouter spend CostTitleClawRouterMonthlyBudget = Monthly budget ResetsAtTime = { "{}" } 重置 diff --git a/rust/src/locale/zh-TW.ftl b/rust/src/locale/zh-TW.ftl index 8900281df9..6829d50a37 100644 --- a/rust/src/locale/zh-TW.ftl +++ b/rust/src/locale/zh-TW.ftl @@ -1364,6 +1364,7 @@ ResetsAtLabel = 重置 { "{}" } ResetsInHoursOnly = { "{}" } 小時後重置 ResetsInDaysOnly = { "{}" } 天後重置 RegeneratesCountdown = { "{}" }後恢復 +RegeneratesNow = 現在恢復 CostTitleClawRouterSpend = ClawRouter spend CostTitleClawRouterMonthlyBudget = Monthly budget ResetsAtTime = { "{}" } 重置