Skip to content

Feat/i18n#38

Open
OffCrazyFreak wants to merge 12 commits into
devfrom
feat/i18n
Open

Feat/i18n#38
OffCrazyFreak wants to merge 12 commits into
devfrom
feat/i18n

Conversation

@OffCrazyFreak

@OffCrazyFreak OffCrazyFreak commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Disscount: Internationalization (i18n) Guide

A complete reference for how Disscount is translated into multiple languages, written to be understandable even if you are new to i18n. Keep it up to date as the setup changes.

Last verified on 2026-07-04: 652 keys in parity across hr/en/de/sl, pnpm i18n:check green, pnpm exec tsc --noEmit clean, no-literal-string clean.

Mental model in one sentence: every user-facing string is a key looked up at render time in a per-language JSON catalog; next-intl picks the language from a cookie (falling back to the browser's Accept-Language), so the URL never changes, and an ESLint rule plus a parity script make sure no string is ever hardcoded or left untranslated.


Table of contents

  1. Quick reference
  2. How it works (end to end)
  3. Locale resolution & the language switcher
  4. Message catalogs & namespaces
  5. Type safety, fallback & time zone
  6. Emails (localized separately)
  7. Guardrails: the ESLint rule & parity script
  8. Key files
  9. What's automatic vs manual
  10. How to: add a language / add a string
  11. Language vs market (the two-axis design)
  12. Gotchas & lessons learned
  13. Files that had hardcoded Croatian (migration checklist)
  14. Future improvements & TODOs

1. Quick reference

Thing Value
Library next-intl ^4.13.0
Locales hr (default) · en · de · sl
Strategy Cookie-based, no URL routing (URLs stay /products, /shopping-lists)
Cookie NEXT_LOCALE (1 year, sameSite=lax)
First-visit fallback Accept-Language header, then hr
Catalog size 652 keys per locale, full parity (2,608 strings)
Catalogs frontend/src/i18n/messages/{hr,en,de,sl}.json
Time zone fixed Europe/Zagreb (avoids date hydration mismatches)
Type-safe keys yes, via frontend/src/typings/next-intl.d.ts
Lint guard i18next/no-literal-string (fails CI on hardcoded strings)
Parity check pnpm i18n:check
New env vars none

Daily workflow when adding text: add the key to all four catalogs (hr is the reference), read it with useTranslations / getTranslations, then run pnpm i18n:check and pnpm exec tsc --noEmit.


2. How it works (end to end)

Nothing about the language lives in the URL. On every request, a small config function reads the locale, loads the matching catalog, and hands the messages to both Server and Client Components through a provider. Components never hold Croatian text; they hold keys.

flowchart TD
    A[Incoming request] --> B{NEXT_LOCALE cookie valid?}
    B -->|yes| C[use cookie locale = explicit choice]
    B -->|no| D{Accept-Language matches hr/en/de/sl?}
    D -->|yes| E[use browser locale]
    D -->|no| F[defaultLocale = hr]
    C --> G[request.ts: load messages/<locale>.json]
    E --> G
    F --> G
    G --> H[RootLayout: <html lang> + IntlClientProvider]
    H --> I[Server Components: await getTranslations ns]
    H --> J[Client Components: useTranslations ns / t.rich / useFormatter]
Loading

The two ways to read a string:

  • Server Components call const t = await getTranslations("namespace") from next-intl/server.
  • Client Components call const t = useTranslations("namespace") from next-intl.

Both return a t("key") function. t.rich("key", { tag: (chunks) => <El>{chunks}</El> }) renders embedded links or icons, and counts use ICU plurals ({count, plural, one {...} few {...} other {...}}) so Croatian and Slovenian plural rules are correct, not just singular/plural.


3. Locale resolution & the language switcher

request.ts resolves the active locale with a clear priority so an explicit choice always wins and a first-time visitor still gets a sensible default:

  1. NEXT_LOCALE cookie (the user's explicit choice, written by the switcher).
  2. Accept-Language header (the browser's preference), matched on the primary subtag so de-AT maps to de.
  3. defaultLocale (hr).

The switcher is frontend/src/components/custom/language-switcher.tsx. It has two variants driven by a variant prop, so the language logic lives in one place:

Variant Where Look
icon (default) header (guests only) and footer (always) icon button; icon goes muted to primary on hover, like the other header/footer icons
sidebar mobile sidebar, above the PWA install banner full-width sidebar row with a Languages icon and a translated "Language" label

On mobile the header/footer switchers are hidden (hidden md:flex) and the sidebar one takes over (md:hidden), matching the convention already used for the dashboard and header-only items in the sidebar.

Selecting a language calls the setLocale server action (frontend/src/i18n/locale-actions.ts), which writes the NEXT_LOCALE cookie, then router.refresh() re-renders with the new locale. Because it is a refresh and not a navigation, the sidebar stays open.

flowchart LR
    U[User picks a language] --> S[LanguageSwitcher]
    S --> A["setLocale() server action writes NEXT_LOCALE cookie"]
    A --> R["router.refresh()"]
    R --> Q[next render re-resolves the locale from the cookie]
Loading

The language names in the dropdown are endonyms (each language written in itself: Hrvatski, English, Deutsch, Slovenščina). These are intentionally not translated, which is the conventional way to list languages.


4. Message catalogs & namespaces

Catalogs live in frontend/src/i18n/messages/<locale>.json and are organized by feature namespace, mirroring the app's feature folders. hr.json is the reference: it defines the full set of keys and also drives the TypeScript types. The other three mirror its keys exactly.

Representative namespaces:

Namespace Covers
common shared verbs and labels (save, cancel, add, search, loading), endonym helpers, placeholders
metadata, pages.* root and per-page metadata, plus page content (products, watchlist, shopping-lists, statistics, dashboard, digital-cards, suggestions, updates, map, offline, notFound, resetPassword, ...)
navigation sidebar and header nav labels, keyed by item id
auth (+ auth.modal, auth.oauthErrors, ...) login/signup/forgot forms, auth modal, social-login errors
userMenu, settings.* user menu and the preferences/profile/security modals
productDetail, addToList, watchModal, scanner product page, both modals, camera scanner
shoppingListDetail (+ .toasts) list detail, item rows, availability tables, mutation-hook toasts
priceHistory, statistics charts, period buttons, chain stats
accountTypes, acquisitionChannels constant label maps rendered via i18n
offline, pwa offline banners, install prompts (rich text)
validation Zod message strings
emails transactional email copy (see section 6)

Data-driven labels (navigation items, account types, acquisition channels) keep their id or enum value in the data file and are translated at render time by key, so the data files never store display strings.


5. Type safety, fallback & time zone

Type-safe keys. frontend/src/typings/next-intl.d.ts augments next-intl's AppConfig so Messages equals the shape of hr.json. That means t("some.key") is checked at compile time: a typo or a missing key is a TypeScript error, so pnpm exec tsc --noEmit passing is a strong guarantee that no key is dangling.

Missing-key fallback. frontend/src/i18n/message-fallback.ts provides getMessageFallback, which resolves a missing key from the default (hr) catalog instead of showing the raw namespace.key path, and onIntlError, which swallows the "missing message" error so it does not throw. These are wired on the server in request.ts and on the client through intl-client-provider.tsx. Parity is enforced (section 7), so this is a safety net, not an everyday path.

Time zone. config.ts exports a fixed TIME_ZONE = "Europe/Zagreb", applied on the server config, the client provider, and the email translator. A fixed zone (rather than the server's own zone) guarantees the server-rendered HTML and the client's first render format dates identically, which prevents hydration mismatches. Without it, next-intl raises an ENVIRONMENT_FALLBACK warning as soon as anything formats a date.

The client wrapper. getMessageFallback and onIntlError are functions, and functions cannot be passed from a Server Component to a Client Component across the React boundary. That is why intl-client-provider.tsx is a small "use client" module: it imports those functions and hands them to NextIntlClientProvider from inside the client boundary.


6. Emails (localized separately)

Transactional emails (verification, password reset, set password, change-email confirmation) are rendered with react-email outside a normal request render, so they cannot use getTranslations (which is request-bound). Instead they use getEmailTranslator(locale) in frontend/src/emails/email-translator.ts, which builds a translator with next-intl's createTranslator over the emails namespace.

The recipient's locale is resolved at send time by getRequestLocale() (frontend/src/i18n/get-request-locale.ts), which reads the cookie or Accept-Language the same way request.ts does. lib/auth.ts calls it inside each Better Auth email callback and threads the locale through EmailService into the templates.

flowchart LR
    CB[Better Auth email callback] --> GL["getRequestLocale()"]
    GL --> ES[EmailService.send...]
    ES --> ET["getEmailTranslator(locale)"]
    ET --> TPL[react-email template renders in the recipient's language]
Loading

The templates keep their react-email PreviewProps (locale defaults to hr) so the pnpm email preview server still works.


7. Guardrails: the ESLint rule & parity script

Two checks make the translation coverage self-defending, so a regression fails CI instead of shipping.

i18next/no-literal-string (from eslint-plugin-i18next ^6.1.5, configured in frontend/eslint.config.mjs, run by pnpm lint). It fails on any hardcoded JSX text and on the user-visible attributes placeholder, alt, title, aria-label, and label. It is tuned to zero false positives with:

  • callees.exclude: skip the key argument of translation and utility calls (t, t*, nav.label, form.register, localeCompare, and similar), so t("key") is not itself flagged.
  • words.exclude: allow symbol/number-only text, hex color examples, and intentional proper names (Disscount, Jakov Jakovac, GitHub, LinkedIn, N/A).
  • ignores: the shadcn ui/ primitives, unused scaffolds (sidebar-08/, shadcn-studio/, the unused landing sections/*), and emails/ (localized separately).

Because the rule's jsx-only mode also flags string literals inside JSX expression containers, one tailwind class ternary in store-chain-select.tsx was hoisted into a module-level helper so its class names live in plain JS rather than JSX.

pnpm i18n:check (frontend/scripts/check-i18n.mjs). It flattens every catalog to dotted key paths and compares each locale against hr, printing any missing or extra keys and exiting non-zero on a mismatch. Suitable for CI.

pnpm exec tsc --noEmit is the third leg: it verifies every t("key") actually exists in the catalog shape.


8. Key files

Path Role
frontend/src/i18n/config.ts single source of truth: locales, defaultLocale, LOCALE_COOKIE, TIME_ZONE, isLocale, matchLocale
frontend/src/i18n/request.ts getRequestConfig: resolves the locale per request, loads the catalog, sets fallback + time zone
frontend/src/i18n/locale-actions.ts "use server" setLocale(locale): writes the NEXT_LOCALE cookie
frontend/src/i18n/get-request-locale.ts server helper to resolve the locale outside a render (used by emails)
frontend/src/i18n/message-fallback.ts getMessageFallback (falls back to hr) and onIntlError
frontend/src/i18n/intl-client-provider.tsx "use client" wrapper adding fallback/onError/time zone to NextIntlClientProvider
frontend/src/i18n/messages/{hr,en,de,sl}.json the message catalogs (hr is the reference)
frontend/src/typings/next-intl.d.ts type augmentation for compile-time key checking
frontend/src/hooks/use-nav-translation.ts translate nav items by id (label / short)
frontend/src/components/custom/language-switcher.tsx the switcher (icon + sidebar variants)
frontend/src/emails/email-translator.ts getEmailTranslator: a createTranslator bound to a locale for emails
frontend/next.config.ts wires createNextIntlPlugin("./src/i18n/request.ts")
frontend/src/app/layout.tsx dynamic <html lang>, generateMetadata, IntlClientProvider
frontend/eslint.config.mjs no-literal-string rule configuration
frontend/scripts/check-i18n.mjs catalog parity checker (pnpm i18n:check)

9. What's automatic vs manual

Automatic Manual
Locale detection on first visit (Accept-Language) Choosing a language (footer/sidebar switcher)
Loading the right catalog per request Adding a new key to all four catalogs
Compile-time checking that every t("key") exists Translating the value in en / de / sl
Failing CI on a hardcoded string (no-literal-string) Fixing the flagged string by wrapping it in t()
Failing CI on a catalog mismatch (i18n:check) Keeping the four catalogs in parity
Correct plural forms via ICU + Intl.PluralRules Writing the ICU {count, plural, ...} message
Falling back to hr for a missing key (nothing: this is a safety net)
Localized email language at send time Adding new email copy to the emails namespace

10. How to: add a language / add a string

Add a new language (about 5 minutes).

  1. Add the code to locales in frontend/src/i18n/config.ts (for example "it").
  2. Add its endonym to LANGUAGE_NAMES in language-switcher.tsx (for example it: "Italiano").
  3. Create frontend/src/i18n/messages/it.json mirroring every key in hr.json.
  4. Run pnpm i18n:check (must pass) and pnpm exec tsc --noEmit.

The switcher, provider, detection, and type-checking pick it up automatically. This is exactly how sl was added on top of hr/en/de.

Add or change a user-facing string.

  1. Add the key to a suitable namespace in hr.json, then translate it in en.json, de.json, sl.json.
  2. Read it in the component: useTranslations (client) or getTranslations (server). Use t.rich for embedded links/icons, ICU plurals for counts, and useFormatter for numbers/currency/dates.
  3. Never hardcode the text: the ESLint rule will fail on it.
  4. Run pnpm i18n:check and pnpm exec tsc --noEmit.

11. Language vs market (the two-axis design)

docs/disscount_knowledge_base.md section 19 specifies i18n as two independent axes, and only the first is implemented today:

language: 'hr' | 'en' | 'de' | 'sl'   // implemented (this subsystem)
market:   'HR' | 'AT' | 'SI'          // not implemented (future)

Language is what this document covers: the UI text the user reads. Market is which country's stores, price sources, currency, and retailer data are shown. They are independent, so combinations like "English language + Croatia market" or "Croatian language + Austria market" are intended.

When the market axis is built, it should be a separate setting (its own cookie/preference), not an overload of NEXT_LOCALE. The fixed Europe/Zagreb time zone can then derive from the market instead of being constant (all current markets HR/AT/SI happen to share CET).


12. Gotchas & lessons learned

Trap Fix
Reading the cookie / headers opts routes into dynamic rendering Expected and unavoidable for cookie-based i18n without routing; not a bug
no-literal-string in jsx-only mode also flags t("key") arguments and className ternaries Exclude translation/util callees via callees.exclude; hoist class ternaries into plain-JS helpers
getMessageFallback / onError cannot be passed from Server to Client Components (they are functions) Wire them inside a "use client" module (intl-client-provider.tsx)
next-intl warns ENVIRONMENT_FALLBACK when a date is formatted with no time zone Set a global TIME_ZONE on server config, client provider, and email translator
Emails render outside a request, so getTranslations is unavailable Use createTranslator (via getEmailTranslator) with the locale resolved by getRequestLocale at send time
Croatian and Slovenian have three plural forms, not two Use ICU {count, plural, one {} few {} other {}}, not a binary singular/plural helper
pnpm exec tsc sometimes runs a deps-status check that fails on ignored build scripts Run ./node_modules/.bin/tsc --noEmit directly if that happens
Endonyms look like untranslated strings They are intentional (languages named in themselves); keep them out of the catalogs
Currency and dates still render the same in every locale Known gap: .toFixed(2)€ and the custom formatDate are not yet locale-aware (see TODOs)
Em dashes in copy Never use em dashes anywhere, including catalog values

13. Files that had hardcoded Croatian (migration checklist)

Every file below contained hardcoded user-facing Croatian and was migrated to t() / t.rich() in this branch. If the app is ever re-internationalized from scratch, use this as the checklist of files to re-verify. It does not include the i18n infrastructure files (catalogs, config.ts, request.ts, the provider, next-intl.d.ts, the switcher, use-nav-translation.ts), which are new rather than migrated.

Deliberately not migrated (intentional, so do not "fix" them): SEO keyword arrays in layout.tsx, manifest.ts and navigation.ts manifest labels, constants/name-mappings.ts (store/city proper nouns), mock content data (updates/posts.ts, suggestions/suggestions.ts), the Slovenščina endonym, and the deferred Zod messages in lib/api/schemas/* (see TODOs).

Root, layout & legal

  • app/layout.tsx
  • app/not-found.tsx
  • app/(root)/page.tsx
  • app/(root)/components/sections/hero-section.tsx
  • app/(root)/components/sections/hero-actions.tsx
  • app/privacy-policy/page.tsx
  • app/terms-of-service/page.tsx
  • app/data-deletion/page.tsx
  • components/custom/legal-page.tsx

Header, footer, sidebar & shared UI

  • components/custom/header/header.tsx
  • components/custom/header/user-menu.tsx
  • components/custom/header/notifications-dropdown.tsx
  • components/custom/footer.tsx
  • components/custom/app-sidebar.tsx
  • components/custom/search-bar.tsx
  • components/custom/coming-soon.tsx
  • components/custom/no-results.tsx
  • components/custom/camera-scanner.tsx
  • components/custom/view-switcher.tsx
  • components/custom/price-history-period-select.tsx
  • components/custom/store-chain-select.tsx
  • components/custom/store-chain-multi-select.tsx
  • components/custom/oauth-error-toast.tsx
  • components/custom/offline/offline-indicator.tsx
  • components/custom/offline/last-synced-label.tsx
  • components/custom/pwa/install-banner.tsx
  • components/custom/pwa/install-sidebar-banner.tsx
  • components/custom/pwa/install-instructions-sheet.tsx

Auth (forms & modals)

  • components/custom/header/forms/auth-modal.tsx
  • components/custom/header/forms/login-form.tsx
  • components/custom/header/forms/signup-form.tsx
  • components/custom/header/forms/forgot-password-form.tsx
  • components/custom/header/forms/inbox-notice.tsx
  • components/custom/header/forms/account-credentials-form.tsx
  • components/custom/header/forms/linked-accounts.tsx
  • components/custom/header/forms/profile-modal.tsx
  • components/custom/header/forms/security-modal.tsx
  • components/custom/header/forms/user-preferences-modal.tsx
  • app/reset-password/page.tsx
  • app/reset-password/reset-password-modal.tsx

Products & product detail

  • app/products/page.tsx
  • app/products/components/products-client.tsx
  • app/products/components/product-info-display.tsx
  • app/products/components/product-info-table.tsx
  • app/products/components/product-action-buttons.tsx
  • app/products/components/watchlist-action-button.tsx
  • app/products/components/product-item/product-info.tsx
  • app/products/components/product-item/product-price.tsx
  • app/products/components/forms/add-to-shopping-list-form.tsx
  • app/products/components/forms/shopping-list-selector.tsx
  • app/products/components/forms/quantity-input.tsx
  • app/products/components/forms/mark-as-checked-checkbox.tsx
  • app/products/components/forms/watchlist-item-modal.tsx
  • app/products/[id]/page.tsx
  • app/products/[id]/components/product-detail-client.tsx
  • app/products/[id]/components/store-item/store-item.tsx
  • app/products/[id]/components/store-item/store-prices-table.tsx
  • app/products/[id]/components/price-history/price-history-base.tsx
  • app/products/[id]/components/price-history/price-history-chart.tsx

Shopping lists

  • app/(user)/shopping-lists/page.tsx
  • app/(user)/shopping-lists/components/shopping-lists-client.tsx
  • app/(user)/shopping-lists/components/shopping-list-item.tsx
  • app/(user)/shopping-lists/components/create-shopping-list-button.tsx
  • app/(user)/shopping-lists/components/forms/shopping-list-modal.tsx
  • app/(user)/shopping-lists/components/forms/delete-shopping-list-dialog.tsx
  • app/(user)/shopping-lists/hooks/use-shopping-list-modal.ts
  • app/(user)/shopping-lists/[id]/page.tsx
  • app/(user)/shopping-lists/[id]/components/shopping-list-detail-client.tsx
  • app/(user)/shopping-lists/[id]/components/shopping-list-header.tsx
  • app/(user)/shopping-lists/[id]/components/shopping-list-info-table.tsx
  • app/(user)/shopping-lists/[id]/components/shopping-list-action-buttons.tsx
  • app/(user)/shopping-lists/[id]/components/shopping-list-price-history.tsx
  • app/(user)/shopping-lists/[id]/components/items/shopping-list-items.tsx
  • app/(user)/shopping-lists/[id]/components/items/shopping-list-item.tsx
  • app/(user)/shopping-lists/[id]/components/stores/shopping-list-stores-list.tsx
  • app/(user)/shopping-lists/[id]/components/stores/shopping-list-store-card.tsx
  • app/(user)/shopping-lists/[id]/components/stores/shopping-list-items-table.tsx
  • app/(user)/shopping-lists/[id]/hooks/use-shopping-list-mutations.ts
  • app/(user)/shopping-lists/[id]/hooks/use-shopping-list-item-mutations.ts

Watchlist

  • app/(user)/watchlist/page.tsx
  • app/(user)/watchlist/components/watchlist-client.tsx
  • app/(user)/watchlist/components/watchlist-item.tsx
  • app/(user)/watchlist/components/watchlist-item-discount-info.tsx
  • app/(user)/watchlist/components/create-discounted-list-button.tsx

Digital cards

  • app/(user)/digital-cards/page.tsx
  • app/(user)/digital-cards/components/digital-cards-client.tsx
  • app/(user)/digital-cards/components/digital-card-item.tsx
  • app/(user)/digital-cards/components/forms/digital-card-modal.tsx

Other pages

  • app/(user)/spending/page.tsx
  • app/map/page.tsx
  • app/map/components/map-client.tsx
  • app/suggestions/page.tsx
  • app/suggestions/components/suggestions-client.tsx
  • app/suggestions/[id]/page.tsx
  • app/suggestions/[id]/components/suggestion-details-client.tsx
  • app/updates/page.tsx
  • app/updates/components/updates-client.tsx
  • app/updates/[id]/page.tsx
  • app/offline/page.tsx
  • app/offline/components/offline-retry-button.tsx

Dashboard & statistics

  • app/dashboard/page.tsx
  • app/dashboard/components/dashboard-content.tsx
  • app/dashboard/components/admin-users-table.tsx
  • app/statistics/page.tsx
  • app/statistics/components/health-status.tsx
  • app/statistics/components/store-item.tsx
  • app/statistics/components/stores-list.tsx

Emails & their wiring

  • emails/verification-email.tsx
  • emails/password-reset-email.tsx
  • emails/set-password-email.tsx
  • emails/change-email-confirmation.tsx
  • emails/components/action-email.tsx
  • emails/components/email-layout.tsx
  • lib/email/email-service.ts
  • lib/auth.ts

14. Future improvements & TODOs

  • Locale-aware currency, number, and date formatting. The knowledge base section 19 explicitly requires this, and it is the largest remaining gap. Prices still render as .toFixed(2)€ and dates via a custom formatDate, so a German user sees Croatian-style numbers. Replace with useFormatter().number(v, { style: "currency", currency: "EUR" }) and format.dateTime(...), ideally behind one shared helper reused everywhere.
  • Market / country axis. Implement market: 'HR' | 'AT' | 'SI' as a separate setting (see section 11), which controls stores, price sources, and currency independently of language.
  • Zod validation messages in lib/api/schemas/*. These schemas are double-duty (form validation and API-response parsing via the @/lib/api/types barrel), so translating them cleanly needs a message-object factory plus passthrough static instances for the DTO schemas. The validation namespace keys are already in the catalogs; only the form-only inline schemas (forgot-password, reset-password) are translated so far.
  • Persist the locale to the user profile. Today the cookie is the source of truth, so a logged-in user on a new device gets detection or the default. A language field via better-auth additionalFields, synced on login, would make the preference follow the account.
  • Native review of de and sl. Current German and Slovenian are solid machine-quality; a native pass (or a TMS like Tolgee/Crowdin/Lokalise if this grows) is the professional next step.
  • Backend-generated content. Product labels/categories (from the cijene API) and notification content are not covered by frontend i18n; localizing them is a backend concern.
  • SEO in other languages. Cookie-based i18n means search engines only index the default (hr). If ranking en/de/sl content matters later, that requires URL-based routing plus hreflang tags and per-locale sitemaps, a deliberate trade-off away from the current simplicity.
  • Update the stale docs. disscount_technical_documentation.md and disscount_state_of_reality.md still describe the app as "Croatian only / i18n not wired"; refresh them now that this subsystem is live.

Changes:
- Add web app manifest (app/manifest.ts) with standalone display, theme
  color, icons, and shortcuts derived from userNavItems
- Generate PWA icon set (192/512/maskable/apple-touch) from the logo via a
  sharp script (scripts/generate-pwa-icons.mjs)
- Add install UX: persistent sidebar banner + dismissible floating banner +
  platform-aware "how to install" sheet, behind a useInstallPrompt hook
- Show install UI whenever the app isn't installed (not only when the native
  beforeinstallprompt has fired), falling back to manual instructions
- Register a minimal service worker (public/sw.js) so browsers offer install
- Wire PWA metadata/viewport into layout and fix stale metadataBase
- Persist floating-banner dismissal in the existing app localStorage object

First stage of the staged PWA roadmap (offline shell, push, and engagement
extras follow). The minimal service worker is replaced by Serwist in Stage 2.

Notes:
- theme_color (#2ec50d) also tints the mobile browser address bar site-wide
- Adds sharp as a devDependency (icon generation only)
… or iOS

  Safari, hide on unsupported browsers and in-app webviews; hide once installed
Changes:
- Lift useInstallPrompt state into a module-level store read via
  useSyncExternalStore, so the floating and sidebar banners share one
  captured beforeinstallprompt event
- Clear the prompt for all consumers after promptInstall(), preventing a
  stale install action in the other banner
- Attach beforeinstallprompt/appinstalled listeners once on first subscribe
  instead of once per component
- Add a `ready` flag (false in the server snapshot) so install UI is gated
  until client detection runs, avoiding a flash in standalone sessions

Fixes CodeRabbit findings on PR #36: separate per-component prompt state
(major) and install UI flashing before hydration (minor).

Notes:
- Public hook API is unchanged, so InstallBanner/InstallSidebarBanner needed
  no edits
- Dropped now-redundant `typeof window` guards in detectIOS/detectStandalone
  since they only run inside the client-only init()
Changes:
- Add Serwist service worker (src/app/sw.ts): precache app shell, /offline
  fallback, StaleWhileRevalidate for public /api/cijene GETs, NetworkOnly for
  other /api (keeps authed responses out of Cache Storage); wire withSerwistInit
  in next.config (disabled in dev), webworker lib in tsconfig, build --webpack
- Persist React Query cache to IndexedDB (src/lib/offline/*): whitelist of
  cijene/lists/watchlist/etc., 7-day maxAge + buster, success-only dehydration;
  swap to PersistQueryClientProvider with gcTime >= maxAge
- Purge persisted cache on logout (user-context handleLogout)
- Add offline UX: online-status hook, offline indicator banner, relative-time
  util, and "last synced" labels on product detail, watchlist, shopping list
- Remove interim Stage-1 sw.js + manual registration (Serwist auto-registers)

Read-only offline; offline writes (queue + replay) follow in Stage 2b. Pins
@TanStack persist packages to 5.101.1 to match react-query (avoids dup
query-core). Adds @serwist/next, serwist, react-query-persist-client,
query-async-storage-persister, idb-keyval.

Notes:
- Service worker is active in production builds only; data-layer offline works
  in dev. TODO(offline) markers mark coming-soon screens (cards/spending/
  updates/map) to wire when they ship.
Changes:
- Add offline mutation allowlist + keys (lib/offline/offline-mutation-keys.ts)
  and replay defaults (lib/offline/offline-mutations.ts): register default
  mutationFn + invalidation per mutationKey so paused writes resume after reload
- Persist only paused, allowlisted mutations (persister shouldDehydrateMutation)
- Register defaults on client creation and resumePausedMutations on cache
  restore (react-query-provider)
- Tag shopping-list and watchlist mutation hooks with their mutationKey
- Offline indicator shows count of queued writes; create/rename list modal
  closes with a "will sync" toast when offline instead of spinning

Builds on Stage 2a read caching: offline edits to shopping lists/items and
watchlist now queue optimistically and replay on reconnect (same session or
after a reload), cross-browser via React Query (no SW Background Sync needed).

Notes:
- Watchlist add/remove and list creation are queued but not yet optimistic
  offline; follow-up enhancement
- No new dependencies
Changes:
- Add .claude/skills/document-subsystem/SKILL.md that generates a docs/<NAME>.md
  reference for a given part of the app, in the style of docs/DEPLOYMENT.md

Gives a reusable, consistent way to produce detailed, beginner-friendly subsystem
docs: explore the code first, then document the flow, automatic vs manual work,
key files, config/env, libraries, gotchas, and future TODOs.

Notes:
- Project-level skill; move to ~/.claude/skills/ to use it across all projects
…l polish

Changes:
- Set status bar / theme color to neutral white (viewport themeColor +
  manifest theme_color), removing the green top bar
- Request persistent storage (navigator.storage.persist) so the offline
  IndexedDB cache isn't evicted, especially on iOS
- Add iOS splash screens: 18 device sizes generated into public/splash via
  scripts/generate-ios-splash.mjs, emitted as apple-touch-startup-image links
  from a shared constants list (rendered in <head>)
- Add manifest screenshots (branded narrow + wide promo cards) for the richer
  install dialog, and launch_handler focus-existing to avoid duplicate windows

Hardens the Stage 2 offline work and completes Stage 1 install UX on iOS.

Notes:
- Splash/screenshot images are generated; re-run the scripts to refresh them
Changes:
- Add next-intl 4.13 with cookie-based locale (NEXT_LOCALE), no URL routing: src/i18n/{config,request,locale-actions,message-fallback,intl-client-provider}.ts, wired via createNextIntlPlugin in next.config.ts and a client provider in the root layout
- Add 4 message catalogs (hr default, en, de, sl), 652 keys each with full parity, plus compile-time key safety (src/typings/next-intl.d.ts)
- Auto-detect locale from Accept-Language on first visit (the cookie still wins); fall back to hr for any missing key; set a global Europe/Zagreb time zone to avoid date/time hydration mismatches
- Localize the root layout (dynamic <html lang>, generateMetadata) and every page.tsx metadata
- Migrate all user-facing surfaces to t()/t.rich()/ICU plurals: header, sidebar, footer, auth modal and forms, settings modals, dashboard, product detail, price history, add-to-list and watchlist modals, scanner, shopping-list detail and its mutation-hook toasts, watchlist/digital-cards/suggestions/updates/map/statistics, offline indicator, and PWA install banners
- Translate attributes too (placeholders, image alts, aria-labels, tooltips) and render account-type/acquisition-channel labels via i18n; add a useNavTranslation hook
- Add a LanguageSwitcher: an icon variant (header for guests, footer) with a muted-to-primary hover, and a sidebar variant shown on mobile above the PWA banner
- Add a "validation" namespace and build the forgot-password and reset-password Zod schemas inside their components so their messages localize

Establishes a type-safe, cookie-based i18n base so the app serves Croatian, English, German, and Slovenian; adding a language is a one-line config change plus a catalog file.

Notes:
- Reading the cookie / Accept-Language header opts routes into dynamic rendering, which is expected for cookie-based i18n without routing.
- next-intl and eslint-plugin-i18next are added via pnpm (package.json / lock are dependency-managed, not hand-edited).
Changes:
- Add getEmailTranslator + an "emails" catalog namespace so the verification, password-reset, set-password, and change-email templates render in the recipient's locale (subject, heading, body, button, footnote, footer)
- Add getRequestLocale to resolve the recipient locale from the cookie / Accept-Language in non-render contexts, and thread it through EmailService and the Better Auth email callbacks in lib/auth.ts
- Set the email <html lang> and time zone per locale; use t.rich for the change-email address and the footer link

Emails previously always sent in Croatian regardless of the user's language; they now match it.

Notes:
- The locale is resolved inside the request context before the fire-and-forget send, so cookies()/headers() are available.
- Email templates keep their react-email PreviewProps (locale defaults to hr) so "pnpm email" preview still works.
Changes:
- Add the i18next/no-literal-string ESLint rule (eslint-plugin-i18next): fails on hardcoded JSX text and on the placeholder/alt/title/aria-label/label attributes, tuned with callees/words excludes for zero false positives
- Add scripts/check-i18n.mjs (run via "pnpm i18n:check") to verify all four catalogs stay in parity
- Document the i18n workflow (rules, scripts, no em dashes) in AGENTS.md

Makes the translation coverage self-defending: a hardcoded string or a missing key now fails CI instead of shipping.

Notes:
- The eslint-plugin-i18next dependency and the i18n:check script entry live in package.json (added in the foundation commit); this commit adds the config and script file that use them.
Copilot AI review requested due to automatic review settings July 4, 2026 20:05
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too many files!

This PR contains 164 files, which is 14 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to a paid plan to raise the limit.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5b30dd37-bfa5-41c3-946a-f6a02e751b4d

📥 Commits

Reviewing files that changed from the base of the PR and between f2989f6 and c8e1516.

⛔ Files ignored due to path filters (25)
  • frontend/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • frontend/public/icons/apple-touch-icon-180.png is excluded by !**/*.png
  • frontend/public/icons/icon-192.png is excluded by !**/*.png
  • frontend/public/icons/icon-512.png is excluded by !**/*.png
  • frontend/public/icons/icon-maskable-512.png is excluded by !**/*.png
  • frontend/public/screenshots/screenshot-narrow.png is excluded by !**/*.png
  • frontend/public/screenshots/screenshot-wide.png is excluded by !**/*.png
  • frontend/public/splash/apple-splash-1080-2340.png is excluded by !**/*.png
  • frontend/public/splash/apple-splash-1125-2436.png is excluded by !**/*.png
  • frontend/public/splash/apple-splash-1170-2532.png is excluded by !**/*.png
  • frontend/public/splash/apple-splash-1179-2556.png is excluded by !**/*.png
  • frontend/public/splash/apple-splash-1206-2622.png is excluded by !**/*.png
  • frontend/public/splash/apple-splash-1242-2208.png is excluded by !**/*.png
  • frontend/public/splash/apple-splash-1242-2688.png is excluded by !**/*.png
  • frontend/public/splash/apple-splash-1284-2778.png is excluded by !**/*.png
  • frontend/public/splash/apple-splash-1290-2796.png is excluded by !**/*.png
  • frontend/public/splash/apple-splash-1320-2868.png is excluded by !**/*.png
  • frontend/public/splash/apple-splash-1536-2048.png is excluded by !**/*.png
  • frontend/public/splash/apple-splash-1620-2160.png is excluded by !**/*.png
  • frontend/public/splash/apple-splash-1640-2360.png is excluded by !**/*.png
  • frontend/public/splash/apple-splash-1668-2224.png is excluded by !**/*.png
  • frontend/public/splash/apple-splash-1668-2388.png is excluded by !**/*.png
  • frontend/public/splash/apple-splash-2048-2732.png is excluded by !**/*.png
  • frontend/public/splash/apple-splash-750-1334.png is excluded by !**/*.png
  • frontend/public/splash/apple-splash-828-1792.png is excluded by !**/*.png
📒 Files selected for processing (164)
  • .claude/skills/document-subsystem/SKILL.md
  • .gitignore
  • AGENTS.md
  • docs/I18N.md
  • frontend/.gitignore
  • frontend/eslint.config.mjs
  • frontend/next.config.ts
  • frontend/package.json
  • frontend/pnpm-workspace.yaml
  • frontend/scripts/check-i18n.mjs
  • frontend/scripts/generate-ios-splash.mjs
  • frontend/scripts/generate-pwa-icons.mjs
  • frontend/scripts/generate-pwa-screenshots.mjs
  • frontend/src/app/(root)/components/sections/hero-actions.tsx
  • frontend/src/app/(root)/components/sections/hero-section.tsx
  • frontend/src/app/(root)/page.tsx
  • frontend/src/app/(user)/digital-cards/components/digital-card-item.tsx
  • frontend/src/app/(user)/digital-cards/components/digital-cards-client.tsx
  • frontend/src/app/(user)/digital-cards/components/forms/digital-card-modal.tsx
  • frontend/src/app/(user)/digital-cards/page.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/items/shopping-list-item.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/items/shopping-list-items.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/shopping-list-action-buttons.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/shopping-list-detail-client.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/shopping-list-header.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/shopping-list-info-table.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/shopping-list-price-history.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/stores/shopping-list-items-table.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/stores/shopping-list-store-card.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/components/stores/shopping-list-stores-list.tsx
  • frontend/src/app/(user)/shopping-lists/[id]/hooks/use-shopping-list-data.ts
  • frontend/src/app/(user)/shopping-lists/[id]/hooks/use-shopping-list-item-mutations.ts
  • frontend/src/app/(user)/shopping-lists/[id]/hooks/use-shopping-list-mutations.ts
  • frontend/src/app/(user)/shopping-lists/[id]/page.tsx
  • frontend/src/app/(user)/shopping-lists/components/create-shopping-list-button.tsx
  • frontend/src/app/(user)/shopping-lists/components/forms/delete-shopping-list-dialog.tsx
  • frontend/src/app/(user)/shopping-lists/components/forms/shopping-list-modal.tsx
  • frontend/src/app/(user)/shopping-lists/components/shopping-list-item.tsx
  • frontend/src/app/(user)/shopping-lists/components/shopping-lists-client.tsx
  • frontend/src/app/(user)/shopping-lists/hooks/use-shopping-list-modal.ts
  • frontend/src/app/(user)/shopping-lists/page.tsx
  • frontend/src/app/(user)/spending/page.tsx
  • frontend/src/app/(user)/watchlist/components/create-discounted-list-button.tsx
  • frontend/src/app/(user)/watchlist/components/watchlist-client.tsx
  • frontend/src/app/(user)/watchlist/components/watchlist-item-discount-info.tsx
  • frontend/src/app/(user)/watchlist/components/watchlist-item.tsx
  • frontend/src/app/(user)/watchlist/page.tsx
  • frontend/src/app/dashboard/components/admin-users-table.tsx
  • frontend/src/app/dashboard/components/dashboard-content.tsx
  • frontend/src/app/dashboard/page.tsx
  • frontend/src/app/data-deletion/page.tsx
  • frontend/src/app/layout.tsx
  • frontend/src/app/manifest.ts
  • frontend/src/app/map/components/map-client.tsx
  • frontend/src/app/map/page.tsx
  • frontend/src/app/not-found.tsx
  • frontend/src/app/offline/components/offline-retry-button.tsx
  • frontend/src/app/offline/page.tsx
  • frontend/src/app/privacy-policy/page.tsx
  • frontend/src/app/products/[id]/components/price-history/price-history-base.tsx
  • frontend/src/app/products/[id]/components/price-history/price-history-chart.tsx
  • frontend/src/app/products/[id]/components/product-detail-client.tsx
  • frontend/src/app/products/[id]/components/store-item/store-item.tsx
  • frontend/src/app/products/[id]/components/store-item/store-prices-table.tsx
  • frontend/src/app/products/[id]/page.tsx
  • frontend/src/app/products/components/forms/add-to-shopping-list-form.tsx
  • frontend/src/app/products/components/forms/mark-as-checked-checkbox.tsx
  • frontend/src/app/products/components/forms/quantity-input.tsx
  • frontend/src/app/products/components/forms/shopping-list-selector.tsx
  • frontend/src/app/products/components/forms/watchlist-item-modal.tsx
  • frontend/src/app/products/components/product-action-buttons.tsx
  • frontend/src/app/products/components/product-info-display.tsx
  • frontend/src/app/products/components/product-info-table.tsx
  • frontend/src/app/products/components/product-item/product-info.tsx
  • frontend/src/app/products/components/product-item/product-price.tsx
  • frontend/src/app/products/components/products-client.tsx
  • frontend/src/app/products/components/watchlist-action-button.tsx
  • frontend/src/app/products/page.tsx
  • frontend/src/app/providers/providers.tsx
  • frontend/src/app/providers/react-query-provider.tsx
  • frontend/src/app/reset-password/page.tsx
  • frontend/src/app/reset-password/reset-password-modal.tsx
  • frontend/src/app/statistics/components/health-status.tsx
  • frontend/src/app/statistics/components/store-item.tsx
  • frontend/src/app/statistics/components/stores-list.tsx
  • frontend/src/app/statistics/page.tsx
  • frontend/src/app/suggestions/[id]/components/suggestion-details-client.tsx
  • frontend/src/app/suggestions/[id]/page.tsx
  • frontend/src/app/suggestions/components/suggestions-client.tsx
  • frontend/src/app/suggestions/page.tsx
  • frontend/src/app/sw.ts
  • frontend/src/app/terms-of-service/page.tsx
  • frontend/src/app/updates/[id]/page.tsx
  • frontend/src/app/updates/components/updates-client.tsx
  • frontend/src/app/updates/page.tsx
  • frontend/src/components/custom/app-sidebar.tsx
  • frontend/src/components/custom/camera-scanner.tsx
  • frontend/src/components/custom/coming-soon.tsx
  • frontend/src/components/custom/footer.tsx
  • frontend/src/components/custom/header/forms/account-credentials-form.tsx
  • frontend/src/components/custom/header/forms/auth-modal.tsx
  • frontend/src/components/custom/header/forms/forgot-password-form.tsx
  • frontend/src/components/custom/header/forms/inbox-notice.tsx
  • frontend/src/components/custom/header/forms/linked-accounts.tsx
  • frontend/src/components/custom/header/forms/login-form.tsx
  • frontend/src/components/custom/header/forms/profile-modal.tsx
  • frontend/src/components/custom/header/forms/security-modal.tsx
  • frontend/src/components/custom/header/forms/signup-form.tsx
  • frontend/src/components/custom/header/forms/user-preferences-modal.tsx
  • frontend/src/components/custom/header/header.tsx
  • frontend/src/components/custom/header/notifications-dropdown.tsx
  • frontend/src/components/custom/header/user-menu.tsx
  • frontend/src/components/custom/language-switcher.tsx
  • frontend/src/components/custom/legal-page.tsx
  • frontend/src/components/custom/no-results.tsx
  • frontend/src/components/custom/oauth-error-toast.tsx
  • frontend/src/components/custom/offline/last-synced-label.tsx
  • frontend/src/components/custom/offline/offline-indicator.tsx
  • frontend/src/components/custom/price-history-period-select.tsx
  • frontend/src/components/custom/pwa/apple-splash-screens.tsx
  • frontend/src/components/custom/pwa/install-banner.tsx
  • frontend/src/components/custom/pwa/install-instructions-sheet.tsx
  • frontend/src/components/custom/pwa/install-sidebar-banner.tsx
  • frontend/src/components/custom/pwa/request-persistent-storage.tsx
  • frontend/src/components/custom/pwa/use-install-prompt.ts
  • frontend/src/components/custom/search-bar.tsx
  • frontend/src/components/custom/store-chain-multi-select.tsx
  • frontend/src/components/custom/store-chain-select.tsx
  • frontend/src/components/custom/view-switcher.tsx
  • frontend/src/constants/ios-splash-screens.json
  • frontend/src/context/user-context.tsx
  • frontend/src/emails/change-email-confirmation.tsx
  • frontend/src/emails/components/action-email.tsx
  • frontend/src/emails/components/email-layout.tsx
  • frontend/src/emails/email-translator.ts
  • frontend/src/emails/password-reset-email.tsx
  • frontend/src/emails/set-password-email.tsx
  • frontend/src/emails/verification-email.tsx
  • frontend/src/hooks/use-nav-translation.ts
  • frontend/src/hooks/use-online-status.ts
  • frontend/src/i18n/config.ts
  • frontend/src/i18n/get-request-locale.ts
  • frontend/src/i18n/intl-client-provider.tsx
  • frontend/src/i18n/locale-actions.ts
  • frontend/src/i18n/message-fallback.ts
  • frontend/src/i18n/messages/de.json
  • frontend/src/i18n/messages/en.json
  • frontend/src/i18n/messages/hr.json
  • frontend/src/i18n/messages/sl.json
  • frontend/src/i18n/request.ts
  • frontend/src/lib/api/shopping-lists/index.ts
  • frontend/src/lib/api/watchlist/index.ts
  • frontend/src/lib/auth.ts
  • frontend/src/lib/email/email-service.ts
  • frontend/src/lib/offline/cached-query-keys.ts
  • frontend/src/lib/offline/offline-mutation-keys.ts
  • frontend/src/lib/offline/offline-mutations.ts
  • frontend/src/lib/offline/persister.ts
  • frontend/src/lib/offline/purge.ts
  • frontend/src/typings/local-storage.ts
  • frontend/src/typings/next-intl.d.ts
  • frontend/src/utils/browser/local-storage.ts
  • frontend/src/utils/date.ts
  • frontend/tsconfig.json

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@netlify

netlify Bot commented Jul 4, 2026

Copy link
Copy Markdown

Deploy Preview for disscount canceled.

Name Link
🔨 Latest commit c8e1516
🔍 Latest deploy log https://app.netlify.com/projects/disscount/deploys/6a496e2f4c57e00008030983

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces cookie-based internationalization (next-intl) across the Next.js frontend, while also adding PWA/offline capabilities (Serwist service worker + React Query persistence) and migrating a large amount of UI copy (and transactional email copy) to translation catalogs.

Changes:

  • Add next-intl configuration (request-based locale resolution, typed message augmentation, locale switcher, message fallback handling) and enforce i18n via ESLint + a catalog parity script.
  • Add offline support via React Query persisted cache (IndexedDB), offline mutation allowlisting, logout cache purge, offline UI indicators, and an /offline fallback page.
  • Add PWA wiring (manifest, icons/splash tooling, install banners, Serwist service worker and runtime caching rules).

Reviewed changes

Copilot reviewed 161 out of 188 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
frontend/tsconfig.json Adds Serwist typings and WebWorker lib; excludes generated SW output.
frontend/src/utils/date.ts Adds relative time formatting helper for UI “last synced” labels.
frontend/src/utils/browser/local-storage.ts Adds persisted “install banner dismissed” flag helpers.
frontend/src/typings/next-intl.d.ts Augments next-intl types with app locale + message schema.
frontend/src/typings/local-storage.ts Extends local storage typing with install banner flag.
frontend/src/lib/offline/purge.ts Adds helper to clear in-memory + persisted React Query caches.
frontend/src/lib/offline/persister.ts Implements IndexedDB persister + persist options for React Query.
frontend/src/lib/offline/offline-mutation-keys.ts Defines allowlisted offline mutation keys and persistence predicate.
frontend/src/lib/offline/cached-query-keys.ts Defines persisted query key prefixes and whitelist check.
frontend/src/lib/email/email-service.ts Localizes transactional email subjects/templates via locale-aware translator.
frontend/src/lib/auth.ts Resolves request locale and passes it into email sending flows.
frontend/src/lib/api/watchlist/index.ts Adds offline mutation keys to watchlist mutations for replay.
frontend/src/lib/api/shopping-lists/index.ts Adds offline mutation keys to shopping list mutations for replay.
frontend/src/i18n/request.ts Adds per-request locale/messages resolution (cookie + Accept-Language).
frontend/src/i18n/message-fallback.ts Adds hr-based fallback resolution and error handling for missing keys.
frontend/src/i18n/locale-actions.ts Adds server action to persist locale cookie.
frontend/src/i18n/intl-client-provider.tsx Wraps NextIntlClientProvider with shared fallback/error handler.
frontend/src/i18n/get-request-locale.ts Adds locale resolver for non-render server contexts (emails).
frontend/src/i18n/config.ts Defines locale list, cookie name, timezone, and Accept-Language matching.
frontend/src/hooks/use-online-status.ts Adds shared online/offline source of truth using React Query onlineManager.
frontend/src/hooks/use-nav-translation.ts Centralizes navigation label lookup via next-intl.
frontend/src/emails/verification-email.tsx Localizes verification email copy via email translator.
frontend/src/emails/set-password-email.tsx Localizes set-password email copy via email translator.
frontend/src/emails/password-reset-email.tsx Localizes password reset email copy via email translator.
frontend/src/emails/email-translator.ts Adds locale-bound translator for email templates (outside request context).
frontend/src/emails/components/email-layout.tsx Localizes footer and sets HTML lang based on locale.
frontend/src/emails/components/action-email.tsx Localizes link fallback text and supports rich footnotes.
frontend/src/emails/change-email-confirmation.tsx Localizes change-email confirmation copy via rich translations.
frontend/src/context/user-context.tsx Purges persisted/offline cache on logout (instead of only in-memory clear).
frontend/src/constants/ios-splash-screens.json Adds iOS device list for generating apple startup images.
frontend/src/components/custom/view-switcher.tsx Translates aria-labels/tooltips for view mode toggle.
frontend/src/components/custom/store-chain-select.tsx Translates placeholder and refactors diff color logic.
frontend/src/components/custom/store-chain-multi-select.tsx Translates placeholder for multi-select.
frontend/src/components/custom/search-bar.tsx Translates default placeholder/labels and aria text.
frontend/src/components/custom/pwa/request-persistent-storage.tsx Requests persistent storage to reduce IndexedDB eviction risk.
frontend/src/components/custom/pwa/install-sidebar-banner.tsx Adds persistent sidebar install CTA with i18n copy.
frontend/src/components/custom/pwa/install-instructions-sheet.tsx Adds i18n’d manual install instructions sheet (iOS vs others).
frontend/src/components/custom/pwa/install-banner.tsx Adds dismissible bottom install banner w/ persisted dismissal.
frontend/src/components/custom/pwa/apple-splash-screens.tsx Renders <link rel="apple-touch-startup-image"> tags from device list.
frontend/src/components/custom/price-history-period-select.tsx Localizes period labels.
frontend/src/components/custom/offline/offline-indicator.tsx Adds offline banner with pending write count.
frontend/src/components/custom/offline/last-synced-label.tsx Adds “last refreshed” label using relative time (client-only).
frontend/src/components/custom/oauth-error-toast.tsx Localizes OAuth error toasts via key mapping.
frontend/src/components/custom/no-results.tsx Localizes default “no results” title/description.
frontend/src/components/custom/legal-page.tsx Converts legal wrapper to server component with localized labels.
frontend/src/components/custom/language-switcher.tsx Adds cookie-based language switcher UI.
frontend/src/components/custom/header/user-menu.tsx Localizes user menu labels and account type badge text.
frontend/src/components/custom/header/notifications-dropdown.tsx Localizes notifications dropdown copy.
frontend/src/components/custom/header/forms/signup-form.tsx Localizes signup UI and error messages.
frontend/src/components/custom/header/forms/login-form.tsx Localizes login UI and error messages.
frontend/src/components/custom/header/forms/linked-accounts.tsx Localizes linked accounts UI and toasts.
frontend/src/components/custom/header/forms/inbox-notice.tsx Localizes spam hint text.
frontend/src/components/custom/header/forms/forgot-password-form.tsx Localizes forgot-password UI and validation message.
frontend/src/components/custom/footer.tsx Localizes footer aria/alt text and integrates language switcher.
frontend/src/components/custom/coming-soon.tsx Localizes “coming soon” badge/default description.
frontend/src/components/custom/camera-scanner.tsx Localizes scanner UI/error messages.
frontend/src/app/updates/page.tsx Localizes metadata via generateMetadata.
frontend/src/app/updates/components/updates-client.tsx Localizes Updates search/heading/button copy.
frontend/src/app/updates/[id]/page.tsx Localizes metadata fallbacks and back link.
frontend/src/app/terms-of-service/page.tsx Localizes legal page content and metadata.
frontend/src/app/privacy-policy/page.tsx Localizes legal page content and metadata.
frontend/src/app/data-deletion/page.tsx Localizes legal page content and metadata.
frontend/src/app/reset-password/page.tsx Localizes metadata via generateMetadata.
frontend/src/app/offline/page.tsx Adds offline fallback page for SW navigation fallback.
frontend/src/app/offline/components/offline-retry-button.tsx Adds client retry button with localized label.
frontend/src/app/not-found.tsx Localizes 404 page metadata and content.
frontend/src/app/map/page.tsx Localizes metadata via generateMetadata.
frontend/src/app/map/components/map-client.tsx Localizes map page placeholder/copy.
frontend/src/app/suggestions/page.tsx Localizes metadata via generateMetadata.
frontend/src/app/suggestions/components/suggestions-client.tsx Localizes suggestions heading/search.
frontend/src/app/suggestions/[id]/page.tsx Localizes suggestion detail metadata.
frontend/src/app/suggestions/[id]/components/suggestion-details-client.tsx Localizes back link and comments section copy.
frontend/src/app/statistics/page.tsx Localizes metadata and page heading/description.
frontend/src/app/statistics/components/stores-list.tsx Localizes statistics list headings/loading/empty state.
frontend/src/app/statistics/components/store-item.tsx Localizes per-chain stats and table labels/unknown copy.
frontend/src/app/statistics/components/health-status.tsx Localizes API health status copy.
frontend/src/app/dashboard/page.tsx Localizes metadata via generateMetadata.
frontend/src/app/dashboard/components/dashboard-content.tsx Localizes dashboard headings and empty/admin copy.
frontend/src/app/products/page.tsx Localizes metadata via generateMetadata.
frontend/src/app/products/components/products-client.tsx Localizes search placeholder, headings, and error/empty states.
frontend/src/app/products/components/watchlist-action-button.tsx Localizes watchlist action label.
frontend/src/app/products/components/product-item/product-price.tsx Localizes unknown price fallback.
frontend/src/app/products/components/product-item/product-info.tsx Localizes unknown product name fallback.
frontend/src/app/products/components/product-info-table.tsx Localizes product info table labels and unknown fallbacks.
frontend/src/app/products/components/product-info-display.tsx Localizes unknown name fallback in product header.
frontend/src/app/products/components/product-action-buttons.tsx Localizes tooltips/aria-labels for product actions.
frontend/src/app/products/components/forms/shopping-list-selector.tsx Localizes labels/placeholders/groups for add-to-list selector.
frontend/src/app/products/components/forms/quantity-input.tsx Localizes quantity label + aria labels.
frontend/src/app/products/components/forms/mark-as-checked-checkbox.tsx Localizes mark-as-checked label and hint.
frontend/src/app/products/[id]/page.tsx Localizes metadata via generateMetadata.
frontend/src/app/products/[id]/components/store-item/store-prices-table.tsx Localizes table headers and unknown fallbacks.
frontend/src/app/products/[id]/components/store-item/store-item.tsx Localizes store item labels and badges.
frontend/src/app/products/[id]/components/product-detail-client.tsx Adds last-synced label and localizes copy/states.
frontend/src/app/products/[id]/components/price-history/price-history-chart.tsx Localizes “average” series label.
frontend/src/app/products/[id]/components/price-history/price-history-base.tsx Localizes headings/toggles/empty states.
frontend/src/app/(root)/page.tsx Localizes home page metadata via generateMetadata.
frontend/src/app/(root)/components/sections/hero-section.tsx Moves taglines into i18n catalogs and picks one at render time.
frontend/src/app/(root)/components/sections/hero-actions.tsx Localizes hero search placeholder and CTA copy.
frontend/src/app/(user)/watchlist/page.tsx Localizes metadata via generateMetadata.
frontend/src/app/(user)/watchlist/components/watchlist-item.tsx Localizes aria labels and toast messages.
frontend/src/app/(user)/watchlist/components/watchlist-item-discount-info.tsx Localizes discount info states and actions.
frontend/src/app/(user)/watchlist/components/create-discounted-list-button.tsx Localizes button labels and toast messages.
frontend/src/app/(user)/spending/page.tsx Localizes metadata and coming-soon copy.
frontend/src/app/(user)/shopping-lists/page.tsx Localizes metadata via generateMetadata.
frontend/src/app/(user)/shopping-lists/components/shopping-lists-client.tsx Localizes search/heading and empty state copy.
frontend/src/app/(user)/shopping-lists/components/shopping-list-item.tsx Localizes public/private tooltip text.
frontend/src/app/(user)/shopping-lists/components/forms/shopping-list-modal.tsx Localizes modal title/labels/placeholders/actions.
frontend/src/app/(user)/shopping-lists/components/forms/delete-shopping-list-dialog.tsx Localizes delete dialog title/description/buttons.
frontend/src/app/(user)/shopping-lists/components/create-shopping-list-button.tsx Localizes create list button label.
frontend/src/app/(user)/shopping-lists/hooks/use-shopping-list-modal.ts Improves offline UX: closes modal immediately and shows info toast.
frontend/src/app/(user)/shopping-lists/[id]/page.tsx Localizes metadata via generateMetadata.
frontend/src/app/(user)/shopping-lists/[id]/hooks/use-shopping-list-data.ts Exposes dataUpdatedAt for last-synced label usage.
frontend/src/app/(user)/shopping-lists/[id]/hooks/use-shopping-list-mutations.ts Localizes toasts and copy suffix for list duplication.
frontend/src/app/(user)/shopping-lists/[id]/hooks/use-shopping-list-item-mutations.ts Localizes item mutation toasts.
frontend/src/app/(user)/shopping-lists/[id]/components/shopping-list-detail-client.tsx Adds last-synced label and localizes error/back copy.
frontend/src/app/(user)/shopping-lists/[id]/components/shopping-list-header.tsx Localizes public/private title tooltip text.
frontend/src/app/(user)/shopping-lists/[id]/components/shopping-list-info-table.tsx Localizes labels for stats table (created/updated/total/etc).
frontend/src/app/(user)/shopping-lists/[id]/components/shopping-list-price-history.tsx Localizes headings/toggles/empty states.
frontend/src/app/(user)/shopping-lists/[id]/components/items/shopping-list-items.tsx Localizes products header and empty state copy.
frontend/src/app/(user)/shopping-lists/[id]/components/items/shopping-list-item.tsx Localizes aria labels for quantity/remove actions.
frontend/src/app/(user)/shopping-lists/[id]/components/stores/shopping-list-stores-list.tsx Localizes store chain section headings/toggles/errors.
frontend/src/app/(user)/shopping-lists/[id]/components/stores/shopping-list-store-card.tsx Localizes availability/status badges and labels.
frontend/src/app/(user)/shopping-lists/[id]/components/stores/shopping-list-items-table.tsx Localizes table headers/tooltips and sr-only labels.
frontend/src/app/(user)/digital-cards/page.tsx Localizes metadata via generateMetadata.
frontend/src/app/(user)/digital-cards/components/digital-cards-client.tsx Localizes headings/placeholders/empty state and FAB label.
frontend/src/app/(user)/digital-cards/components/digital-card-item.tsx Localizes confirm/toasts and labels.
frontend/src/app/layout.tsx Integrates next-intl provider, localized metadata, offline/install UI, and PWA metadata.
frontend/src/app/manifest.ts Adds PWA web manifest with shortcuts and screenshots.
frontend/src/app/sw.ts Adds Serwist service worker with API caching rules and offline fallback.
frontend/src/app/providers/react-query-provider.tsx Switches to PersistQueryClientProvider and registers offline replay defaults.
frontend/src/app/providers/providers.tsx Adds persistent storage request and iOS splash link rendering into provider tree.
frontend/scripts/check-i18n.mjs Adds catalog parity checker for CI/local gating.
frontend/scripts/generate-pwa-icons.mjs Adds icon generation script for manifest/layout icons.
frontend/scripts/generate-pwa-screenshots.mjs Adds placeholder screenshot generator for manifest screenshots.
frontend/scripts/generate-ios-splash.mjs Adds iOS splash generator driven by shared device list.
frontend/eslint.config.mjs Adds i18next/no-literal-string rule for JSX + key attributes.
frontend/next.config.ts Adds next-intl plugin and Serwist integration; precaches offline route.
frontend/package.json Adds i18n check script and new dependencies for next-intl + offline/PWA.
frontend/pnpm-workspace.yaml Allows builds for additional native deps used in the toolchain.
frontend/.gitignore Ignores generated Serwist outputs in public/.
AGENTS.md Documents i18n conventions and required checks.
.gitignore Adds ignore for docs/disscount_* artifacts.
.claude/skills/document-subsystem/SKILL.md Adds a documentation skill description for subsystem docs.
Files not reviewed (1)
  • frontend/pnpm-lock.yaml: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +7 to +10
export async function purgeOfflineCache(queryClient: QueryClient) {
queryClient.clear();
await offlinePersister.removeClient();
}
Comment on lines +42 to +44
onSuccess={() => {
queryClient.resumePausedMutations();
}}
Comment on lines +11 to +12
import RequestPersistentStorage from "@/components/custom/pwa/request-persistent-storage";
import AppleSplashScreens from "@/components/custom/pwa/apple-splash-screens";
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.

2 participants