feat(i18n): migrate to i18next with official TypeScript module augmentation for compile-time key safety#1963
feat(i18n): migrate to i18next with official TypeScript module augmentation for compile-time key safety#1963Copilot wants to merge 9 commits intodevelopmentfrom
Conversation
…safety Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
|
@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>
Done in b15a497. Migrated from
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:
If you need me to access, download, or install something from one of these locations, you can either:
|
There was a problem hiding this comment.
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-jsdependency withi18nextandreact-i18next. - Adds i18next initialization/resources wiring and
CustomTypeOptionsmodule 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.
apps/expo/lib/i18n/index.ts
Outdated
| export default i18next; | ||
|
|
apps/expo/lib/i18n/index.ts
Outdated
| i18next.use(initReactI18next).init({ | ||
| resources, | ||
| lng: Localization.getLocales()[0]?.languageCode ?? 'en', | ||
| fallbackLng: 'en', | ||
| defaultNS, | ||
| interpolation: { | ||
| escapeValue: false, // React already escapes values | ||
| }, | ||
| }); |
apps/expo/lib/i18n/types.ts
Outdated
| 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; |
apps/expo/lib/i18n/README.md
Outdated
|
|
||
| ```tsx | ||
| import { t } from 'expo-app/lib/i18n'; | ||
| import i18next from 'i18next'; |
apps/expo/lib/i18n/EXAMPLES.tsx
Outdated
| 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'; | ||
|
|
apps/expo/I18N_SUMMARY.md
Outdated
| import i18next from 'i18next'; | ||
| const message = i18next.t('errors.somethingWentWrong'); |
|
@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>
Done in 0ef2c31. Implemented all relevant Copilot reviewer feedback:
|
|
@copilot failing actions |
…ipt types, Biome lint Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Fixed in 4eabe94. The CI had two types of failures: Biome failures (in
TypeScript failures (strict i18next key checking catching real bugs):
|
|
@copilot see PR feedback and comments |
…Quantity, weightEach, weight, unit) Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Fixed in 19c05ba. The previous fix commit had accidentally dropped 4 |
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>
|
@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
Fixed in 51f3b03. The development branch merge introduced three CI failures: Biome ( TypeScript ( TypeScript ( One remaining TypeScript error ( |
|
@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
Fixed in 29b1f79. The last failing action was Fix: created 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:
If you need me to access, download, or install something from one of these locations, you can either:
|
texport tolib/i18n/index.ts— fixes 6 callsites importing{ t }!i18next.isInitialized— prevents Fast Refresh re-init warningsTranslationFunctiontolib/i18n/types.tsasTFunctionalias{ t }fromexpo-app/lib/i18nerrors.errorandpacks.itemDetailstoen.jsonpacks.weightAndQuantity,packs.weightEach,packs.weight,packs.unitpacks.itemDetailsintroduced by development mergetrips.daysfrom nested{one,other}to flatdays_one/days_otherplural format (i18next ParseKeys requires flat keys fort('trips.days', { count })to type-check)Shareimport toTripDetailScreen.tsxreportReasonTranslationKeysasRecord<ReportReason, TranslationKeys>|| item.reasonfallback inReportedContentScreen.tsxlabelKeyasTranslationKeysincredentials.tsxandreset-password.tsxgetPriorityKey(): TranslationKeysinshopping-list.tsxnoExplicitAny,noUnusedFunctionParameters, trailing newlines@packrat-ai/nativewindui@1.1.0—TS2749: 'FlashList' refers to a value, but is being used as a typeinList.tsx:34; fixReact.Ref<FlashList<T>>→React.Ref<React.ElementRef<typeof FlashList>>viapatchedDependenciesinpackage.jsonOriginal prompt
📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.