Skip to content

i18n #14

Description

@kevingrondin

Problem

The dashboard is English-only. Every user-facing string is hardcoded in the
components, so there is no way to run OpenReply in another language without
forking and patching the UI.

This hurts three groups:

  • Non-English creators/brands who have to configure things that matter —
    keyword matching, DM copy, follow-gate wording, campaign settings — through
    an interface they only partially understand. A misread setting here is a
    misconfigured campaign, not just a cosmetic annoyance.
  • Agencies and self-hosters who deploy an instance for a client or an
    internal team and cannot hand over a dashboard in the team's working language.
  • Contributors who would happily translate the product but have nowhere to
    put the strings today.

Secondary issue: dates, numbers and percentages (clicks, CTR) are rendered in
en-US format regardless of who is looking at them.

Proposed Solution

Introduce a proper i18n layer, English staying the source locale.

  1. Externalize strings — extract all user-facing copy into message catalogs
    (messages/en.json as the single source of truth), one namespace per feature
    area (campaigns, links, analytics, settings, auth).
  2. Runtimenext-intl if the dashboard is a Next.js App Router app
    (lowest friction, server-component friendly); react-i18next otherwise.
  3. Locale resolution — user preference (persisted on the account) →
    Accept-Languageen. Cookie/header-based rather than /[locale]/...
    path prefixes, so existing links and — importantly — Meta webhook and API
    routes are untouched.
  4. Language switcher in Settings, next to the existing preferences.
  5. Locale-aware formatting via Intl.DateTimeFormat / Intl.NumberFormat.
    Timestamps stay UTC in Postgres, rendering happens in the user's locale and
    timezone.
  6. Ship en + one reference locale (I can provide fr) so pluralization,
    interpolation and layout overflow are actually exercised, then open the rest
    to community PRs.
  7. Guardrail in CI — a check that fails on missing or orphaned keys, so
    locales don't silently drift as features land.

Deliberately out of scope for a v1, unless you want it in: translating
outbound content (DMs, public replies). That's user-authored copy and should
probably become per-campaign language settings in a separate issue.

Target User

  • Agency
  • Creator
  • Brand
  • Self-hoster
  • Other: contributors/translators

Acceptance Criteria

  • No user-facing string hardcoded in a component; all copy resolves through
    the translation function
  • messages/en.json exists as the source locale, with at least one
    additional locale shipped end to end
  • Language switcher in Settings; the choice is persisted per account and
    survives reload and re-login
  • Initial locale resolves as: account preference → Accept-Languageen
  • A missing key falls back to en — it never renders a raw key or breaks
    the page
  • Dates, click counts and CTR are formatted through Intl.* in the active
    locale
  • Plurals and interpolation are covered (e.g. "1 reply sent" /
    "12 replies sent")
  • /api/* routes, webhook handling and keyword matching are unaffected —
    no localized path segments, no locale-dependent matching logic
  • Only the active locale's catalog reaches the client bundle
  • Transactional emails (login links) are sent in the recipient's locale, or
    explicitly documented as out of scope for v1
  • CONTRIBUTING documents how to add a locale

Notes

Prior art worth copying: Cal.com runs next-intl with community
translations managed in Crowdin and carries ~30 locales without the
maintainers doing the translating. Documenso and Ghost follow the same
pattern. Crowdin and Weblate both have free tiers for open-source projects, so
the ongoing cost is a review workflow, not money.

The main risk is contributor churn on catalogs, which is exactly why the CI key
check matters more than the number of locales at launch.

Happy to open the PR myself: string extraction + wiring + the fr locale, in
two reviewable commits (mechanical extraction first, then the runtime), if the
approach above suits you.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions