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.
- 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).
- Runtime —
next-intl if the dashboard is a Next.js App Router app
(lowest friction, server-component friendly); react-i18next otherwise.
- Locale resolution — user preference (persisted on the account) →
Accept-Language → en. Cookie/header-based rather than /[locale]/...
path prefixes, so existing links and — importantly — Meta webhook and API
routes are untouched.
- Language switcher in Settings, next to the existing preferences.
- Locale-aware formatting via
Intl.DateTimeFormat / Intl.NumberFormat.
Timestamps stay UTC in Postgres, rendering happens in the user's locale and
timezone.
- 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.
- 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
Acceptance Criteria
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.
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:
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.
internal team and cannot hand over a dashboard in the team's working language.
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.
(
messages/en.jsonas the single source of truth), one namespace per featurearea (campaigns, links, analytics, settings, auth).
next-intlif the dashboard is a Next.js App Router app(lowest friction, server-component friendly);
react-i18nextotherwise.Accept-Language→en. Cookie/header-based rather than/[locale]/...path prefixes, so existing links and — importantly — Meta webhook and API
routes are untouched.
Intl.DateTimeFormat/Intl.NumberFormat.Timestamps stay UTC in Postgres, rendering happens in the user's locale and
timezone.
en+ one reference locale (I can providefr) so pluralization,interpolation and layout overflow are actually exercised, then open the rest
to community PRs.
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
Acceptance Criteria
the translation function
messages/en.jsonexists as the source locale, with at least oneadditional locale shipped end to end
survives reload and re-login
Accept-Language→enen— it never renders a raw key or breaksthe page
Intl.*in the activelocale
"12 replies sent")
/api/*routes, webhook handling and keyword matching are unaffected —no localized path segments, no locale-dependent matching logic
explicitly documented as out of scope for v1
CONTRIBUTINGdocuments how to add a localeNotes
Prior art worth copying: Cal.com runs
next-intlwith communitytranslations 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
frlocale, intwo reviewable commits (mechanical extraction first, then the runtime), if the
approach above suits you.