Part of the coding standards. docs/architecture.md owns how interface and AI-output language work. docs/testing.md owns the Hungarian parity checks.
- Every user-facing string, including toasts, errors, and accessible names, goes through
t().src/test/i18n-keys.test.tsfails when a literal key is missing from the catalogs or acountcall lacks its plural forms. It cannot see dynamic keys (t(`nav.${key}`)) or raw values rendered as text, so check those by hand: an English word in a Hungarian screen is the usual sign (#11, #43). - Store a translation key or error kind in state and translate at render time. English strings stored in state stay English after a reload (#12).
- Use the localized feature name everywhere in a catalog: "Interrogatorio", "Keresztkérdezés" (#12, #13 review). The glossary below is the one name per language for each tool, role, and format.
- A count goes into the string as
count, so i18next picks the plural form:t('libraryPage.sourceCount', { count }), never`${n} ${t('sources')}`. Format numbers and dates withsrc/lib/date-format.ts, which follows the interface language. src/test/i18n-untranslated.test.tsfails when a Spanish or Hungarian value is identical to English. When that is intended, such as a product or format name, add it to the test's list under a reason.
| Concept | English | Spanish | Hungarian |
|---|---|---|---|
| Crossfire (tool and saved round) | Crossfire | Interrogatorio | Keresztkérdezés |
| Sparring (tool and saved round) | Sparring | Sparring | Vitapárbaj |
| Timer | Timer | Cronómetro | Időmérő |
| Case | Case | Caso | Eset (Vitaeset in titles) |
| Ruthless (difficulty, intensity) | Ruthless | Despiadado | Kíméletlen |
| School format | School 4-6-6-3 | Formato escolar 4-6-6-3 | Iskolai 4-6-6-3 |
| School-format roles (the Timer spells out R1 and R2) | Introductor, R1, R2, Conclusor | Introductor, R1, R2, Conclusor | Bevezető, R1, R2, Záró |
| Sides in a Sparring turn | Pro, Con | a favor, en contra | Pro, Kontra |
| Competition formats | Public Forum, Lincoln-Douglas, Policy, British Parliamentary, World Schools, Asian Parliamentary | same as English | same as English |
| Load (button) / Loading | Load / Loading | Cargar / Cargando | Megnyitás / Betöltés |
| AI | AI | IA | MI |
| DebateLab with a suffix | DebateLabba, DebateLabban, DebateLabhoz, DebateLabot (no hyphen, back vowels) |
- Icon-only links and buttons get an
aria-label. The nav label hides belowxl, which left the link nameless (#13). - Repeated row actions include the row's name ("Delete <title>"), so screen readers and role queries can tell them apart (#19).
- A label for a group of buttons uses
role="group"andaria-labelledby(#11). - Lists rendered from model output use index keys, because model strings can repeat (#11).
When shouldReduceMotion is true, render a static element. Passing the flag to the phase but not to initial/animate still animates (#20).
- When a store action rejects input (too long, expired, invalid), the UI shows why. A submit button that silently does nothing looks broken (#13).
- Clear a composer only after the handler accepts the message, so a rejected draft is not lost (#66 outside diff).
- Enforce rules such as deadlines in the store, and add the UI guard as a second layer (#13).
- Keep a confirmation dialog open while its request is pending, and close it only on success. Radix closes on click unless you call
preventDefault()(#26). - Side effects such as scrolling run on a success signal, not on "loading became false" (#20).
- Open a new tab within the click handler. After an awaited request the user activation has expired and popup blockers drop the tab. Show a fallback link (#63).
- On phones (below
md) the bottom tab bar is the navigation. Frommdup the header nav is the navigation; there is no menu button at any width, so never show two navs at once (#20).