feat(mobile): ship 87 languages, lazy catalogs, and mechanical i18n checks - #5451
Merged
Conversation
Contributor
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (2 files)
Previous Review Summary (commit a2e6a67)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit a2e6a67)Status: 2 Issues Found | Recommendation: Address before merge Overview
Fix these issues in Kilo Cloud Issue Details (click to expand)CRITICAL
WARNING
Files Reviewed (62 files)
Reviewed by grok-4.6 · Input: 74.4K · Output: 6.2K · Cached: 218.1K Review guidance: REVIEW.md from base branch |
- languages.ts now carries all 86 locales from the language list, with each endonym, each English name, and the six RTL scripts. - The catalog loader parses one catalog per start instead of every catalog. - The picker gains search, endonym-first rows, and the active language pinned. - Language and Device sessions move from Profile into Preferences. - Kilo Pass web-management copy renders from the catalog, not a shared const. - iOS declares every locale so OS-drawn text follows the device language. - Translated catalogs are marked generated so their diffs collapse.
The raw-string rule now reports a copy prop on an object literal, a SCREAMING_SNAKE constant in a copy position, and an ordinary lowercase word that its test-ID pattern used to wave through. Test files are out of scope, and the rule runs over packages/app-shared too, where an English Kilo Pass string had been shipping to every locale. A new catalog check proves each locale is a key-for-key copy of English: missing, extra, duplicate, and empty keys, matching interpolation tokens, every used key defined, and every defined key used. Both run in CI. What the checks caught, now translated: the auth error map, the default and device-admission errors, the Kilo Pass legal disclosure and its link labels, the PR link toasts, the free-model labels, the remote spawn toasts, the session-exited toast, and the voice input labels. Fifteen dead keys deleted. Also from the PR 5444 review: the push locale cache could answer already-registered for another account's row, so the server row now decides, and the Notifications screen sends the active language when it registers.
…nglish A session list built a currency and a relative-time formatter per row per render; constructing one is the expensive part. One cache keyed by locale and options serves every call site. Three formatters read the wrong locale: session day headings were pinned to en-US, and the message time labels took the device locale instead of the active language.
Every locale on the language list now has a mobile catalog and a push notification catalog, translated key for key from English with the interpolation tokens preserved. Portuguese ships as two locales, because the words a phone app uses most are exactly the ones that differ: pt reads ficheiro, utilizador, ecrã, and aplicação, and pt-BR keeps arquivo, usuário, tela, and aplicativo. A pt-BR device matches pt-BR exactly; every other Portuguese tag falls back to pt.
…e search field Hermes does not implement Array.prototype.toSorted, so the picker crashed on open. The repository already uses .sort() on a copied array for this reason. The search field controlled its text with value plus state, which drops keystrokes on iOS. The field is now uncontrolled; the state only drives the filtered list.
iscekic
force-pushed
the
mobile-i18n-full
branch
from
August 24, 2026 16:05
a2e6a67 to
ecfacd5
Compare
Contributor
|
Little misscomm but lgtm. |
iscekic
added a commit
that referenced
this pull request
Aug 24, 2026
* feat(cloud-agent-sdk): add reconnect exhaustion signal and retry
Add an additive two-edge reconnect-exhaustion signal so mobile can show an explicit recovery action instead of an indefinite Reconnecting label. The auth-failure stop path fires the same terminal edge.
* feat(mobile): show explicit recovery action on reconnect exhaustion
Render Connection lost with a Retry action when the user-web transport exhausts reconnect attempts, instead of an indefinite Reconnecting label.
* feat(security-agent): add single command-type authority with drift tests
Export SECURITY_COMMAND_TYPES as the shared tuple and derive
SecurityCommandType from it. Add an app-shared invalidation-scope test
and an apps/web drift test asserting the db union matches the shared
tuple. Type the web create-call literals and the SecurityAgentAdmissionAction
redeclaration with the shared type.
* feat(notifications): add typed security lifecycle push payload
Add an additive security_lifecycle variant to pushDataSchema with an
eight-value event enum mapped 1:1 to SecurityAuditLogAction. Route the
variant to the existing security channel and add lifecycle lock-screen
copy in both exhaustive presentation switches.
* feat(analytics): extend settled-outcome catalog for security and code review
Extend SECURITY_INTENTS to the four command intents with an authority-linked
SECURITY_INTENT_FOR_COMMAND_TYPE map. Add the code_review_settled terminal
event with a privacy-minimal schema and add the code_review operation
ledger domain.
* feat(mobile): add Home product choices section
Add an Explore section with Code Reviewer, Security Agent, and PR Review
rows so an empty or erroring Home still offers an outcome-led first
action. Scope resolves as organizationId or the personal security scope;
PR Review stays feature-flag gated.
* fix(mobile): distinguish security capability loading, error, and offline
The settings overview now renders a skeleton, a permissions error with retry, or the offline variant instead of permission-denied copy when the org-role query is unresolved or paused.
* feat(security-agent): add bounded batch command status procedure
Add getCommandStatuses on the personal and org routers with a
min(1).max(100) uuid-array input schema. Batch-fetch owner-scoped
commands, omit unknown or foreign ids, and settle terminal commands
through the existing ledger settle helper. Keep getCommandStatus for
older mobile clients.
* feat(organizations): gate GitLab OAuth replacement to billing roles
Require ORGANIZATION_BILLING_ROLES when an org already has a GitLab
integration, on both the OAuth connect and callback paths; first-time
connect keeps member access. Add a role matrix over every
organization-security-agent procedure and map access denials to
permission-specific error codes instead of connection errors.
* feat(user): expose notification producer capabilities
Extend the notification preferences getter with a capabilities map for
the seven category keys, computing availability from organization
membership, enabled Security config, and KiloClaw instances.
* fix(mobile): guard optimistic rollback by mutation generation
Add a per-key generation guard so a failing older mutation cannot roll back cache state a newer mutation owns, and serialize same-entity mutations with scope ids for security, org, and model-preferences hooks.
* feat(mobile): add bounded batch command status observer
* feat(mobile): render patch parts with file-list summary
* feat(mobile): add native security audit report screen
* feat(mobile): persist attention items in encrypted KV
* feat(mobile): consume security lifecycle pushes with invalidation
* feat(mobile): drive notification controls from producer capabilities
* feat(security-agent): add auto-remediation approval gate with trust controls
* feat(organizations): narrow member-visible DTOs by role
* fix(mobile): guard session, reviewer, discussion, and notification mutations
Add the generation guard and the named network-order mechanisms to the remaining mutation hooks: session list, code-reviewer, PR discussion threads, and push-token/notification preferences.
* feat(code-reviews): add paginated review memory with native screen
* feat(mobile): add route-scoped foreground refresh hook
Add useRouteForegroundRefresh to invalidate route query keys on the focused AppState foreground transition and on focus regain after the first. Disable the blanket refetchOnWindowFocus default so frozen background tabs no longer refetch.
* fix(security-agent): complete approval_required reason surface
* fix(mobile): type review-memory FlashList test mock
* fix(code-reviews): keep array listProposals and add paginated page
* fix(mobile): complete review-memory review fixes
* fix(mobile): resolve automation-settings test lint errors
* test(mobile): wait for sibling row before unavailable assertion
* feat(mobile): mount route-scoped foreground refresh owners
Mount useRouteForegroundRefresh at the app-wide, home-tab, profile-tab, and pushed-route surfaces from plan section 10. Each path-prefix key uses the nested tRPC form so invalidation prefix-matches tRPC v11 keys. Remove the profile credits card's bespoke focus invalidation, now subsumed by the profile-tab owner. Add a real-QueryClient key-matching test.
* feat(mobile): gate Agents foreground refresh on tab focus
Gate the Agents list AppState foreground listener on the tab's focus state so a frozen background tab no longer refetches. The focused tab refreshes the stored list and invalidates the active-sessions tray. Add a mounted test covering focused, unfocused, and blur-after-mount foreground transitions.
* test(mobile): add offline flapping and process-kill chaos evidence
Extend the offline banner, query-client lifecycle, and mutation outbox tests with EV-03-style chaos scenarios: NetInfo flapping drives onlineManager and the banner debounce correctly, and an outbox row survives a simulated relaunch with its operationKey reused.
* fix(mobile): make security capability status type local
* refactor(mobile): merge duplicate offline branch in security settings
* feat(security-agent): settle remediation ledger rows at every terminal transition
* fix(cloud-agent-sdk): reach reconnect exhaustion cap on live sessions
Thread the reconnect attempt through refreshAndConnect so reconnects that refresh auth before connecting no longer reset the attempt counter to zero. Live user-web sessions now reach the exhaustion cap and fire the two-edge signal, instead of showing Reconnecting forever.
* fix(mobile): stamp session mutation generation after cancelQueries
Stamp the generation inside snapshotAndUpdate after cancelQueries resolves, so the stamp order matches the write order and an older mutation cannot clobber a newer list write.
* fix(mobile): read Agents tab focus live on foreground
Read navigation.isFocused() in the AppState callback instead of a focusedRef that a frozen tab never updates. A frozen unfocused Agents tab no longer refetches or invalidates the active-sessions tray on foreground.
* feat(code-reviews): emit settled outcomes for code review and security commands
* feat(web): narrow list DTOs for findings, code reviews, and organizations
Narrow the security findings list, code reviews list, and organization list responses so heavy content is fetched only by detail procedures. The findings list nulls raw_data after decoration; the code reviews list drops council_result, manual_config, and previous_summary_body; the organization list selects explicit columns. Each narrowing ships with a response-width test and a detail-preserved test.
* feat(notifications): add security lifecycle push producers
Emit analysis_* and remediation_* lifecycle pushes from post-commit, env-holding sites in the security-auto-analysis service. The web internal notifications route accepts a lifecycle body and dispatches push only; the notifications worker gains a security_lifecycle dispatch kind gated on securityFindingsEnabled. Terminal emits fire only when the persist actually made the attempt terminal.
* test(security-agent): cover terminal launch-failure emit after retry exhaustion
* feat(code-reviews): add delta repository save with webhook sync
* test(mobile): guard root layout startup order
* feat(mobile): defer profile queries until interactions settle
* refactor(mobile): extract pure bootstrap decision
* feat(mobile): bound invalid route params with fallback state
* feat(mobile): validate URL scheme and production host
* build(mobile): pin EAS CLI and assert expo config
* ci(mobile): preflight and inspect signed release artifacts
* feat(security-agent): add remediation progress timeline to finding detail
* fix(mobile): derive expo config env keys from live map
The expo config contract assertion hardcoded the ten ENV_KEYS names, so a
new key would silently drop out of the preflight check. Import ENV_KEYS
from src/lib/env-keys.js and iterate its keys instead.
* feat(mobile): route remediation PR buttons into native PR review
* feat(mobile): debounce repo selection saves into deltas
* chore(format): run oxfmt on changed files
* refactor(mobile): drop dead bootstrap decision fields
The routing effect reads only decision.tag and the render gating reads only
the three error flags and hidden. The seven intermediate derivations were
exported but never consumed; keep them as local consts inside the helper.
* fix(mobile): gate runtime host check on build intent
The runtime production-host check keyed off the Sentry environment, so a
preview release build with an unset EXPO_PUBLIC_SENTRY_ENVIRONMENT failed
closed and crashed on launch. Bake EAS_BUILD_PROFILE === 'production' into
expo extra and gate the check on that flag.
* fix(organizations): type member DTO narrowing without unsafe cast
The withMembers procedure stripped member-only fields at runtime but cast
the result back to the admin superset type, so TypeScript reported
stripe_customer_id as present on member responses. Replace the cast with a
typed member variant and wire the member schemas into the response union,
then narrow the union in web and mobile consumers.
* fix(organizations): hide copy-invite control without invite URL
A Kilo admin whose org role is member receives the stripped member DTO
without inviteUrl, so the copy badge rendered but did nothing. Gate the
control on inviteUrl presence.
* fix(mobile): drop unused repo-selection delta export
* fix(security-agent): enqueue include-existing backlog when approval turns off
* fix(organizations): validate Bitbucket deltas before patch save
* fix(code-reviews): redact raw ids in detail and settle local cancels
* fix(code-reviews): admit ledger row after review transaction commits
* fix(mobile): allowlist committed production hosts
Add chat.kiloapps.io, events.kiloapps.io, and notifications.kiloapps.io to the production host allowlist, and add a host-contract test that asserts every committed .env URL value against the allowlist.
* fix(code-reviews): settle ledger for superseded and user-cancelled reviews
* fix(code-reviews): use two-argument jest.fn generic in ledger test
* fix(security-agent): default missing approval flag to not required
* fix(mobile): refetch analysis on security lifecycle recovery
* fix(mobile): keep personal audit-report auth failures retryable
* fix(code-reviews): label hidden check run instead of None for members
* fix(mobile): default missing remediation timeline to empty
A separately released client can talk to an old backend that omits the
new remediationTimeline field, so the non-nullable type is not a runtime
guarantee. Treat a missing field as an empty list instead of throwing.
* fix(mobile): show enable CTA while reviewer permission unresolved
Only a definitively resolved read-only permission shows the member copy.
Loading and error states show the Enable button, because the mutation is
server-authorized.
* feat(session-ingest-contracts): add optional gitUrl to cloud agent create
* test(mobile): preserve backend repository order on empty search
* feat(cloud-agent): send normalized repository gitUrl to session ingest
* feat(session-ingest): persist idempotent repository use on cloud agent create
* feat(web): order cloud agent repositories by usage history
* feat(dev-seed): add frequent repository order e2e fixture
* test(cloud-agent): expect repository gitUrl in prepare session call
* feat(session-ingest-contracts): add clone source and result to cloud agent create
Add an optional cloneFromKiloSessionId to the create schema and change the RPC method to return a discriminated ready/in_progress/rejected result. Old callers can ignore the returned value.
* feat(cloud-agent-next): carry clone source through prepare and metadata
Add cloneFromKiloSessionId to PrepareSessionInput, map it into the grouped SessionCreateRequest clone object, and persist it in grouped session metadata via registerSession. Old inputs without the field keep the empty-session bootstrap.
* feat(session-ingest): add clone export and staging DO primitives
Add a bounded source export method and destination stage/inspect/finalize/reset methods to SessionIngestDO. Export reads only session, message, part, and session_diff rows in (ingested_at, id) order with a stable digest; reset wipes SQLite before the R2 delete await.
* feat(web): forward clone source through cloud agent prepare
Add cloneFromKiloSessionId to basePrepareSessionNextSchema; both personal and organization prepare forward it unchanged to the worker.
* feat(session-ingest): clone destination transcript atomically
Add a bounded resumable clone helper that copies source ingest rows to the destination DO with identity rewriting and destination-owned R2 bodies. Wire it into createSessionForCloudAgent with the typed result, detect destination claims before any write, and add matching-source delete for clone rollback.
* feat(cloud-agent-next): carry clone source through allocation and ownership
Forward cloneFromKiloSessionId into the session-ingest create call and map
its typed result: rejected to BAD_REQUEST session_clone_failed, a missing
acknowledgement to SERVICE_UNAVAILABLE session_clone_unavailable, and
in_progress to CONFLICT creation_in_progress. Route retryable and unknown
clone outcomes through the reconcile-pending ledger hook, persist sandbox
allocation progress before the ingest call, include the clone source in the
create-intent fingerprint and registration command, and make rollback
allocation-aware with a matching-source delete.
* feat(cloud-agent-next): reconcile and resume clone allocation
Extract the post-allocation registration and admission effect into a
helper, rebuild a clone allocation from ledger progress with the stored
initial message id, resume a clone with its stored ids in reconcile step
(b) when the ownership row is absent, and tombstone destination ids after
an explicit rejection so the next intent never resumes them.
* feat(cloud-agent-next): restore cloned snapshot on first preparation
Prefer snapshot restore when session metadata carries a clone or a
preparedAt, set requireSnapshot for clones, and reject a missing clone
snapshot with a typed error instead of importing an empty session on both
the worker and wrapper sides. Non-clone sessions keep the 404 fallback.
* feat(mobile): clone source session on continue
Replace the bounded text-seed continuation with a full Cloud Agent clone.
The continue action sends cloneFromKiloSessionId with a fixed continuation
prompt and a fresh initial message id, never drains history, never queries
connected instances, and never writes a share payload. A missing Cloud Agent
destination shows a terminal message. Delete the continuation seed and the
remote continuation builders.
* fix(mobile): resolve oxlint findings in continue session
* fix(cloud-agent-next): apply inspected clone session fixes
* feat(mobile): port screen header hit-area and form-sheet inset helper
* feat(dev-seed): port mobile sheet fixtures
* test(cloud-agent-next): match merged session-ingest create signature
* test(session-ingest): assert ready result for repository retries
* feat(dev-seed): add session_diff to mobile sheet fixtures
* fix(mobile): give screen header back control a 44-point target
The back Pressable used h-13 w-13 (52 points). The shipped icon-button
geometry is h-11 w-11 (44 points). The mounted test now derives the title
font size from the rendered class and fails when title size plus top and
bottom slop drops below 44 points.
* feat(mobile): clear PR review content above the Android system inset
Overview uses DetailScreenScrollView. Discussion and Files state chrome
use useDetailScreenBottomPadding. Files floating actions and form-sheet
footers add the system inset directly. The file navigator list carries the
Android inset in its bottom content padding. Mounted tests cover zero and
nonzero insets across the named states.
* feat(mobile): add shared session page sheet wrapper
iOS keeps the native pageSheet Modal. Android gets a transparent Modal with
a dimmed blocking scrim and a bottom-aligned half-height surface. Part
detail, session context, and message details migrate to the wrapper.
* feat(mobile): migrate remaining session sheets to the shared wrapper
File part preview, attachment text preview, and the child session sheet
render through SessionPageSheet, keeping their loading, retry, terminal,
and empty states.
* feat(mobile): keep subagent sheet dark during Android dismissal
Add a subagent-only bg-background layer that stays mounted while the
sheet identity is held and visible is false, so the Android slide-out
dismissal never reveals session content as a white frame. iOS keeps its
native pageSheet dismissal. Extract the layer into a focused component
and cover its lifetime with a mounted test.
* feat(cloud-agent-next): omit synthetic turn for clone-only prepare
Make PrepareSessionInput a discriminated union: the clone-only variant
requires the source session, autoInitiate true, and an operationKey, and
forbids prompt, initialPayload, and initialMessageId. prepareInputTo
SessionCreateRequest omits initialTurn for a clone, the fingerprint
hashes the omitted turn as { type: 'none' }, and clone-only creation
registers the session without admitting an initial message.
* feat(web): discriminate clone-only prepare session input
Make basePrepareSessionNextSchema and the web PrepareSessionInput type two-variant unions. The clone-only variant requires cloneFromKiloSessionId, autoInitiate true, and operationKey, and forbids prompt, initialPayload, and initialMessageId.
* fix(cloud-agent-next): pass clone source in resume-clone ingest call
* fix(cloud-agent-next): surface ingest rejection code on clone
* fix(mobile): clear sheet content above the Android system inset
* test(mobile): cover sheet bottom spacer rendering
* fix(mobile): complete clone-only continue prepare and retry UX
* fix(mobile): make session sheet content scrollable
* fix(mobile): correct continue destination mapping and connect action
Map only an unmatched gitUrl to Connect repository and a matched repo with an unresolved model to Back to sessions. Set persistent retry guidance on repository fetch failure. Open GitHub integration setup instead of the expired repo-picker and clear terminal guidance after the Connect action runs. Cover the Connect action with a unit test.
* fix(mobile): satisfy oxlint in connect action test
* fix(mobile): correct continue loading and idle label
* feat(dev-seed): add continue session fixtures to mobile sheet seed
* fix(mobile): remove duplicate Platform mock after merge
* fix(mobile): resolve mounted-test typecheck errors
* fix(mobile): remove unused exports flagged by knip
* fix(dev-seed): reset mobile sheet fixtures without DO tombstone
* refactor(mobile): remove dead continue picker and inline helpers
* fix(cloud-agent-next): settle clone-only create on reconcile and retry
* fix(mobile): keep the Android session sheet full height below the status bar
Replace the half-height scrim sheet with an opaque full-window Modal. The
surface pads the top safe-area inset, so the content clears the system
status bar. iOS keeps the native pageSheet.
* refactor(mobile): drop the dead Android guard in the form-sheet detent
The next line already discards the Android detent on iOS, so the guard
changed nothing.
* refactor(mobile): drop the Android subagent dismiss layer
The layer covered the session while the transparent Android Modal slid
out. The Modal is now opaque and full height, so the cover is dead
weight. Three recorded Back dismissals show no white frame without it.
* fix(mobile): keep code-reviewer hooks under max-lines and mock foreground refresh
* feat(notifications): store push token locale and translate per token
* feat(mobile): wire i18n runtime, locale formatters, and leftover CI
Add the i18next runtime, language resolution with Chinese script subtags, locale formatters, RTL apply flow, and the leftover-text CI gate. Fix zh-Hant-TW/HK resolution and make applyLanguagePreference never reject.
* feat(mobile): extract app route strings into the i18n catalog
* fix(mobile): define exec-policy and per-month catalog keys
The exec-policy rows rendered raw key paths because the four label and description keys were missing from every catalog. The billing price suffix /month was still a literal. Add the five keys to all 20 catalogs and render the suffix through t().
* feat(mobile): extract top-level component strings into the catalog
* fix(mobile): make no-literal-copy flag Alert and toast literals
* feat(mobile): extract agent component strings into the catalog
* feat(mobile): extract session and chat strings into the catalog
* feat(mobile): extract review component strings into the catalog
* feat(mobile): extract remaining component strings into the catalog
* chore(format): apply oxfmt to previously committed mobile files
* fix(mobile): wire i18n helpers and repair full-check debt
Wire renameAndroidNotificationChannels, readLanguageReturnTarget, and the locale-aware money formatters; repair the 15 failing test files; clear the 47 lint errors; and extract the remaining leftover English. All 20 catalogs share one 2204-leaf key tree. Full apps/mobile checks are green: format, typecheck, lint, check:unused, test (563 files / 5743 tests), and check:i18n.
* fix(mobile): fix device apply, reload retry, and login drafts
* fix(mobile): flip directional icons in RTL
* fix(mobile): pass active locale to mobile date formatters
* fix(mobile): extract remaining user-facing strings into the catalog
* fix(mobile): mock push reconciliation in profile tests
* fix(mobile): repair language-control UX from advocate review
Address six pre-E2E user-advocate findings and two review findings:
- show a form-sized skeleton while the login draft restores
- open the language picker at once; flush drafts before the RTL reload
- toast a specific catalog-load failure and keep Done enabled
- proceed with the RTL reload when the return-target write fails
- settle the splash and show Retry/Continue on a cold-start RTL reload failure
- pad picker rows on the logical end side in both directions
Adds language.catalogLoadFailed and common.continue to all 20 catalogs.
* fix(analytics): add language-error startup outcome
Keep STARTUP_OUTCOMES in sync with the mobile StartupOutcome union after the cold-start RTL reload-fail settle path.
* fix(mobile): reload only on language direction change
Reload only when the native direction changes. Same-direction apply switches the catalog in place and persists after the catalog loads, rolling back on persist failure. Screen-header back and headerRight margins are RTL-aware.
* fix(mobile): add Android back handling and block reload-fail dismiss
Add a BackHandler so Android back cancels the picker when idle. Keep Retry as the only action after an RTL reload fails: drop onCancel and ignore the backdrop and back press.
* fix(mobile): toast persist failure in the kept language
Pass the pre-switch language to setAsync so the LTR persist-fail toast renders in the kept language, not the rejected one.
* fix(mobile): format session costs with the active locale
Use Intl currency with the active language in formatSessionTotalCost and formatCost, and gate the older-activity residual on a numeric zero threshold instead of the en-US string.
* fix(mobile): mirror picker header in RTL and consume Android back
The picker header derived its Cancel/Done physical side from I18nManager.isRTL, a module-load-time constant in RN 0.86 that can be stale after forceRTL plus reload, so the RTL picker kept Cancel at the left edge. Derive the side from i18n.dir() instead. The Android back handler re-registered on every parent render because onClose was an inline arrow, which left the event unconsumed on device; register once and read the latest values from refs.
* fix(mobile): close language picker on Android back
* fix(mobile): detect picker header direction from resolved language
* fix(mobile): mirror picker header via logical start/end classes
The JS-derived physical left-0/right-0 classes did not mirror on Android after an RTL apply. Use the logical start-0/end-0 utilities, which Yoga resolves from the native layout direction that forceRTL sets before the reload.
* refactor(i18n): remove dead code and duplicate type definitions
* fix(mobile): preserve reload return target and login drafts on failure
* feat(mobile): ship 87 languages, lazy catalogs, and mechanical i18n checks (#5451)
* feat(mobile): expand i18n to 86 languages and lazy-load catalogs
- languages.ts now carries all 86 locales from the language list, with each
endonym, each English name, and the six RTL scripts.
- The catalog loader parses one catalog per start instead of every catalog.
- The picker gains search, endonym-first rows, and the active language pinned.
- Language and Device sessions move from Profile into Preferences.
- Kilo Pass web-management copy renders from the catalog, not a shared const.
- iOS declares every locale so OS-drawn text follows the device language.
- Translated catalogs are marked generated so their diffs collapse.
* feat(mobile): make the i18n checks mechanical and fix what they caught
The raw-string rule now reports a copy prop on an object literal, a
SCREAMING_SNAKE constant in a copy position, and an ordinary lowercase word
that its test-ID pattern used to wave through. Test files are out of scope,
and the rule runs over packages/app-shared too, where an English Kilo Pass
string had been shipping to every locale.
A new catalog check proves each locale is a key-for-key copy of English:
missing, extra, duplicate, and empty keys, matching interpolation tokens,
every used key defined, and every defined key used. Both run in CI.
What the checks caught, now translated: the auth error map, the default and
device-admission errors, the Kilo Pass legal disclosure and its link labels,
the PR link toasts, the free-model labels, the remote spawn toasts, the
session-exited toast, and the voice input labels. Fifteen dead keys deleted.
Also from the PR 5444 review: the push locale cache could answer
already-registered for another account's row, so the server row now decides,
and the Notifications screen sends the active language when it registers.
* perf(mobile): cache Intl formatters and localize the ones pinned to English
A session list built a currency and a relative-time formatter per row per
render; constructing one is the expensive part. One cache keyed by locale and
options serves every call site.
Three formatters read the wrong locale: session day headings were pinned to
en-US, and the message time labels took the device locale instead of the
active language.
* feat(mobile): translate all 87 catalogs
Every locale on the language list now has a mobile catalog and a push
notification catalog, translated key for key from English with the
interpolation tokens preserved.
Portuguese ships as two locales, because the words a phone app uses most are
exactly the ones that differ: pt reads ficheiro, utilizador, ecrã, and
aplicação, and pt-BR keeps arquivo, usuário, tela, and aplicativo. A pt-BR
device matches pt-BR exactly; every other Portuguese tag falls back to pt.
* fix(mobile): sort the language rows without toSorted and uncontrol the search field
Hermes does not implement Array.prototype.toSorted, so the picker crashed on
open. The repository already uses .sort() on a copied array for this reason.
The search field controlled its text with value plus state, which drops
keystrokes on iOS. The field is now uncontrolled; the state only drives the
filtered list.
* fix(mobile): name the sign-in sheet by key, not by its English words
Fifty catalogs spelled the "More sign-in options" button inside the four
auth error messages. Twenty-seven kept the English words, so a Spanish
reader was pointed at a button that reads "Más opciones de inicio de
sesión". Twenty-three wrote a phrase that matched no button at all.
Every catalog now writes `$t(login.moreSignInOptions)`, which resolves in
the active language, and English does the same so no translator can copy
the words again.
Also fixed:
- 66 strings dropped or added a leading or trailing space that the
surrounding markup depends on. `consent.privacyPolicyPrefix` and
`consent.retentionPrefix` carried a trailing space that the JSX already
writes, so English rendered a double space; the space is gone from the
catalog and the other four keys now match English.
- `lo`, `my` and `ne` left "Settings" in English in seven strings, and
`my` left "single sign-on" where the rest of that catalog writes "SSO".
- `am` and `si` left both `securityAgent.remediation` pull request labels
in English; `fil` and `sw` left one string each.
- `om` quoted the English "Details" tab; `lt` baked quote marks into the
"Cloud Agent" picker label.
- `am` and `mi` held a double space, and `mi consent.retentionPrefix` was
a broken fragment.
check-catalogs now enforces the value rules mechanically, so they hold for
a language nobody here reads: `$t()` references, edge spacing, double
spaces and direction marks must match English, and a message must never
quote an English label. It reports 177 problems against the previous
catalogs and none against these.
A new test resolves the four messages in all 87 languages and asserts each
one carries that language's own button label.
The five files this branch added or changed failed `pnpm run format:check`;
they are formatted now.
* fix(mobile): drop the unused AUTH_ERROR_KEYS export
* test: assert the localized tab labels and the failed-push i18n key
* test: assert the push i18n key on failed and interrupted settlements
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #5444. Base branch is
mobile-i18n-61ad, so review this diff on its own.Summary
Languages
SUPPORTED_LANGUAGESis the single source of truth: the catalog loader, the picker, the push catalog, and the iOS bundle all read it.Portuguese ships twice. The words a phone app uses most are exactly the ones that differ, so
ptreads ficheiro, utilizador, ecrã, aplicação andpt-BRkeeps arquivo, usuário, tela, aplicativo. Apt-BRdevice matches exactly; every other Portuguese tag falls back topt. Six scripts lay out right to left:ar he fa ur ckb ps.CFBundleLocalizationsnow lists every locale. Without it iOS treats the app as English-only, so OS-drawn text we cannot translate — the native Sign in with Apple button above all — stays English on a localized device. That button's label comes from the OS and follows the device language; there is no locale prop, so an in-app override that differs from the device still shows the device's wording.Performance
src/i18n/catalogs.tsholds one loader per language, and the i18next backend calls only the active one. Metro keeps every catalog in the bundle, but a start parses one instead of 87.src/lib/intl-cache.tscachesIntlformatters by locale and options. A session list was building a currency and a relative-time formatter per row per render, and constructing one is the expensive part. Three formatters also read the wrong locale: session day headings were pinned toen-US, and message time labels took the device locale instead of the active language.The picker renders 87 rows through a
FlatList.The picker
Rows lead with the endonym, because someone lost in a language they cannot read scans for their own name. The English name is the dimmed second line, so the row is findable either way. No flags: flags mark countries, and Arabic, Portuguese, Spanish, Swahili, and Kurdish have no correct one. Search matches the endonym, the English name, and the tag, and folds diacritics, so
espanolfindsEspañol. The active language pins to the top, each row reads in its own direction, and an RTL reload now returns to Preferences.The checks
check:i18nruns both, in CI:no-literal-copy(hardened). It now reports a copy prop on an object literal and aSCREAMING_SNAKEconstant in a copy position, and its old test-ID pattern no longer waves through an ordinary lowercase word —title="settings"andaccessibilityLabel="hello"were both silently exempt. Test files are out of scope. It runs overpackages/app-sharedtoo, where an English Kilo Pass string had been shipping to every locale.tools/i18n/check-catalogs.mjs(new). Proves each locale is a key-for-key copy of English: missing, extra, duplicate, and empty keys, matching{{token}}sets, every used key defined, and every defined key used. The dead-key direction reads every string literal, not justt()arguments, so a key named in a lookup table counts as used.What they caught, all now in the catalog: the auth error map (15 strings), the default and device-admission errors, the Kilo Pass legal disclosure and its link labels, the PR link toasts, the free-model labels, the three remote-spawn toasts, the session-exited toast, and the voice input labels. Fifteen dead keys deleted. Four constants are allowlisted by name — a brand, an acronym, a number, and a URL — so the exemption list is the review record.
Review comments on #5444
already-registeredfor another account's row — one device keeps one Expo token across a sign-out and sign-in.getMyPushTokensnow returnslocaleand the server row decides; the client keeps only a hint that can bypass the spacing skip, never suppress a write. The Notifications screen also registered without a locale, so a device enrolled there received English pushes.TEST_ID_PATTERN. Removed from the shared ignore, as described above.Review comments on this PR
toSorted. Hermes does not implement it, so the picker crashed on open. The rows now sort a mapped copy with.sort(), the pattern the repository already uses for this reason.valueplus state, which drops keystrokes on iOS. The field is uncontrolled now; the state only drives the filtered list.Diffs
Translated catalogs are marked
linguist-generated, so a review reads the English change instead of 86 copies of it.Verification
pnpm --filter kilo-app run lint0,typecheckclean,check:i18nclean, 5800 mobile tests and 22 notification tests pass.