Files
GearBox/.planning/phases/34-i18n-foundation/34-REVIEW.md

13 KiB

phase, reviewed, depth, files_reviewed, files_reviewed_list, findings, status
phase reviewed depth files_reviewed files_reviewed_list findings status
34-i18n-foundation 2026-04-17T00:00:00Z standard 23
src/client/components/ClassificationBadge.tsx
src/client/components/ImageUpload.tsx
src/client/components/ImpactDeltaBadge.tsx
src/client/components/PlanningView.tsx
src/client/components/PublicSetupCard.tsx
src/client/components/SetupImpactSelector.tsx
src/client/components/ThreadCard.tsx
src/client/components/ThreadTabs.tsx
src/client/components/TotalsBar.tsx
src/client/locales/de/collection.json
src/client/locales/de/common.json
src/client/locales/de/onboarding.json
src/client/locales/de/settings.json
src/client/locales/de/setups.json
src/client/locales/de/threads.json
src/client/locales/en/collection.json
src/client/locales/en/common.json
src/client/locales/en/settings.json
src/client/locales/en/setups.json
src/client/locales/en/threads.json
src/client/routes/index.tsx
src/client/routes/profile.tsx
src/client/routes/settings.tsx
critical warning info total
0 7 4 11
issues_found

Phase 34: Code Review Report

Reviewed: 2026-04-17T00:00:00Z Depth: standard Files Reviewed: 23 Status: issues_found

Summary

This review covers the i18n foundation work: wiring components to react-i18next, locale JSON files for English and German, and three routes (index, profile, settings). The EN locale is complete and internally consistent. The DE locale has significant gaps — several keys used by components exist only in EN, meaning German users will see raw key strings or English fallback text in multiple places. Additionally, one component hardcodes a locale in a toLocaleDateString call (bypassing i18n entirely), one component shadows the t translation function with a filter callback variable, and two hardcoded English strings in ImageUpload were not extracted to the locale.


Warnings

WR-01: DE locale missing tabs.setups, totals, and classificationBadge keys

File: src/client/locales/de/collection.json Issue: The EN collection.json contains three top-level sections absent from the DE file: tabs (tabs.setups), totals (totals.totalWeight, totals.totalCost), and classificationBadge (classificationBadge.base, classificationBadge.worn, classificationBadge.consumable). CollectionTabs calls t("tabs.setups") and ClassificationBadge calls t("classificationBadge.base") as a defaultValue. When the language is German, these keys resolve to the raw key string unless a fallback language is configured in i18next. Fix: Add the missing keys to de/collection.json:

"tabs": {
  "setups": "Setups"
},
"totals": {
  "totalWeight": "Gesamtgewicht",
  "totalCost": "Gesamtkosten"
},
"classificationBadge": {
  "base": "Basisgewicht",
  "worn": "Getragen",
  "consumable": "Verbrauchsmaterial"
}

WR-02: DE locale missing card.by, card.anonymous, impact.compareWith in setups

File: src/client/locales/de/setups.json Issue: The EN setups.json contains card.by, card.anonymous, and impact.compareWith. The DE file omits all three. PublicSetupCard calls t("card.by", { name: ... }) and t("card.anonymous") unconditionally; SetupImpactSelector calls t("impact.compareWith") as the blank option label. German users will see raw key strings in these locations. Fix: Add to de/setups.json:

"card": {
  "items": "{{count}} Gegenstände",
  "items_one": "{{count}} Gegenstand",
  "weight": "Gewicht",
  "price": "Preis",
  "by": "von {{name}}",
  "anonymous": "Anonym"
},
"impact": {
  "title": "Auswirkungsvorschau",
  "adding": "Hinzufügen",
  "removing": "Entfernen",
  "compareWith": "Mit Setup vergleichen..."
}

WR-03: DE locale missing card.candidates and planning.* keys in threads

File: src/client/locales/de/threads.json Issue: The EN threads.json contains card.candidates (with pluralisation) and the entire planning.* subtree (8 keys). ThreadCard calls t("card.candidates", { count: candidateCount }) and PlanningView calls t("threads:planning.title"), t("threads:planning.emptyTitle"), t("threads:planning.createFirst"), and three sets of step titles/descriptions. All of these fall back to raw key strings in German. Fix: Add to de/threads.json:

"card": {
  "candidates": "{{count}} Kandidaten",
  "candidates_one": "{{count}} Kandidat"
},
"planning": {
  "title": "Planungs-Threads",
  "emptyTitle": "Planen Sie Ihren nächsten Kauf",
  "createFirst": "Ersten Thread erstellen",
  "step1Title": "Thread erstellen",
  "step1Description": "Starten Sie einen Recherche-Thread für Ausrüstung, die Sie in Betracht ziehen",
  "step2Title": "Kandidaten hinzufügen",
  "step2Description": "Fügen Sie Produkte hinzu, die Sie mit Preisen und Gewichten vergleichen",
  "step3Title": "Gewinner wählen",
  "step3Description": "Schließen Sie den Thread ab und der Gewinner wird Ihrer Sammlung hinzugefügt"
}

WR-04: DE locale missing currency.suggestion, currency.switch, and showConversions keys in settings

File: src/client/locales/de/settings.json Issue: The EN settings.json contains currency.suggestion, currency.switch, and the entire showConversions block. SettingsPage renders a currency suggestion banner using t("currency.suggestion", { symbol, code }) and t("currency.switch"), and the "Show Converted Prices" toggle uses t("showConversions.title") and t("showConversions.description"). German users see raw key strings for all four of these. Fix: Add to de/settings.json:

"currency": {
  "title": "Währung",
  "description": "Ändert das angezeigte Währungssymbol. Werte werden nicht umgerechnet.",
  "suggestion": "Basierend auf Ihrer Region empfehlen wir {{symbol}} ({{code}})",
  "switch": "Wechseln"
},
"showConversions": {
  "title": "Umgerechnete Preise anzeigen",
  "description": "Ungefähre Umrechnungen anzeigen, wenn kein lokaler Preis verfügbar ist"
}

WR-05: DE locale missing home.*, imageUpload.*, and profile.* keys in common

File: src/client/locales/de/common.json Issue: The EN common.json contains three top-level sections absent from DE: home (3 keys used by index.tsx), imageUpload (4 keys used by ImageUpload.tsx), and profile (22 keys used by profile.tsx). These are the most user-facing gaps — the entire profile page renders raw key strings in German. Fix: Add the missing sections to de/common.json. The profile section is large; key translations include:

"home": {
  "popularSetups": "Beliebte Setups",
  "recentlyAdded": "Kürzlich hinzugefügt",
  "trendingCategories": "Beliebte Kategorien"
},
"imageUpload": {
  "clickToAdd": "Klicken zum Hinzufügen eines Fotos",
  "invalidType": "Bitte wählen Sie ein JPG-, PNG- oder WebP-Bild.",
  "tooLarge": "Das Bild muss kleiner als 5 MB sein.",
  "uploadFailed": "Upload fehlgeschlagen. Bitte versuchen Sie es erneut."
},
"profile": {
  "title": "Profil",
  "account": "Konto",
  "accountInfo": "Ihre Kontoinformationen",
  "email": "E-Mail",
  "noEmail": "Keine E-Mail-Adresse hinterlegt",
  "change": "Ändern",
  "newEmailPlaceholder": "Neue E-Mail-Adresse",
  "updating": "Wird aktualisiert...",
  "updateEmail": "E-Mail aktualisieren",
  "emailUpdated": "E-Mail aktualisiert",
  "memberSince": "Mitglied seit",
  "security": "Sicherheit",
  "managePassword": "Ihr Passwort verwalten",
  "currentPassword": "Aktuelles Passwort",
  "newPassword": "Neues Passwort",
  "password": "Passwort",
  "confirmPassword": "Passwort bestätigen",
  "passwordRequirements": "Das Passwort muss mindestens 8 Zeichen mit Groß-, Kleinbuchstaben und einer Zahl enthalten.",
  "passwordUpdated": "Passwort aktualisiert",
  "changingPassword": "Wird geändert...",
  "changePassword": "Passwort ändern",
  "setPassword": "Passwort festlegen",
  "dangerZone": "Gefahrenzone",
  "dangerZoneDescription": "Löschen Sie Ihr Konto und alle persönlichen Daten. Öffentliche Setups werden \"Gelöschter Benutzer\" zugeordnet.",
  "deleteAccount": "Konto löschen",
  "deleteConfirmMessage": "Diese Aktion ist dauerhaft. Geben Sie LÖSCHEN ein, um zu bestätigen.",
  "deleteConfirmPlaceholder": "LÖSCHEN eingeben, um zu bestätigen"
}

WR-06: ThreadCard.formatDate hardcodes "en-US" locale

File: src/client/components/ThreadCard.tsx:21 Issue: formatDate calls d.toLocaleDateString("en-US", ...) with a hardcoded locale. Regardless of the user's language setting, thread card dates will always format in English (e.g. "Apr 17" not "17. Apr"). This is a locale bypass that survives even a correct i18next setup. Fix: Pass undefined (or the active i18n language) so the browser respects the user's locale:

function formatDate(iso: string): string {
  const d = new Date(iso);
  return d.toLocaleDateString(undefined, { month: "short", day: "numeric" });
}

Alternatively, import i18n from ../lib/i18n and use i18n.language as the first argument.

WR-07: t variable shadowed by filter callback parameter in PlanningView

File: src/client/components/PlanningView.tsx:32 Issue: The file destructures { t } from useTranslation at line 11, then on line 32 uses t as the name of the filter callback parameter:

const filteredThreads = (threads ?? [])
  .filter((t) => t.status === activeTab)
  .filter((t) => (categoryFilter ? t.categoryId === categoryFilter : true));

The inner t shadows the outer translation function. While JS resolves this correctly inside the arrow functions, it makes the code misleading and will cause a lint warning (or error with strict shadowing rules). A future developer editing the filter body could accidentally call t("some.key") expecting a translation, and instead receive an object. Fix: Rename the filter parameter:

const filteredThreads = (threads ?? [])
  .filter((thread) => thread.status === activeTab)
  .filter((thread) => (categoryFilter ? thread.categoryId === categoryFilter : true));

Info

IN-01: Two hardcoded English strings in ImageUpload not extracted to locale

File: src/client/components/ImageUpload.tsx:119,136 Issue: Line 119 has alt="Item" and line 136 has title="Adjust framing" as hardcoded English strings. The component already imports useTranslation("common") and uses it for other strings. These two are not in the EN locale file and will not be translated. Fix: Add keys to en/common.json (and de/common.json) under imageUpload:

"imageUpload": {
  "clickToAdd": "Click to add photo",
  "invalidType": "...",
  "tooLarge": "...",
  "uploadFailed": "...",
  "altText": "Item photo",
  "adjustFraming": "Adjust framing"
}

Then use them in the component:

// line 119
alt={t("imageUpload.altText")}
// line 136
title={t("imageUpload.adjustFraming")}

IN-02: CollectionTabs uses inconsistent key depth for first two tabs

File: src/client/components/ThreadTabs.tsx:13-15 Issue: The tabs array mixes key depths:

{ key: "gear", label: t("gear") },
{ key: "planning", label: t("planning") },
{ key: "setups", label: t("tabs.setups") },

"gear" and "planning" are looked up at the root of the collection namespace, while "setups" is nested under "tabs". This asymmetry is accidental — if "gear" or "planning" ever gain sub-keys, lookups will break silently. Consistent nesting (all under "tabs") is cleaner and mirrors standard i18n patterns. Fix: Move all three into a tabs grouping in the locale files and look them up uniformly:

{ key: "gear", label: t("tabs.gear") },
{ key: "planning", label: t("tabs.planning") },
{ key: "setups", label: t("tabs.setups") },

IN-03: profile.tsx silently swallows account-deletion error after logging to console

File: src/client/routes/profile.tsx:327-332 Issue: handleDelete catches errors with console.error but provides no feedback to the user. If deleteAccount.mutateAsync() throws, the user sees nothing — no error message, no state change. Fix: Add error state and display an error message:

const [deleteError, setDeleteError] = useState<string | null>(null);

async function handleDelete() {
  setDeleteError(null);
  try {
    await deleteAccount.mutateAsync();
    window.location.href = "/logout";
  } catch (err) {
    setDeleteError((err as Error).message || t("errors.somethingWentWrong"));
  }
}

Render {deleteError && <p className="text-sm text-red-600">{deleteError}</p>} in the danger zone form.

IN-04: de/threads.json uses "Abgeschlossen" for status.resolved but empty.noThreads says "noch keine" vs EN "No threads found"

File: src/client/locales/de/threads.json:42 Issue: The DE empty.noThreads reads "Noch keine Recherche-Threads" (meaning "No research threads yet") while the EN version reads "No threads found". These are used for two different states: the EN string is shown when a category filter returns no matches (not an "empty collection" state), but the DE string implies the collection is empty. When filtered to a category with no results, German users receive a misleading message. Fix: Align the DE translation with the EN intent:

"empty": {
  "noThreads": "Keine Threads gefunden",
  "noCandidates": "Noch keine Kandidaten"
}

Reviewed: 2026-04-17T00:00:00Z Reviewer: Claude (gsd-code-reviewer) Depth: standard