diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 31649e0..941393d 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -16,7 +16,7 @@ "hoursLabel": "ساعات", "minutesLabel": "دقائق", "secondsLabel": "ثوانٍ", - "durationHoursMinutesSeconds": "{hours} ? {minutes} ? {seconds} ?", + "durationHoursMinutesSeconds": "{hours} س {minutes} د {seconds} ث", "@durationHoursMinutesSeconds": { "placeholders": { "hours": {}, @@ -24,20 +24,20 @@ "seconds": {} } }, - "durationMinutesSeconds": "{minutes} ? {seconds} ?", + "durationMinutesSeconds": "{minutes} د {seconds} ث", "@durationMinutesSeconds": { "placeholders": { "minutes": {}, "seconds": {} } }, - "durationMinutesOnly": "{minutes} ?", + "durationMinutesOnly": "{minutes} د", "@durationMinutesOnly": { "placeholders": { "minutes": {} } }, - "durationSecondsOnly": "{seconds} ?", + "durationSecondsOnly": "{seconds} ث", "@durationSecondsOnly": { "placeholders": { "seconds": {} diff --git a/lib/l10n/app_bn.arb b/lib/l10n/app_bn.arb index efd3386..47f7427 100644 --- a/lib/l10n/app_bn.arb +++ b/lib/l10n/app_bn.arb @@ -16,7 +16,7 @@ "hoursLabel": "ঘণ্টা", "minutesLabel": "মিনিট", "secondsLabel": "সেকেন্ড", - "durationHoursMinutesSeconds": "{hours} ? {minutes} ?? {seconds} ??", + "durationHoursMinutesSeconds": "{hours} ঘণ্টা {minutes} মিনিট {seconds} সেকেন্ড", "@durationHoursMinutesSeconds": { "placeholders": { "hours": {}, @@ -24,20 +24,20 @@ "seconds": {} } }, - "durationMinutesSeconds": "{minutes} ?? {seconds} ??", + "durationMinutesSeconds": "{minutes} মিনিট {seconds} সেকেন্ড", "@durationMinutesSeconds": { "placeholders": { "minutes": {}, "seconds": {} } }, - "durationMinutesOnly": "{minutes} ??", + "durationMinutesOnly": "{minutes} মিনিট", "@durationMinutesOnly": { "placeholders": { "minutes": {} } }, - "durationSecondsOnly": "{seconds} ??", + "durationSecondsOnly": "{seconds} সেকেন্ড", "@durationSecondsOnly": { "placeholders": { "seconds": {} diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index e7f3122..83c92b7 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -16,7 +16,7 @@ "hoursLabel": "घंटे", "minutesLabel": "मिनट", "secondsLabel": "सेकंड", - "durationHoursMinutesSeconds": "{hours} ?? {minutes} ?? {seconds} ??", + "durationHoursMinutesSeconds": "{hours} घंटे {minutes} मिनट {seconds} सेकंड", "@durationHoursMinutesSeconds": { "placeholders": { "hours": {}, @@ -24,20 +24,20 @@ "seconds": {} } }, - "durationMinutesSeconds": "{minutes} ?? {seconds} ??", + "durationMinutesSeconds": "{minutes} मिनट {seconds} सेकंड", "@durationMinutesSeconds": { "placeholders": { "minutes": {}, "seconds": {} } }, - "durationMinutesOnly": "{minutes} ??", + "durationMinutesOnly": "{minutes} मिनट", "@durationMinutesOnly": { "placeholders": { "minutes": {} } }, - "durationSecondsOnly": "{seconds} ??", + "durationSecondsOnly": "{seconds} सेकंड", "@durationSecondsOnly": { "placeholders": { "seconds": {} diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index df3b66d..70af09d 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -16,7 +16,7 @@ "hoursLabel": "時間", "minutesLabel": "分", "secondsLabel": "秒", - "durationHoursMinutesSeconds": "{hours}?? {minutes}? {seconds}?", + "durationHoursMinutesSeconds": "{hours}時間{minutes}分{seconds}秒", "@durationHoursMinutesSeconds": { "placeholders": { "hours": {}, @@ -24,20 +24,20 @@ "seconds": {} } }, - "durationMinutesSeconds": "{minutes}? {seconds}?", + "durationMinutesSeconds": "{minutes}分{seconds}秒", "@durationMinutesSeconds": { "placeholders": { "minutes": {}, "seconds": {} } }, - "durationMinutesOnly": "{minutes}?", + "durationMinutesOnly": "{minutes}分", "@durationMinutesOnly": { "placeholders": { "minutes": {} } }, - "durationSecondsOnly": "{seconds}?", + "durationSecondsOnly": "{seconds}秒", "@durationSecondsOnly": { "placeholders": { "seconds": {} diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index d0b96e1..58f5563 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -16,7 +16,7 @@ "hoursLabel": "Часы", "minutesLabel": "Минуты", "secondsLabel": "Секунды", - "durationHoursMinutesSeconds": "{hours} ? {minutes} ??? {seconds} ?", + "durationHoursMinutesSeconds": "{hours} ч {minutes} мин {seconds} сек", "@durationHoursMinutesSeconds": { "placeholders": { "hours": {}, @@ -24,20 +24,20 @@ "seconds": {} } }, - "durationMinutesSeconds": "{minutes} ??? {seconds} ?", + "durationMinutesSeconds": "{minutes} мин {seconds} сек", "@durationMinutesSeconds": { "placeholders": { "minutes": {}, "seconds": {} } }, - "durationMinutesOnly": "{minutes} ???", + "durationMinutesOnly": "{minutes} мин", "@durationMinutesOnly": { "placeholders": { "minutes": {} } }, - "durationSecondsOnly": "{seconds} ?", + "durationSecondsOnly": "{seconds} сек", "@durationSecondsOnly": { "placeholders": { "seconds": {} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 08b2607..504d75b 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -16,7 +16,7 @@ "hoursLabel": "小时", "minutesLabel": "分钟", "secondsLabel": "秒", - "durationHoursMinutesSeconds": "{hours} ?? {minutes} ? {seconds} ?", + "durationHoursMinutesSeconds": "{hours}小时{minutes}分钟{seconds}秒", "@durationHoursMinutesSeconds": { "placeholders": { "hours": {}, @@ -24,20 +24,20 @@ "seconds": {} } }, - "durationMinutesSeconds": "{minutes} ? {seconds} ?", + "durationMinutesSeconds": "{minutes}分钟{seconds}秒", "@durationMinutesSeconds": { "placeholders": { "minutes": {}, "seconds": {} } }, - "durationMinutesOnly": "{minutes} ?", + "durationMinutesOnly": "{minutes}分钟", "@durationMinutesOnly": { "placeholders": { "minutes": {} } }, - "durationSecondsOnly": "{seconds} ?", + "durationSecondsOnly": "{seconds}秒", "@durationSecondsOnly": { "placeholders": { "seconds": {} diff --git a/openspec/changes/archive/2026-07-21-i18n-locale-audit/apply-progress.md b/openspec/changes/archive/2026-07-21-i18n-locale-audit/apply-progress.md new file mode 100644 index 0000000..415ab27 --- /dev/null +++ b/openspec/changes/archive/2026-07-21-i18n-locale-audit/apply-progress.md @@ -0,0 +1,19 @@ +# Apply Progress: i18n-locale-audit +Status: complete (all 13 tasks done, 1 batch) + +## Phase 1: Content Repair — done +All 24 exact-string line replacements applied across the 6 locale files (`app_ar.arb`, `app_bn.arb`, `app_hi.arb`, `app_ja.arb`, `app_ru.arb`, `app_zh.arb`), lines 19/27/34/40 each, matching the pinned spec table byte-exactly. No JSON re-serialization; surgical single-line `Edit` calls only. + +## Phase 2: Static Validation — done +- 2.1 JSON parse: all 6 files parse successfully. +- 2.2 UTF-8 strict decode: no `�` replacement characters. +- 2.3 Diff scope: `git diff --stat` shows exactly 24 insertions / 24 deletions across the 6 files (8 changed lines each); per-file hunk headers confirm only lines 19/27/34/40 touched. +- 2.4 Zero literal `?` in the 24 target values. +- 2.5 Placeholder integrity: extracted `{...}` tokens match expected `{hours}`/`{minutes}`/`{seconds}` set/order per key; `@durationXxx.placeholders` metadata blocks unchanged (implicit — not part of the diff). +- 2.6 Byte-exact match: all 24 values equal their pinned spec-table cell. +- 2.7 `git diff --check`: exit 0, no whitespace/EOL damage. + +## Notes +- Did not run `flutter build`, `flutter gen-l10n`, `dart analyze`, or `flutter analyze` per instructions (they hang in this environment). +- Validation performed with `python` (not `python3`, which is unavailable on this Windows/git-bash environment; `python` resolves to `/c/Python/Python312/python`). +- No files other than the 6 target ARB files were touched. diff --git a/openspec/changes/archive/2026-07-21-i18n-locale-audit/archive-report.md b/openspec/changes/archive/2026-07-21-i18n-locale-audit/archive-report.md new file mode 100644 index 0000000..0a174d7 --- /dev/null +++ b/openspec/changes/archive/2026-07-21-i18n-locale-audit/archive-report.md @@ -0,0 +1,75 @@ +# Archive Report: i18n-locale-audit + +**Change**: i18n-locale-audit +**Status**: Archived +**Archive Location**: `openspec/changes/archive/2026-07-21-i18n-locale-audit/` +**Date Archived**: 2026-07-21 + +## Summary + +The i18n-locale-audit change has been completed, verified (PASS verdict with zero CRITICAL/WARNING/SUGGESTION findings), and archived. This change repaired 24 corrupted sleep-timer duration strings across 6 non-Latin-script locales (ar, bn, hi, ja, ru, zh) by replacing literal ASCII `?` characters with their correct native abbreviations, while preserving all ICU placeholders and file integrity. + +## Artifact Traceability + +All artifacts originate from the SDD pipeline and have been traced to their Engram observations: + +| Artifact | Engram Observation ID | Type | Status | +|----------|----------------------|------|--------| +| Proposal | 1303 | proposal | Complete | +| Exploration | N/A | explore | Complete (file-only) | +| Specification | 1304 | spec | Complete | +| Design | 1305 | design | Complete | +| Tasks | 1306 | tasks | Complete (all 13 tasks checked) | +| Apply Progress | N/A | apply-progress | Complete (file-only) | +| Verify Report | 1308 | verify-report | Complete — PASS verdict | + +All observation IDs recorded for audit trail and traceability. + +## Specs Synced + +| Domain | Action | Details | +|--------|--------|---------| +| sleep-timer-duration-i18n | Created | New spec domain, 4 requirements covering native abbreviations across 6 locales (ja, zh, ru, ar, hi, bn). Added to `openspec/specs/sleep-timer-duration-i18n/spec.md`. | + +## Archive Contents + +- proposal.md ✅ — Proposal: Repair corrupted sleep-timer duration abbreviations in 6 ARB locales +- explore.md ✅ — Exploration: i18n locale audit (13 ARB locales) +- design.md ✅ — Design: Repair corrupted sleep-timer duration abbreviations in 6 ARB locales +- tasks.md ✅ — Tasks: all 13 tasks complete (7 Phase 1 content repairs + 7 Phase 2 static validation checks) +- apply-progress.md ✅ — Apply Progress: complete (all 24 exact-string line replacements applied) +- verify-report.md ✅ — Verify Report: PASS (all requirements met, all 24 values byte-identical to spec) +- specs/sleep-timer-duration-i18n/spec.md ✅ — Sleep Timer Duration I18n Specification + +## Source of Truth Updated + +The following specs now reflect the new behavior and are locked into the main specifications tree: + +- `openspec/specs/sleep-timer-duration-i18n/spec.md` — NEW domain, defining correct native abbreviations for 4 ICU-templated duration strings across 6 locales with comprehensive scenarios and requirements. + +## Implementation Summary + +**Changed lines**: 24 insertions + 24 deletions (exact-string line replacement, no JSON re-serialization) +**Files modified**: lib/l10n/app_ar.arb, app_bn.arb, app_hi.arb, app_ja.arb, app_ru.arb, app_zh.arb (4 keys per file, lines 19/27/34/40) +**Quality gate**: Verify-report PASS with zero critical/warning/suggestion findings + +All 24 corrupted duration string values have been replaced with their correct byte-exact pinned strings: +- ja: compact no-space notation (時間/分/秒) +- zh: compact no-space notation (小时/分钟/秒) +- ru: short abbreviation with leading space (ч/мин/сек) +- ar: short abbreviation with leading space (س/د/ث) +- hi: full label words, space-separated (घंटे/मिनट/सेकंड) +- bn: full label words, space-separated (ঘণ্টা/মিনिট/সেকেন्ড) + +ICU placeholders `{hours}`, `{minutes}`, `{seconds}` remain unchanged and correctly named. All other keys (386 per file) and metadata blocks remain byte-unchanged. + +## SDD Cycle Complete + +The change has been fully planned (proposal + explore), specified (spec), designed (design), implemented (apply), verified (verify-report PASS), and archived. Ready for the next change. + +## Additional Notes + +- Task completion gate: PASS — all 13 tasks marked as complete in tasks.md with supporting evidence in apply-progress.md and verify-report.md. +- Verify gate: PASS — zero CRITICAL/WARNING/SUGGESTION findings. Spec requirements fully met. +- No stale checkboxes found; archive-time reconciliation not required. +- Spec merge: delta spec copied directly to new main domain (no merge with existing spec required, as this is a new domain). diff --git a/openspec/changes/archive/2026-07-21-i18n-locale-audit/design.md b/openspec/changes/archive/2026-07-21-i18n-locale-audit/design.md new file mode 100644 index 0000000..d1f088e --- /dev/null +++ b/openspec/changes/archive/2026-07-21-i18n-locale-audit/design.md @@ -0,0 +1,82 @@ +# Design: Repair corrupted sleep-timer duration abbreviations in 6 ARB locales + +## Technical Approach + +Content-only data repair. For each of the 6 corrupted ARB files (`app_{ar,bn,hi,ja,ru,zh}.arb`), replace the value of exactly 4 duration keys (`durationHoursMinutesSeconds`, `durationMinutesSeconds`, `durationMinutesOnly`, `durationSecondsOnly`, physical lines 19/27/34/40) with the byte-exact pinned strings from the spec table. No Dart, no CI, no generated-file changes. Validation is static-only (tooling hangs here). Maps directly to the proposal's "Approach" and the spec's four requirements. + +## Architecture Decisions + +### Decision: Full-value line replacement (not `?`-glyph swap) + +| Option | Tradeoff | Decision | +|---|---|---| +| Swap only `?` → abbrev in place | Fails for ja/zh: pinned strings also collapse inter-unit spaces (`{hours}?? {minutes}? {seconds}?` → `{hours}時間{minutes}分{seconds}秒`), so a glyph-only swap yields wrong output | Rejected | +| Replace the entire quoted value on each target line | Spec pins the exact final string per cell; one deterministic edit per line, no per-locale spacing logic | **Chosen** | + +**Rationale**: The spec table is the single source of truth. The edit unit is the whole line `"": "",`; `old_string` = current corrupted line, `new_string` = pinned line. Surrounding bytes (and the other ~386 keys) are never touched. + +### Decision: Surgical single-line edits, no JSON re-serialization + +| Option | Tradeoff | Decision | +|---|---|---| +| Parse → mutate → re-dump JSON | Reflows/re-escapes all ~386 keys, reorders/renormalizes unicode, huge noisy diff, risk to untouched cells | Rejected | +| Exact-string edit of only the 4 target lines/file | Diff is exactly 4 lines/file (24 total); all other bytes provably identical | **Chosen** | + +### Decision: Static validation instead of flutter/dart tooling + +`flutter build`, `gen-l10n`, `dart analyze`, `flutter analyze` all hang in this environment and MUST NOT run. A short Python one-off (stdlib `json`, byte reads) gives equivalent safety for a content-only change: JSON well-formedness, UTF-8 integrity, diff scope, residual-`?`, and placeholder integrity. + +## Data Flow + + spec table (24 pinned cells) + │ authoritative source + ▼ + Edit tool ──(4 exact-string line edits/file)──▶ app_{locale}.arb ×6 + │ + ▼ + static validator (Python + git diff) + │ pass/fail gate + ▼ + single commit (6 files) + +## File Changes + +| File | Action | Description | +|------|--------|-------------| +| `lib/l10n/app_ar.arb` | Modify | Lines 19/27/34/40: values → pinned `ar` cells | +| `lib/l10n/app_bn.arb` | Modify | Lines 19/27/34/40: values → pinned `bn` cells | +| `lib/l10n/app_hi.arb` | Modify | Lines 19/27/34/40: values → pinned `hi` cells | +| `lib/l10n/app_ja.arb` | Modify | Lines 19/27/34/40: values → pinned `ja` cells (spaces collapse) | +| `lib/l10n/app_ru.arb` | Modify | Lines 19/27/34/40: values → pinned `ru` cells | +| `lib/l10n/app_zh.arb` | Modify | Lines 19/27/34/40: values → pinned `zh` cells (spaces collapse) | + +**Explicitly out of scope (unchanged):** `lib/l10n/gen/app_localizations.dart` (generated; do NOT hand-edit — ICU placeholder shape `{hours}/{minutes}/{seconds}` is identical pre/post, so no regeneration is needed for these keys to render correctly), `app_localizations_ext.dart`, any CI/workflow file, and the `@durationXxx.placeholders` metadata blocks. + +## Interfaces / Contracts + +No interface change. ICU placeholder names, casing, and count are invariant: `{hours}`, `{minutes}`, `{seconds}`. The generated `AppLocalizations` method signatures are unaffected. + +## Testing Strategy + +| Layer | What to Test | Approach | +|-------|-------------|----------| +| Static | All 6 files parse as JSON | Python `json.load` each file | +| Static | UTF-8 integrity, no replacement char | Read bytes, `decode('utf-8')` strict; assert no `�` | +| Static | Diff scope = only the 4 lines/file | `git diff --stat` / per-line diff; assert exactly 24 changed lines total, additions==deletions | +| Static | Zero literal `?` in 24 target values | Assert `'?' not in value` for each target key | +| Static | Placeholder integrity | Regex-extract `{...}` tokens from each fixed value; assert equals expected set/order per key; `@`-metadata blocks byte-unchanged | +| Static | Byte-exact match to spec | Assert each fixed value `==` its pinned spec cell | + +`git diff --check` additionally guards against whitespace/EOL damage. No runtime/widget test (no code path changes; tooling forbidden). + +## Threat Matrix + +N/A — no routing, shell, subprocess, VCS/PR automation, executable-file classification, or process-integration boundary. Pure ARB content edit. + +## Migration / Rollout + +No migration. Single commit touching only 6 ARB files; `git revert` fully restores prior state with zero cross-dependencies. + +## Open Questions + +- [ ] None — the spec pins all 24 target cells byte-exactly; no content or architectural decision remains. diff --git a/openspec/changes/archive/2026-07-21-i18n-locale-audit/explore.md b/openspec/changes/archive/2026-07-21-i18n-locale-audit/explore.md new file mode 100644 index 0000000..f921639 --- /dev/null +++ b/openspec/changes/archive/2026-07-21-i18n-locale-audit/explore.md @@ -0,0 +1,65 @@ +# Exploration: i18n locale audit (13 ARB locales) + +## Current State +l10n.yaml: arb-dir=lib/l10n, template-arb-file=app_es.arb, output-dir=lib/l10n/gen, output-class=AppLocalizations. +13 ARB files present: app_{ar,bn,de,en,es,fr,hi,id,it,ja,pt,ru,zh}.arb. +lib/l10n/gen/app_localizations.dart (generated, checked into git, not gitignored) declares supportedLocales with the same 13 Locale() entries in the same order. +lib/app.dart:83 wires `supportedLocales: AppLocalizations.supportedLocales` into MaterialApp — inventory matches exactly, no drift. +A second, hand-maintained translation source exists outside the ARB pipeline: lib/l10n/app_localizations_ext.dart (PluriAppLocalizationsExt) — per-locale Map for weekdayLong/monthName/dateTimeSentence, covering all 13 locale codes with `values['en']` fallback if a code is missing. + +## Key Parity (template = app_es.arb) +Verified via `grep -c '^ "[a-zA-Z][a-zA-Z0-9]*":'` (excludes "@key" ICU metadata and "@@locale") on all 13 files: every single file — es included — has exactly 387 top-level translation keys. No missing keys, no extra/orphaned keys, no empty values found in sampled content. + +Previously-flagged line-count mismatch (es=637 lines vs all others=674 lines, per prior audit note) — RE-VERIFIED, confirmed as a FALSE ALARM / pure JSON-formatting difference, NOT a key/content defect. Root cause: app_es.arb compacts several `"@key": {"placeholders": {...}}` ICU metadata blocks onto a single line (e.g. es:471 `"@miniPlayerOpenLabel": {"placeholders": {"stationName": {}}},`), while all 12 other locale files expand the identical metadata object across 4-5 lines. This inflates their line count by ~37 with zero actual key/content difference (confirmed by the matching 387-key count above). + +## Untranslated Content +Project-memory-flagged "SEARCH pilot block" issue (app_ja.arb/app_zh.arb/app_ru.arb allegedly still using English fallback for search* keys) — RE-VERIFIED AND RESOLVED. All three files now have genuine native translations: ja "電波を探す"/"ラジオ・オリゾンテ、ジャズ、ニュース..."; zh "搜索信号"/"地平线电台、爵士、新闻..."; ru "Поиск сигнала"/"Радио Горизонт, джаз, новости...". +Broad regex scan for 3+ consecutive Latin words inside string values, across app_{ar,bn,hi,ja,ru,zh}.arb: ZERO matches — no leftover English/copy-paste blocks found in any non-Latin-script locale. + +## Encoding Integrity — CRITICAL FINDING +- No U+FFFD replacement characters in any of the 13 ARB files. +- No double-encoded UTF-8 mojibake (é-style) in any of the 13 ARB files. +- Accented Latin (es/fr/de/pt/it) and non-Latin scripts (ja/zh/ar/hi/bn/ru) render correctly and intact across hundreds of sampled lines per file. +- HOWEVER: literal ASCII "?" (U+003F) characters — genuine data-loss corruption, not mojibake — replace native duration-unit abbreviations in exactly 4 keys × 6 non-Latin-script locale files (24 corrupted lines total, always at file lines 19/27/34/40): + - Keys: durationHoursMinutesSeconds, durationMinutesSeconds, durationMinutesOnly, durationSecondsOnly. + - lib/l10n/app_ar.arb:19,27,34,40 e.g. `"{hours} ? {minutes} ? {seconds} ?"` + - lib/l10n/app_bn.arb:19,27,34,40 e.g. `"{hours} ? {minutes} ?? {seconds} ??"` + - lib/l10n/app_hi.arb:19,27,34,40 e.g. `"{hours} ?? {minutes} ?? {seconds} ??"` + - lib/l10n/app_ja.arb:19,27,34,40 e.g. `"{hours}?? {minutes}? {seconds}?"` + - lib/l10n/app_ru.arb:19,27,34,40 e.g. `"{hours} ? {minutes} ??? {seconds} ?"` + - lib/l10n/app_zh.arb:19,27,34,40 e.g. `"{hours} ?? {minutes} ? {seconds} ?"` + - Every other string in these 6 files (383 other keys each) is correctly translated — corruption is isolated to exactly these 4 keys, consistent with one past edit/tool that wrote these 4 lines through a non-UTF-8-safe path, not a whole-file encoding problem. + - Latin-script locales unaffected (their duration abbreviations are plain ASCII: en h/min/s, de Std/Min/Sek, fr/it/pt h/min/s, id jam/mnt/dtk — nothing to corrupt). + - ICU placeholder tokens {hours}/{minutes}/{seconds} are still present and correctly named in the corrupted lines — no placeholder/crash risk, purely visual content corruption. + - User impact: these 4 strings feed `_formatearDuracionTimer()` in lib/app.dart:492-512, rendering the sleep-timer countdown/duration in the "Timer de sueño" bottom sheet (`_mostrarTimerDialog`, lib/app.dart). Users on ar/bn/hi/ja/ru/zh locales see literal "?" instead of their language's h/min/s abbreviation. + - Not caught by CI: JSON stays valid, ICU placeholders intact, so `flutter analyze`/gen-l10n do not fail. + +## ICU/Placeholder Consistency +Sampled ~35 placeholder/plural-bearing keys across all 13 locales (duration family, skipCurrentAlarmExecution, languageUpdated, equalizer*, preferredStation*, backup*, recordingsMaxSize*, favoriteGroups*, favorites*, stationsCount, nearYouInCountry, alarmsCount, activeAlarmsWithoutNextSubtitle, alarmFadeIn*, alarmNextExecution, alarmSkipped*, alarmVacation*, alarmSnoozeOptionLabel, androidReliabilityStatus, miniPlayerOpenLabel, stationSemanticLabel, equalizerBand*, radio*Error, alarmSchedule*, localMusicFolderSaveError, advancedEqDevicePresetLabel, *Countdown, stationCount plural). All placeholder tokens present/correctly named everywhere sampled. stationCount plural forms correctly vary per CLDR: es/en use =1/other; ja/zh use only "other" (correct, no plural distinction in these languages); ru correctly uses one/few/other. No mismatches found. + +## Remaining Hardcoded Literals +Broad regex scan of lib/pantallas/**/*.dart and lib/widgets/**/*.dart for Text('...')/Text("...") literals and label:/title:/tooltip:/hintText:/content: literal assignments found only ONE match: lib/widgets/pluri_onboarding_dialog.dart:115 `title: Text('v${nota.version}')` — a version-number prefix, technical format, not translatable (consistent with documented "technical formats" exclusion). No other hardcoded user-facing literal found — the June 2026 i18n migration appears complete across sampled screens/widgets. +lib/pantallas/pantalla_ajustes.dart:497-509 hardcodes 13 language-picker autonyms (English/Español/中文/हिन्दी/العربية/Português/Français/Русский/Deutsch/日本語/Bahasa Indonesia/বাংলা/Italiano) — documented intentional autonym exception, not a gap. +Structural risk (not currently broken): lib/l10n/app_localizations_ext.dart is a hand-maintained Map per locale (not validated by gen-l10n/CI) — a future locale added to supportedLocales without updating this file would silently fall back to English for weekday/month/date-sentence text. + +## CI Relevance +.gitea/workflows/build.yml job `analizar` runs `flutter analyze --no-fatal-infos --no-fatal-warnings` (line 31) — warnings do not fail the build. `flutter build apk`/`appbundle` (lines 110/113) implicitly regenerate AppLocalizations from ARB but only fail on invalid JSON/ICU syntax, not on wrong/corrupted string content. The "?" duration-key corruption would ship silently past this CI today. + +## Severity-Ranked Findings +1. [HIGH] Broken encoding/content — literal "?" replacing native duration-unit abbreviations in durationHoursMinutesSeconds/durationMinutesSeconds/durationMinutesOnly/durationSecondsOnly, in app_ar.arb, app_bn.arb, app_hi.arb, app_ja.arb, app_ru.arb, app_zh.arb (lines 19/27/34/40 each). User-visible in sleep-timer feature; not caught by CI. +2. [MEDIUM] Structural risk in lib/l10n/app_localizations_ext.dart — hand-maintained, unvalidated by tooling; latent risk for future locale additions (not currently broken, all 13 current locales present). +3. [LOW/INFO — confirmed non-issue] es=637 vs others=674 line-count disparity is pure JSON-formatting difference (compacted vs expanded @metadata), zero key/content impact. +4. [LOW/INFO — confirmed RESOLVED] Prior "SEARCH pilot English fallback" in ja/zh/ru is fixed; genuinely translated now. +5. [NONE FOUND] No missing/extra/empty keys anywhere; no placeholder/ICU mismatches; no U+FFFD or mojibake; no remaining hardcoded literals beyond one benign technical-format false positive. + +## Tooling/Environment Limitation (risk for orchestrator) +This explore-phase execution had access only to Read/Grep/Glob/WebFetch/WebSearch/mem_save/codegraph_explore — no Bash/git tool and no file-write (Write/Edit) tool were available. +- Could NOT run `git fetch`/`git pull` to check for latest remote changes (part of the user's original request) — needs to happen in a phase/agent with git access before any push. +- Could NOT write `openspec/changes/i18n-locale-audit/explore.md` to disk (hybrid artifact-store mode requires both Engram + filesystem; only the Engram half could be completed here). The orchestrator or a phase with file-write access should persist this content to that path. +- Did not run `flutter build`/`flutter gen-l10n`/`dart analyze`/`flutter analyze` per hard environment constraint (these hang in this sandbox) — all findings are from static reads/grep only; full JSON schema/ICU validation via the real Dart intl tooling was not performed (structural sampling strongly suggests valid JSON/ICU throughout, but this is not a substitute for the real generator). + +## Recommendation +Proceed to sdd-propose for a fix scoped narrowly to: (a) replace the 24 corrupted "?" duration-format strings in the 6 affected ARB files with correct native translations, sourced from a native speaker or a verified translation reference (do not machine-translate blindly given the project's "real native translations" quality bar); (b) optionally add a lightweight CI/pre-commit check that greps ARB files for stray literal "?" adjacent to placeholders, to prevent silent recurrence; (c) note the app_localizations_ext.dart structural risk for a future hardening task (not urgent, no current locale is missing). + +## Ready for Proposal +Yes — findings are concrete, file/line-cited, and scoped small enough (4 keys × 6 files) for a low-risk, low-line-count fix PR. diff --git a/openspec/changes/archive/2026-07-21-i18n-locale-audit/proposal.md b/openspec/changes/archive/2026-07-21-i18n-locale-audit/proposal.md new file mode 100644 index 0000000..df30f7a --- /dev/null +++ b/openspec/changes/archive/2026-07-21-i18n-locale-audit/proposal.md @@ -0,0 +1,50 @@ +# Proposal: Repair corrupted sleep-timer duration abbreviations in 6 ARB locales + +## Intent +Six non-Latin-script locales ship literal ASCII `?`/`??`/`???` where the native hour/minute/second abbreviation belongs, in the 4 sleep-timer duration keys. Users on ar/bn/hi/ja/ru/zh see `1 ? 30 ? 0 ?` instead of e.g. `1時間 30分 0秒` in the "Timer de sueño" bottom sheet (`_formatearDuracionTimer`, lib/app.dart:492-512). JSON/ICU stay valid, so CI (`flutter analyze`) never catches it — a silent, user-visible quality defect from a prior non-UTF-8-safe edit. Fix now: the corruption is isolated, verified, and cheap to repair correctly. + +## Scope + +### In Scope +- Repair 24 corrupted strings = 4 keys (`durationHoursMinutesSeconds`, `durationMinutesSeconds`, `durationMinutesOnly`, `durationSecondsOnly`) × 6 files: `app_ar.arb`, `app_bn.arb`, `app_hi.arb`, `app_ja.arb`, `app_ru.arb`, `app_zh.arb` (lines 19/27/34/40 each). +- Replace each `?` with the genuine native short-form unit abbreviation for that locale, keeping ICU placeholders `{hours}`/`{minutes}`/`{seconds}` intact and correctly named. +- Static validation only: JSON-parse + UTF-8 byte integrity + grep for residual `?`-adjacent-placeholder. + +### Out of Scope (future-work notes only) +- No refactor of `lib/l10n/app_localizations_ext.dart` (hand-maintained date map — latent structural risk, not broken; separate hardening task). +- No new CI/pre-commit ARB lint (recommended but deferred; separate change). +- No broader i18n rework, no other keys/locales, no re-formatting of the 386 untouched keys per file. + +## Capabilities + +### New Capabilities +None. + +### Modified Capabilities +None — data-only content repair; no requirement/behavior change, no generated-code signature change (ICU shape identical). + +## Approach +Per-locale, source the correct native abbreviation from the existing verified full labels already present in each file (`hoursLabel`/`minutesLabel`/`secondsLabel`, lines 16-18) plus locale conventions (ja 時間/分/秒, zh 小时/分/秒, ru ч/мин/с, ar/hi/bn native short forms). Edit only the 4 corrupted lines per file via exact-string replacement; preserve surrounding bytes and UTF-8 encoding. Never run flutter/dart tooling (hangs here). + +## Affected Areas +| Area | Impact | Description | +|------|--------|-------------| +| `lib/l10n/app_{ar,bn,hi,ja,ru,zh}.arb` | Modified | 4 duration lines each repaired | + +## Risks +| Risk | Likelihood | Mitigation | +|------|------------|------------| +| Wrong/non-native abbreviation glyph | Med | Anchor to in-file labels + verified references; spec pins exact strings | +| Re-encoding side effects to untouched keys | Low | Exact-string edits; post-edit UTF-8 + JSON diff check | +| Generated `app_localizations.dart` drift | Low | ICU shape unchanged; content-only | + +## Rollback Plan +Single commit touching only 6 ARB files; `git revert` restores prior state with zero cross-dependencies. + +## Dependencies +- Native/reference-verified abbreviation set for ar/bn/hi/ja/ru/zh (resolved in sdd-spec). + +## Success Criteria +- [ ] Zero literal `?` remaining in the 24 target strings; placeholders intact. +- [ ] Each locale renders its native h/min/s abbreviation. +- [ ] All 6 files parse as valid UTF-8 JSON; 386 other keys per file byte-unchanged. diff --git a/openspec/changes/archive/2026-07-21-i18n-locale-audit/specs/sleep-timer-duration-i18n/spec.md b/openspec/changes/archive/2026-07-21-i18n-locale-audit/specs/sleep-timer-duration-i18n/spec.md new file mode 100644 index 0000000..2d34a0d --- /dev/null +++ b/openspec/changes/archive/2026-07-21-i18n-locale-audit/specs/sleep-timer-duration-i18n/spec.md @@ -0,0 +1,60 @@ +# Sleep Timer Duration I18n Specification + +## Purpose + +Defines the correct native-language content for the 4 ICU-templated sleep-timer duration strings (`durationHoursMinutesSeconds`, `durationMinutesSeconds`, `durationMinutesOnly`, `durationSecondsOnly`) across the 6 currently corrupted ARB locales: `ar`, `bn`, `hi`, `ja`, `ru`, `zh`. This is a new spec domain — no prior spec covered these strings. + +## Requirements + +### Requirement: Native short-form duration abbreviations +The system MUST render, for each of the 6 locales, the exact pinned native hour/minute/second abbreviation text below in place of every literal `?`/`??`/`???` corruption, with no other content in the 24 target strings altered. + +| Locale | `durationHoursMinutesSeconds` | `durationMinutesSeconds` | `durationMinutesOnly` | `durationSecondsOnly` | +|---|---|---|---|---| +| ja | `{hours}時間{minutes}分{seconds}秒` | `{minutes}分{seconds}秒` | `{minutes}分` | `{seconds}秒` | +| zh | `{hours}小时{minutes}分钟{seconds}秒` | `{minutes}分钟{seconds}秒` | `{minutes}分钟` | `{seconds}秒` | +| ru | `{hours} ч {minutes} мин {seconds} сек` | `{minutes} мин {seconds} сек` | `{minutes} мин` | `{seconds} сек` | +| ar | `{hours} س {minutes} د {seconds} ث` | `{minutes} د {seconds} ث` | `{minutes} د` | `{seconds} ث` | +| hi | `{hours} घंटे {minutes} मिनट {seconds} सेकंड` | `{minutes} मिनट {seconds} सेकंड` | `{minutes} मिनट` | `{seconds} सेकंड` | +| bn | `{hours} ঘণ্টা {minutes} মিনিট {seconds} সেকেন্ড` | `{minutes} মিনिট {seconds} সেকেন্ড` | `{minutes} মিনিট` | `{seconds} সেকেন्ड` | + +Rationale: ja/zh use conventional no-space compact duration notation (e.g. `1時間30分15秒`, `2小时30分钟15秒`); ru/ar use the OS-standard single/short abbreviation with a leading space per unit (`ч`/`мин`/`сек`, `س`/`د`/`ث`); hi/bn lack a reliable standard short form, so the full native label words (already verified at `hoursLabel`/`minutesLabel`/`secondsLabel`) are reused verbatim, space-separated. + +#### Scenario: Full duration display renders native units +- GIVEN a user on locale `ja` opens the sleep-timer bottom sheet with a duration of 1 hour, 30 minutes, 15 seconds +- WHEN `durationHoursMinutesSeconds` is resolved +- THEN the rendered text is `1時間30分15秒` with no literal `?` present + +#### Scenario: Each of the 6 locales matches its pinned string exactly +- GIVEN any of `ar`, `bn`, `hi`, `ja`, `ru`, `zh` +- WHEN any of the 4 duration keys is read from its ARB file after the fix +- THEN its value is byte-identical to the corresponding cell in the pinned table above + +### Requirement: ICU placeholder integrity +The system MUST preserve the ICU placeholder names `{hours}`, `{minutes}`, `{seconds}` unchanged in name, casing, and count across all 24 target strings, and MUST NOT alter the associated `@durationXxx.placeholders` metadata blocks. + +#### Scenario: Placeholder names unchanged after fix +- GIVEN the pre-fix and post-fix versions of `durationMinutesSeconds` in `app_ru.arb` +- WHEN the placeholder tokens are extracted from each +- THEN both extractions yield exactly `{minutes}` and `{seconds}`, in that order + +### Requirement: File integrity preserved +The system MUST keep each of the 6 ARB files valid UTF-8 encoded JSON after the fix, and MUST leave every key other than the 4 duration keys per file byte-for-byte unchanged (all other ~386 keys, including `hoursLabel`/`minutesLabel`/`secondsLabel`). + +#### Scenario: File remains valid JSON and UTF-8 +- GIVEN `app_ar.arb` after the fix is applied +- WHEN the file is parsed as JSON and decoded as UTF-8 +- THEN parsing succeeds with no encoding errors + +#### Scenario: Unrelated keys are untouched +- GIVEN `app_zh.arb` before and after the fix +- WHEN every key except the 4 duration keys is diffed +- THEN the diff is empty + +### Requirement: No residual corruption markers +The system MUST NOT contain any literal `?` character within the value of the 24 target strings after the fix. + +#### Scenario: No stray `?` remains +- GIVEN all 6 fixed ARB files +- WHEN the 24 target string values are scanned for the character `?` +- THEN zero occurrences are found diff --git a/openspec/changes/archive/2026-07-21-i18n-locale-audit/tasks.md b/openspec/changes/archive/2026-07-21-i18n-locale-audit/tasks.md new file mode 100644 index 0000000..dc66423 --- /dev/null +++ b/openspec/changes/archive/2026-07-21-i18n-locale-audit/tasks.md @@ -0,0 +1,46 @@ +# Tasks: Repair corrupted sleep-timer duration abbreviations in 6 ARB locales + +## Review Workload Forecast + +| Field | Value | +|-------|-------| +| Estimated changed lines | 24-48 (24 lines modified, 24 add + 24 del per `git diff --stat`) | +| 400-line budget risk | Low | +| Chained PRs recommended | No | +| Suggested split | Single PR | +| Delivery strategy | single-pr | +| Chain strategy | pending | + +Decision needed before apply: No +Chained PRs recommended: No +Chain strategy: pending +400-line budget risk: Low + +### Suggested Work Units + +| Unit | Goal | Likely PR | Focused test command | Runtime harness | Rollback boundary | +|------|------|-----------|----------------------|-----------------|-------------------| +| 1 | Fix all 24 pinned strings across 6 ARB files + static validation | PR 1 (single) | `python scripts/validate_arb_fix.py` (JSON/UTF-8/diff-scope/placeholder/`?`-scan checks) | N/A — static content-only change, no runtime code path affected | `git revert` on the single commit restores all 6 files atomically | + +## Phase 1: Content Repair (per-locale edits) + +- [x] 1.1 Edit `lib/l10n/app_ar.arb` lines 19/27/34/40: replace `durationHoursMinutesSeconds`/`durationMinutesSeconds`/`durationMinutesOnly`/`durationSecondsOnly` values with pinned `ar` cells (spec Requirement: Native short-form duration abbreviations) +- [x] 1.2 Edit `lib/l10n/app_bn.arb` lines 19/27/34/40: replace same 4 keys with pinned `bn` cells +- [x] 1.3 Edit `lib/l10n/app_hi.arb` lines 19/27/34/40: replace same 4 keys with pinned `hi` cells +- [x] 1.4 Edit `lib/l10n/app_ja.arb` lines 19/27/34/40: replace same 4 keys with pinned `ja` cells (no-space compact notation) +- [x] 1.5 Edit `lib/l10n/app_ru.arb` lines 19/27/34/40: replace same 4 keys with pinned `ru` cells (leading-space short abbreviations) +- [x] 1.6 Edit `lib/l10n/app_zh.arb` lines 19/27/34/40: replace same 4 keys with pinned `zh` cells (no-space compact notation) + +Tasks 1.1-1.6 are file-independent and may run in parallel; each is a self-contained exact-string replacement per the design's pinned table, touching no other key. + +## Phase 2: Static Validation (sequential, depends on Phase 1) + +- [x] 2.1 Parse all 6 `app_{ar,bn,hi,ja,ru,zh}.arb` files as JSON (`json.load`); assert success (spec: File integrity preserved) +- [x] 2.2 Decode each file's bytes as strict UTF-8; assert no `�` replacement characters present +- [x] 2.3 Run `git diff --stat` and per-line diff; assert exactly 24 changed lines total across the 6 files, additions == deletions, and no key outside the 4 duration keys per file differs (spec: Unrelated keys are untouched) +- [x] 2.4 Scan the 24 target string values for literal `?`; assert zero occurrences (spec: No residual corruption markers) +- [x] 2.5 Regex-extract `{...}` placeholder tokens from each of the 24 fixed values; assert each matches expected `{hours}`/`{minutes}`/`{seconds}` set/order and that `@durationXxx.placeholders` metadata blocks are byte-unchanged (spec: ICU placeholder integrity) +- [x] 2.6 Assert each of the 24 fixed values is byte-identical to its pinned spec-table cell (spec: Each of the 6 locales matches its pinned string exactly) +- [x] 2.7 Run `git diff --check` to confirm no whitespace/EOL damage + +Do NOT run `flutter build`, `flutter gen-l10n`, `dart analyze`, or `flutter analyze` — these hang in this environment. diff --git a/openspec/changes/archive/2026-07-21-i18n-locale-audit/verify-report.md b/openspec/changes/archive/2026-07-21-i18n-locale-audit/verify-report.md new file mode 100644 index 0000000..badc662 --- /dev/null +++ b/openspec/changes/archive/2026-07-21-i18n-locale-audit/verify-report.md @@ -0,0 +1,36 @@ +# Verify Report: i18n-locale-audit + +Independently re-derived (not trusting apply-progress.md self-report). All checks re-run against current working-tree file content and `git diff`. + +## Per-requirement verdicts + +### Requirement: Native short-form duration abbreviations +**PASS.** Read all 4 duration keys (lines 19/27/34/40) in all 6 files (`app_ar.arb`, `app_bn.arb`, `app_hi.arb`, `app_ja.arb`, `app_ru.arb`, `app_zh.arb`) and compared byte-for-byte against the pinned spec table. All 24 values match exactly: +- ja: `{hours}時間{minutes}分{seconds}秒` / `{minutes}分{seconds}秒` / `{minutes}分` / `{seconds}秒` — match +- zh: `{hours}小时{minutes}分钟{seconds}秒` / `{minutes}分钟{seconds}秒` / `{minutes}分钟` / `{seconds}秒` — match +- ru: `{hours} ч {minutes} мин {seconds} сек` / `{minutes} мин {seconds} сек` / `{minutes} мин` / `{seconds} сек` — match +- ar: `{hours} س {minutes} د {seconds} ث` / `{minutes} د {seconds} ث` / `{minutes} د` / `{seconds} ث` — match +- hi: `{hours} घंटे {minutes} मिनट {seconds} सेकंड` / `{minutes} मिनट {seconds} सेकंड` / `{minutes} मिनट` / `{seconds} सेकंड` — match +- bn: `{hours} ঘণ্টা {minutes} মিনিট {seconds} সেকেন্ড` / `{minutes} মিনিট {seconds} সেকেন্ড` / `{minutes} মিনিট` / `{seconds} সেকেন্ড` — match + +### Requirement: ICU placeholder integrity +**PASS.** `{hours}`, `{minutes}`, `{seconds}` present, correctly named/cased, correct count in every applicable string. `git diff` confirms all `@durationXxx.placeholders` metadata blocks are unchanged (context lines only, no `+`/`-` in those blocks in any of the 6 diffs). + +### Requirement: File integrity preserved +**PASS.** +- All 6 files parse successfully via `json.loads` after strict UTF-8 decode (independently re-run with Python, not trusting the prior report). +- `git diff --stat` shows exactly `24 insertions(+), 24 deletions(-)` across the 6 files (8 changed lines per file = 4 keys × 1 line each, old+new). +- Full `git diff` inspected line-by-line: only the 4 duration-key value lines changed per file; all `@durationXxx.placeholders` blocks and all surrounding keys (`hoursLabel`, `minutesLabel`, `secondsLabel`, etc.) are untouched context lines. + +### Requirement: No residual corruption markers +**PASS.** Independently scanned all 24 target values (via the `git diff` `+` lines and a direct Python scan of the parsed JSON values) — zero literal `?` characters remain in any of the 24 target strings. (The pre-fix `?`/`??`/`???` corruption is visible only on the `-` diff lines, confirming the fix replaced it.) + +## Scope check +`git status --porcelain` shows only the 6 target ARB files modified (`M lib/l10n/app_ar.arb`, `app_bn.arb`, `app_hi.arb`, `app_ja.arb`, `app_ru.arb`, `app_zh.arb`). The only other entry is `?? openspec/changes/i18n-locale-audit/`, which is the untracked SDD change-artifact directory itself (spec/design/tasks/proposal/apply-progress/this report) — not a code change. No other files in the working tree were touched. + +## Tooling note +Per instructions, did not run `flutter build`, `flutter gen-l10n`, `dart analyze`, or `flutter analyze`. Verification used direct file reads, `git diff`/`git status`, and a standalone Python JSON/UTF-8/`?`-scan check. + +## Overall verdict: PASS + +No CRITICAL, WARNING, or SUGGESTION findings. Implementation matches spec exactly; tasks.md items are all legitimately complete; apply-progress.md's self-report is accurate. diff --git a/openspec/specs/sleep-timer-duration-i18n/spec.md b/openspec/specs/sleep-timer-duration-i18n/spec.md new file mode 100644 index 0000000..4b97611 --- /dev/null +++ b/openspec/specs/sleep-timer-duration-i18n/spec.md @@ -0,0 +1,60 @@ +# Sleep Timer Duration I18n Specification + +## Purpose + +Defines the correct native-language content for the 4 ICU-templated sleep-timer duration strings (`durationHoursMinutesSeconds`, `durationMinutesSeconds`, `durationMinutesOnly`, `durationSecondsOnly`) across the 6 currently corrupted ARB locales: `ar`, `bn`, `hi`, `ja`, `ru`, `zh`. This is a new spec domain — no prior spec covered these strings. + +## Requirements + +### Requirement: Native short-form duration abbreviations +The system MUST render, for each of the 6 locales, the exact pinned native hour/minute/second abbreviation text below in place of every literal `?`/`??`/`???` corruption, with no other content in the 24 target strings altered. + +| Locale | `durationHoursMinutesSeconds` | `durationMinutesSeconds` | `durationMinutesOnly` | `durationSecondsOnly` | +|---|---|---|---|---| +| ja | `{hours}時間{minutes}分{seconds}秒` | `{minutes}分{seconds}秒` | `{minutes}分` | `{seconds}秒` | +| zh | `{hours}小时{minutes}分钟{seconds}秒` | `{minutes}分钟{seconds}秒` | `{minutes}分钟` | `{seconds}秒` | +| ru | `{hours} ч {minutes} мин {seconds} сек` | `{minutes} мин {seconds} сек` | `{minutes} мин` | `{seconds} сек` | +| ar | `{hours} س {minutes} د {seconds} ث` | `{minutes} د {seconds} ث` | `{minutes} د` | `{seconds} ث` | +| hi | `{hours} घंटे {minutes} मिनट {seconds} सेकंड` | `{minutes} मिनट {seconds} सेकंड` | `{minutes} मिनट` | `{seconds} सेकंड` | +| bn | `{hours} ঘণ্টা {minutes} মিনিট {seconds} সেকেন্ড` | `{minutes} মিনিট {seconds} সেকেন্ড` | `{minutes} মিনিট` | `{seconds} সেকেন্ড` | + +Rationale: ja/zh use conventional no-space compact duration notation (e.g. `1時間30分15秒`, `2小时30分钟15秒`); ru/ar use the OS-standard single/short abbreviation with a leading space per unit (`ч`/`мин`/`сек`, `س`/`د`/`ث`); hi/bn lack a reliable standard short form, so the full native label words (already verified at `hoursLabel`/`minutesLabel`/`secondsLabel`) are reused verbatim, space-separated. + +#### Scenario: Full duration display renders native units +- GIVEN a user on locale `ja` opens the sleep-timer bottom sheet with a duration of 1 hour, 30 minutes, 15 seconds +- WHEN `durationHoursMinutesSeconds` is resolved +- THEN the rendered text is `1時間30分15秒` with no literal `?` present + +#### Scenario: Each of the 6 locales matches its pinned string exactly +- GIVEN any of `ar`, `bn`, `hi`, `ja`, `ru`, `zh` +- WHEN any of the 4 duration keys is read from its ARB file after the fix +- THEN its value is byte-identical to the corresponding cell in the pinned table above + +### Requirement: ICU placeholder integrity +The system MUST preserve the ICU placeholder names `{hours}`, `{minutes}`, `{seconds}` unchanged in name, casing, and count across all 24 target strings, and MUST NOT alter the associated `@durationXxx.placeholders` metadata blocks. + +#### Scenario: Placeholder names unchanged after fix +- GIVEN the pre-fix and post-fix versions of `durationMinutesSeconds` in `app_ru.arb` +- WHEN the placeholder tokens are extracted from each +- THEN both extractions yield exactly `{minutes}` and `{seconds}`, in that order + +### Requirement: File integrity preserved +The system MUST keep each of the 6 ARB files valid UTF-8 encoded JSON after the fix, and MUST leave every key other than the 4 duration keys per file byte-for-byte unchanged (all other ~386 keys, including `hoursLabel`/`minutesLabel`/`secondsLabel`). + +#### Scenario: File remains valid JSON and UTF-8 +- GIVEN `app_ar.arb` after the fix is applied +- WHEN the file is parsed as JSON and decoded as UTF-8 +- THEN parsing succeeds with no encoding errors + +#### Scenario: Unrelated keys are untouched +- GIVEN `app_zh.arb` before and after the fix +- WHEN every key except the 4 duration keys is diffed +- THEN the diff is empty + +### Requirement: No residual corruption markers +The system MUST NOT contain any literal `?` character within the value of the 24 target strings after the fix. + +#### Scenario: No stray `?` remains +- GIVEN all 6 fixed ARB files +- WHEN the 24 target string values are scanned for the character `?` +- THEN zero occurrences are found