Skip to content

feat(i18n): migrate to i18next with official TypeScript module augmentation for compile-time key safety#1963

Draft
Copilot wants to merge 9 commits intodevelopmentfrom
copilot/setup-i18next-typescript-types
Draft

feat(i18n): migrate to i18next with official TypeScript module augmentation for compile-time key safety#1963
Copilot wants to merge 9 commits intodevelopmentfrom
copilot/setup-i18next-typescript-types

Conversation

Copy link
Contributor

Copilot AI commented Mar 13, 2026

  • Migrate from i18n-js to i18next with CustomTypeOptions module augmentation
  • Add named t export to lib/i18n/index.ts — fixes 6 callsites importing { t }
  • Guard i18next init with !i18next.isInitialized — prevents Fast Refresh re-init warnings
  • Restore TranslationFunction to lib/i18n/types.ts as TFunction alias
  • Update "outside components" examples to use { t } from expo-app/lib/i18n
  • Fix CI: Add missing errors.error and packs.itemDetails to en.json
  • Fix CI: Restore accidentally dropped keys — packs.weightAndQuantity, packs.weightEach, packs.weight, packs.unit
  • Fix CI: Remove duplicate packs.itemDetails introduced by development merge
  • Fix CI: Convert trips.days from nested {one,other} to flat days_one/days_other plural format (i18next ParseKeys requires flat keys for t('trips.days', { count }) to type-check)
  • Fix CI: Add missing Share import to TripDetailScreen.tsx
  • Fix CI: Type reportReasonTranslationKeys as Record<ReportReason, TranslationKeys>
  • Fix CI: Remove unreachable || item.reason fallback in ReportedContentScreen.tsx
  • Fix CI: Type labelKey as TranslationKeys in credentials.tsx and reset-password.tsx
  • Fix CI: Type getPriorityKey(): TranslationKeys in shopping-list.tsx
  • Fix CI: Biome — noExplicitAny, noUnusedFunctionParameters, trailing newlines
  • Fix CI: Patch @packrat-ai/nativewindui@1.1.0TS2749: 'FlashList' refers to a value, but is being used as a type in List.tsx:34; fix React.Ref<FlashList<T>>React.Ref<React.ElementRef<typeof FlashList>> via patchedDependencies in package.json
  • Code review + CodeQL: 0 alerts
Original prompt

Setup proper i18next typescript types so we are alerted to missing translation values. Check docs


📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.

…safety

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Copilot AI changed the title [WIP] Add i18next TypeScript types to handle missing translations feat(i18n): auto-derive TranslationKeys from en.json for compile-time key safety Mar 13, 2026
Copilot AI requested a review from andrew-bierman March 13, 2026 14:31
@andrew-bierman
Copy link
Collaborator

@copilot we should be following this https://www.i18next.com/overview/typescript

…dule augmentation

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 13, 2026

@copilot we should be following this https://www.i18next.com/overview/typescript

Done in b15a497. Migrated from i18n-js to i18next + react-i18next and set up TypeScript types following the official i18next docs pattern:

  • lib/i18n/index.ts — init i18next with resources as const + initReactI18next
  • lib/i18n/i18next.d.ts (new) — module augmentation declaring CustomTypeOptions with defaultNS and resources, so i18next's own t() and useTranslation().t are natively type-safe against en.json
  • lib/i18n/types.ts — simplified to re-export ParseKeys from i18next as TranslationKeys
  • lib/hooks/useTranslation.ts — thin wrapper around react-i18next's useTranslation

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • www.i18next.com
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node /home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps /home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title feat(i18n): auto-derive TranslationKeys from en.json for compile-time key safety feat(i18n): migrate to i18next with official TypeScript module augmentation for compile-time key safety Mar 13, 2026
@andrew-bierman andrew-bierman requested review from Copilot and removed request for andrew-bierman March 13, 2026 16:52
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the Expo app’s localization layer from i18n-js to i18next + react-i18next, aiming to provide stronger TypeScript key-safety via i18next module augmentation.

Changes:

  • Replaces i18n-js dependency with i18next and react-i18next.
  • Adds i18next initialization/resources wiring and CustomTypeOptions module augmentation for typed translation keys.
  • Updates i18n documentation and example usage to reflect the new setup.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
apps/expo/package.json Swaps i18n dependencies to i18next + react-i18next.
apps/expo/lib/i18n/index.ts New i18next initialization and typed resources export.
apps/expo/lib/i18n/i18next.d.ts Adds i18next module augmentation for compile-time key safety.
apps/expo/lib/i18n/types.ts Replaces manual key union with ParseKeys re-export.
apps/expo/lib/hooks/useTranslation.ts Switches hook implementation to react-i18next’s useTranslation.
apps/expo/lib/i18n/README.md Updates docs to describe i18next setup and type-safety.
apps/expo/lib/i18n/EXAMPLES.tsx Updates examples to use i18next/react-i18next APIs.
apps/expo/I18N_SUMMARY.md Updates summary docs for the new i18next-based approach.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 28 to 29
export default i18next;

Comment on lines +18 to +26
i18next.use(initReactI18next).init({
resources,
lng: Localization.getLocales()[0]?.languageCode ?? 'en',
fallbackLng: 'en',
defaultNS,
interpolation: {
escapeValue: false, // React already escapes values
},
});
Comment on lines +17 to +20
import type { ParseKeys } from 'i18next';

export type TranslationKeys =
// Common
| 'common.welcome'
| 'common.application'
| 'common.continue'
| 'common.retry'
| 'common.cancel'
| 'common.save'
| 'common.delete'
| 'common.edit'
| 'common.create'
| 'common.back'
| 'common.loading'
| 'common.error'
| 'common.success'
| 'common.yes'
| 'common.no'
| 'common.ok'
| 'common.done'
| 'common.close'
| 'common.search'
| 'common.filter'
| 'common.sort'
| 'common.name'
| 'common.email'
| 'common.password'
| 'common.username'
// Errors
| 'errors.somethingWentWrong'
| 'errors.notFound'
| 'errors.pageNotFound'
| 'errors.goToHome'
| 'errors.looksLikeError'
| 'errors.tryAgain'
// Auth
| 'auth.signIn'
| 'auth.signUp'
| 'auth.signOut'
| 'auth.logOut'
| 'auth.loggedOut'
| 'auth.loggedOutMessage'
| 'auth.stayLoggedOut'
| 'auth.signInAgain'
| 'auth.logoutFailed'
| 'auth.forgotPassword'
| 'auth.resetPassword'
// Profile
| 'profile.profile'
| 'profile.editProfile'
| 'profile.accountInformation'
| 'profile.memberSince'
| 'profile.stats'
| 'profile.myPacks'
| 'profile.noPacksYet'
| 'profile.createNewPack'
| 'profile.items'
| 'profile.public'
| 'profile.notifications'
| 'profile.settings'
| 'profile.syncInProgress'
| 'profile.syncMessage'
| 'profile.dangerZone'
// Navigation
| 'navigation.dashboard'
| 'navigation.packs'
| 'navigation.trips'
| 'navigation.catalog'
| 'navigation.profile'
| 'navigation.home'
// Packs
| 'packs.currentPack'
| 'packs.gearInventory'
| 'packs.packDetails'
| 'packs.packCategories'
| 'packs.enterNewPackDetails'
| 'packs.visibility'
| 'packs.deletePack'
| 'packs.packDeleted'
| 'packs.createPack'
| 'packs.editPack'
| 'packs.noPacks'
| 'packs.packName'
| 'packs.packDescription'
| 'packs.packWeight'
| 'packs.packItems'
| 'packs.addItem'
| 'packs.removeItem'
| 'packs.totalWeight'
| 'packs.baseWeight'
| 'packs.categoriesLabel'
| 'packs.categories'
| 'packs.itemsTitleCase'
| 'packs.items'
| 'packs.itemTitleCase'
| 'packs.item'
| 'packs.baseWeightLabelUpper'
| 'packs.totalWeightLabelUpper'
| 'packs.itemsCountLabelUpper'
| 'packs.noCategorizedItems'
| 'packs.organizeGear'
| 'packs.packStats'
| 'packs.weightHistory'
| 'packs.categoryDistribution'
| 'packs.packWeightOverMonths'
| 'packs.weightDistribution'
| 'packs.packInsights'
| 'packs.lighterThanSimilar'
| 'packs.basedOnData'
| 'packs.reducedWeight'
| 'packs.weightReduction'
| 'packs.heaviestCategory'
| 'packs.considerUltralight'
| 'packs.noStatsAvailable'
| 'packs.weightAnalysis'
| 'packs.consumablesWeight'
| 'packs.wornWeight'
| 'packs.weightBreakdown'
| 'packs.detailedAnalysis'
| 'packs.addItemsForBreakdown'
// Items
| 'items.itemDetails'
| 'items.itemName'
| 'items.itemCategory'
| 'items.itemWeight'
| 'items.itemQuantity'
| 'items.addToInventory'
| 'items.removeFromInventory'
| 'items.consumable'
| 'items.worn'
// Trips
| 'trips.myTrips'
| 'trips.createTrip'
| 'trips.tripDetails'
| 'trips.tripName'
| 'trips.tripDate'
| 'trips.tripLocation'
| 'trips.noTrips'
| 'trips.notStarted'
| 'trips.complete'
| 'trips.inProgress'
// Catalog
| 'catalog.itemsCatalog'
| 'catalog.searchItems'
| 'catalog.categories'
| 'catalog.allCategories'
// Welcome
| 'welcome.features.profileManagement.title'
| 'welcome.features.profileManagement.description'
| 'welcome.features.secureMessaging.title'
| 'welcome.features.secureMessaging.description'
| 'welcome.features.activityTracking.title'
| 'welcome.features.activityTracking.description'
| 'welcome.byPressingContinue'
| 'welcome.termsOfService'
| 'welcome.and'
| 'welcome.thatYouHaveRead'
| 'welcome.privacyPolicy'
// AI
| 'ai.aiChat'
| 'ai.aiAssistant'
| 'ai.askQuestion'
| 'ai.sendMessage'
| 'ai.aiPacks'
| 'ai.generating'
| 'ai.generationError'
| 'ai.askAnythingOutdoors'
| 'ai.askAboutItem'
| 'ai.askAboutPack'
| 'ai.suggestions'
// Weather
| 'weather.weather'
| 'weather.temperature'
| 'weather.conditions'
| 'weather.forecast'
| 'weather.humidity'
| 'weather.windSpeed'
| 'weather.addLocation'
// Location
| 'location.location'
| 'location.currentLocation'
| 'location.selectLocation'
| 'location.searchLocation'
| 'location.searchForPlace'
| 'location.searchButton'
| 'location.confirmLocation'
| 'location.notFound'
| 'location.noLocationFound'
| 'location.searchError'
| 'location.somethingWentWrong'
| 'location.missingApiKey'
| 'location.apiKeyNotConfigured'
// Shopping
| 'shopping.shoppingList'
| 'shopping.addToList'
| 'shopping.removeFromList'
| 'shopping.clearList'
// Admin
| 'admin.admin'
| 'admin.adminPanel'
| 'admin.manage'
// Seasons
| 'seasons.seasonSuggestions'
| 'seasons.spring'
| 'seasons.summer'
| 'seasons.fall'
| 'seasons.winter'
// Experience
| 'experience.beginner'
| 'experience.intermediate'
| 'experience.advanced'
| 'experience.expert';
/** Union of every valid translation key derived from `en.json`. */
export type TranslationKeys = ParseKeys;

```tsx
import { t } from 'expo-app/lib/i18n';
import i18next from 'i18next';
Comment on lines 8 to 11
import i18next from 'i18next';
import { useTranslation } from 'expo-app/lib/hooks/useTranslation';
import { t } from 'expo-app/lib/i18n';
import { Button, Text, View } from 'react-native';

Comment on lines +111 to +112
import i18next from 'i18next';
const message = i18next.t('errors.somethingWentWrong');
@andrew-bierman
Copy link
Collaborator

@copilot see copilot feedback and implement where relevant

…on type, fix doc examples

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 13, 2026

@copilot see copilot feedback and implement where relevant

Done in 0ef2c31. Implemented all relevant Copilot reviewer feedback:

  • Named t exportexport const t = i18next.t.bind(i18next) in lib/i18n/index.ts, fixing the 6 files that import { t } from 'expo-app/lib/i18n' and ensuring the configured instance is always used
  • Idempotent init guardif (!i18next.isInitialized) wraps the .init() call to prevent repeated warnings on Fast Refresh
  • TranslationFunction restored — re-added to lib/i18n/types.ts as a TFunction alias from i18next, fixing the 2 files (_layout.tsx, upcoming-trips.tsx) that import it
  • Doc/example examples fixed — replaced import i18next from 'i18next'; i18next.t(...) with import { t } from 'expo-app/lib/i18n'; t(...) in EXAMPLES.tsx, README.md, and I18N_SUMMARY.md

@andrew-bierman
Copy link
Collaborator

@copilot failing actions

…ipt types, Biome lint

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 13, 2026

@copilot failing actions

Fixed in 4eabe94. The CI had two types of failures:

Biome failures (in EXAMPLES.tsx, index.ts, types.ts, useTranslation.ts):

  • noExplicitAny → changed any to Record<string, unknown>
  • noUnusedFunctionParameters → removed unused userName prop from example
  • Trailing newline format issues fixed

TypeScript failures (strict i18next key checking catching real bugs):

  • Added missing keys errors.error and packs.itemDetails to en.json
  • Typed reportReasonTranslationKeys as Record<ReportReason, TranslationKeys> (was string)
  • Removed unreachable || item.reason fallback in ReportedContentScreen.tsx
  • Typed labelKey as TranslationKeys in credentials.tsx and reset-password.tsx
  • Typed getPriorityKey(): TranslationKeys in shopping-list.tsx

@andrew-bierman
Copy link
Collaborator

@copilot see PR feedback and comments

…Quantity, weightEach, weight, unit)

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 13, 2026

@copilot see PR feedback and comments

Fixed in 19c05ba. The previous fix commit had accidentally dropped 4 packs.* keys from en.json during incremental edits — weightAndQuantity, weightEach, weight, and unit. The TypeScript CI was catching these as missing keys. Rebuilt en.json cleanly from the baseline, so now only the two intentionally-new keys differ from main: errors.error and packs.itemDetails.

Resolve conflict in apps/expo/lib/i18n/types.ts by keeping the
auto-derived ParseKeys type (the purpose of this PR) instead of
the manually maintained union type from development.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link
Contributor

Deploying packrat-guides with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9fe0dc2
Status:🚫  Build failed.

View logs

@cloudflare-workers-and-pages
Copy link
Contributor

Deploying packrat-landing with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9fe0dc2
Status:🚫  Build failed.

View logs

@andrew-bierman
Copy link
Collaborator

@copilot fix failing actions

…te key, plural format, Share import)

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Agent-Logs-Url: https://github.com/PackRat-AI/PackRat/sessions/987d2c3e-c2af-4747-a2f2-219ab7d0603a
Copy link
Contributor Author

Copilot AI commented Mar 21, 2026

@copilot fix failing actions

Fixed in 51f3b03. The development branch merge introduced three CI failures:

Biome (noDuplicateObjectKeys): packs.itemDetails existed at line 198 (from development) and again at line 335 (our earlier fix) — removed the duplicate at line 335.

TypeScript (trips.days not a valid key): trips.days was stored as a nested {one, other} object in en.json, which isn't a leaf string key in i18next's ParseKeys. Converted to flat plural format (days_one/days_other) — i18next resolves t('trips.days', { count }) to the correct suffix at runtime, and TypeScript now accepts the base key.

TypeScript (Cannot find name 'Share'): Added Share to the react-native import in TripDetailScreen.tsx.

One remaining TypeScript error (FlashList type error in @packrat-ai/nativewindui/src/components/List.tsx) was already present in PR #1979 before it was merged, and is in a private package we don't own — it needs a fix in that package.

@andrew-bierman andrew-bierman changed the base branch from main to development March 21, 2026 16:50
@andrew-bierman
Copy link
Collaborator

@copilot fix failing actions pls, pre existing or new we need all passing

…r in List.tsx

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Agent-Logs-Url: https://github.com/PackRat-AI/PackRat/sessions/ce4fd168-3c5a-4ca7-8b69-b7212f746ea6
Copy link
Contributor Author

Copilot AI commented Mar 21, 2026

@copilot fix failing actions pls, pre existing or new we need all passing

Fixed in 29b1f79. The last failing action was check-types due to TS2749: 'FlashList' refers to a value, but is being used as a type here in @packrat-ai/nativewindui@1.1.0's List.tsx:34. The package ships TypeScript source files (main: "src/index.ts") with no compiled .d.ts, so tsc compiled the source and hit the error when @shopify/flash-list@2.0.2 changed FlashList from a class to a function component.

Fix: created patches/@packrat-ai+nativewindui@1.1.0.patch (changes React.Ref<FlashList<T>>React.Ref<React.ElementRef<typeof FlashList>>) and added patchedDependencies to package.json so bun automatically applies it after bun install.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/graphql
    • Triggering command: /usr/bin/gh gh auth status (http block)
  • https://api.github.com/repos/mrzachnugent/react-native-reusables/contents/packages/reusables/src/components/ui
    • Triggering command: /usr/bin/curl curl -s REDACTED -H Authorization: token ****** (http block)
  • https://api.github.com/repos/mrzachnugent/react-native-reusables/git/trees/main
    • Triggering command: /usr/bin/curl curl -s REDACTED -H Authorization: token ****** (http block)

If you need me to access, download, or install something from one of these locations, you can either:

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.

3 participants