Skip to content

Add i18n localization (English/German) with per-user language switching - #38

Merged
panteLx merged 1 commit into
mainfrom
feature/i18n-localization
Aug 1, 2026
Merged

Add i18n localization (English/German) with per-user language switching#38
panteLx merged 1 commit into
mainfrom
feature/i18n-localization

Conversation

@panteLx

@panteLx panteLx commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds next-intl and translates the app UI into English and German
  • Users can switch their language from Profile → Language; self-hosters set a default via DEFAULT_LOCALE (defaults to en-US)
  • Fixes a missing timeZone configuration that caused next-intl ENVIRONMENT_FALLBACK markup-mismatch warnings on both server and client rendering

Details

  • lib/i18n/ — locale config, supported locales (en-US, de-DE), request config for next-intl
  • messages/en-US/*, messages/de-DE/* — translation catalogs, one file per app section
  • app/api/locale/route.ts — sets the locale cookie
  • components/profile/language-card.tsx — language switcher in the profile page
  • app/layout.tsx / components/providers.tsx — pass timeZone (from env.timezone/TZ) through to NextIntlClientProvider so client components no longer fall back to an unconfigured time zone
  • .env.example / README.md — document DEFAULT_LOCALE and TZ

Test plan

  • npx tsc --noEmit passes
  • npm run dev — verify no ENVIRONMENT_FALLBACK warning in console
  • Switch language in Profile → Language and confirm UI updates
  • Confirm a fresh browser (no cookie set) defaults to English

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added English (US) and German localization across authentication, dashboards, administration, trackers, transactions, schedules, statistics, profiles, and public sharing.
    • Added a profile language selector with persistent locale switching.
    • Added localized date, currency, calendar, frequency, navigation, accessibility, and error messaging.
    • Added configurable default locale and timezone settings.
  • Bug Fixes

    • Replaced hardcoded language-specific messages with translations across forms, notifications, validation, and API responses.
    • Improved locale-aware formatting for dates, months, amounts, and relative day labels.

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>
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This 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.

Changes

Internationalization foundation

Layer / File(s) Summary
Locale runtime and configuration
lib/i18n/*, components/providers.tsx, app/layout.tsx, next.config.ts, package.json
Adds next-intl, supported locale configuration, request-level message loading, locale cookies, translated metadata, timezone handling, and client provider wiring.
Locale-aware formatting
lib/utils.ts, components/ui/amount.tsx, components/ui/calendar.tsx, components/ui/date-picker.tsx
Uses the active locale for currency, dates, calendars, day labels, and frequency labels.

Translated application surfaces

Layer / File(s) Summary
Server pages and API routes
app/**
Replaces hardcoded text with translated page content, error messages, metadata, and API responses.
Shared and account interfaces
components/auth/*, components/layout/*, components/admin/*, components/profile/*
Localizes authentication, navigation, command palette, administration, profile, theme, footer, and language-selection interfaces.
Core feature interfaces
components/dashboard/*, components/schedules/*, components/statistics/*, components/trackers/*, components/transactions/*
Localizes forms, filters, statuses, charts, imports, confirmations, notifications, accessibility labels, and empty states.

Translation resources

Layer / File(s) Summary
German and English catalogs
messages/de-DE/*, messages/en-US/*
Adds matching translation catalogs for administration, authentication, common controls, dashboard, errors, navigation, profile, public sharing, schedules, statistics, trackers, and transactions.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: English/German localization and per-user language switching.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/i18n-localization

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.

❤️ Share

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

@panteLx
panteLx merged commit 42fc298 into main Aug 1, 2026
1 of 2 checks passed
@panteLx
panteLx deleted the feature/i18n-localization branch August 1, 2026 20:39

@coderabbitai coderabbitai Bot 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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 14e6858 and 7a4bb36.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (94)
  • .env.example
  • README.md
  • app/admin/error.tsx
  • app/admin/logs/page.tsx
  • app/admin/page.tsx
  • app/admin/settings/page.tsx
  • app/admin/trackers/page.tsx
  • app/admin/users/page.tsx
  • app/api/admin/trackers/[id]/self-share/route.ts
  • app/api/locale/route.ts
  • app/api/statistics/route.ts
  • app/api/trackers/[id]/import/route.ts
  • app/error.tsx
  • app/layout.tsx
  • app/login/page.tsx
  • app/page.tsx
  • app/profile/page.tsx
  • app/register/page.tsx
  • app/schedules/page.tsx
  • app/statistics/page.tsx
  • app/t/[slug]/error.tsx
  • app/t/[slug]/page.tsx
  • app/trackers/[id]/settings/page.tsx
  • app/transactions/page.tsx
  • components/admin/admin-logs-client.tsx
  • components/admin/admin-nav.tsx
  • components/admin/admin-overview.tsx
  • components/admin/admin-settings-client.tsx
  • components/admin/admin-trackers-client.tsx
  • components/admin/admin-users-client.tsx
  • components/auth/auth-redirect-alert.tsx
  • components/auth/login-form.tsx
  • components/auth/oidc-button.tsx
  • components/auth/register-form.tsx
  • components/dashboard/dashboard-client.tsx
  • components/layout/app-footer.tsx
  • components/layout/app-header.tsx
  • components/layout/command-palette.tsx
  • components/layout/mobile-nav.tsx
  • components/layout/quick-add-fab.tsx
  • components/layout/quick-add-sheet.tsx
  • components/layout/theme-toggle.tsx
  • components/layout/user-menu.tsx
  • components/profile/appearance-card.tsx
  • components/profile/language-card.tsx
  • components/profile/sign-out-button.tsx
  • components/providers.tsx
  • components/schedules/schedules-client.tsx
  • components/statistics/statistics-client.tsx
  • components/trackers/csv-import-dialog.tsx
  • components/trackers/tracker-color-picker.tsx
  • components/trackers/tracker-pill-row.tsx
  • components/trackers/tracker-settings-client.tsx
  • components/transactions/entity-picker.tsx
  • components/transactions/transaction-edit.tsx
  • components/transactions/transactions-client.tsx
  • components/ui/amount.tsx
  • components/ui/calendar.tsx
  • components/ui/combobox.tsx
  • components/ui/date-picker.tsx
  • components/ui/dialog.tsx
  • components/ui/direction-toggle.tsx
  • components/ui/sheet.tsx
  • lib/env.ts
  • lib/i18n/config.ts
  • lib/i18n/request.ts
  • lib/i18n/types.d.ts
  • lib/utils.ts
  • messages/de-DE/admin.json
  • messages/de-DE/auth.json
  • messages/de-DE/common.json
  • messages/de-DE/dashboard.json
  • messages/de-DE/errors.json
  • messages/de-DE/nav.json
  • messages/de-DE/profile.json
  • messages/de-DE/public-share.json
  • messages/de-DE/schedules.json
  • messages/de-DE/statistics.json
  • messages/de-DE/trackers.json
  • messages/de-DE/transactions.json
  • messages/en-US/admin.json
  • messages/en-US/auth.json
  • messages/en-US/common.json
  • messages/en-US/dashboard.json
  • messages/en-US/errors.json
  • messages/en-US/nav.json
  • messages/en-US/profile.json
  • messages/en-US/public-share.json
  • messages/en-US/schedules.json
  • messages/en-US/statistics.json
  • messages/en-US/trackers.json
  • messages/en-US/transactions.json
  • next.config.ts
  • package.json

Comment thread app/api/locale/route.ts
Comment on lines +5 to +10
export async function POST(request: Request) {
const body = (await request.json()) as { locale?: unknown };

if (!isSupportedLocale(body.locale)) {
return badRequest("Unsupported locale");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
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.

Comment on lines +157 to +163
<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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.tsx

Repository: 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.tsx

Repository: 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 the Freigeben add-member button.
  • components/trackers/tracker-settings-client.tsx#L956-L1001: Translate the category archive/reactivate labels and Noch keine Kategorien vorhanden. message.
  • components/trackers/tracker-settings-client.tsx#L1034-L1075: Translate the payee archive/reactivate labels and Noch keine Einzahler vorhanden. message.
  • components/transactions/transactions-client.tsx#L627: Translate the Anonym payee fallback (row.anonymous).
  • components/transactions/transactions-client.tsx#L637-L638: Translate the created-by creator labels.
  • components/transactions/transactions-client.tsx#L673: Translate Seite {page} (transactions.page).
📍 Affects 3 files
  • components/admin/admin-logs-client.tsx#L157-L163 (this comment)
  • components/trackers/tracker-settings-client.tsx#L756-L791
  • components/trackers/tracker-settings-client.tsx#L896-L981
  • components/trackers/tracker-settings-client.tsx#L1007-L1059
  • components/transactions/transactions-client.tsx#L610-L615
  • components/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");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

Comment on lines +1 to +4
"use client";

import Link from "next/link";
import { useTranslations } from "next-intl";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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 || true

Repository: 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

Comment on lines +30 to +36
startTransition(async () => {
await fetchJson("/api/locale", {
method: "POST",
body: JSON.stringify({ locale: next }),
});
router.refresh();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.ts

Repository: 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 || true

Repository: 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"
fi

Repository: 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.ts

Repository: 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.ts

Repository: 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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
"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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

Comment on lines +34 to +82
"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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
"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.

Comment on lines +39 to +50
"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."

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

Comment on lines +126 to +131
"descriptionConfigure": "{count} rows detected – choose the columns to import.",
"descriptionResult": "Import complete.",
"cancel": "Cancel",
"back": "Back",
"importingButton": "Importing…",
"importButton": "Import {count} rows",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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}")
PY

Repository: 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, and csvImport.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-L151
  • messages/de-DE/admin.json#L45-L45
  • messages/en-US/schedules.json#L33-L33
  • messages/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.

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.

1 participant