Skip to content

Localize tab titles, validation and server errors; add ?lang= links (T12: IN-01, SE-01, IN-14, IN-03, IN-04, IN-11) - #222

Merged
Programcode merged 9 commits into
mainfrom
fix/t12-i18n-errors-titles
Oct 23, 2026
Merged

Programcode merged 9 commits into
mainfrom
fix/t12-i18n-errors-titles

Conversation

@Programcode

Copy link
Copy Markdown
Owner

Summary

T12 from the 22 Oct 2026 QA report on prod: tab titles, form validation, error messages and language routing now follow the interface language (EN, ES, HU).

Per issue

IN-01 (Medium) + SE-01 (Low): tab titles English-only; app routes titled just "DebateLab"

  • App routes (/app/*, /login, /share/*) were already titled <page> · DebateLab through usePrivateRouteHead (landed in Fix sign-in links, the OAuth callback and per-account state (T2: AUTH-01, 18, 17, 02, 19, 06, 07, 13) #202), but they had no description. They now also get the landing description in the interface language.
  • Public pages (landing, terms, privacy, sources, the five /tools pages, 404) take their title and description from a new pageHead section in the catalogs. usePageHead translates them and re-titles the page on a language switch. Title and description now run in their own effect, so a switch doesn't drop the page's JSON-LD or canonical link.
  • The English heads come from en.json through src/lib/seo/head-text.ts, so the prerendered HTML and the client say the same thing.
  • Descriptions are 160 characters or fewer in every language (a test checks all 30). The landing/shell description went from 173 to 150 characters (index.html too), and the debate-timer (186) and motion-generator (161) descriptions were trimmed.
  • A missing share link keeps its "not found" title after a language switch (usePageLabel).
  • Verified: src/test/seo/page-head.test.tsx (EN matches the prerender, HU/ES after a switch, 404, app routes, length cap); browser: recipe loc-titles (see table below).

IN-14 (Medium): Zod validation errors English in every language

  • packages/core/src/ai/schemas/{attack,generate,brainstorm,factoids,ask}.ts now emit catalog keys (validation.caseTextRequired, …). The Generate, Attack, Brainstorm and Factoids forms translate them. English text is unchanged, so agentic/forms.e2e.ts still matches.
  • No Edge Function imports these input schemas, so server-side validation is unchanged.
  • The red error text shows only after a submit: the forms validate on submit (react-hook-form's default), and there is no error border on first load.
  • Verified: src/test/i18n/localized-errors.test.tsx (HU error after an empty submit, nothing before it, ES after a switch); browser: recipe loc-validation.

IN-03 (Low): language switcher only on the landing page

  • The existing LanguageSwitcher is now in PublicPageShell (terms, privacy, sources, share, 404) and on /login. The app shell already had it.
  • Verified: browser recipe loc-switcher on /login and the 404 page.

IN-04 (Low): ?lang= ignored; /es and /hu 404

  • ?lang=hu|es|en is applied in mountApp before the first render waits for the language, sets <html lang>, is saved like a switcher choice, and is removed from the address. UrlLanguage handles one reached by in-app navigation.
  • /es and /hu are client routes to /?lang=…, plus one vercel.json redirect for full loads:
    { "source": "/:lang(es|hu)", "destination": "/?lang=:lang", "statusCode": 302 }. That is the only vercel.json change. Sitemap and hreflang are unchanged.
  • For a signed-in user the link can arrive before the account's settings load. It saves only the language column, and hydration prefers it over the saved language. A later switcher choice or a sign-out cancels it (review rounds 1 and 2).
  • Verified: src/test/i18n/url-language.test.tsx, two cases in src/test/settings/settings-migration.test.ts, the redirect rule in src/test/vercel-deployment-config.test.ts; browser: recipe loc-links.

IN-11 (Low): server and client error strings English-only

  • src/lib/server-errors.ts maps server codes to catalog keys and falls back to the server's English for unknown codes.
  • Server (additive, old frontends ignore the new fields):
    • _shared/entitlements.ts: the two 500 refusals now carry usage_check_failed / access_check_failed. The names avoid "quota" because clients read any code matching /quota|billing|…|rate/ as a provider failure.
    • create-portal: code: "no_active_subscription" beside the existing English.
    • redeem-promo: a code on every answer, via _shared/promo-result.ts, which also maps the database function's English messages. The browser does the same mapping, so promo answers translate before redeem-promo is redeployed.
  • Client:
    • AI errors: session expired (401), sign-in required, banned (403), request/message too large, usage_check_failed/access_check_failed, and the generic "AI service error: …" wrapper are now translated.
    • Billing: stripe.ts throws translated BillingErrors carrying the server's code, and Settings and the pricing dialog detect "no active subscription" by code (or the legacy English). The literal "Failed to open billing portal" fallback is now serverErrors.portalFailed.
    • Promo redemption answers, and create-checkout's existing already_subscribed/promo_expired/promo_exhausted codes, are translated.
  • Limit and subscription refusals (T6) are untouched.
  • Verified: localized-errors.test.tsx (code → message per language, unknown code keeps server text, legacy promo messages); edge tests updated for the new promo code; browser: recipe loc-server-error (refused promo code in Hungarian).

Edge Functions to deploy (not deployed by me)

ai, ai-stream, voice (bundle _shared/entitlements.ts), create-portal, redeem-promo. All changes are additive. The current frontend works with both the old and the new versions. No migrations.

Tests

New: src/test/seo/page-head.test.tsx, src/test/i18n/localized-errors.test.tsx, src/test/i18n/url-language.test.tsx, two cases in src/test/settings/settings-migration.test.ts, one in src/test/vercel-deployment-config.test.ts, one in src/test/pages/SharedPage.test.tsx. I checked the title, linked-language and switcher-cancel tests by breaking each fix once and watching them fail. Five Login/Shared page tests now wrap the page in TooltipProvider, because those pages show the switcher.

New browser spec: e2e/localization.spec.ts. It is credential-free and runs in the full e2e suite on desktop and mobile Chromium, but not in the smoke set.

New maintained verify recipe: .claude/skills/verify-debatelab/recipes/localization.spec.ts, with features/localization.md and the index entry.

Verification

This is a local run of the full npm run verify, not a CI pass, because GitHub Actions minutes are used up. Every step passed at 6b5d0830, the PR head. That head merges origin/main at ed6b1c57 (#216, which changed one string per catalog) with a normal merge:

  • docs:check, lint (eslint, knip, repo rules), typecheck: pass
  • npm test: 84 files, 619 tests passed
  • test:collaboration: 5 files, 30 tests passed
  • test:edge: 457 passed, 0 failed
  • build (vite build, prerender of all 10 public pages, sitemap, site check, build boundaries): pass
  • test:sw: all checks true, including languageSwitchOffline and publicPagesKeepTheirOwnHtml
  • test:e2e:smoke: 47 passed

The browser evidence comes from two runs. Both are kept locally under .verify/artifacts/, because gh can't attach them.

  1. Credential-free, at 1280 px (desktop-chromium) and 375 px (mobile-chromium): 10/10 passed. This is the new e2e/localization.spec.ts. It runs on this worktree's port (18123) with a stubbed session, and every request off the origin is stubbed: /rest/v1/* returns empty tables and the ai function returns a 403 banned. It proves the client only. Evidence: .verify/artifacts/20261023T-t12-stackless/.
  2. Real local backend (verify-debatelab, fake provider: local Auth, Postgres and Edge Functions), desktop 1280 px only: 4/5 passed before I released the shared stack lock for the PR Move default, Auto and test-agent model calls onto the Anthropic BYOK key, with a no-fallback guard #220 run. These were loc-titles, loc-validation, loc-links and loc-server-error. The fifth, loc-switcher, failed on a recipe bug: it expected the Hungarian login title to end with "DebateLab", but "Bejelentkezés a DebateLab-ba" already names the app. The assertion is fixed, and the same check passes in run 1. Evidence: .verify/artifacts/20261023T103204Z-localization/. The recipe was not rerun on mobile against the stack.

document.title per route and language, signed in. Run 1 (stub user, 375 px) and run 2 (real local user, 1280 px) recorded the same values:

Route EN ES HU
/app Tools · DebateLab Herramientas · DebateLab Eszközök · DebateLab
/app/generate Generate · DebateLab Generar · DebateLab Generálás · DebateLab
/app/attack Attack · DebateLab Atacar · DebateLab Támadás · DebateLab
/app/history History · DebateLab Historial · DebateLab Előzmények · DebateLab
/app/settings Settings · DebateLab Ajustes · DebateLab Beállítások · DebateLab
/terms Terms of Service | DebateLab by Pi-Stack Términos del servicio | … Felhasználási feltételek | …
/privacy Privacy Policy | … Política de privacidad | … Adatvédelmi tájékoztató | …
/tools Free Debate Tools: Timer, Motions, Flow Sheets | … Herramientas de debate gratis: cronómetro, mociones, hojas de flujo | … Ingyenes vitaeszközök: időzítő, indítványok, jegyzetlapok | …
404 Page not found | … Página no encontrada | … Az oldal nem található | …

Signed out, /login in Hungarian is titled "Bejelentkezés a DebateLab-ba". That label already names the app, so it gets no suffix, the same way the English "Sign in to DebateLab" works. The landing page via /?lang=hu is titled "DebateLab by Pi-Stack | MI-alapú vitafelkészülés, érvek és cáfolatok". After a language switch, /app/generate and the 404 page re-title without a reload.

Screenshots, at both widths unless noted:

  • /app/attack in Hungarian before any submit shows no error and no red border. After an empty submit it reads "Az érv szövegének megadása kötelező", and after switching to Spanish, "El texto del argumento es obligatorio".
  • The switcher sits in the /login header (EN, then HU) and on the 404 page (HU, then ES).
  • /?lang=hu opens the landing page in Hungarian and drops the ?lang from the address. /es lands on / in Spanish. /hu lands on / in Hungarian and stays Hungarian after a reload.
  • A translated server error, in two places:
    • Run 1, both widths: the ai function answers 403 banned, and Attack shows "Támadás elemzés sikertelen: A fiókodat felfüggesztettük. Fordulj az ügyfélszolgálathoz." with no English.
    • Run 2, 1280 px, real redeem-promo: an unknown promo code returns { success: false, code: "promo_invalid", message: "Invalid or expired promo code" }, and the Hungarian toast reads "Érvénytelen vagy lejárt promóciós kód".

Review rounds (fresh-context Claude subagent, claude-opus-5-5, full diff against main)

  1. Round 1: 1 Medium, 1 Low, both fixed.
    • Medium: a ?lang= link opened while signed in raced account hydration. It could flip back, or save this device's older settings snapshot over the account's.
    • Low: a missing share link lost its title on a language switch.
  2. Round 2: 1 Medium, fixed. The pending link language was never cleared, so a later switcher choice or another account in the same tab could get it.
  3. Round 3: 0 blocking, 2 Low.
    • Fixed: the switcher now saves only the language column, since it now also shows on pages that open before the account's settings load.
    • Left open (below): Low Add Vercel Web Analytics to project #1.

Left open

  • Low, from review round 3. If the stored session turns out to be revoked at startup, AuthProvider's sign-out reset sets the device language back to English, which also overrides a ?lang= link. This behaviour predates the PR, and changing it touches auth logic, so it is left for a follow-up.
  • T13 owns bulk translation and the glossary. I added only the keys this batch needs.

To check on the Vercel preview or staging

These need the real host or backend:

  • The /es and /hu redirect on the host is a vercel.json rule. Locally only the client route ran. curl -sI <preview>/hu should answer 302 with Location: /?lang=hu.
  • The real prerendered HTML on the host: view-source: of /terms should still have the English title, plus the new 150-character landing description in / and /app.
  • The localization recipe on mobile against a real backend. Locally it ran on desktop only, before the stack was handed over; the credential-free spec covers 375 px.
  • A signed-in ?lang= link against real settings hydration (the round 1 and 2 fixes). This is covered by unit tests against a PostgREST-like mock, not by a browser run.
  • The billing portal's no-subscription message and Checkout's already_subscribed / promo_expired codes in a browser. These are unit-tested only; the batch rules exclude real Stripe.
  • A shared-link 404 after a language switch. This is unit-tested only.

🤖 Generated with Claude Code

Programcode and others added 8 commits October 23, 2026 09:36
…nks (T12)

IN-01, SE-01, IN-14, IN-03, IN-04, IN-11.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k's title (review round 1)

A link opened before the account's settings load no longer saves this device's older snapshot or flips back after hydration. A page can name its tab over its route's label, so a language switch keeps the not-found title.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iew round 2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Low)

The switcher now shows on pages that open before the account's settings load; writing this device's whole snapshot could overwrite newer settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in title check

e2e/localization.spec.ts proves T12 in the browser with every off-origin request stubbed: titles per route and language, the empty-Attack error, the switcher on /login and 404, ?lang= and /hu, and a translated refusal, at 1280 and 375 px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 23, 2026 10:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Next included review available in 38 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between f1cb90c and 39247fe.

📒 Files selected for processing: 53
  • .claude/skills/verify-debatelab/features/README.md
  • .claude/skills/verify-debatelab/features/localization.md
  • .claude/skills/verify-debatelab/recipes/localization.spec.ts
  • e2e/localization.spec.ts
  • index.html
  • packages/core/src/ai/schemas/ask.ts
  • packages/core/src/ai/schemas/attack.ts
  • packages/core/src/ai/schemas/brainstorm.ts
  • packages/core/src/ai/schemas/factoids.ts
  • packages/core/src/ai/schemas/generate.ts
  • packages/core/src/supabase/edge-function-error.ts
  • src/App.tsx
  • src/components/features/attack/AttackForm.tsx
  • src/components/features/brainstorm/BrainstormForm.tsx
  • src/components/features/factoids/FactoidsForm.tsx
  • src/components/features/generate/GenerateForm.tsx
  • src/components/features/subscription/PricingModal.tsx
  • src/components/layout/PublicPageShell.tsx
  • src/lib/ai/errors.ts
  • src/lib/ai/features/ask.ts
  • src/lib/ai/request.ts
  • src/lib/ai/streaming.ts
  • src/lib/seo/head-text.ts
  • src/lib/seo/pages.ts
  • src/lib/seo/tool-pages.ts
  • src/lib/seo/use-page-head.ts
  • src/lib/server-errors.ts
  • src/lib/stores/settings-store.ts
  • src/lib/stripe.ts
  • src/lib/url-language.ts
  • src/locales/en.json
  • src/locales/es.json
  • src/locales/hu.json
  • src/main.tsx
  • src/pages/LoginPage.tsx
  • src/pages/SettingsPage.tsx
  • src/pages/SharedPage.tsx
  • src/test/auth/account-switch-route.test.tsx
  • src/test/auth/login-form.test.tsx
  • src/test/auth/login-google-redirect.test.tsx
  • src/test/auth/login-otp.test.tsx
  • src/test/i18n/localized-errors.test.tsx
  • src/test/i18n/url-language.test.tsx
  • src/test/pages/SharedPage.test.tsx
  • src/test/seo/page-head.test.tsx
  • src/test/settings/settings-migration.test.ts
  • src/test/vercel-deployment-config.test.ts
  • supabase/functions/_shared/entitlements.ts
  • supabase/functions/_shared/promo-result.ts
  • supabase/functions/create-portal/index.ts
  • supabase/functions/redeem-promo/auth_test.ts
  • supabase/functions/redeem-promo/index.ts
  • vercel.json
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

@Programcode
Programcode merged commit 79cb1f8 into main Oct 23, 2026
3 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants