diff --git a/README.md b/README.md index b4e7147..347bdf5 100644 --- a/README.md +++ b/README.md @@ -851,7 +851,7 @@ fixed through confirmation and any retry of an uncertain request; it never silently switches credits. A credit that expires or disappears before a new request is submitted requires a fresh confirmation. -An amber warning such as `⚠ RESET EXPIRES IN 2D 4H` appears immediately before +An amber warning such as `⚠ RESET EXPIRES IN 2D 4H` appears immediately before the normal `[ RESET // N ]` button when a known available reset has less than 72 hours left by default, even below `--reset-threshold`. Clicking the warning opens **Quota → Resets** without arming confirmation or submitting a reset. It @@ -861,6 +861,10 @@ warning, not a recommendation to reset unused quota. Redemption still requires fresh account data and explicit confirmation. The Resets view always permits review regardless of usage percentage. +The Resets page repeats the actual countdown, for example +`FIRST EXPIRATION IN 2D 4H // within 72 hours`: the first value is the remaining time +until the earliest known expiry; the second is your configured warning lead time. + The confirmation explicitly warns that unused allowance does not carry over or stack and that the weekly reset schedule changes. Treat an expiry warning as a prompt to **review**, not a recommendation to redeem immediately. When expiry diff --git a/internal/i18n/locales/da.json b/internal/i18n/locales/da.json index a901d45..e8f2369 100644 --- a/internal/i18n/locales/da.json +++ b/internal/i18n/locales/da.json @@ -1,12 +1,12 @@ { "Expiry order unavailable; backend chooses the credit.": "Udløbsrækkefølgen er ukendt; serveren vælger kreditten.", - "EXPIRING SOON // within %d hours": "UDLØBER SNART // inden for %d timer", + "FIRST EXPIRATION IN %s // within %d hours": "FØRSTE UDLØB OM %s // inden for %d timer", "Expiry information unavailable.": "Udløbsoplysninger er ikke tilgængelige.", "Unused allowance does not carry over or stack.": "Ubrugt kvote overføres ikke eller lægges til.", "Expiry information unavailable. No warning does not mean no expiry.": "Udløbsoplysninger er ikke tilgængelige. Ingen advarsel betyder ikke, at intet udløber.", "Expiry information incomplete. Other credits may expire sooner.": "Udløbsoplysningerne er ufuldstændige. Andre kreditter kan udløbe tidligere.", - "⚠ RESET EXPIRES IN %s": "⚠ NULSTILLING UDLØBER OM %s", - "⚠ EXPIRES %s": "⚠ UDLØBER %s", + "⚠ RESET EXPIRES IN %s": "⚠ NULSTILLING UDLØBER OM %s", + "⚠ EXPIRES %s": "⚠ UDLØBER %s", "RESETS": "NULSTILLINGER", "╭ RESETS ╮": "╭ NULSTILLINGER ╮", "APPROVAL — OPEN DETAIL →": "GODKENDELSE — ÅBN DETALJER →", diff --git a/internal/i18n/locales/de.json b/internal/i18n/locales/de.json index 6c3f5cc..a4941f7 100644 --- a/internal/i18n/locales/de.json +++ b/internal/i18n/locales/de.json @@ -1,12 +1,12 @@ { "Expiry order unavailable; backend chooses the credit.": "Ablaufreihenfolge unbekannt; das Backend wählt das Guthaben.", - "EXPIRING SOON // within %d hours": "BALD ABGELAUFEN // innerhalb von %d Stunden", + "FIRST EXPIRATION IN %s // within %d hours": "ERSTER ABLAUF IN %s // innerhalb von %d Stunden", "Expiry information unavailable.": "Ablaufinformationen nicht verfügbar.", "Unused allowance does not carry over or stack.": "Ungenutztes Kontingent wird nicht übertragen oder addiert.", "Expiry information unavailable. No warning does not mean no expiry.": "Ablaufinformationen nicht verfügbar. Keine Warnung bedeutet nicht, dass nichts verfällt.", "Expiry information incomplete. Other credits may expire sooner.": "Ablaufinformationen unvollständig. Andere Guthaben können früher verfallen.", - "⚠ RESET EXPIRES IN %s": "⚠ RESET VERFÄLLT IN %s", - "⚠ EXPIRES %s": "⚠ VERFÄLLT %s", + "⚠ RESET EXPIRES IN %s": "⚠ RESET VERFÄLLT IN %s", + "⚠ EXPIRES %s": "⚠ VERFÄLLT %s", "RESETS": "ZURÜCKSETZUNGEN", "╭ RESETS ╮": "╭ ZURÜCKSETZUNGEN ╮", "APPROVAL — OPEN DETAIL →": "FREIGABE — DETAILS ÖFFNEN →", diff --git a/internal/i18n/locales/en-GB.json b/internal/i18n/locales/en-GB.json index 2c8497c..3ea46bf 100644 --- a/internal/i18n/locales/en-GB.json +++ b/internal/i18n/locales/en-GB.json @@ -1,12 +1,12 @@ { "Expiry order unavailable; backend chooses the credit.": "Expiry order unavailable; backend chooses the credit.", - "EXPIRING SOON // within %d hours": "EXPIRING SOON // within %d hours", + "FIRST EXPIRATION IN %s // within %d hours": "FIRST EXPIRATION IN %s // within %d hours", "Expiry information unavailable.": "Expiry information unavailable.", "Unused allowance does not carry over or stack.": "Unused allowance does not carry over or stack.", "Expiry information unavailable. No warning does not mean no expiry.": "Expiry information unavailable. No warning does not mean no expiry.", "Expiry information incomplete. Other credits may expire sooner.": "Expiry information incomplete. Other credits may expire sooner.", - "⚠ RESET EXPIRES IN %s": "⚠ RESET EXPIRES IN %s", - "⚠ EXPIRES %s": "⚠ EXPIRES %s", + "⚠ RESET EXPIRES IN %s": "⚠ RESET EXPIRES IN %s", + "⚠ EXPIRES %s": "⚠ EXPIRES %s", "RESETS": "RESETS", "╭ RESETS ╮": "╭ RESETS ╮", "APPROVAL — OPEN DETAIL →": "APPROVAL — OPEN DETAIL →", diff --git a/internal/i18n/locales/es.json b/internal/i18n/locales/es.json index 0e73b01..590a5bd 100644 --- a/internal/i18n/locales/es.json +++ b/internal/i18n/locales/es.json @@ -1,12 +1,12 @@ { "Expiry order unavailable; backend chooses the credit.": "Orden de caducidad desconocido; el servidor elige el crédito.", - "EXPIRING SOON // within %d hours": "CADUCA PRONTO // en %d horas", + "FIRST EXPIRATION IN %s // within %d hours": "PRIMER VENCIMIENTO EN %s // en %d horas", "Expiry information unavailable.": "Información de caducidad no disponible.", "Unused allowance does not carry over or stack.": "La cuota sin usar no se transfiere ni se acumula.", "Expiry information unavailable. No warning does not mean no expiry.": "Información de caducidad no disponible. Sin aviso no significa sin caducidad.", "Expiry information incomplete. Other credits may expire sooner.": "Información de caducidad incompleta. Otros créditos pueden caducar antes.", - "⚠ RESET EXPIRES IN %s": "⚠ REINICIO CADUCA EN %s", - "⚠ EXPIRES %s": "⚠ CADUCA %s", + "⚠ RESET EXPIRES IN %s": "⚠ REINICIO CADUCA EN %s", + "⚠ EXPIRES %s": "⚠ CADUCA %s", "RESETS": "REINICIOS", "╭ RESETS ╮": "╭ REINICIOS ╮", "APPROVAL — OPEN DETAIL →": "APROBACIÓN — ABRIR DETALLES →", diff --git a/internal/i18n/locales/et.json b/internal/i18n/locales/et.json index 609e511..3fea773 100644 --- a/internal/i18n/locales/et.json +++ b/internal/i18n/locales/et.json @@ -1,12 +1,12 @@ { "Expiry order unavailable; backend chooses the credit.": "Aegumise järjekord pole teada; krediidi valib server.", - "EXPIRING SOON // within %d hours": "AEGUB PEAGI // %d tunni jooksul", + "FIRST EXPIRATION IN %s // within %d hours": "ESIMENE AEGUMINE %s PÄRAST // %d tunni jooksul", "Expiry information unavailable.": "Aegumise teave pole saadaval.", "Unused allowance does not carry over or stack.": "Kasutamata kvooti ei kanta üle ega liideta.", "Expiry information unavailable. No warning does not mean no expiry.": "Aegumise teave pole saadaval. Hoiatuse puudumine ei tähenda aegumise puudumist.", "Expiry information incomplete. Other credits may expire sooner.": "Aegumise teave on puudulik. Muud krediidid võivad varem aeguda.", - "⚠ RESET EXPIRES IN %s": "⚠ LÄHTESTUS AEGUB %s PÄRAST", - "⚠ EXPIRES %s": "⚠ AEGUB %s", + "⚠ RESET EXPIRES IN %s": "⚠ LÄHTESTUS AEGUB %s PÄRAST", + "⚠ EXPIRES %s": "⚠ AEGUB %s", "RESETS": "LÄHTESTUSED", "╭ RESETS ╮": "╭ LÄHTESTUSED ╮", "APPROVAL — OPEN DETAIL →": "KINNITAMINE — AVA ÜKSIKASJAD →", diff --git a/internal/i18n/locales/fi.json b/internal/i18n/locales/fi.json index 72d3a25..5dc5b29 100644 --- a/internal/i18n/locales/fi.json +++ b/internal/i18n/locales/fi.json @@ -1,12 +1,12 @@ { "Expiry order unavailable; backend chooses the credit.": "Vanhenemisjärjestys ei ole tiedossa; palvelin valitsee krediitin.", - "EXPIRING SOON // within %d hours": "VANHENEE PIAN // %d tunnin kuluessa", + "FIRST EXPIRATION IN %s // within %d hours": "ENSIMMÄINEN VANHENEMINEN %s KULUTTUA // %d tunnin kuluessa", "Expiry information unavailable.": "Vanhenemistietoja ei ole saatavilla.", "Unused allowance does not carry over or stack.": "Käyttämätön kiintiö ei siirry eikä kerry.", "Expiry information unavailable. No warning does not mean no expiry.": "Vanhenemistietoja ei ole saatavilla. Varoituksen puuttuminen ei tarkoita, ettei mikään vanhene.", "Expiry information incomplete. Other credits may expire sooner.": "Vanhenemistiedot ovat puutteelliset. Muut krediitit voivat vanhentua aiemmin.", - "⚠ RESET EXPIRES IN %s": "⚠ NOLLAUS VANHENEE %s KULUTTUA", - "⚠ EXPIRES %s": "⚠ VANHENEE %s", + "⚠ RESET EXPIRES IN %s": "⚠ NOLLAUS VANHENEE %s KULUTTUA", + "⚠ EXPIRES %s": "⚠ VANHENEE %s", "RESETS": "NOLLAUKSET", "╭ RESETS ╮": "╭ NOLLAUKSET ╮", "APPROVAL — OPEN DETAIL →": "HYVÄKSYNTÄ — AVAA TIEDOT →", diff --git a/internal/i18n/locales/fr.json b/internal/i18n/locales/fr.json index 7e10858..55bc492 100644 --- a/internal/i18n/locales/fr.json +++ b/internal/i18n/locales/fr.json @@ -1,12 +1,12 @@ { "Expiry order unavailable; backend chooses the credit.": "Ordre d’expiration inconnu ; le serveur choisit le crédit.", - "EXPIRING SOON // within %d hours": "EXPIRE BIENTÔT // sous %d heures", + "FIRST EXPIRATION IN %s // within %d hours": "PREMIÈRE EXPIRATION DANS %s // sous %d heures", "Expiry information unavailable.": "Informations d’expiration indisponibles.", "Unused allowance does not carry over or stack.": "Le quota inutilisé n’est ni reporté ni cumulé.", "Expiry information unavailable. No warning does not mean no expiry.": "Informations d’expiration indisponibles. L’absence d’alerte ne signifie pas l’absence d’expiration.", "Expiry information incomplete. Other credits may expire sooner.": "Informations d’expiration incomplètes. D’autres crédits peuvent expirer plus tôt.", - "⚠ RESET EXPIRES IN %s": "⚠ RÉINITIALISATION EXPIRE DANS %s", - "⚠ EXPIRES %s": "⚠ EXPIRE %s", + "⚠ RESET EXPIRES IN %s": "⚠ RÉINITIALISATION EXPIRE DANS %s", + "⚠ EXPIRES %s": "⚠ EXPIRE %s", "RESETS": "RÉINITIALISATIONS", "╭ RESETS ╮": "╭ RÉINITIALISATIONS ╮", "APPROVAL — OPEN DETAIL →": "APPROBATION — OUVRIR LES DÉTAILS →", diff --git a/internal/i18n/locales/it.json b/internal/i18n/locales/it.json index 8c35e5b..cfe62e9 100644 --- a/internal/i18n/locales/it.json +++ b/internal/i18n/locales/it.json @@ -1,12 +1,12 @@ { "Expiry order unavailable; backend chooses the credit.": "Ordine di scadenza sconosciuto; il backend sceglie il credito.", - "EXPIRING SOON // within %d hours": "IN SCADENZA // entro %d ore", + "FIRST EXPIRATION IN %s // within %d hours": "PRIMA SCADENZA TRA %s // entro %d ore", "Expiry information unavailable.": "Informazioni sulla scadenza non disponibili.", "Unused allowance does not carry over or stack.": "La quota inutilizzata non viene riportata né cumulata.", "Expiry information unavailable. No warning does not mean no expiry.": "Informazioni sulla scadenza non disponibili. Nessun avviso non significa nessuna scadenza.", "Expiry information incomplete. Other credits may expire sooner.": "Informazioni sulla scadenza incomplete. Altri crediti potrebbero scadere prima.", - "⚠ RESET EXPIRES IN %s": "⚠ RIPRISTINO SCADE TRA %s", - "⚠ EXPIRES %s": "⚠ SCADE %s", + "⚠ RESET EXPIRES IN %s": "⚠ RIPRISTINO SCADE TRA %s", + "⚠ EXPIRES %s": "⚠ SCADE %s", "RESETS": "RIPRISTINI", "╭ RESETS ╮": "╭ RIPRISTINI ╮", "APPROVAL — OPEN DETAIL →": "APPROVAZIONE — APRI DETTAGLI →", diff --git a/internal/i18n/locales/ja.json b/internal/i18n/locales/ja.json index 23f3087..73a384c 100644 --- a/internal/i18n/locales/ja.json +++ b/internal/i18n/locales/ja.json @@ -1,12 +1,12 @@ { "Expiry order unavailable; backend chooses the credit.": "有効期限の順序は不明です。バックエンドがクレジットを選択します。", - "EXPIRING SOON // within %d hours": "まもなく期限切れ // %d 時間以内", + "FIRST EXPIRATION IN %s // within %d hours": "最初の有効期限まで %s // %d 時間以内", "Expiry information unavailable.": "有効期限情報を取得できません。", "Unused allowance does not carry over or stack.": "未使用の利用枠は繰り越しも加算もされません。", "Expiry information unavailable. No warning does not mean no expiry.": "有効期限情報を取得できません。警告がなくても期限がないとは限りません。", "Expiry information incomplete. Other credits may expire sooner.": "有効期限情報が不完全です。他のクレジットが先に失効する可能性があります。", - "⚠ RESET EXPIRES IN %s": "⚠ リセット期限まで %s", - "⚠ EXPIRES %s": "⚠ 期限 %s", + "⚠ RESET EXPIRES IN %s": "⚠ リセット期限まで %s", + "⚠ EXPIRES %s": "⚠ 期限 %s", "RESETS": "リセット", "╭ RESETS ╮": "╭ リセット ╮", "APPROVAL — OPEN DETAIL →": "承認 — 詳細を開く →", diff --git a/internal/i18n/locales/nb.json b/internal/i18n/locales/nb.json index 6d649c2..376e007 100644 --- a/internal/i18n/locales/nb.json +++ b/internal/i18n/locales/nb.json @@ -1,12 +1,12 @@ { "Expiry order unavailable; backend chooses the credit.": "Utløpsrekkefølgen er ukjent; serveren velger kreditten.", - "EXPIRING SOON // within %d hours": "UTLØPER SNART // innen %d timer", + "FIRST EXPIRATION IN %s // within %d hours": "FØRSTE UTLØP OM %s // innen %d timer", "Expiry information unavailable.": "Utløpsinformasjon er utilgjengelig.", "Unused allowance does not carry over or stack.": "Ubrukt kvote overføres ikke eller legges til.", "Expiry information unavailable. No warning does not mean no expiry.": "Utløpsinformasjon er utilgjengelig. Ingen advarsel betyr ikke at ingenting utløper.", "Expiry information incomplete. Other credits may expire sooner.": "Utløpsinformasjonen er ufullstendig. Andre kreditter kan utløpe tidligere.", - "⚠ RESET EXPIRES IN %s": "⚠ NULLSTILLING UTLØPER OM %s", - "⚠ EXPIRES %s": "⚠ UTLØPER %s", + "⚠ RESET EXPIRES IN %s": "⚠ NULLSTILLING UTLØPER OM %s", + "⚠ EXPIRES %s": "⚠ UTLØPER %s", "RESETS": "NULLSTILLINGER", "╭ RESETS ╮": "╭ NULLSTILLINGER ╮", "APPROVAL — OPEN DETAIL →": "GODKJENNING — ÅPNE DETALJER →", diff --git a/internal/i18n/locales/nl.json b/internal/i18n/locales/nl.json index e12fa15..2b84412 100644 --- a/internal/i18n/locales/nl.json +++ b/internal/i18n/locales/nl.json @@ -1,12 +1,12 @@ { "Expiry order unavailable; backend chooses the credit.": "Vervolgorde onbekend; de backend kiest het tegoed.", - "EXPIRING SOON // within %d hours": "VERLOOPT BINNENKORT // binnen %d uur", + "FIRST EXPIRATION IN %s // within %d hours": "EERSTE VERVAL OVER %s // binnen %d uur", "Expiry information unavailable.": "Vervalinformatie niet beschikbaar.", "Unused allowance does not carry over or stack.": "Ongebruikte capaciteit wordt niet meegenomen of opgeteld.", "Expiry information unavailable. No warning does not mean no expiry.": "Vervalinformatie niet beschikbaar. Geen waarschuwing betekent niet dat niets verloopt.", "Expiry information incomplete. Other credits may expire sooner.": "Vervalinformatie onvolledig. Andere tegoeden kunnen eerder verlopen.", - "⚠ RESET EXPIRES IN %s": "⚠ RESET VERLOOPT OVER %s", - "⚠ EXPIRES %s": "⚠ VERLOOPT %s", + "⚠ RESET EXPIRES IN %s": "⚠ RESET VERLOOPT OVER %s", + "⚠ EXPIRES %s": "⚠ VERLOOPT %s", "RESETS": "RESETS", "╭ RESETS ╮": "╭ RESETS ╮", "APPROVAL — OPEN DETAIL →": "GOEDKEURING — OPEN DETAILS →", diff --git a/internal/i18n/locales/pt-BR.json b/internal/i18n/locales/pt-BR.json index 6672fbd..193ab73 100644 --- a/internal/i18n/locales/pt-BR.json +++ b/internal/i18n/locales/pt-BR.json @@ -1,12 +1,12 @@ { "Expiry order unavailable; backend chooses the credit.": "Ordem de vencimento desconhecida; o servidor escolhe o crédito.", - "EXPIRING SOON // within %d hours": "VENCE EM BREVE // em %d horas", + "FIRST EXPIRATION IN %s // within %d hours": "PRIMEIRO VENCIMENTO EM %s // em %d horas", "Expiry information unavailable.": "Informações de validade indisponíveis.", "Unused allowance does not carry over or stack.": "A cota não utilizada não é transferida nem acumulada.", "Expiry information unavailable. No warning does not mean no expiry.": "Informações de validade indisponíveis. A ausência de aviso não significa ausência de vencimento.", "Expiry information incomplete. Other credits may expire sooner.": "Informações de validade incompletas. Outros créditos podem vencer antes.", - "⚠ RESET EXPIRES IN %s": "⚠ REDEFINIÇÃO EXPIRA EM %s", - "⚠ EXPIRES %s": "⚠ EXPIRA %s", + "⚠ RESET EXPIRES IN %s": "⚠ REDEFINIÇÃO EXPIRA EM %s", + "⚠ EXPIRES %s": "⚠ EXPIRA %s", "RESETS": "REDEFINIÇÕES", "╭ RESETS ╮": "╭ REDEFINIÇÕES ╮", "APPROVAL — OPEN DETAIL →": "APROVAÇÃO — ABRIR DETALHES →", diff --git a/internal/i18n/locales/pt-PT.json b/internal/i18n/locales/pt-PT.json index f0de615..6d6b79b 100644 --- a/internal/i18n/locales/pt-PT.json +++ b/internal/i18n/locales/pt-PT.json @@ -1,12 +1,12 @@ { "Expiry order unavailable; backend chooses the credit.": "Ordem de expiração desconhecida; o servidor escolhe o crédito.", - "EXPIRING SOON // within %d hours": "EXPIRA EM BREVE // dentro de %d horas", + "FIRST EXPIRATION IN %s // within %d hours": "PRIMEIRA EXPIRAÇÃO EM %s // dentro de %d horas", "Expiry information unavailable.": "Informações de validade indisponíveis.", "Unused allowance does not carry over or stack.": "A quota não utilizada não é transferida nem acumulada.", "Expiry information unavailable. No warning does not mean no expiry.": "Informações de validade indisponíveis. A ausência de aviso não significa ausência de expiração.", "Expiry information incomplete. Other credits may expire sooner.": "Informações de validade incompletas. Outros créditos podem expirar mais cedo.", - "⚠ RESET EXPIRES IN %s": "⚠ REPOSIÇÃO EXPIRA EM %s", - "⚠ EXPIRES %s": "⚠ EXPIRA %s", + "⚠ RESET EXPIRES IN %s": "⚠ REPOSIÇÃO EXPIRA EM %s", + "⚠ EXPIRES %s": "⚠ EXPIRA %s", "RESETS": "REPOSIÇÕES", "╭ RESETS ╮": "╭ REPOSIÇÕES ╮", "APPROVAL — OPEN DETAIL →": "APROVAÇÃO — ABRIR DETALHES →", diff --git a/internal/i18n/locales/ru.json b/internal/i18n/locales/ru.json index 68c561d..d085e38 100644 --- a/internal/i18n/locales/ru.json +++ b/internal/i18n/locales/ru.json @@ -1,12 +1,12 @@ { "Expiry order unavailable; backend chooses the credit.": "Порядок истечения неизвестен; кредит выбирает сервер.", - "EXPIRING SOON // within %d hours": "СКОРО ИСТЕЧЁТ // в течение %d часов", + "FIRST EXPIRATION IN %s // within %d hours": "ПЕРВОЕ ИСТЕЧЕНИЕ ЧЕРЕЗ %s // в течение %d часов", "Expiry information unavailable.": "Сведения о сроках недоступны.", "Unused allowance does not carry over or stack.": "Неиспользованный лимит не переносится и не суммируется.", "Expiry information unavailable. No warning does not mean no expiry.": "Сведения о сроках недоступны. Отсутствие предупреждения не означает отсутствие срока действия.", "Expiry information incomplete. Other credits may expire sooner.": "Сведения о сроках неполные. Другие кредиты могут истечь раньше.", - "⚠ RESET EXPIRES IN %s": "⚠ СБРОС ИСТЕКАЕТ ЧЕРЕЗ %s", - "⚠ EXPIRES %s": "⚠ ИСТЕКАЕТ %s", + "⚠ RESET EXPIRES IN %s": "⚠ СБРОС ИСТЕКАЕТ ЧЕРЕЗ %s", + "⚠ EXPIRES %s": "⚠ ИСТЕКАЕТ %s", "RESETS": "СБРОСЫ", "╭ RESETS ╮": "╭ СБРОСЫ ╮", "APPROVAL — OPEN DETAIL →": "ОДОБРЕНИЕ — ОТКРЫТЬ ПОДРОБНОСТИ →", diff --git a/internal/i18n/locales/sv.json b/internal/i18n/locales/sv.json index 2403216..e9d87ba 100644 --- a/internal/i18n/locales/sv.json +++ b/internal/i18n/locales/sv.json @@ -1,12 +1,12 @@ { "Expiry order unavailable; backend chooses the credit.": "Utgångsordningen är okänd; servern väljer krediten.", - "EXPIRING SOON // within %d hours": "LÖPER SNART UT // inom %d timmar", + "FIRST EXPIRATION IN %s // within %d hours": "FÖRSTA UTGÅNG OM %s // inom %d timmar", "Expiry information unavailable.": "Information om utgångsdatum saknas.", "Unused allowance does not carry over or stack.": "Oanvänd kvot förs inte över eller läggs till.", "Expiry information unavailable. No warning does not mean no expiry.": "Information om utgångsdatum saknas. Ingen varning betyder inte att inget löper ut.", "Expiry information incomplete. Other credits may expire sooner.": "Information om utgångsdatum är ofullständig. Andra krediter kan löpa ut tidigare.", - "⚠ RESET EXPIRES IN %s": "⚠ ÅTERSTÄLLNING UPPHÖR OM %s", - "⚠ EXPIRES %s": "⚠ UPPHÖR %s", + "⚠ RESET EXPIRES IN %s": "⚠ ÅTERSTÄLLNING UPPHÖR OM %s", + "⚠ EXPIRES %s": "⚠ UPPHÖR %s", "RESETS": "ÅTERSTÄLLNINGAR", "╭ RESETS ╮": "╭ ÅTERSTÄLLNINGAR ╮", "APPROVAL — OPEN DETAIL →": "GODKÄNNANDE — ÖPPNA DETALJER →", diff --git a/internal/i18n/locales/tr.json b/internal/i18n/locales/tr.json index d87a901..c0ff221 100644 --- a/internal/i18n/locales/tr.json +++ b/internal/i18n/locales/tr.json @@ -1,12 +1,12 @@ { "Expiry order unavailable; backend chooses the credit.": "Sona erme sırası bilinmiyor; krediyi sunucu seçer.", - "EXPIRING SOON // within %d hours": "YAKINDA SONA ERİYOR // %d saat içinde", + "FIRST EXPIRATION IN %s // within %d hours": "İLK SONA ERME %s İÇİNDE // %d saat içinde", "Expiry information unavailable.": "Son kullanma bilgisi yok.", "Unused allowance does not carry over or stack.": "Kullanılmayan kota devredilmez veya birikmez.", "Expiry information unavailable. No warning does not mean no expiry.": "Son kullanma bilgisi yok. Uyarı olmaması süre dolmayacağı anlamına gelmez.", "Expiry information incomplete. Other credits may expire sooner.": "Son kullanma bilgisi eksik. Diğer kredilerin süresi daha erken dolabilir.", - "⚠ RESET EXPIRES IN %s": "⚠ SIFIRLAMA %s İÇİNDE SONA ERER", - "⚠ EXPIRES %s": "⚠ SONA ERER %s", + "⚠ RESET EXPIRES IN %s": "⚠ SIFIRLAMA %s İÇİNDE SONA ERER", + "⚠ EXPIRES %s": "⚠ SONA ERER %s", "RESETS": "SIFIRLAMALAR", "╭ RESETS ╮": "╭ SIFIRLAMALAR ╮", "APPROVAL — OPEN DETAIL →": "ONAY — AYRINTILARI AÇ →", diff --git a/internal/i18n/locales/zh-Hans.json b/internal/i18n/locales/zh-Hans.json index f5e2dea..8d53200 100644 --- a/internal/i18n/locales/zh-Hans.json +++ b/internal/i18n/locales/zh-Hans.json @@ -1,12 +1,12 @@ { "Expiry order unavailable; backend chooses the credit.": "有效期顺序未知;由后端选择重置额度。", - "EXPIRING SOON // within %d hours": "即将过期 // %d 小时内", + "FIRST EXPIRATION IN %s // within %d hours": "首次过期倒计时 %s // %d 小时内", "Expiry information unavailable.": "有效期信息不可用。", "Unused allowance does not carry over or stack.": "未使用的额度不会结转或叠加。", "Expiry information unavailable. No warning does not mean no expiry.": "有效期信息不可用。没有警告不代表不会过期。", "Expiry information incomplete. Other credits may expire sooner.": "有效期信息不完整。其他重置额度可能更早过期。", - "⚠ RESET EXPIRES IN %s": "⚠ 重置将在 %s 后过期", - "⚠ EXPIRES %s": "⚠ 过期 %s", + "⚠ RESET EXPIRES IN %s": "⚠ 重置将在 %s 后过期", + "⚠ EXPIRES %s": "⚠ 过期 %s", "RESETS": "重置", "╭ RESETS ╮": "╭ 重置 ╮", "APPROVAL — OPEN DETAIL →": "审批 — 打开详情 →", diff --git a/internal/ui/localisation_test.go b/internal/ui/localisation_test.go index 959f699..45677a1 100644 --- a/internal/ui/localisation_test.go +++ b/internal/ui/localisation_test.go @@ -57,6 +57,7 @@ func TestLocalisedScreensHelper(t *testing.T) { t.Run("tab_click_surfaces", TestEveryRenderedTabCellIsClickableAcrossWidths) t.Run("reset_warning_surfaces", TestResetWarningClickSurfaces) t.Run("reset_warning_language", TestResetWarningsUseLocale) + t.Run("reset_expiry_countdown", TestResetExpiryCountdownAndSpacing) t.Run("header_click_surfaces", TestHeaderClickTargets) t.Run("detail_activity", TestDetailSentWaveLifecycle) t.Run("session_context_activity", TestMainSessionContextDots) diff --git a/internal/ui/quota_reset.go b/internal/ui/quota_reset.go index b4f502d..993b7fd 100644 --- a/internal/ui/quota_reset.go +++ b/internal/ui/quota_reset.go @@ -156,6 +156,20 @@ type resetControls struct { tabsWidth, extraRows int } +func resetExpiryRemaining(expiresAt int64, compact bool) string { + hours := max(int(time.Until(time.Unix(expiresAt, 0)).Hours()), 0) + if hours >= 24 { + if compact { + return fmt.Sprintf("%dD", hours/24) + } + return fmt.Sprintf("%dD %dH", hours/24, hours%24) + } + if hours == 0 { + return "<1H" + } + return fmt.Sprintf("%dH", hours) +} + // One geometry source for rendering, tab allocation and both click surfaces. func (m Model) resetControlsLayout(width int) resetControls { c := resetControls{tabsWidth: width, button: m.resetLabel()} @@ -165,19 +179,10 @@ func (m Model) resetControlsLayout(width int) resetControls { c.buttonX = max(width-lipgloss.Width(c.button), 0) credits := m.availableResetCredits() if m.resetWarningHours > 0 && len(credits) > 0 && credits[0].ExpiresAt != nil && time.Until(time.Unix(*credits[0].ExpiresAt, 0)) < m.resetExpiryWarning() && !m.loading && m.err == nil && !m.snapshot.FetchedAt.IsZero() && time.Since(m.snapshot.FetchedAt) <= 2*m.refreshEvery { - hours := max(int(time.Until(time.Unix(*credits[0].ExpiresAt, 0)).Hours()), 0) - remaining := fmt.Sprintf("%dH", hours) - if hours >= 24 { - remaining = fmt.Sprintf("%dD %dH", hours/24, hours%24) - } else if hours == 0 { - remaining = "<1H" - } - c.warning = i18n.Format("⚠ RESET EXPIRES IN %s", remaining) + remaining := resetExpiryRemaining(*credits[0].ExpiresAt, false) + c.warning = i18n.Format("⚠ RESET EXPIRES IN %s", remaining) if lipgloss.Width(c.warning)+1+lipgloss.Width(c.button)+12 > width { - if hours >= 24 { - remaining = fmt.Sprintf("%dD", hours/24) - } - c.warning = i18n.Format("⚠ EXPIRES %s", remaining) + c.warning = i18n.Format("⚠ EXPIRES %s", resetExpiryRemaining(*credits[0].ExpiresAt, true)) } } total := lipgloss.Width(c.button) @@ -386,8 +391,8 @@ func (m Model) resetDetailLines(width int, colors palette) (result []string) { if notice := m.resetExpiryDataNotice(); notice != "" { lines = append(lines, colors.label().Foreground(colors.warning).Render(notice)) } - if m.resetExpiringSoon() { - lines = append(lines, colors.label().Foreground(colors.warning).Render(i18n.Format("EXPIRING SOON // within %d hours", m.resetWarningHours))) + if m.resetExpiringSoon() && len(credits) > 0 && credits[0].ExpiresAt != nil { + lines = append(lines, colors.label().Foreground(colors.warning).Render(i18n.Format("FIRST EXPIRATION IN %s // within %d hours", resetExpiryRemaining(*credits[0].ExpiresAt, false), m.resetWarningHours))) } if summary.AvailableCount == 0 { return append(lines, "No resets available.") diff --git a/internal/ui/reset_warning_test.go b/internal/ui/reset_warning_test.go index 40bb473..dae9961 100644 --- a/internal/ui/reset_warning_test.go +++ b/internal/ui/reset_warning_test.go @@ -90,7 +90,7 @@ func TestResetWarningsUseLocale(t *testing.T) { } m.snapshot.RateLimitResetCredits.Credits = []codex.ResetCredit{credit("first", time.Hour)} body = strings.Join(m.resetDetailLines(1000, paletteFor(m.theme)), "\n") - if !strings.Contains(body, i18n.Format("EXPIRING SOON // within %d hours", m.resetWarningHours)) { + if !strings.Contains(body, i18n.Format("FIRST EXPIRATION IN %s // within %d hours", resetExpiryRemaining(*m.snapshot.RateLimitResetCredits.Credits[0].ExpiresAt, false), m.resetWarningHours)) { t.Fatal("expiry status is not localised") } unknown := codex.ResetCredit{} @@ -122,6 +122,36 @@ func TestResetWarningVisibility(t *testing.T) { } } +func TestResetExpiryCountdownAndSpacing(t *testing.T) { + m, _ := resetModel() + m.width = 180 + m.snapshot.RateLimitResetCredits.Credits = []codex.ResetCredit{credit("first", 51*time.Hour+30*time.Minute)} + warning := m.resetControlsLayout(176).warning + if !strings.HasPrefix(warning, "⚠ ") || !strings.Contains(warning, "2D 3H") { + t.Fatalf("warning spacing/countdown: %q", warning) + } + want := i18n.Format("FIRST EXPIRATION IN %s // within %d hours", "2D 3H", 72) + if body := strings.Join(m.resetDetailLines(1000, paletteFor(m.theme)), "\n"); !strings.Contains(body, want) { + t.Fatalf("actual expiry missing: %q", body) + } + m.snapshot.RateLimitResetCredits.Credits[0] = credit("first", 50*time.Hour+30*time.Minute) + want = i18n.Format("FIRST EXPIRATION IN %s // within %d hours", "2D 2H", 72) + if body := strings.Join(m.resetDetailLines(1000, paletteFor(m.theme)), "\n"); !strings.Contains(body, want) { + t.Fatal("countdown did not update independently of warning setting") + } + if compact := m.resetControlsLayout(50).warning; !strings.HasPrefix(compact, "⚠ ") { + t.Fatalf("compact spacing: %q", compact) + } + for _, test := range []struct { + duration time.Duration + want string + }{{30 * time.Minute, "<1H"}, {3*time.Hour + 30*time.Minute, "3H"}, {51*time.Hour + 30*time.Minute, "2D 3H"}} { + if got := resetExpiryRemaining(time.Now().Add(test.duration).Unix(), false); got != test.want { + t.Fatalf("duration %v: %q", test.duration, got) + } + } +} + func TestResetWarningHoursOverride(t *testing.T) { m, _ := resetModel() m.snapshot.RateLimits.Primary.UsedPercent = 1