Repository navigation
Conversation
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.
348362f to
eb1a689
Compare
|
There was a problem hiding this comment.
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-i18nextsingleton withmobile+ synced webtranslationnamespaces, locale detection viaexpo-localization, and typed keys derived from theenfiles. - Language provider + setting —
I18nProviderapplies a persistedlanguageoverride (System / English / Polish); new language picker screen registered in the dashboard layout. - Web-translation sync tooling —
tools/mobile-i18n-sync.mjscopies the webtranslationnamespace into mobile and, with--check, fails on stale copies andmobile-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 underapps/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 noactions.retrykey. 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 defaultcancelTextchanged from "Cancel" tot("search.close")("Close"); a dedicated cancel key would preserve the previous wording.apps/mobile/lib/i18n/locales/{en,pl}.jsondefinestats.morning_12am/morning_6am/noon_12pm/evening_6pm, but the migratedUsageStatsScreennow derives these viaIntl.DateTimeFormat, so the keys are dead.- Completeness: the PR description says all user-visible strings were migrated, but
apps/mobile/components/highlights/HighlightCard.tsx:107still hardcodesNote:(abookmarks.note_titlekey already exists), and some untouched files remain English-only (components/bookmarks/BookmarkContentLoading.tsx,components/sharing/LoadingAnimation.tsx,components/settings/settings-list.tsx'saccessibilityHint), alongsideUsageStatsScreen's accessibility labels /formatAssetTypeand the "OK" button insettings/index.tsx.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
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
There was a problem hiding this comment.
✅ 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, andemoji.resultsnow passcountto i18next, with_one/_few/_many/_otherdefined 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.tsxusestoLocaleString(getIntlLocale()), andUsageStatsScreenderives its formatters plus hour/weekday labels from the active app language. - Sync tool language discovery —
tools/mobile-i18n-sync.mjsnow derives the language set fromlocales/*.jsoninstead of a hardcoded list. - Plural/date regression tests — asserts UI plural forms across
pl(1/2/4/5/22/25) anden, storage/emoji counts, and that everyIntl.PluralRulescategory 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).
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
There was a problem hiding this comment.
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, andoffline-lists.tsxnow use thesettings.offline_*keys;formatStorageSizegained anIntl-locale argument and every caller passesgetIntlLocale(). - List-sharing flows translated — new
components/lists/list-sharing-settings.tsxandpending-list-invitations.tsx, pluslists/[slug]/sharing.tsxand the entry points inedit.tsx/[slug]/index.tsx, use the newlist_sharing.*keys. - Attachments box translated —
AttachmentBox.tsxusesattachments.*and the sharedcommon.attachmentsstring. - 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.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
|
Rebased this onto the latest 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
There was a problem hiding this comment.
✅ 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 renders5 bookmarks/5 zakładekagain.language.test.tsexpectations updated to match. - Corrected the retry mapping — a new
actions.retrykey was added to the mobile namespace anduseCommonActions().retrynow uses it instead ofactions.refresh. - Restored the iOS search cancel wording —
SearchInput.ios.tsxuses the sharedactions.cancel("Cancel") rather thansearch.close. - Translated the hardcoded note label —
HighlightCard.tsxusesbookmarks.note_titleinstead of the literalNote:. - Fixed a string typo —
connection_test.non_success_codenow reads "Received non-success error code". - Removed dead keys — the unused
stats.morning_12am/morning_6am/noon_12pm/evening_6pmentries 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.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Summary
Adds full internationalization/localization (i18n) support to the Karakeep mobile app (
apps/mobile) together with a complete Polish translation.apps/mobile/lib/i18n/(singleton, provider, hooks, typed keys).date-fns+Intlfollow the app language).Architecture
i18next+react-i18next— the same libraries the web app uses, so interpolation ({{name}},{{count}}) and plural handling behave identically.expo-localizationfor OS locale detection.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.types.d.ts(derived from theenfiles).useSharedTranslation/useCommonActions/useCommonStrings, never viat("translation:...")on themobile-namespaced function (that pattern is a type error with typed resources and tripped the TS 5.9 checker whentwas 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
resourcesToBackendimports. Instead,tools/mobile-i18n-sync.mjscopies the sharedtranslationnamespace intoapps/mobile/lib/i18n/web-translations/, keeping one source of truth while satisfying Metro's static bundling.Polish translation
plfor themobilenamespace, 1:1 key parity withen(443 keys each, enforced by tooling + tests + CI).Intl.PluralRules(pinned by tests).Locale resolution
Exact tag → base language → English fallback:
pl-PL → pl,pl → plen-US → en,en-GB → ende-DE,uk-UA) →enQuality / Tests
All verified on this branch:
tsc --noEmitonapps/mobile): clean.oxlint): 0 warnings, 0 errors.oxfmt --check): clean.vitest run --root apps/mobile): 46 passed — locale matching (pl-PL,pl,en-US,en-GB, unsupported),system → pl → en → systemswitching, 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.aaddd9ed.node tools/mobile-i18n-sync.mjs --check): in sync, 443 keys.i18nextinstance, 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), producingapp-release.apksigned with the standard Gradle debug keystore (no signing secrets in the repo, no Metro/dev-client server needed — the release bundle is embedded andexpo-dev-launcheris inert in release builds).Green run: https://github.com/Dragonk/karakeep/actions/runs/35357143694 — APK published as the
karakeep-mobile-i18n-pl-release-apkworkflow 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.mjssyncs the shared webtranslationnamespace into mobile and (with--check, also wired as a CI job) fails on stale copies ormobile-namespace key drift between languages. Adding a language = sync + one JSON file + one line inSUPPORTED_LANGUAGES(seeapps/mobile/lib/i18n/README.md).Closes #3096
Update: rebased onto the latest
mainThis 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:formatStorageSizenow takes an optionalIntllocale, and dates usedate-fnslocales.main), format check, unit tests (79/79), andmobile-i18n-sync.mjs --checkkey 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, alocales/<lang>.json>file, and one entry inSUPPORTED_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.