chore(frontend): typed locales, drop unused deps, fix AGENTS.md drift (#1061)

- Derive TranslationShape from the en-US locale and add 'satisfies
  TranslationShape' to the other 13 locales so missing/extra i18n keys
  fail tsc at compile time (previously only the runtime parity test)
- Remove unused dependencies next-themes and @monaco-editor/react
  (zero imports in src/; theming is the hand-rolled zustand theme-store)
- Fix frontend/AGENTS.md drift: 14 locales (not 7), dark mode mechanism
  is the zustand theme-store setting the 'dark' class (not next-themes)
This commit is contained in:
Luis Novo 2026-07-11 18:45:57 -03:00 • committed by GitHub
parent ac30f4d976
commit 97ef02702b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 47 additions and 60 deletions

View file

@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Re-enabled the ruff rules for unused imports (`F401`), unused local variables (`F841`) and bare `except:` (`E722`) that were ignored to silence legacy Streamlit-era noise, and cleaned up the remaining fallout (10 unused imports, 2 unused test variables; no bare excepts remained)
- Chat, source chat, Ask and transformation prompts now steer models to write math as `$$...$$` (display) / `$...$` (inline) so formulas render via KaTeX, reserving fenced `latex` code blocks for when the user explicitly asks for the LaTeX source (#1051)
- Frontend locale files are now type-checked at compile time: every non-en-US locale declares `satisfies TranslationShape` (derived from the en-US object), so a missing or extra i18n key fails `tsc` in the editor instead of only the runtime parity test. Also removed two unused frontend dependencies (`next-themes`, `@monaco-editor/react`) and fixed `frontend/AGENTS.md` drift (14 locales, not 7; dark mode is the hand-rolled zustand theme-store, not next-themes)
### Removed
- Dead Streamlit-era service layer (~2,000 lines): `api/client.py` (a synchronous HTTP client that called the app's own API) and 13 `api/*_service.py` wrappers that consumed the app's own HTTP API — none were imported by any router, command or test. Also removed the toy `process_text`/`analyze_data` demo commands (`commands/example_commands.py`) from the background worker

View file

@ -11,7 +11,7 @@ Normative rules for working on the Next.js frontend. Architecture and flow walkt
## Hard rules
- **i18n is mandatory**: every UI string goes through `t('section.key')` and the key must exist in **all 7 locales** (`en-US`, `pt-BR`, `zh-CN`, `zh-TW`, `ja-JP`, `ru-RU`, `bn-IN`) under `src/lib/locales/`. Missing keys fall back to en-US silently — keep locales in sync.
- **i18n is mandatory**: every UI string goes through `t('section.key')` and the key must exist in **all locales** under `src/lib/locales/` (currently 14; en-US is the reference). Missing keys fall back to en-US silently — keep locales in sync. Each non-en-US locale ends with `satisfies TranslationShape` (type derived from en-US), so a missing/extra key fails `tsc`; the parity test (`src/lib/locales/index.test.ts`) checks the same at runtime.
- All requests go through `apiClient` (`src/lib/api/client.ts`); never create a second axios instance. Auth token is auto-added from localStorage key `auth-storage`.
- Data fetching uses TanStack Query hooks in `src/lib/hooks/` with `QUERY_KEYS`; mutations invalidate caches and show toasts (sonner). Follow the existing hook shape.
- FormData requests: nested objects/arrays must be `JSON.stringify`-ed before appending; the interceptor strips Content-Type so the browser sets the multipart boundary — don't re-add it.
@ -24,7 +24,7 @@ Normative rules for working on the Next.js frontend. Architecture and flow walkt
- SSE (`useAsk`): incomplete lines stay in the buffer between reads; incomplete JSON is silently skipped — don't "simplify" the buffer handling.
- `useSourceStatus` polls every 2s while status is `running`/`queued`/`new`.
- Cache invalidation is deliberately broad (e.g. `['sources']` hits all source queries) — a simplicity/perf trade-off, be precise only when it matters.
- Dark mode requires the `dark` class on the document root (next-themes), not just `prefers-color-scheme`.
- Dark mode requires the `dark` class on the document root, not just `prefers-color-scheme`. It's hand-rolled: the zustand `theme-store` (persisted as `theme-storage`) sets the class via `ThemeProvider` — no next-themes.
- Dialogs don't auto-reset form state (parent clears it) and auto-focus the first input (layout shift risk with conditional inputs). Fixed overlays use `z-50`.
- Provider nesting order in `app/layout.tsx` matters: ErrorBoundary → ThemeProvider → QueryProvider → I18nProvider → ConnectionGuard → Toaster. ErrorBoundary is a class component and uses the raw `enUS` locale object (no hooks).
- `i18n` runs with `useSuspense: false` (no SSR for translations).

View file

@ -9,7 +9,6 @@
"version": "0.1.0",
"dependencies": {
"@hookform/resolvers": "^5.1.1",
"@monaco-editor/react": "^4.7.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-checkbox": "^1.3.2",
@ -40,7 +39,6 @@
"katex": "^0.16.47",
"lucide-react": "^0.525.0",
"next": "^16.2.6",
"next-themes": "^0.4.6",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-hook-form": "^7.60.0",
@ -1828,27 +1826,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@monaco-editor/loader": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz",
"integrity": "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==",
"dependencies": {
"state-local": "^1.0.6"
}
},
"node_modules/@monaco-editor/react": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
"integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
"dependencies": {
"@monaco-editor/loader": "^1.5.0"
},
"peerDependencies": {
"monaco-editor": ">= 0.25.0 < 1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
@ -9876,12 +9853,6 @@
"node": ">= 18"
}
},
"node_modules/monaco-editor": {
"version": "0.52.2",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
"integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==",
"peer": true
},
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz",
@ -9988,15 +9959,6 @@
}
}
},
"node_modules/next-themes": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
"peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/node-releases": {
"version": "2.0.47",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
@ -11541,11 +11503,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/state-local": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="
},
"node_modules/std-env": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",

View file

@ -14,7 +14,6 @@
},
"dependencies": {
"@hookform/resolvers": "^5.1.1",
"@monaco-editor/react": "^4.7.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-checkbox": "^1.3.2",
@ -45,7 +44,6 @@
"katex": "^0.16.47",
"lucide-react": "^0.525.0",
"next": "^16.2.6",
"next-themes": "^0.4.6",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-hook-form": "^7.60.0",

View file

@ -1,3 +1,5 @@
import type { TranslationShape } from '../en-US';
export const bnIN = {
common: {
search: "অনুসন্ধান...",
@ -947,4 +949,4 @@ export const bnIN = {
goToSettings: "সেটিংসে যান",
viewDocs: "ডকুমেন্টেশন দেখুন",
},
}
} satisfies TranslationShape;

View file

@ -1,3 +1,5 @@
import type { TranslationShape } from '../en-US';
export const caES = {
common: {
search: "Cerca...",
@ -946,4 +948,4 @@ export const caES = {
goToSettings: "Ves a la configuració",
viewDocs: "Visualitza la documentació",
},
}
} satisfies TranslationShape;

View file

@ -1,3 +1,5 @@
import type { TranslationShape } from '../en-US';
// German locale.
// Translate values only; do not change keys, placeholders or structure.
@ -949,4 +951,4 @@ export const deDE = {
goToSettings: "Zu den Einstellungen",
viewDocs: "Dokumentation anzeigen",
},
}
} satisfies TranslationShape;

View file

@ -948,3 +948,8 @@ export const enUS = {
viewDocs: "View docs",
},
}
// Compile-time shape of the en-US translations. Every other locale must
// `satisfies` this type so missing or extra keys fail `tsc`, not just the
// runtime parity test.
export type TranslationShape = typeof enUS;

View file

@ -1,3 +1,5 @@
import type { TranslationShape } from '../en-US';
export const esES = {
common: {
search: "Buscar...",
@ -947,4 +949,4 @@ export const esES = {
goToSettings: "Ir a configuración",
viewDocs: "Ver documentación",
},
}
} satisfies TranslationShape;

View file

@ -1,3 +1,5 @@
import type { TranslationShape } from '../en-US';
export const frFR = {
common: {
search: "Recherche...",
@ -946,4 +948,4 @@ export const frFR = {
goToSettings: "Aller aux paramètres",
viewDocs: "Voir la documentation",
},
}
} satisfies TranslationShape;

View file

@ -1,3 +1,5 @@
import type { TranslationShape } from '../en-US';
export const itIT = {
common: {
search: "Cerca...",
@ -946,4 +948,4 @@ export const itIT = {
goToSettings: "Vai alle Impostazioni",
viewDocs: "Vedi documentazione",
},
}
} satisfies TranslationShape;

View file

@ -1,3 +1,5 @@
import type { TranslationShape } from '../en-US';
export const jaJP = {
common: {
search: "検索...",
@ -946,4 +948,4 @@ export const jaJP = {
goToSettings: "設定へ移動",
viewDocs: "ドキュメントを見る",
},
}
} satisfies TranslationShape;

View file

@ -1,3 +1,5 @@
import type { TranslationShape } from '../en-US';
export const plPL = {
common: {
search: "Szukaj...",
@ -946,4 +948,4 @@ export const plPL = {
goToSettings: "Przejdź do Ustawień",
viewDocs: "Zobacz dokumentację",
},
}
} satisfies TranslationShape;

View file

@ -1,3 +1,5 @@
import type { TranslationShape } from '../en-US';
export const ptBR = {
common: {
search: "Buscar...",
@ -946,4 +948,4 @@ export const ptBR = {
goToSettings: "Ir para Configurações",
viewDocs: "Ver documentação",
},
}
} satisfies TranslationShape;

View file

@ -1,3 +1,5 @@
import type { TranslationShape } from '../en-US';
export const ruRU = {
common: {
search: "Поиск...",
@ -946,4 +948,4 @@ export const ruRU = {
goToSettings: "Перейти к настройкам",
viewDocs: "Документация",
},
}
} satisfies TranslationShape;

View file

@ -1,3 +1,5 @@
import type { TranslationShape } from '../en-US';
export const trTR = {
common: {
search: "Ara...",
@ -946,4 +948,4 @@ export const trTR = {
goToSettings: "Ayarlara Git",
viewDocs: "Belgeleri Görüntüle",
},
}
} satisfies TranslationShape;

View file

@ -1,3 +1,5 @@
import type { TranslationShape } from '../en-US';
export const zhCN = {
common: {
search: "搜索...",
@ -946,4 +948,4 @@ export const zhCN = {
goToSettings: "前往设置",
viewDocs: "查看文档",
},
}
} satisfies TranslationShape;

View file

@ -1,3 +1,5 @@
import type { TranslationShape } from '../en-US';
export const zhTW = {
common: {
search: "搜尋...",
@ -946,4 +948,4 @@ export const zhTW = {
goToSettings: "前往設定",
viewDocs: "查看文件",
},
}
} satisfies TranslationShape;