Skip to content

feat(mobile): add i18n support and Polish translation - #3097

Open
Dragonk wants to merge 8 commits into
karakeep-app:mainfrom
Dragonk:feat/mobile-i18n-polish-upstream
Open

Dragonk wants to merge 8 commits into
karakeep-app:mainfrom
Dragonk:feat/mobile-i18n-polish-upstream

Conversation

@Dragonk

@Dragonk Dragonk commented Sep 18, 2026 •

Copy link
Copy Markdown

Summary

Adds full internationalization/localization (i18n) support to the Karakeep mobile app (apps/mobile) together with a complete Polish translation.

  • New i18n infrastructure in apps/mobile/lib/i18n/ (singleton, provider, hooks, typed keys).
  • English as source/fallback language, Polish as the second supported language.
  • Automatic language selection from the Android/iOS system locale, with a manual override in Settings: System / English / Polish.
  • All user-visible mobile UI strings migrated from hardcoded English to translation keys.
  • Locale-aware date/number formatting (date-fns + Intl follow the app language).

Architecture

  • i18next + react-i18next — the same libraries the web app uses, so interpolation ({{name}}, {{count}}) and plural handling behave identically.
  • expo-localization for OS locale detection.
  • Two namespaces:
    • mobile — mobile-only strings (apps/mobile/lib/i18n/locales/{en,pl}.json).
    • translation — the web app's locale files, reused verbatim so shared concepts (actions.*, common.*, dialogs.*, toasts.*) render exactly the same wording as on web, including the community Polish translations.
  • Typed translation keys via types.d.ts (derived from the en files).
  • Shared translations are consumed via useSharedTranslation / useCommonActions / useCommonStrings, never via t("translation:...") on the mobile-namespaced function (that pattern is a type error with typed resources and tripped the TS 5.9 checker when t was passed as an argument).

Mobile translations are intentionally not a direct import of all web translations: only a subset of strings overlaps, and Metro/React Native cannot bundle the web's dynamic resourcesToBackend imports. Instead, tools/mobile-i18n-sync.mjs copies the shared translation namespace into apps/mobile/lib/i18n/web-translations/, keeping one source of truth while satisfying Metro's static bundling.

Polish translation

  • Full pl for the mobile namespace, 1:1 key parity with en (443 keys each, enforced by tooling + tests + CI).
  • Existing Karakeep web terminology reused where applicable.
  • Polish pluralization supported via Intl.PluralRules (pinned by tests).

Locale resolution

Exact tag → base language → English fallback:

  • pl-PL → pl, pl → pl
  • en-US → en, en-GB → en
  • unsupported (e.g. de-DE, uk-UA) → en

Quality / Tests

All verified on this branch:

  • TypeScript (tsc --noEmit on apps/mobile): clean.
  • Lint (oxlint): 0 warnings, 0 errors.
  • Format (oxfmt --check): clean.
  • Tests (vitest run --root apps/mobile): 46 passed — locale matching (pl-PL, pl, en-US, en-GB, unsupported), system → pl → en → system switching, en↔pl key parity, runtime interpolation, both namespaces, locale-aware date formatting, and plural resolution (zakładka/zakładki/zakładek, element/elementy/elementów, wynik/wyniki/wyników) with a guard that every plural category a language can produce has a translation.
  • Review feedback from the first round (device-locale dates, two-form plurals, hardcoded language list in the sync tool) is addressed in aaddd9ed.
  • Translation parity (node tools/mobile-i18n-sync.mjs --check): in sync, 443 keys.
  • Runtime i18n checks (real i18next instance, PL strings, interpolations, shared namespace): pass.

Android test build

A standalone release APK was built from the companion fork branch via a GitHub Actions workflow (expo prebuild + ./gradlew assembleRelease), producing app-release.apk signed with the standard Gradle debug keystore (no signing secrets in the repo, no Metro/dev-client server needed — the release bundle is embedded and expo-dev-launcher is inert in release builds).

Green run: https://github.com/Dragonk/karakeep/actions/runs/35357143694 — APK published as the karakeep-mobile-i18n-pl-release-apk workflow artifact.

The workflow itself lives only on the fork: it exists to hand out test APKs and is not proposed for upstream in this PR. Happy to contribute a mobile CI build separately if maintainers want one.

Manual testing

Tested on a physical Android device: the APK installs and runs correctly, and the language picker (System / English / Polish) works as expected — Polish strings render throughout the UI and system-language detection picks the right language.

Existing translation tooling

tools/mobile-i18n-sync.mjs syncs the shared web translation namespace into mobile and (with --check, also wired as a CI job) fails on stale copies or mobile-namespace key drift between languages. Adding a language = sync + one JSON file + one line in SUPPORTED_LANGUAGES (see apps/mobile/lib/i18n/README.md).

Closes #3096

Update: rebased onto the latest main

This PR is now based on the current upstream/main (all merge conflicts resolved), and the rebase was used to bring the i18n coverage fully up to date with the newest code:

  • Resolved all conflicts with recent upstream changes, including the new offline-download screens and the reworked list sharing flows.
  • New upstream screens/flows are translated as well: offline settings (downloads, articles, lists), list sharing & access, pending list invitations, and the attachments box.
  • Locale-aware formatting follows the selected language: formatStorageSize now takes an optional Intl locale, and dates use date-fns locales.
  • Offline unit tests mock the i18n module so the platform-independent offline logic stays testable in plain Node.
  • All gates pass on the rebased tree: typecheck, lint (no new issues vs main), format check, unit tests (79/79), and mobile-i18n-sync.mjs --check key parity (542 keys).

As noted above, this PR also opens up adding further languages to the mobile app — a new language is just a synced web-translations/<lang>.json, a locales/<lang>.json> file, and one entry in SUPPORTED_LANGUAGES; no additional code changes are needed.

cc @MohamedBassem — as the maintainer with merge rights, could you take a look when you get a chance? Happy to address any feedback.

Add i18next + react-i18next + expo-localization based localization
for the mobile app:

- apps/mobile/lib/i18n scaffolding (singleton, hooks, provider,
  typed keys) with 'mobile' and shared web 'translation' namespaces
- English source strings (mobile namespace)
- web translation sync via tools/mobile-i18n-sync.mjs
- language setting plumbing (system/en + override)
- locale-aware date/number formatting helpers
- vitest config
Add full Polish (pl) strings for the mobile namespace, keeping
1:1 key parity with English (443 keys). Reuses existing Karakeep
web terminology where applicable and supports Polish
pluralization via Intl.PluralRules.

Add vitest coverage for locale matching (pl-PL, en-US, en-GB,
unsupported), system/pl/en/system switching, en-pl parity,
runtime interpolation and both namespaces.
Migrate all user-visible mobile UI strings to the translation
system: navigation titles, tab bar, auth, bookmarks, lists,
tags, highlights, search, emoji picker, settings (incl. new
Language picker with System/English/Polish) and shared lib
helpers. Shared web concepts reuse the translation namespace
via useSharedTranslation/useCommonActions/useCommonStrings.

Also add the mobile-i18n CI check job.
@Dragonk
Dragonk force-pushed the feat/mobile-i18n-polish-upstream branch from 348362f to eb1a689 Compare September 18, 2026 15:13
@Dragonk
Dragonk marked this pull request as ready for review September 18, 2026 15:34
@greptile-apps

greptile-apps Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

The PR appears safe to merge, with non-blocking localization correctness and tooling gaps that should be addressed.

Findings

  1. P2 Dates Ignore App Language ▶
  2. P2 Polish Plural Forms Missing ▶
  3. P2 Future Locales Skip Validation ▶
Fix with agent prompt
### Issue 1
apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx:631
When the selected app language differs from the device locale, these timestamps still use `toLocaleString()` without the app locale. The surrounding labels are translated, but the dates remain formatted in the device language. Pass `getIntlLocale()` to both the created and modified date calls so they follow the selected language.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

### Issue 2
apps/mobile/app/dashboard/(tabs)/(tags)/index.tsx:116-118
This count only distinguishes `1` from every other value, but Polish requires separate `one`, `few`, and `many` forms. For example, five bookmarks renders as “5 zakładki” instead of “5 zakładek.” Use count-based i18next keys such as `_one`, `_few`, and `_many`. The same two-form handling affects storage item counts, and `emoji.results` uses one base string that renders values such as “5 wyniki.”

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

### Issue 3
tools/mobile-i18n-sync.mjs:41
The default sync and CI check hardcode English and Polish. Adding another entry to `SUPPORTED_LANGUAGES` therefore leaves that locale outside stale-copy and key-parity validation, allowing incomplete translations to merge unnoticed. Derive the checked languages from the registered mobile resources or another shared manifest.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR adds mobile internationalization with English and Polish resources, system/manual language selection, translated navigation and UI strings, app-language formatting helpers, shared web translations, and CI synchronization checks.

  • Introduces a typed i18next singleton, provider, hooks, and locale-resolution tests.
  • Adds a language settings screen and persists the selected language with existing mobile settings.
  • Migrates user-facing mobile strings across bookmark, list, tag, search, settings, sharing, and offline flows.
  • Adds tooling that copies shared web translations into Metro-bundleable mobile resources.
  • Remaining concerns are localized date formatting, Polish plural forms, and future-language CI coverage.

Reviews (1) · Last reviewed commit: "feat(mobile): migrate UI strings to i18n..."

Comment thread apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx Outdated
Comment thread apps/mobile/app/dashboard/(tabs)/(tags)/index.tsx Outdated
Comment thread tools/mobile-i18n-sync.mjs Outdated

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

Important

Polish pluralization renders the wrong noun form for counts of 5+ in two places, and emoji.results has no plural forms at all (wrong Polish for every count, and "1 results" in English). The new mobile vitest suite is also never executed by CI, so these regressions cannot fail the build.

Reviewed changes

  • i18n infrastructure — i18next/react-i18next singleton with mobile + synced web translation namespaces, locale detection via expo-localization, and typed keys derived from the en files.
  • Language provider + setting — I18nProvider applies a persisted language override (System / English / Polish); new language picker screen registered in the dashboard layout.
  • Web-translation sync tooling — tools/mobile-i18n-sync.mjs copies the web translation namespace into mobile and, with --check, fails on stale copies and mobile-namespace key drift.
  • String migration — ~70 screens/components moved from hardcoded English to translation keys, with locale-aware date/number formatting for the active language.
  • Tests — locale matching/resolution, en↔pl key parity, runtime interpolation, and Intl.PluralRules("pl") cases under apps/mobile.

⚠️ The new mobile test suite is never run in CI

The mobile-i18n job only runs the sync/parity script, and the tests job enumerates packages (shared, trpc, workers, e2e_tests) without apps/mobile. So apps/mobile/lib/i18n/__tests__/language.test.ts — the tests the PR description leans on for language resolution and plural handling — never executes on any PR.

Technical details
# Mobile tests are not run in CI

## Affected sites
- `.github/workflows/ci.yml:45-56` — `mobile-i18n` job runs only `node tools/mobile-i18n-sync.mjs --check`
- `.github/workflows/ci.yml:70-98` — `tests` job lists per-package steps and omits `apps/mobile`
- `apps/mobile/package.json:16` — the new `"test": "vitest run"` script is never invoked by a workflow

## Required outcome
- The `apps/mobile` vitest suite runs on PRs touching the mobile app / i18n, so locale-matching, en↔pl parity, and plural regressions fail the build.

## Suggested approach
- Add a step to the `tests` job (`working-directory: apps/mobile` + `run: pnpm test`), or extend the `mobile-i18n` job to also run the suite.

ℹ️ Nitpicks

  • apps/mobile/lib/i18n/hooks.ts:68 — retry: t("actions.refresh") maps "Retry" to "Refresh" and there is no actions.retry key. The field is currently unused, so it is cosmetic, but the mapping is wrong if it gets wired up.
  • apps/mobile/lib/i18n/locales/en.json:76 — "Recieve non success error code: " should be "Receive".
  • apps/mobile/components/ui/SearchInput/SearchInput.ios.tsx:52 — the default cancelText changed from "Cancel" to t("search.close") ("Close"); a dedicated cancel key would preserve the previous wording.
  • apps/mobile/lib/i18n/locales/{en,pl}.json define stats.morning_12am / morning_6am / noon_12pm / evening_6pm, but the migrated UsageStatsScreen now derives these via Intl.DateTimeFormat, so the keys are dead.
  • Completeness: the PR description says all user-visible strings were migrated, but apps/mobile/components/highlights/HighlightCard.tsx:107 still hardcodes Note: (a bookmarks.note_title key already exists), and some untouched files remain English-only (components/bookmarks/BookmarkContentLoading.tsx, components/sharing/LoadingAnimation.tsx, components/settings/settings-list.tsx's accessibilityHint), alongside UsageStatsScreen's accessibility labels / formatAssetType and the "OK" button in settings/index.tsx.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread apps/mobile/app/dashboard/(tabs)/(tags)/index.tsx Outdated
Comment thread apps/mobile/components/settings/UsageStatsScreen.tsx Outdated
Comment thread apps/mobile/components/ui/emoji-picker.tsx
Address review findings:

- bookmark info timestamps now format with the selected app language
  instead of the device locale
- bookmark/item/emoji counts use i18next plural keys, so Polish gets
  proper one/few/many forms ("5 zakładek", not "5 zakładki")
- mobile-i18n-sync derives the checked languages from the committed
  mobile locale files, so a newly registered language is automatically
  synced and key-checked instead of silently skipping validation
- extend the tests to cover plural categories per language and
  locale-aware date formatting

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

✅ No new issues found.

Reviewed changes

Delta since the prior pullfrog review (eb1a689e → aaddd9ed), which addresses the three inline plural findings:

  • Polish plural forms fixed — tags_tab.bookmarks, stats.item, and emoji.results now pass count to i18next, with _one/_few/_many/_other defined for both locales (English carries the extra categories to keep the sync check's key parity).
  • Device-locale dates/numbers wired up — bookmarks/[slug]/info.tsx uses toLocaleString(getIntlLocale()), and UsageStatsScreen derives its formatters plus hour/weekday labels from the active app language.
  • Sync tool language discovery — tools/mobile-i18n-sync.mjs now derives the language set from locales/*.json instead of a hardcoded list.
  • Plural/date regression tests — asserts UI plural forms across pl (1/2/4/5/22/25) and en, storage/emoji counts, and that every Intl.PluralRules category has a translation; also pins locale-aware dates.

Verified locally: pnpm --filter @karakeep/mobile test (46 passed), node tools/mobile-i18n-sync.mjs --check (in sync, 451-key parity), pnpm --filter @karakeep/mobile typecheck (clean).

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

…lish-upstream

# Conflicts:
#	apps/mobile/app/dashboard/(tabs)/(lists)/index.tsx
#	apps/mobile/app/dashboard/(tabs)/(tags)/index.tsx
#	apps/mobile/app/dashboard/_layout.tsx
#	apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx
#	apps/mobile/app/dashboard/settings/offline.tsx
#	apps/mobile/components/bookmarks/BookmarkList.tsx
#	apps/mobile/components/bookmarks/BookmarkTextView.tsx
#	apps/mobile/components/bookmarks/useOfflineAvailability.ts
#	apps/mobile/components/highlights/HighlightList.tsx
#	apps/mobile/components/search/BookmarkSearchResults.tsx
#	apps/mobile/lib/providers.tsx
#	apps/mobile/package.json
#	pnpm-lock.yaml

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

Important

One regression to address before merging: the tags list stopped showing bookmark counts.

Reviewed changes

Delta since the prior pullfrog review (aaddd9ed → 5c98e706) — a merge of upstream/main with i18n re-applied on top:

  • Offline-downloads screens translated — settings/offline.tsx, offline-articles.tsx, and offline-lists.tsx now use the settings.offline_* keys; formatStorageSize gained an Intl-locale argument and every caller passes getIntlLocale().
  • List-sharing flows translated — new components/lists/list-sharing-settings.tsx and pending-list-invitations.tsx, plus lists/[slug]/sharing.tsx and the entry points in edit.tsx / [slug]/index.tsx, use the new list_sharing.* keys.
  • Attachments box translated — AttachmentBox.tsx uses attachments.* and the shared common.attachments string.
  • New locale keys — list_sharing.* / attachments.* added with en↔pl parity; offline_recent_limit_* / offline_note_failed_* / offline_lists_selected_* embed {{count}}.
  • Expo 56→57 / RN 0.85→0.86 and the responsive-layout changes enter via the upstream merge, not this PR.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment thread apps/mobile/app/dashboard/(tabs)/(tags)/index.tsx
@Dragonk

Dragonk commented Oct 22, 2026

Copy link
Copy Markdown
Author

Rebased this onto the latest main (all conflicts resolved, and the new upstream screens are translated as well — details in the updated description).

CI is green and the PR is mergeable. This also lays the groundwork for adding more languages to the mobile app.

@MohamedBassem — since you're the one with merge rights here, could you take a look when you have a moment? Happy to address any feedback.

- add {{count}} to tags_tab.bookmarks plural forms so rows show the count again
- add mobile actions.retry key and fix the retry mapping (was actions.refresh)
- use shared actions.cancel for the iOS search cancel button (restores pre-i18n wording)
- translate the hardcoded Note: label in HighlightCard via bookmarks.note_title
- fix Recieve typo in connection_test.non_success_code
- drop unused stats.morning_12am/morning_6am/noon_12pm/evening_6pm keys

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

✅ No new issues found.

Reviewed changes

Delta since the prior pullfrog review (5c98e706 → 029e3c9c), which clears the outstanding inline count-drop finding and the earlier nitpicks:

  • Restored bookmark counts on the tags list — tags_tab.bookmarks_{one,other,few,many} now embed {{count}} in both locales, and the row call site passes { count } with no separate prepend, so it renders 5 bookmarks / 5 zakładek again. language.test.ts expectations updated to match.
  • Corrected the retry mapping — a new actions.retry key was added to the mobile namespace and useCommonActions().retry now uses it instead of actions.refresh.
  • Restored the iOS search cancel wording — SearchInput.ios.tsx uses the shared actions.cancel ("Cancel") rather than search.close.
  • Translated the hardcoded note label — HighlightCard.tsx uses bookmarks.note_title instead of the literal Note:.
  • Fixed a string typo — connection_test.non_success_code now reads "Received non-success error code".
  • Removed dead keys — the unused stats.morning_12am/morning_6am/noon_12pm/evening_6pm entries are gone from both locales.

Verified: the mobile namespace has defaultNS: "mobile", so actions.retry resolves locally; search.close remains live as an a11y label elsewhere, so it is not orphaned; en/pl key parity is preserved.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@Dragonk

Dragonk commented Oct 24, 2026 •

Copy link
Copy Markdown
Author

Two small updates to keep this merge-ready:

  • Merged the latest main (docs-only commits) - no conflicts.
  • Wired the mobile vitest suite into the CI tests job (apps/mobile -> pnpm test), which closes the remaining review finding that the mobile unit tests were never executed in CI.

Re-verified locally on the updated tree: 79/79 mobile unit tests, tools/mobile-i18n-sync.mjs --check (539-key en/pl parity), typecheck, lint, and format all clean.

Note: the upstream CI workflows are still awaiting approval to run (first-time contributor), so the checks section will populate once approved - @MohamedBassem.�

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

✅ No new issues found.

Reviewed changes

Delta since the prior pullfrog review (029e3c9c → 31b4543):

  • Mobile unit tests now run in CI — the tests job gained a Mobile Tests step (working-directory: apps/mobile, run: pnpm test), so the 79 vitest cases (i18n locale matching / plural forms and the offline sync + library suites) can now fail the build. This closes the outstanding request from the prior review. Verified locally: 4 files, 79 tests passed; ./tooling/github/setup installs the workspace deps the step needs.

The intervening 0226bd38 is a merge of upstream/main and contributes no net change against the base.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

This branch has not been deployed

No deployments
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.

[Mobile] Add i18n/localization support and Polish translation

1 participant