Add i18n localization (English/German) with per-user language switching - #38
Conversation
Introduces next-intl across the app so users can switch between English and German, with a self-hosted default configurable via DEFAULT_LOCALE (defaults to en-US). Also fixes a missing timeZone configuration that caused next-intl markup-mismatch warnings on both server and client rendering. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis change adds English and German localization across the application. It adds locale configuration, translated message catalogs, locale selection, localized server and client interfaces, localized API errors, and locale-aware date and currency formatting. ChangesInternationalization foundation
Translated application surfaces
Translation resources
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
🤖 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 `@app/api/locale/route.ts`:
- Around line 5-10: Update POST to parse request.json() inside a try block and
validate that the parsed body is a non-null object before accessing body.locale.
Convert malformed JSON and non-object payloads, including null, into the
existing badRequest("Unsupported locale") response while preserving
supported-locale handling.
In `@components/admin/admin-logs-client.tsx`:
- Around line 157-163: Replace all remaining German user-visible literals with
the existing translation keys: update the severity options in
components/admin/admin-logs-client.tsx lines 157-163; the Freigeben add-member
button in components/trackers/tracker-settings-client.tsx lines 756-791;
category archive/reactivate labels and empty-state message in lines 896-981;
payee archive/reactivate labels and empty-state message in lines 1007-1059; the
anonymous payee fallback, creator labels, and pagination text in
components/transactions/transactions-client.tsx lines 610-615 and 683-692. Reuse
the appropriate existing translation namespaces and pass the page value to
transactions.page.
In `@components/admin/admin-trackers-client.tsx`:
- Line 35: Update the action labels near the Admin.trackers translation setup,
replacing the hardcoded “Speichern”, “Mich freigeben”, and “Discord testen”
strings with the corresponding keys resolved through the existing t function.
Add or reuse the appropriate Admin.trackers locale entries so English renders
localized labels.
In `@components/layout/app-footer.tsx`:
- Around line 1-4: Update AppFooter so its version metadata remains available
after client conversion: do not read the unexposed process.env.COMMIT_SHA
directly in the client component, and instead pass build metadata from a
server-side API/env setup or remove the client boundary and keep AppFooter
server-rendered. Preserve the release-link behavior by using the commit URL when
COMMIT_SHA is available and the existing fallback otherwise.
In `@components/profile/language-card.tsx`:
- Around line 47-55: Add an accessible name to the locale SelectTrigger by
setting aria-label to t("label") in the Select component using selected and
selectLocale. If the shared SelectTrigger supports an established label-prop
pattern, use that instead while preserving the existing selection behavior.
- Around line 30-36: Update the locale transition in selectLocale to catch
fetchJson failures, clear any pending or error prompt state as appropriate, and
reset selected to the active locale when POST /api/locale fails. Preserve the
existing router.refresh behavior for successful requests.
In `@components/statistics/statistics-client.tsx`:
- Around line 657-660: Update the savingsRate value formatting in the statistics
tile to use Intl.NumberFormat with the active locale, percent style, and at most
one fractional digit, formatting savingsRate divided by 100. Preserve the
existing dash output when savingsRate is null and use the locale already
available in the component.
In `@messages/de-DE/admin.json`:
- Line 39: Update the affected entries in the German admin catalog: translate
“Severity,” “Settings,” and “Schedules” into German, and replace the ASCII
transliterations “Uebertraegt” and “Uebernahme” with their proper umlaut
spellings, matching the catalog’s existing terminology and orthography.
In `@messages/de-DE/auth.json`:
- Line 15: Update the German `heroSubtext` translation to use the grammatically
correct phrase `an einem Ort` instead of `in einem Ort`, preserving the rest of
the message unchanged.
In `@messages/de-DE/common.json`:
- Line 45: Update the German translation key payeeLabel from the
deposit-specific “Einzahler” to a direction-neutral payee term such as
“Zahlungspartner”, ensuring the label is appropriate for both income and expense
transactions.
In `@messages/de-DE/errors.json`:
- Line 11: Update the trackerAlreadyOwned translation in errors.json by
replacing “gehoert” with the correctly spelled German “gehört”, leaving the rest
of the message unchanged.
In `@messages/de-DE/trackers.json`:
- Around line 34-82: The German tracker localization still contains untranslated
or incorrect text: update the deleteTracker confirmation to remove “für”,
replace “Settings” in noTrackerDescription with the appropriate German term, and
translate permissionRead, permissionWrite, permissionAdmin, and owner in the
members section into German.
In `@messages/en-US/schedules.json`:
- Around line 39-50: Align the inactive tab label and empty-state description in
the schedules translations: update the "inactive" label to indicate both
completed and archived schedules, or revise "inactiveDescription" to exclude
archived schedules. Keep the label and description semantically consistent.
In `@messages/en-US/trackers.json`:
- Around line 126-131: Replace hard-coded count-based nouns with ICU plural
branches at messages/en-US/trackers.json lines 126-131 and 151-151,
messages/de-DE/admin.json line 45, messages/en-US/schedules.json line 33, and
messages/en-US/transactions.json line 30. Cover singular and plural forms for
each relevant count, including {count}, {shown}, and {total}, while preserving
the existing message meaning and locale-specific wording.
🪄 Autofix (Beta)
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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: aed89e95-73a2-4529-9ea0-e0db0c4e23f5
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (94)
.env.exampleREADME.mdapp/admin/error.tsxapp/admin/logs/page.tsxapp/admin/page.tsxapp/admin/settings/page.tsxapp/admin/trackers/page.tsxapp/admin/users/page.tsxapp/api/admin/trackers/[id]/self-share/route.tsapp/api/locale/route.tsapp/api/statistics/route.tsapp/api/trackers/[id]/import/route.tsapp/error.tsxapp/layout.tsxapp/login/page.tsxapp/page.tsxapp/profile/page.tsxapp/register/page.tsxapp/schedules/page.tsxapp/statistics/page.tsxapp/t/[slug]/error.tsxapp/t/[slug]/page.tsxapp/trackers/[id]/settings/page.tsxapp/transactions/page.tsxcomponents/admin/admin-logs-client.tsxcomponents/admin/admin-nav.tsxcomponents/admin/admin-overview.tsxcomponents/admin/admin-settings-client.tsxcomponents/admin/admin-trackers-client.tsxcomponents/admin/admin-users-client.tsxcomponents/auth/auth-redirect-alert.tsxcomponents/auth/login-form.tsxcomponents/auth/oidc-button.tsxcomponents/auth/register-form.tsxcomponents/dashboard/dashboard-client.tsxcomponents/layout/app-footer.tsxcomponents/layout/app-header.tsxcomponents/layout/command-palette.tsxcomponents/layout/mobile-nav.tsxcomponents/layout/quick-add-fab.tsxcomponents/layout/quick-add-sheet.tsxcomponents/layout/theme-toggle.tsxcomponents/layout/user-menu.tsxcomponents/profile/appearance-card.tsxcomponents/profile/language-card.tsxcomponents/profile/sign-out-button.tsxcomponents/providers.tsxcomponents/schedules/schedules-client.tsxcomponents/statistics/statistics-client.tsxcomponents/trackers/csv-import-dialog.tsxcomponents/trackers/tracker-color-picker.tsxcomponents/trackers/tracker-pill-row.tsxcomponents/trackers/tracker-settings-client.tsxcomponents/transactions/entity-picker.tsxcomponents/transactions/transaction-edit.tsxcomponents/transactions/transactions-client.tsxcomponents/ui/amount.tsxcomponents/ui/calendar.tsxcomponents/ui/combobox.tsxcomponents/ui/date-picker.tsxcomponents/ui/dialog.tsxcomponents/ui/direction-toggle.tsxcomponents/ui/sheet.tsxlib/env.tslib/i18n/config.tslib/i18n/request.tslib/i18n/types.d.tslib/utils.tsmessages/de-DE/admin.jsonmessages/de-DE/auth.jsonmessages/de-DE/common.jsonmessages/de-DE/dashboard.jsonmessages/de-DE/errors.jsonmessages/de-DE/nav.jsonmessages/de-DE/profile.jsonmessages/de-DE/public-share.jsonmessages/de-DE/schedules.jsonmessages/de-DE/statistics.jsonmessages/de-DE/trackers.jsonmessages/de-DE/transactions.jsonmessages/en-US/admin.jsonmessages/en-US/auth.jsonmessages/en-US/common.jsonmessages/en-US/dashboard.jsonmessages/en-US/errors.jsonmessages/en-US/nav.jsonmessages/en-US/profile.jsonmessages/en-US/public-share.jsonmessages/en-US/schedules.jsonmessages/en-US/statistics.jsonmessages/en-US/trackers.jsonmessages/en-US/transactions.jsonnext.config.tspackage.json
| export async function POST(request: Request) { | ||
| const body = (await request.json()) as { locale?: unknown }; | ||
|
|
||
| if (!isSupportedLocale(body.locale)) { | ||
| return badRequest("Unsupported locale"); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Handle malformed JSON and non-object request bodies.
request.json() rejects malformed JSON. JSON null makes body.locale throw. Both cases bypass badRequest and return a 500 response.
Parse the body in a try block. Validate that it is an object before reading locale.
Proposed fix
export async function POST(request: Request) {
- const body = (await request.json()) as { locale?: unknown };
+ let body: unknown;
+ try {
+ body = await request.json();
+ } catch {
+ return badRequest("Unsupported locale");
+ }
+
+ const locale =
+ typeof body === "object" && body !== null && "locale" in body
+ ? body.locale
+ : undefined;
- if (!isSupportedLocale(body.locale)) {
+ if (!isSupportedLocale(locale)) {
return badRequest("Unsupported locale");
}
const store = await cookies();
- store.set(LOCALE_COOKIE, body.locale, {
+ store.set(LOCALE_COOKIE, locale, {
path: "/",
maxAge: 60 * 60 * 24 * 365,
sameSite: "lax",
});
- return ok({ locale: body.locale });
+ return ok({ locale });
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function POST(request: Request) { | |
| const body = (await request.json()) as { locale?: unknown }; | |
| if (!isSupportedLocale(body.locale)) { | |
| return badRequest("Unsupported locale"); | |
| } | |
| export async function POST(request: Request) { | |
| let body: unknown; | |
| try { | |
| body = await request.json(); | |
| } catch { | |
| return badRequest("Unsupported locale"); | |
| } | |
| const locale = | |
| typeof body === "object" && body !== null && "locale" in body | |
| ? body.locale | |
| : undefined; | |
| if (!isSupportedLocale(locale)) { | |
| return badRequest("Unsupported locale"); | |
| } | |
| const store = await cookies(); | |
| store.set(LOCALE_COOKIE, locale, { | |
| path: "/", | |
| maxAge: 60 * 60 * 24 * 365, | |
| sameSite: "lax", | |
| }); | |
| return ok({ locale }); | |
| } |
🤖 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 `@app/api/locale/route.ts` around lines 5 - 10, Update POST to parse
request.json() inside a try block and validate that the parsed body is a
non-null object before accessing body.locale. Convert malformed JSON and
non-object payloads, including null, into the existing badRequest("Unsupported
locale") response while preserving supported-locale handling.
| <Label>{t("filters.severity")}</Label> | ||
| <Select value={severityFilter} onValueChange={setSeverityFilter}> | ||
| <SelectTrigger> | ||
| <SelectValue /> | ||
| </SelectTrigger> | ||
| <SelectContent> | ||
| <SelectItem value={SEVERITY_ALL}>Alle</SelectItem> | ||
| <SelectItem value={SEVERITY_ALL}>{t("filters.severityAll")}</SelectItem> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n \
-e 'Freigeben' \
-e 'Archivieren' \
-e 'Reaktivieren' \
-e 'Noch keine Kategorien vorhanden' \
-e 'Noch keine Einzahler vorhanden' \
-e '"Anonym"' \
-e '"dir"' \
-e '"Unbekannt"' \
-e 'Seite \{page\}' \
components/admin/admin-logs-client.tsx \
components/trackers/tracker-settings-client.tsx \
components/transactions/transactions-client.tsxRepository: panteLx/BetterTracker
Length of output: 1136
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== candidate files and sizes =="
wc -l components/admin/admin-logs-client.tsx components/trackers/tracker-settings-client.tsx components/transactions/transactions-client.tsx || true
echo "== next docs availability =="
ls -d node_modules/next/dist/docs 2>/dev/null || true
echo "== relevant snippets =="
for f in components/admin/admin-logs-client.tsx components/trackers/tracker-settings-client.tsx components/transactions/transactions-client.tsx; do
echo "--- $f ---"
sed -n '1,230p' "$f" | cat -n | sed -n '1,230p'
done
echo "== imports/usages of severity and related translators =="
rg -n "severity|severityAll|severity|SEVERITY_ALL|useTranslations|t\\(" components/admin/admin-logs-client.tsx components/trackers/tracker-settings-client.hppx components/transactions/transactions-client.tsxRepository: panteLx/BetterTracker
Length of output: 37678
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== tracker-settings relevant snippets =="
cat -n components/trackers/tracker-settings-client.tsx | sed -n '720,785p'
cat -n components/trackers/tracker-settings-client.tsx | sed -n '920,1017p'
cat -n components/trackers/tracker-settings-client.tsx | sed -n '1028,1082p'
echo "== transactions relevant snippets =="
cat -n components/transactions/transactions-client.tsx | sed -n '600,645p'
cat -n components/transactions/transactions-client.tsx | sed -n '665,700p'
echo "== locale dict candidates =="
find components -maxdepth 4 -type f \( -name '*de*' -o -name '*en*' -o -name '*.json' \) | sed -n '1,120p'
rg -n '"severity(All)?":|severityAll|Freigeben|Archivieren|Reaktivieren|Noch keine .* vorhanden|Anonym|Unbekannt|Seite' -S .Repository: panteLx/BetterTracker
Length of output: 19704
Replace the remaining user-visible literals with translation keys.
These literals remain rendered in German for non-German locales and should use the existing translation keys:
components/admin/admin-logs-client.tsx#L164-L167: Translate the severity filter options.components/trackers/tracker-settings-client.tsx#L778: Translate theFreigebenadd-member button.components/trackers/tracker-settings-client.tsx#L956-L1001: Translate the category archive/reactivate labels andNoch keine Kategorien vorhanden.message.components/trackers/tracker-settings-client.tsx#L1034-L1075: Translate the payee archive/reactivate labels andNoch keine Einzahler vorhanden.message.components/transactions/transactions-client.tsx#L627: Translate theAnonympayee fallback (row.anonymous).components/transactions/transactions-client.tsx#L637-L638: Translate the created-by creator labels.components/transactions/transactions-client.tsx#L673: TranslateSeite {page}(transactions.page).
📍 Affects 3 files
components/admin/admin-logs-client.tsx#L157-L163(this comment)components/trackers/tracker-settings-client.tsx#L756-L791components/trackers/tracker-settings-client.tsx#L896-L981components/trackers/tracker-settings-client.tsx#L1007-L1059components/transactions/transactions-client.tsx#L610-L615components/transactions/transactions-client.tsx#L683-L692
🤖 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 `@components/admin/admin-logs-client.tsx` around lines 157 - 163, Replace all
remaining German user-visible literals with the existing translation keys:
update the severity options in components/admin/admin-logs-client.tsx lines
157-163; the Freigeben add-member button in
components/trackers/tracker-settings-client.tsx lines 756-791; category
archive/reactivate labels and empty-state message in lines 896-981; payee
archive/reactivate labels and empty-state message in lines 1007-1059; the
anonymous payee fallback, creator labels, and pagination text in
components/transactions/transactions-client.tsx lines 610-615 and 683-692. Reuse
the appropriate existing translation namespaces and pass the page value to
transactions.page.
| type TrackerDraft = Omit<Tracker, "id" | "slug">; | ||
|
|
||
| export function AdminTrackersClient() { | ||
| const t = useTranslations("Admin.trackers"); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Localize the remaining action labels.
Lines 280, 288, and 296 still render "Speichern", "Mich freigeben", and "Discord testen". Replace them with Admin.trackers translation keys so the English locale does not show German action labels.
🤖 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 `@components/admin/admin-trackers-client.tsx` at line 35, Update the action
labels near the Admin.trackers translation setup, replacing the hardcoded
“Speichern”, “Mich freigeben”, and “Discord testen” strings with the
corresponding keys resolved through the existing t function. Add or reuse the
appropriate Admin.trackers locale entries so English renders localized labels.
| "use client"; | ||
|
|
||
| import Link from "next/link"; | ||
| import { useTranslations } from "next-intl"; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -i -C 3 'NEXT_PUBLIC|environment variables|process\.env' node_modules/next/dist/docs
fd -HI '^next\.config\.' . -x rg -n -C 3 'COMMIT_SHA|APP_VERSION|env:' {}
rg -n -C 2 'COMMIT_SHA|APP_VERSION' components app .env* 2>/dev/null || trueRepository: panteLx/BetterTracker
Length of output: 50380
Keep version metadata available after the client conversion.
AppFooter now runs as a client component and reads process.env.COMMIT_SHA and process.env.APP_VERSION. APP_VERSION can still be inlined if it is tracked, but COMMIT_SHA is not tracked and is not prefixed with NEXT_PUBLIC_. The footer can render the release URL instead of the commit URL. Use build metadata passed from a server API/env setup, or keep AppFooter server-rendered for this data.
🤖 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 `@components/layout/app-footer.tsx` around lines 1 - 4, Update AppFooter so its
version metadata remains available after client conversion: do not read the
unexposed process.env.COMMIT_SHA directly in the client component, and instead
pass build metadata from a server-side API/env setup or remove the client
boundary and keep AppFooter server-rendered. Preserve the release-link behavior
by using the commit URL when COMMIT_SHA is available and the existing fallback
otherwise.
Source: Coding guidelines
| startTransition(async () => { | ||
| await fetchJson("/api/locale", { | ||
| method: "POST", | ||
| body: JSON.stringify({ locale: next }), | ||
| }); | ||
| router.refresh(); | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
ast-grep outline app/api/locale/route.ts --items all
ast-grep outline lib/client-fetch.ts --items all
rg -n -C 5 'locale|throw|!response\.ok|cookies\(' app/api/locale/route.ts lib/client-fetch.tsRepository: panteLx/BetterTracker
Length of output: 2157
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
# Inspect the localized UI control and supporting helpers without running repo code.
printf '--- language-card.tsx ---\n'
nl -ba components/profile/language-card.tsx | sed -n '1,120p'
printf '\n--- fetchJson helper ---\n'
nl -ba lib/client-fetch.ts | sed -n '1,80p'
printf '\n--- locale config helpers ---\n'
nl -ba lib/i18n/config.ts | sed -n '1,120p'
printf '\n--- next/docs locale/useLocale/updateLocale mentions ---\n'
rg -n "useLocale|updateLocale|locale" node_modules/next/dist/docs 2>/dev/null | head -n 200 || trueRepository: panteLx/BetterTracker
Length of output: 250
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
# Inspect Next.js docs availability and any documented useLocale/updateLocale API.
if [ -d node_modules/next/dist/docs ]; then
rg -n "useLocale|updateLocale|locale" node_modules/next/dist/docs | head -n 200 || true
else
echo "node_modules/next/dist/docs not present"
fiRepository: panteLx/BetterTracker
Length of output: 41051
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '--- language-card.tsx ---\n'
sed -n '1,120p' components/profile/language-card.tsx
printf '\n--- fetchJson helper ---\n'
sed -n '1,80p' lib/client-fetch.ts
printf '\n--- locale config helpers ---\n'
sed -n '1,160p' lib/i18n/config.tsRepository: panteLx/BetterTracker
Length of output: 250
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- language-card.tsx ---'
sed -n '1,140p' components/profile/language-card.tsx
printf '%s\n' ''
printf '%s\n' '--- fetchJson helper ---'
sed -n '1,80p' lib/client-fetch.ts
printf '%s\n' ''
printf '%s\n' '--- locale config helpers ---'
sed -n '1,180p' lib/i18n/config.tsRepository: panteLx/BetterTracker
Length of output: 3389
Restore selected when the locale request fails.
fetchJson throws for non-OK responses, but selectLocale does not catch that error. The request then leaves selected as next, while POST /api/locale only sets the cookie on success. Add rejection handling, unset the pending/error prompt if needed, and reset selected to the active locale.
🤖 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 `@components/profile/language-card.tsx` around lines 30 - 36, Update the locale
transition in selectLocale to catch fetchJson failures, clear any pending or
error prompt state as appropriate, and reset selected to the active locale when
POST /api/locale fails. Preserve the existing router.refresh behavior for
successful requests.
| "dateLabel": "Datum", | ||
| "amountLabel": "Betrag", | ||
| "categoryLabel": "Kategorie", | ||
| "payeeLabel": "Einzahler", |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use a direction-neutral label for the payee field.
Einzahler identifies a depositor. It is incorrect for an expense transaction. Use a neutral term such as Zahlungspartner, or provide separate labels for income and expense directions.
Proposed fix
- "payeeLabel": "Einzahler",
+ "payeeLabel": "Zahlungspartner",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "payeeLabel": "Einzahler", | |
| "payeeLabel": "Zahlungspartner", |
🤖 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 `@messages/de-DE/common.json` at line 45, Update the German translation key
payeeLabel from the deposit-specific “Einzahler” to a direction-neutral payee
term such as “Zahlungspartner”, ensuring the label is appropriate for both
income and expense transactions.
| "invalidYear": "Ungültiges Jahr", | ||
| "trackerArchived": "Tracker ist archiviert und kann nicht bearbeitet werden", | ||
| "invalidJsonBody": "Ungültiger JSON-Body", | ||
| "trackerAlreadyOwned": "Tracker gehoert dir bereits", |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Correct the German spelling.
"gehoert" is misspelled. Replace it with "gehört".
🤖 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 `@messages/de-DE/errors.json` at line 11, Update the trackerAlreadyOwned
translation in errors.json by replacing “gehoert” with the correctly spelled
German “gehört”, leaving the rest of the message unchanged.
| "confirm": { | ||
| "deleteCategory": "Kategorie \"{name}\" wirklich löschen?", | ||
| "deletePayee": "Einzahler \"{name}\" wirklich löschen?", | ||
| "removeMember": "Freigabe für \"{label}\" wirklich entfernen?", | ||
| "deleteTracker": "Tracker \"{name}\" wirklich für löschen? Er kann nur von einem Administrator wiederhergestellt werden." | ||
| }, | ||
| "empty": { | ||
| "noTrackerTitle": "Kein Tracker verfügbar", | ||
| "noTrackerDescription": "Lege zuerst einen Tracker an, bevor du die Settings verwaltest.", | ||
| "backToTransactions": "Zurück zu den Buchungen", | ||
| "noCategories": "Noch keine Kategorien vorhanden.", | ||
| "noPayees": "Noch keine Einzahler vorhanden." | ||
| }, | ||
| "back": "Zurück", | ||
| "form": { | ||
| "sectionTitle": "Tracker-Einstellungen", | ||
| "name": "Name", | ||
| "description": "Beschreibung", | ||
| "descriptionPlaceholder": "Optionaler Kontext für diesen Tracker", | ||
| "color": "Farbe", | ||
| "currency": "Währung", | ||
| "webhookUrl": "Discord Webhook URL", | ||
| "pingRoleId": "Discord Ping Role ID", | ||
| "optional": "Optional", | ||
| "discordDebug": "Discord Debug", | ||
| "discordDebugDescription": "Zusatzinfos mitsenden.", | ||
| "archiveTracker": "Tracker archivieren", | ||
| "archiveTrackerDescription": "Es können keine neuen Buchungen angelegt werden.", | ||
| "saving": "Speichert...", | ||
| "save": "Tracker speichern" | ||
| }, | ||
| "dangerZone": { | ||
| "title": "Gefahrenzone", | ||
| "deleting": "Wird gelöscht…", | ||
| "delete": "Tracker löschen", | ||
| "note": "Diese Aktion kann durch User nicht rückgängig gemacht werden." | ||
| }, | ||
| "members": { | ||
| "title": "Freigaben", | ||
| "searchPlaceholder": "Name oder E-Mail suchen", | ||
| "permissionRead": "Read", | ||
| "permissionWrite": "Write", | ||
| "permissionAdmin": "Admin", | ||
| "grant": "Freigeben", | ||
| "noResults": "Keine passenden Benutzer gefunden.", | ||
| "searchHint": "Mindestens 2 Zeichen eingeben.", | ||
| "owner": "Owner", | ||
| "remove": "Entfernen", | ||
| "removeAriaLabel": "Freigabe für {email} entfernen" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Complete the German localization.
Line 38 contains "wirklich für löschen". Remove "für".
Line 42 contains the English term "Settings".
Lines 74-75 and 80 expose English permission labels in the German interface.
Proposed correction
- "deleteTracker": "Tracker \"{name}\" wirklich für löschen? Er kann nur von einem Administrator wiederhergestellt werden."
+ "deleteTracker": "Tracker \"{name}\" wirklich löschen? Er kann nur von einem Administrator wiederhergestellt werden."
...
- "noTrackerDescription": "Lege zuerst einen Tracker an, bevor du die Settings verwaltest.",
+ "noTrackerDescription": "Lege zuerst einen Tracker an, bevor du die Einstellungen verwaltest.",
...
- "permissionRead": "Read",
- "permissionWrite": "Write",
+ "permissionRead": "Lesen",
+ "permissionWrite": "Schreiben",
...
- "owner": "Owner",
+ "owner": "Eigentümer",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "confirm": { | |
| "deleteCategory": "Kategorie \"{name}\" wirklich löschen?", | |
| "deletePayee": "Einzahler \"{name}\" wirklich löschen?", | |
| "removeMember": "Freigabe für \"{label}\" wirklich entfernen?", | |
| "deleteTracker": "Tracker \"{name}\" wirklich für löschen? Er kann nur von einem Administrator wiederhergestellt werden." | |
| }, | |
| "empty": { | |
| "noTrackerTitle": "Kein Tracker verfügbar", | |
| "noTrackerDescription": "Lege zuerst einen Tracker an, bevor du die Settings verwaltest.", | |
| "backToTransactions": "Zurück zu den Buchungen", | |
| "noCategories": "Noch keine Kategorien vorhanden.", | |
| "noPayees": "Noch keine Einzahler vorhanden." | |
| }, | |
| "back": "Zurück", | |
| "form": { | |
| "sectionTitle": "Tracker-Einstellungen", | |
| "name": "Name", | |
| "description": "Beschreibung", | |
| "descriptionPlaceholder": "Optionaler Kontext für diesen Tracker", | |
| "color": "Farbe", | |
| "currency": "Währung", | |
| "webhookUrl": "Discord Webhook URL", | |
| "pingRoleId": "Discord Ping Role ID", | |
| "optional": "Optional", | |
| "discordDebug": "Discord Debug", | |
| "discordDebugDescription": "Zusatzinfos mitsenden.", | |
| "archiveTracker": "Tracker archivieren", | |
| "archiveTrackerDescription": "Es können keine neuen Buchungen angelegt werden.", | |
| "saving": "Speichert...", | |
| "save": "Tracker speichern" | |
| }, | |
| "dangerZone": { | |
| "title": "Gefahrenzone", | |
| "deleting": "Wird gelöscht…", | |
| "delete": "Tracker löschen", | |
| "note": "Diese Aktion kann durch User nicht rückgängig gemacht werden." | |
| }, | |
| "members": { | |
| "title": "Freigaben", | |
| "searchPlaceholder": "Name oder E-Mail suchen", | |
| "permissionRead": "Read", | |
| "permissionWrite": "Write", | |
| "permissionAdmin": "Admin", | |
| "grant": "Freigeben", | |
| "noResults": "Keine passenden Benutzer gefunden.", | |
| "searchHint": "Mindestens 2 Zeichen eingeben.", | |
| "owner": "Owner", | |
| "remove": "Entfernen", | |
| "removeAriaLabel": "Freigabe für {email} entfernen" | |
| "confirm": { | |
| "deleteCategory": "Kategorie \"{name}\" wirklich löschen?", | |
| "deletePayee": "Einzahler \"{name}\" wirklich löschen?", | |
| "removeMember": "Freigabe für \"{label}\" wirklich entfernen?", | |
| "deleteTracker": "Tracker \"{name}\" wirklich löschen? Er kann nur von einem Administrator wiederhergestellt werden." | |
| }, | |
| "empty": { | |
| "noTrackerTitle": "Kein Tracker verfügbar", | |
| "noTrackerDescription": "Lege zuerst einen Tracker an, bevor du die Einstellungen verwaltest.", | |
| "backToTransactions": "Zurück zu den Buchungen", | |
| "noCategories": "Noch keine Kategorien vorhanden.", | |
| "noPayees": "Noch keine Einzahler vorhanden." | |
| }, | |
| "back": "Zurück", | |
| "form": { | |
| "sectionTitle": "Tracker-Einstellungen", | |
| "name": "Name", | |
| "description": "Beschreibung", | |
| "descriptionPlaceholder": "Optionaler Kontext für diesen Tracker", | |
| "color": "Farbe", | |
| "currency": "Währung", | |
| "webhookUrl": "Discord Webhook URL", | |
| "pingRoleId": "Discord Ping Role ID", | |
| "optional": "Optional", | |
| "discordDebug": "Discord Debug", | |
| "discordDebugDescription": "Zusatzinfos mitsenden.", | |
| "archiveTracker": "Tracker archivieren", | |
| "archiveTrackerDescription": "Es können keine neuen Buchungen angelegt werden.", | |
| "saving": "Speichert...", | |
| "save": "Tracker speichern" | |
| }, | |
| "dangerZone": { | |
| "title": "Gefahrenzone", | |
| "deleting": "Wird gelöscht…", | |
| "delete": "Tracker löschen", | |
| "note": "Diese Aktion kann durch User nicht rückgängig gemacht werden." | |
| }, | |
| "members": { | |
| "title": "Freigaben", | |
| "searchPlaceholder": "Name oder E-Mail suchen", | |
| "permissionRead": "Lesen", | |
| "permissionWrite": "Schreiben", | |
| "permissionAdmin": "Admin", | |
| "grant": "Freigeben", | |
| "noResults": "Keine passenden Benutzer gefunden.", | |
| "searchHint": "Mindestens 2 Zeichen eingeben.", | |
| "owner": "Eigentümer", | |
| "remove": "Entfernen", | |
| "removeAriaLabel": "Freigabe für {email} entfernen" |
🤖 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 `@messages/de-DE/trackers.json` around lines 34 - 82, The German tracker
localization still contains untranslated or incorrect text: update the
deleteTracker confirmation to remove “für”, replace “Settings” in
noTrackerDescription with the appropriate German term, and translate
permissionRead, permissionWrite, permissionAdmin, and owner in the members
section into German.
| "inactive": "Completed" | ||
| }, | ||
| "empty": { | ||
| "noMatchesTitle": "No matches for these filters", | ||
| "noMatchesDescription": "Loosen the filters to see more.", | ||
| "resetFilters": "Reset filters", | ||
| "dueTitle": "Nothing due", | ||
| "dueDescription": "No schedule is currently ready to book.", | ||
| "upcomingTitle": "Nothing on the horizon", | ||
| "upcomingDescription": "There are no upcoming schedules set up.", | ||
| "inactiveTitle": "Nothing completed yet", | ||
| "inactiveDescription": "Completed and archived schedules end up here." |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Align the inactive-tab label with its contents.
The empty-state description says that completed and archived schedules appear in this tab. The tab label says only Completed. Use Inactive or Completed and archived, or update the description if archived schedules are excluded.
🤖 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 `@messages/en-US/schedules.json` around lines 39 - 50, Align the inactive tab
label and empty-state description in the schedules translations: update the
"inactive" label to indicate both completed and archived schedules, or revise
"inactiveDescription" to exclude archived schedules. Keep the label and
description semantically consistent.
| "descriptionConfigure": "{count} rows detected – choose the columns to import.", | ||
| "descriptionResult": "Import complete.", | ||
| "cancel": "Cancel", | ||
| "back": "Back", | ||
| "importingButton": "Importing…", | ||
| "importButton": "Import {count} rows", |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== candidate message files =="
git ls-files 'messages/**/*.json' | sed -n '1,120p'
echo
echo "== target excerpts =="
for f in messages/en-US/trackers.json messages/de-DE/admin.json messages/en-US/schedules.json messages/en-US/transactions.json; do
echo "--- $f ---"
if [ -f "$f" ]; then
nl -ba "$f" | sed -n '1,180p'
else
echo "missing"
fi
done
echo
echo "== usages of affected keys =="
rg -n '"(descriptionConfigure|importButton|previewHeading|reset|resetFiltersCount|entriesCount)"|descriptionConfigure|importButton|previewHeading|resetFiltersCount|entriesCount|resetFilters' . || true
echo
echo "== message format hints in codebase =="
rg -n "IntlMessageFormat|intl|formatMessage|plural|intl-formatter|messages/|trackers\.json|admin\.json|schedules\.json|transactions\.json" -S . -g '!node_modules' -g '!dist' -g '!build' | sed -n '1,220p'Repository: panteLx/BetterTracker
Length of output: 976
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== target excerpts =="
for f in messages/en-US/trackers.json messages/de-DE/admin.json messages/en-US/schedules.json messages/en-US/transactions.json; do
echo "--- $f ---"
if [ -f "$f" ]; then
awk '{printf "%6d\t%s\n", NR, $0}' "$f" | sed -n '1,180p'
else
echo "missing"
fi
done
echo
echo "== usages of affected keys and values =="
grep -RInE 'descriptionConfigure|importButton|previewHeading|resetFiltersCount|entriesCount|"reset"|Reset' . \
--exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build | sed -n '1,220p'
echo
echo "== message format setup hints =="
grep -RInE 'IntlMessageFormat|useIntl\(|formatMessage\(|intl-formatter|react-intl|format\(|messages/|trackers.json|admin.json|schedules.json|transactions.json' . \
--exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build | sed -n '1,260p'Repository: panteLx/BetterTracker
Length of output: 27712
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== i18n request file =="
awk '{printf "%6d\t%s\n", NR, $0}' lib/i18n/request.ts | sed -n '1,220p'
echo
echo "== package deps and ICU support hints =="
for f in package.json pnpm-lock.yaml package-lock.json yarn.lock bun.lockb; do
if [ -f "$f" ]; then
echo "--- $f ---"
if [ "$f" = "package.json" ]; then
cat "$f"
else
grep -nE "IntlMessageFormat|intl-formatter|formatjs|react-intl|react-intl-formatted-string|`@formatjs`|next-intl" "$f" | sed -n '1,120p' || true
fi
fi
done
python3 - <<'PY'
import json
from pathlib import Path
for path in [Path("lib/i18n/request.ts")]:
if path.exists():
text = path.read_text()
print("request.ts includes formatMessage:", "formatMessage" in text)
PY
python3 - <<'PY'
# Read-only semantic probe: simulate the current hard-coded outputs for singular count values.
samples = [
("messages/en-US/trackers.json#csvImport.descriptionConfigure", "{count} rows detected – choose the columns to import.", {"count": 1}, "1 row detected – choose the columns to import." not in None),
("messages/en-US/trackers.json#csvImport.importButton", "Import {count} rows", {"count": 1}, "Import 1 row" not in None),
("messages/en-US/trackers.json#csvImport.previewHeading", "Preview (first {shown} of {total} rows)", {"shown": 1, "total": 1}, "Preview (first 1 of 1 rows)" not in None),
("messages/de-DE/admin.json#overview.entriesCount", "{count} Einträge", {"count": 1}, "1 Eintrag" not in None),
("messages/en-US/schedules.json#filters.reset", "Reset {count} filters", {"count": 1}, "Reset 1 filter" not in None),
("messages/en-US/transactions.json#filters.resetFiltersCount", "Reset {count} filters", {"count": 1}, "Reset 1 filter" not in None),
]
for name, template, values, _ in samples:
output = template.format(**values)
expected_singular = "1 " + output.rsplit(" ", 1)[-1].replace("rows", "row").replace("filters", "filter").replace("Einträge", "Eintrag")
print(f"{name}: output={output!r}, has_singular={output == expected_singular}")
PYRepository: panteLx/BetterTracker
Length of output: 6360
Handle pluralization for all count-based messages.
The app uses next-intl, which supports ICU message syntax like the nearby import result messages. Replace hard-coded plural nouns at the listed sites with locale-specific ICU plural branches so singular inputs do not render as 1 rows, 1 filters, or 1 Einträge.
messages/en-US/trackers.json:csvImport.descriptionConfigure,csvImport.importButton, andcsvImport.configure.previewHeading({shown}and{total}).messages/de-DE/admin.json:overview.entriesCount.messages/en-US/schedules.json:filters.reset.messages/en-US/transactions.json:filters.resetFiltersCount.
📍 Affects 4 files
messages/en-US/trackers.json#L126-L131(this comment)messages/en-US/trackers.json#L151-L151messages/de-DE/admin.json#L45-L45messages/en-US/schedules.json#L33-L33messages/en-US/transactions.json#L30-L30
🤖 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 `@messages/en-US/trackers.json` around lines 126 - 131, Replace hard-coded
count-based nouns with ICU plural branches at messages/en-US/trackers.json lines
126-131 and 151-151, messages/de-DE/admin.json line 45,
messages/en-US/schedules.json line 33, and messages/en-US/transactions.json line
30. Cover singular and plural forms for each relevant count, including {count},
{shown}, and {total}, while preserving the existing message meaning and
locale-specific wording.
Summary
next-intland translates the app UI into English and GermanDEFAULT_LOCALE(defaults toen-US)timeZoneconfiguration that caused next-intlENVIRONMENT_FALLBACKmarkup-mismatch warnings on both server and client renderingDetails
lib/i18n/— locale config, supported locales (en-US,de-DE), request config for next-intlmessages/en-US/*,messages/de-DE/*— translation catalogs, one file per app sectionapp/api/locale/route.ts— sets the locale cookiecomponents/profile/language-card.tsx— language switcher in the profile pageapp/layout.tsx/components/providers.tsx— passtimeZone(fromenv.timezone/TZ) through toNextIntlClientProviderso client components no longer fall back to an unconfigured time zone.env.example/README.md— documentDEFAULT_LOCALEandTZTest plan
npx tsc --noEmitpassesnpm run dev— verify noENVIRONMENT_FALLBACKwarning in console🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes