fix translation length + repair localized /data/ - #949
Conversation
Tell the edge translator to keep copy near source length so UI layouts do not break, honor translate=no/notranslate regions, and bump the translation cache. Move /data metrics bootstrap into a JSON script and skip translating live metric tables so localized pages keep SSR data. Co-authored-by: Martin DONADIEU <martindonadieu@gmail.com>
Match the existing SEO JSON script pattern so Astro leaves the bootstrap payload untouched. Co-authored-by: Martin DONADIEU <martindonadieu@gmail.com>
📝 WalkthroughWalkthroughThe translation worker now skips ChangesTranslation and metrics handling
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
There was a problem hiding this comment.
Risk: medium. Left a non-blocking comment (not approving): this changes the edge translation worker and forces a sitewide locale cache refresh, which is above the low-risk auto-approve threshold. Cursor Bugbot was not present; reviewers will be assigned for human review.
Sent by Cursor Approval Agent: Pull Request Approver External
Prevent </script> breakouts when bootstrapping live-update metrics via set:html by encoding <, >, &, and unicode line separators. Co-authored-by: Martin DONADIEU <martindonadieu@gmail.com>
|
There was a problem hiding this comment.
2 issues found and verified against the latest diff
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/translation-worker/src/index.ts">
<violation number="1" location="apps/translation-worker/src/index.ts:812">
P2: A translate="no" / notranslate element's own translatable attributes (title, alt, aria-label, placeholder) are still collected as translation segments. In collectSegments, appendTag(parts, segments, tag, false, insideBody) runs before shouldSkipElementText is consulted, so the opening tag of the skipped element is split by appendTag and any translatable attribute on it becomes an attribute segment that gets translated and re-injected. The new test only places the title attribute on a nested <td> (inside the raw skip region, so it is correctly preserved), so it does not cover this case. If a live metric region is marked translate="no" on an element that itself carries a title/aria-label, that label would still be translated, contradicting the intent to keep metric regions English/stable.</violation>
<violation number="2" location="apps/translation-worker/src/index.ts:1398">
P1: The ±30% layout constraint is only a prompt instruction; any schema-valid overlong model response passes validation and is cached. Validate each output length and reject/retry or retain source when it exceeds the intended bound.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| 'Translate naturally for the user cultural context; adapt idioms, grammar, tone, and phrasing instead of translating word for word.', | ||
| 'Translate every human-readable label, heading, sentence, and paragraph into the target language, including short navigation labels.', | ||
| translationContextSystemHint(), | ||
| translationLengthSystemHint(), |
There was a problem hiding this comment.
P1: The ±30% layout constraint is only a prompt instruction; any schema-valid overlong model response passes validation and is cached. Validate each output length and reject/retry or retain source when it exceeds the intended bound.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/translation-worker/src/index.ts, line 1398:
<comment>The ±30% layout constraint is only a prompt instruction; any schema-valid overlong model response passes validation and is cached. Validate each output length and reject/retry or retain source when it exceeds the intended bound.</comment>
<file context>
@@ -1379,8 +1395,9 @@ async function translateBatchWithJsonMode(env: Env, targetLanguage: string, batc
'Translate naturally for the user cultural context; adapt idioms, grammar, tone, and phrasing instead of translating word for word.',
'Translate every human-readable label, heading, sentence, and paragraph into the target language, including short navigation labels.',
translationContextSystemHint(),
+ translationLengthSystemHint(),
'Preserve brand names, product names, developer terms, URLs, code identifiers, file paths, package names, language codes, numbers, punctuation, and whitespace meaning.',
- 'Do not translate or transliterate literal tokens such as Capgo, Capacitor, code, API, SDK, CLI, npm, bun, GitHub, Cloudflare, package names, command names, and framework names.',
</file context>
|
|
||
| const translate = readAttributeValue(tag, 'translate') | ||
| if (translate?.trim().toLowerCase() === 'no') return true | ||
| if (hasNoTranslateClass(readAttributeValue(tag, 'class'))) return true |
There was a problem hiding this comment.
P2: A translate="no" / notranslate element's own translatable attributes (title, alt, aria-label, placeholder) are still collected as translation segments. In collectSegments, appendTag(parts, segments, tag, false, insideBody) runs before shouldSkipElementText is consulted, so the opening tag of the skipped element is split by appendTag and any translatable attribute on it becomes an attribute segment that gets translated and re-injected. The new test only places the title attribute on a nested (inside the raw skip region, so it is correctly preserved), so it does not cover this case. If a live metric region is marked translate="no" on an element that itself carries a title/aria-label, that label would still be translated, contradicting the intent to keep metric regions English/stable.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/translation-worker/src/index.ts, line 812:
<comment>A translate="no" / notranslate element's own translatable attributes (title, alt, aria-label, placeholder) are still collected as translation segments. In collectSegments, appendTag(parts, segments, tag, false, insideBody) runs before shouldSkipElementText is consulted, so the opening tag of the skipped element is split by appendTag and any translatable attribute on it becomes an attribute segment that gets translated and re-injected. The new test only places the title attribute on a nested <td> (inside the raw skip region, so it is correctly preserved), so it does not cover this case. If a live metric region is marked translate="no" on an element that itself carries a title/aria-label, that label would still be translated, contradicting the intent to keep metric regions English/stable.</comment>
<file context>
@@ -796,9 +796,21 @@ function languageSelectorTargetLocale(tag: string): string | null {
+ const translate = readAttributeValue(tag, 'translate')
+ if (translate?.trim().toLowerCase() === 'no') return true
+ if (hasNoTranslateClass(readAttributeValue(tag, 'class'))) return true
+
const id = readAttributeValue(tag, 'id')
</file context>
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/translation-worker/scripts/verify-parser.ts`:
- Around line 99-106: Extend the raw-preservation assertion for
noTranslateParsed.parts to verify the complete div.notranslate fragment: opening
and closing tags, their attributes, and the unchanged “Keep metrics English”
text. Keep the existing tbody translate="no" checks intact and ensure the
assertions inspect string parts.
In `@apps/translation-worker/src/index.ts`:
- Around line 1365-1367: Update the shared post-response flow used by
translateSingleText(), assertTranslatedBatch(), and refreshCacheIncrementally()
to validate each restored translation against its source length before rendering
or caching. Enforce the intended roughly ±30% bound, and on violation retry
through the existing translation mechanism or return a safe fallback; apply the
same behavior to both batch and single-text results while preserving
unchanged-string validation.
In `@apps/web/src/lib/liveUpdateMetrics.ts`:
- Around line 153-162: Update jsonForInlineScript to use replaceAll() with
String.raw escape literals instead of the current global-regex replacements,
while preserving the existing escaping order and behavior for JSON characters,
U+2028, and U+2029.
In `@apps/web/src/pages/data.astro`:
- Line 85: Add aria-live="polite" to concise status nodes covering every
client-updated metric rendered by renderMetrics() and renderDaily(), including
the success KPI, daily chart, country/platform/updater tables, and failure lead.
Prefer dedicated status elements rather than making whole tables or charts live,
while preserving the existing timestamp, empty-state, and failure-list regions.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: fbe14a72-ed3f-4549-9495-5e401dd43172
📒 Files selected for processing (4)
apps/translation-worker/scripts/verify-parser.tsapps/translation-worker/src/index.tsapps/web/src/lib/liveUpdateMetrics.tsapps/web/src/pages/data.astro
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
Cap-go/capacitor-updater(manual)
| assert( | ||
| noTranslateBody.every((text) => !text.includes('United States') && !text.includes('Download failure') && !text.includes('Keep metrics English')), | ||
| 'Parser collected text from translate=no / notranslate regions', | ||
| ) | ||
| assert( | ||
| noTranslateParsed.parts.some((part) => typeof part === 'string' && part.includes('United States') && part.includes('Download failure (46%)')), | ||
| 'Parser did not preserve translate=no markup as raw HTML', | ||
| ) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Assert raw preservation for .notranslate.
The test checks that Keep metrics English is absent from body segments. The raw-preservation assertion covers only the <tbody translate="no"> fragment. A regression could drop or rewrite the <div class="notranslate"> wrapper and still pass. Assert that the .notranslate opening tag, closing tag, attributes, and unchanged text remain in noTranslateParsed.parts.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/translation-worker/scripts/verify-parser.ts` around lines 99 - 106,
Extend the raw-preservation assertion for noTranslateParsed.parts to verify the
complete div.notranslate fragment: opening and closing tags, their attributes,
and the unchanged “Keep metrics English” text. Keep the existing tbody
translate="no" checks intact and ensure the assertions inspect string parts.
| function translationLengthSystemHint(): string { | ||
| return 'Keep each translation about the same length as the source (similar character count, roughly within ±30%). Do not expand short UI labels, buttons, nav items, table headers, or headings into longer sentences. Over-long translations break Capgo page layouts and UI spacing.' | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -u
rg -n -C 12 'function assertTranslatedBatch|assertTranslatedBatch\(' \
apps/translation-worker/src/index.ts || true
rg -n -C 8 'translationLengthSystemHint|translateSingleText|translateBatchWithJsonMode|length' \
apps/translation-worker --glob '*.ts' || trueRepository: Cap-go/website
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -u
sed -n '1220,1555p' apps/translation-worker/src/index.tsRepository: Cap-go/website
Length of output: 14007
🏁 Script executed:
#!/bin/bash
set -u
rg -n -C 10 'translateBatch\(|translateSingleText\(|translateBatchIndividually\(|putTranslation|translations|cache' \
apps/translation-worker/src/index.ts | head -n 260
python3 - <<'PY'
from pathlib import Path
import re
source = Path("apps/translation-worker/src/index.ts").read_text()
batch = re.search(
r"function assertTranslatedBatch\(.*?\n\}\n\nfunction bodyTranslationStats",
source,
re.S,
)
single = re.search(
r"async function translateSingleText\(.*?\n\}\n\nasync function translateBatchIndividually",
source,
re.S,
)
assert batch and single
batch_body = batch.group(0)
single_body = single.group(0)
# The batch assertion compares normalized equality only. It has no ratio or
# length-bound calculation.
assert "source === target" in batch_body
assert "length" not in batch_body.split("function bodyTranslationStats", 1)[0]
assert "±30%" not in batch_body
# The single-text success path returns restored model output directly.
assert "return protectedText.restore(translated)" in single_body
assert "assertTranslatedBatch" not in single_body
source_text = "Deploy updates safely to every device"
overlong_translation = source_text + " with detailed guidance for all supported environments and release channels"
assert len(overlong_translation) > len(source_text) * 1.3
print({
"source_length": len(source_text),
"overlong_translation_length": len(overlong_translation),
"exceeds_30_percent": len(overlong_translation) > len(source_text) * 1.3,
"batch_length_check_present": False,
"single_length_check_present": False,
"single_returns_restored_output": True,
})
PYRepository: Cap-go/website
Length of output: 10293
🏁 Script executed:
#!/bin/bash
set -u
rg -n 'async function|function .*Translation|translatedBatches|StoredTranslatedResponse|renderTranslatedHtml|assertTranslatedBody|translateBatch\(' \
apps/translation-worker/src/index.ts | tail -n 140
python3 - <<'PY'
from pathlib import Path
import re
source = Path("apps/translation-worker/src/index.ts").read_text()
batch = re.search(
r"function assertTranslatedBatch\(.*?\n\}\n\nfunction bodyTranslationStats",
source,
re.S,
)
single = re.search(
r"async function translateSingleText\(.*?\n\}\n\nasync function translateBatchIndividually",
source,
re.S,
)
assert batch and single
batch_body = batch.group(0)
single_body = single.group(0)
# The batch assertion has no source/translated length comparison.
comparison_lines = [
line for line in batch_body.splitlines()
if "source" in line and "translated" in line
]
assert any("source === target" in line for line in batch_body.splitlines())
assert not any(
("source.length" in line or "translated.length" in line or "target.length" in line)
for line in comparison_lines
)
# The single-text success path returns restored model output directly.
assert "return protectedText.restore(translated)" in single_body
assert "assertTranslatedBatch" not in single_body
assert "length" not in single_body.split("return protectedText.restore(translated)", 1)[0].rsplit("\n", 8)[-8:]
source_text = "Deploy updates safely to every device"
overlong_translation = source_text + " with detailed guidance for all supported environments and release channels"
ratio = len(overlong_translation) / len(source_text)
assert ratio > 1.30
print({
"source_length": len(source_text),
"overlong_translation_length": len(overlong_translation),
"length_ratio": round(ratio, 2),
"batch_length_comparison_present": False,
"single_length_check_present": False,
"single_returns_restored_output": True,
})
PYRepository: Cap-go/website
Length of output: 8357
🏁 Script executed:
#!/bin/bash
set -u
sed -n '2140,2245p' apps/translation-worker/src/index.ts
sed -n '1680,1742p' apps/translation-worker/src/index.tsRepository: Cap-go/website
Length of output: 7367
Enforce the translation length bound before caching.
translationLengthSystemHint() provides prompt guidance only. translateSingleText() returns restored model output without a length check, and assertTranslatedBatch() checks only unchanged strings. refreshCacheIncrementally() then renders and stores these results. Add a shared post-response check against each restored translation and its source, with retry or a safe fallback, for both batch and single-text translations.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/translation-worker/src/index.ts` around lines 1365 - 1367, Update the
shared post-response flow used by translateSingleText(),
assertTranslatedBatch(), and refreshCacheIncrementally() to validate each
restored translation against its source length before rendering or caching.
Enforce the intended roughly ±30% bound, and on violation retry through the
existing translation mechanism or return a safe fallback; apply the same
behavior to both batch and single-text results while preserving unchanged-string
validation.
|
|
||
| /** JSON safe to embed inside <script> via set:html (blocks </script> breakouts). */ | ||
| export function jsonForInlineScript(value: unknown): string { | ||
| return JSON.stringify(value) | ||
| .replace(/</g, '\\u003c') | ||
| .replace(/>/g, '\\u003e') | ||
| .replace(/&/g, '\\u0026') | ||
| .replace(/\u2028/g, '\\u2028') | ||
| .replace(/\u2029/g, '\\u2029') | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
fd -aH -t f 'package\.json|tsconfig.*\.json|\.browserslistrc|astro\.config.*' . \
-x rg -n 'browserslist|engines|target|compatibility|ES20' {}Repository: Cap-go/website
Length of output: 367
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- target/config files ---'
fd -aH -t f 'package.json|tsconfig.*\.json|\.browserslistrc|astro.config.*|sonar-project.*' . \
-x sh -c 'echo "### $1"; sed -n "1,180p" "$1"' sh {}
printf '%s\n' '--- function and usages ---'
rg -n -A20 -B8 'jsonForInlineScript|replaceAll|String\.raw' apps package.json tsconfig*.json 2>/dev/null || true
printf '%s\n' '--- relevant project files ---'
git ls-files '*liveUpdateMetrics.ts' '*package.json' '*tsconfig*.json' '*astro.config*' '*sonar*'Repository: Cap-go/website
Length of output: 50371
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import json
p = Path("apps/web/src/lib/liveUpdateMetrics.ts")
text = p.read_text()
start = text.index("export function jsonForInlineScript")
end = text.find("\n}", start) + 2
print(text[start:end])
for path in sorted(Path(".").rglob("package.json")):
try:
data = json.loads(path.read_text())
except Exception:
continue
for key in ("engines", "browserslist"):
if key in data:
print(f"{path}: {key}={data[key]!r}")
PY
PYRepository: Cap-go/website
Length of output: 448
🏁 Script executed (no clone):
node - <<'JS'
const current = value => JSON.stringify(value)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026')
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');
const proposed = value => JSON.stringify(value)
.replaceAll('<', String.raw`\u003c`)
.replaceAll('>', String.raw`\u003e`)
.replaceAll('&', String.raw`\u0026`)
.replaceAll('\u2028', String.raw`\u2028`)
.replaceAll('\u2029', String.raw`\u2029`);
const value = {
text: '<>&\u2028\u2029</script>',
repeated: '<<>>&&\u2028\u2029',
nested: ['<', '>', '&'],
};
console.log({ current: current(value), proposed: proposed(value), equal: current(value) === proposed(value) });
JS
Length of output: 530
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- TypeScript target declarations ---'
rg -n --glob '!**/*.webp' --glob '!**/*.png' --glob '!**/*.jpg' \
'"target"|"lib"|extends.*astro/tsconfigs|browserslist|engines' \
package.json apps/*/package.json apps/*/tsconfig*.json tsconfig*.json 2>/dev/null || true
printf '%s\n' '--- package and lock metadata ---'
rg -n --glob '!**/*.webp' --glob '!**/*.png' --glob '!**/*.jpg' \
'astro/tsconfigs/strict|typescript@|astro@|target:|target =' \
bun.lock bun.lockb package.json apps 2>/dev/null | head -80 || true
printf '%s\n' '--- exact function context ---'
nl -ba apps/web/src/lib/liveUpdateMetrics.ts | sed -n '145,168p'Repository: Cap-go/website
Length of output: 6536
🌐 Web query:
Astro 7 astro/tsconfigs/strict default target TypeScript compilerOptions target
💡 Result:
Astro provides three extensible TypeScript configuration presets—base, strict, and strictest—which are located in the astro/tsconfigs/ directory [1][2][3]. These presets are designed to be extended in your project's tsconfig.json file [1][3]. The astro/tsconfigs/strict preset does not mandate a specific target; instead, it inherits from the base configuration and enables a suite of strict type-checking options [4]. In modern Astro projects, the target is typically determined by your own tsconfig.json settings or left to the default behavior of the TypeScript compiler environment [5]. When using these presets, you are encouraged to explicitly set your compilerOptions in your project's tsconfig.json to meet your specific environment needs [6][7]. A common modern configuration for the target option in Astro projects is ESNext, which allows you to leverage the latest ECMAScript features supported by your target deployment environment [5]. To use the strict preset, configure your tsconfig.json as follows: { "extends": "astro/tsconfigs/strict", "compilerOptions": { "target": "ESNext" } } The base template, which underlies the others, is maintained by the Astro team to enable support for modern JavaScript features while remaining compatible with Astro's architecture [1][3]. For the most accurate and up-to-date look at the specific compiler options included in the strict preset, you can inspect the source code directly in the official Astro repository under packages/astro/tsconfigs/ [1][3].
Citations:
- 1: https://v7.docs.astro.build/en/guides/typescript/
- 2: https://docs.astro.build/en/guides/typescript/
- 3: https://github.com/withastro/docs/blob/main/src/content/docs/en/guides/typescript.mdx
- 4: https://deepwiki.com/ragnarok22/whatsapp-numbers/5.3-typescript-configuration
- 5: https://deepwiki.com/alharkan7/alharkan7.github.io/7.2-typescript-configuration-and-dependencies
- 6: https://borjalofe.com/blog/how-to-adapt-astro-to-typescript/
- 7: https://withastro-astro.mintlify.app/config/typescript
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- shared build target ---'
sed -n '96,120p' apps/shared/astro-config.mjs
printf '%s\n' '--- function and call sites ---'
rg -n -A18 -B6 'jsonForInlineScript' apps/web/src apps/sharedRepository: Cap-go/website
Length of output: 6140
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
source = Path("apps/web/src/lib/liveUpdateMetrics.ts").read_text()
shared = Path("apps/shared/astro-config.mjs").read_text()
assert "target: 'es2022'" in shared
assert re.search(r"\.replace\(/</g, '\\\\\\\\u003c'\)", source)
assert re.search(r"\.replace\(/>/g, '\\\\\\\\u003e'\)", source)
assert re.search(r"\.replace\(/&/g, '\\\\\\\\u0026'\)", source)
assert ".replace(/\\u2028/g" in source
assert ".replace(/\\u2029/g" in source
print("build target: es2022")
print("current serializer contains all five global escapes")
print("replaceAll is available in ES2021 and later")
print("String.raw is available in ES2015 and later")
PYRepository: Cap-go/website
Length of output: 240
Replace the global regex calls with replaceAll() and String.raw.
The shared Vite build target is ES2022, and this form preserves the current escaping behavior.
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis
[warning] 161-161: Prefer String#replaceAll() over String#replace().
[warning] 159-159: Prefer String#replaceAll() over String#replace().
[warning] 160-160: Prefer String#replaceAll() over String#replace().
[warning] 157-157: Prefer String#replaceAll() over String#replace().
[warning] 157-157: String.raw should be used to avoid escaping \.
[warning] 161-161: String.raw should be used to avoid escaping \.
[warning] 158-158: String.raw should be used to avoid escaping \.
[warning] 159-159: String.raw should be used to avoid escaping \.
[warning] 160-160: String.raw should be used to avoid escaping \.
[warning] 158-158: Prefer String#replaceAll() over String#replace().
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/lib/liveUpdateMetrics.ts` around lines 153 - 162, Update
jsonForInlineScript to use replaceAll() with String.raw escape literals instead
of the current global-regex replacements, while preserving the existing escaping
order and behavior for JSON characters, U+2028, and U+2029.
Source: Linters/SAST tools
|
|
||
| <div class="data-kpis" aria-label="Reliability summary"> | ||
| <div><p>Success rate</p><strong data-kpi="success-rate">{Math.ceil(metrics.success_rate)}%</strong></div> | ||
| <div><p>Success rate</p><strong data-kpi="success-rate" translate="no">{Math.ceil(metrics.success_rate)}%</strong></div> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Add live-region coverage for all client-updated metric regions.
renderMetrics() and renderDaily() update the success KPI, daily chart, country table, platform table, updater table, and failure lead. These changed elements do not expose aria-live. The existing live regions for the timestamp, empty states, and failure list do not cover these siblings. Screen-reader users can miss refreshed values after fetchMetrics() completes.
Add aria-live="polite" to concise status nodes for these updates. Prefer a dedicated status node when making an entire table or chart live would be verbose.
As per coding guidelines, **/*.{astro,jsx,tsx,js,ts} requires dynamic content to use aria-live regions.
Also applies to: 93-93, 121-121, 164-164, 193-193, 246-246
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/pages/data.astro` at line 85, Add aria-live="polite" to concise
status nodes covering every client-updated metric rendered by renderMetrics()
and renderDaily(), including the success KPI, daily chart,
country/platform/updater tables, and failure lead. Prefer dedicated status
elements rather than making whole tables or charts live, while preserving the
existing timestamp, empty-state, and failure-list regions.
Source: Coding guidelines





Summary
translate="no"andclass="notranslate"when collecting HTML segments.Live Updateproduct name and bumpTRANSLATION_CACHE_VERSIONso stale locale caches refresh./data/: bootstrap metrics live in an inline JSON script (not a huge attribute), and live metric regions are markedtranslate="no".jsonForInlineScript) so</script>cannot break out of the tag.Why
/fr/data/was brokenLocalized
/data/pages were still serving the pre-#947 loading shell (spinner + empty tables, no bootstrap metrics). English already SSR-boots metrics. All locales (fr,de,es, …) were stuck on that stale translated HTML.After deploy of web + translation worker, the cache version bump forces a fresh translation of the fixed English page.
Test plan
bun run checkinapps/translation-worker(parser tests includetranslate=no)jsonForInlineScriptround-trips and blocks</script>breakouts/fr/data/and confirm SSR tables + no loading shellNeed help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by CodeRabbit
New Features
Bug Fixes