Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions apps/expo/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,7 @@ export default function AppLayout() {
animation: 'slide_from_bottom',
}}
/>
<Stack.Screen
name="weather/preview"
options={{
headerShown: false,
}}
/>
<Stack.Screen name="weather/preview" />
<Stack.Screen
name="weather/[id]"
options={{
Expand Down
21 changes: 20 additions & 1 deletion apps/expo/app/(app)/ai-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import {
releaseLocalModel,
} from 'expo-app/features/ai/lib/localModelManager';
import { createLocalTools } from 'expo-app/features/ai/lib/tools';
import { useSpeedUnit } from 'expo-app/features/auth/hooks/useSpeedUnit';
import { useTemperatureUnit } from 'expo-app/features/auth/hooks/useTemperatureUnit';
import { useWeightUnit } from 'expo-app/features/auth/hooks/useWeightUnit';
import { getPackItems, packItemsStore } from 'expo-app/features/packs/store/packItems';
import { packsStore } from 'expo-app/features/packs/store/packs';
import { useActiveLocation } from 'expo-app/features/weather/hooks';
Expand Down Expand Up @@ -160,11 +163,21 @@ export default function AIChat() {
releaseLocalModel().then(() => initLocalModel(isAuthenticated));
}, [isAuthenticated]);

const { unit: weightUnit } = useWeightUnit();
const { unit: temperatureUnit } = useTemperatureUnit();
const { unit: speedUnit } = useSpeedUnit();

// Keep a ref for context body values so the transport closure stays fresh
const contextRef = React.useRef(context);
contextRef.current = context;
const isAuthenticatedRef = React.useRef(isAuthenticated);
isAuthenticatedRef.current = isAuthenticated;
const weightUnitRef = React.useRef(weightUnit);
weightUnitRef.current = weightUnit;
const temperatureUnitRef = React.useRef(temperatureUnit);
temperatureUnitRef.current = temperatureUnit;
const speedUnitRef = React.useRef(speedUnit);
speedUnitRef.current = speedUnit;

// Build the right transport based on current AI mode.
// Recreated when aiMode or modelStatus changes (modelStatus drives local readiness).
Expand All @@ -188,7 +201,10 @@ export default function AIChat() {

Context:
- User id is ${userId}
- Current date is ${new Date().toLocaleString()}`;
- Current date is ${new Date().toLocaleString()}
- User's preferred weight unit is ${weightUnitRef.current} (always display weights in this unit)
- User's preferred temperature unit is °${temperatureUnitRef.current} (always display temperatures in this unit)
- User's preferred wind/distance unit is ${speedUnitRef.current === 'mph' ? 'mph / miles' : 'km/h / km'} (always display wind speed and distances in this unit)`;

if (contextRef.current.contextType === 'pack' && contextRef.current.packId) {
systemPrompt += `\n- You are currently helping with a pack with ID: ${contextRef.current.packId}.`;
Expand Down Expand Up @@ -241,6 +257,9 @@ export default function AIChat() {
packId: contextRef.current.packId,
location: locationRef.current,
date: new Date().toLocaleString(),
weightUnit: weightUnitRef.current,
temperatureUnit: temperatureUnitRef.current,
speedUnit: speedUnitRef.current,
}),
}),
transportKey: 'remote',
Expand Down
20 changes: 14 additions & 6 deletions apps/expo/app/(app)/current-pack/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Avatar, AvatarFallback, AvatarImage, Text } from '@packrat/ui/nativewindui';
import { getAppBarOptions } from '@packrat/ui/src/app-bar';
import { userStore } from 'expo-app/features/auth/store';
import { parseWeightUnit } from '@packrat/units';
import { useWeightUnit } from 'expo-app/features/auth/hooks/useWeightUnit';
import { usePackDetailsFromStore } from 'expo-app/features/packs/hooks/usePackDetailsFromStore';
import type { PackItem } from 'expo-app/features/packs/types';
import { type CategorySummary, computeCategorySummaries } from 'expo-app/features/packs/utils';
Expand All @@ -22,13 +23,14 @@ function WeightCard({
weight: number;
className?: string;
}) {
const { unit, convertWeight } = useWeightUnit();
return (
<View className={cn('flex-1 rounded-lg bg-card p-4', className)}>
<Text variant="subhead" className="text-muted-foreground">
{title}
</Text>
<Text variant="title2" className="mt-1 font-semibold">
{weight} g
{convertWeight({ weight, fromUnit: 'g' })} {unit}
</Text>
</View>
);
Expand All @@ -54,6 +56,7 @@ function CustomList<T>({
function CategoryItem({ category, index }: { category: CategorySummary; index: number }) {
const { colors } = useColorScheme();
const { t } = useTranslation();
const { unit: weightUnit } = useWeightUnit();
const itemLabel = category.items === 1 ? t('packs.item') : t('packs.items');

return (
Expand All @@ -66,8 +69,7 @@ function CategoryItem({ category, index }: { category: CategorySummary; index: n
<View>
<Text>{category.name}</Text>
<Text variant="footnote" className="text-muted-foreground">
{category.weight} {userStore.preferredWeightUnit.peek() ?? 'g'} • {category.items}{' '}
{itemLabel}
{category.weight} {weightUnit} • {category.items} {itemLabel}
</Text>
</View>
<View
Expand All @@ -84,6 +86,7 @@ function CategoryItem({ category, index }: { category: CategorySummary; index: n

function ItemRow({ item, index }: { item: PackItem; index: number }) {
const { t } = useTranslation();
const { unit, convertWeight } = useWeightUnit();

return (
<View
Expand Down Expand Up @@ -114,7 +117,11 @@ function ItemRow({ item, index }: { item: PackItem; index: number }) {
</View>
)}
<Text variant="subhead" className="text-muted-foreground">
{item.weight} {item.weightUnit}
{convertWeight({
weight: item.weight,
fromUnit: parseWeightUnit({ value: item.weightUnit }),
})}{' '}
{unit}
</Text>
</View>
</View>
Expand All @@ -126,7 +133,8 @@ export default function CurrentPackScreen() {
const { t } = useTranslation();

const pack = usePackDetailsFromStore(params.id as string);
const uniqueCategories = computeCategorySummaries(pack);
const { unit: weightUnit } = useWeightUnit();
const uniqueCategories = computeCategorySummaries({ pack, preferredUnit: weightUnit });

return (
<SafeAreaView className="flex-1" edges={['bottom']}>
Expand Down
11 changes: 7 additions & 4 deletions apps/expo/app/(app)/pack-categories/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Text } from '@packrat/ui/nativewindui';
import { getAppBarOptions } from '@packrat/ui/src/app-bar';
import { Icon, type MaterialIconName } from 'expo-app/components/Icon';
import { userStore } from 'expo-app/features/auth/store';
import { useWeightUnit } from 'expo-app/features/auth/hooks/useWeightUnit';
import { usePackDetailsFromStore } from 'expo-app/features/packs/hooks/usePackDetailsFromStore';
import { computeCategorySummaries } from 'expo-app/features/packs/utils';
import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme';
Expand All @@ -11,6 +11,7 @@ import { ScrollView, View } from 'react-native';

function CategoryCard({
category,
weightUnit,
}: {
category: {
name: string;
Expand All @@ -19,6 +20,7 @@ function CategoryCard({
percentage: number;
icon?: MaterialIconName;
};
weightUnit: string;
}) {
const { colors } = useColorScheme();
const { t } = useTranslation();
Expand All @@ -45,7 +47,7 @@ function CategoryCard({
<View className="flex-row items-center gap-1">
<Icon name="dumbbell" size={14} color={colors.grey3} />
<Text variant="subhead" className="text-muted-foreground">
{category.weight} {userStore.preferredWeightUnit.peek() ?? 'g'}
{category.weight} {weightUnit}
</Text>
</View>
</View>
Expand All @@ -60,7 +62,8 @@ export default function PackCategoriesScreen() {
const pack = usePackDetailsFromStore(params.id as string);
const { t } = useTranslation();

const categories = computeCategorySummaries(pack);
const { unit: weightUnit } = useWeightUnit();
const categories = computeCategorySummaries({ pack, preferredUnit: weightUnit });

return (
<>
Expand All @@ -75,7 +78,7 @@ export default function PackCategoriesScreen() {

<View className="pb-4">
{categories.map((category) => (
<CategoryCard key={category.name} category={category} />
<CategoryCard key={category.name} category={category} weightUnit={weightUnit} />
))}
</View>
</ScrollView>
Expand Down
10 changes: 5 additions & 5 deletions apps/expo/app/(app)/pack-stats/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Button, Text } from '@packrat/ui/nativewindui';
import { getAppBarOptions } from '@packrat/ui/src/app-bar';
import { featureFlags } from 'expo-app/config';
import { userStore } from 'expo-app/features/auth/store';
import { useWeightUnit } from 'expo-app/features/auth/hooks/useWeightUnit';
import { usePackDetailsFromStore } from 'expo-app/features/packs/hooks/usePackDetailsFromStore';
import { usePackWeightHistory } from 'expo-app/features/packs/hooks/usePackWeightHistory';
import { computeCategorySummaries } from 'expo-app/features/packs/utils';
Expand All @@ -18,8 +18,9 @@ export default function PackStatsScreen() {

const pack = usePackDetailsFromStore(packId);
const weightHistory = usePackWeightHistory(packId);
const { unit: weightUnit, convertWeight } = useWeightUnit();

const categories = computeCategorySummaries(pack);
const categories = computeCategorySummaries({ pack, preferredUnit: weightUnit });
const CATEGORY_DISTRIBUTION = categories.map((category) => ({
name: category.name,
weight: category.weight,
Expand Down Expand Up @@ -63,7 +64,7 @@ export default function PackStatsScreen() {
{item.month}
</Text>
<Text variant="caption2" className="text-muted-foreground">
{item.weight.toFixed(1)} g
{convertWeight({ weight: item.weight, fromUnit: 'g' })} {weightUnit}
</Text>
</View>
);
Expand Down Expand Up @@ -106,8 +107,7 @@ export default function PackStatsScreen() {
<View className="mb-1 flex-row justify-between">
<Text variant="subhead">{item.name}</Text>
<Text variant="subhead">
{item.weight.toFixed(1)} {userStore.preferredWeightUnit.peek() ?? 'g'}(
{item.percentage}%)
{item.weight.toFixed(1)} {weightUnit}({item.percentage}%)
</Text>
</View>
<View className="h-2 overflow-hidden rounded-full bg-muted">
Expand Down
67 changes: 66 additions & 1 deletion apps/expo/app/(app)/settings/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ActivityIndicator, Text } from '@packrat/ui/nativewindui';
import { ActivityIndicator, SegmentedControl, Text } from '@packrat/ui/nativewindui';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as Burnt from 'burnt';
import { appAlert } from 'expo-app/app/_layout';
Expand All @@ -17,9 +17,13 @@ import {
} from 'expo-app/features/ai/lib/localModelManager';
import { DeleteAccountButton } from 'expo-app/features/auth/components/DeleteAccountButton';
import { useAuth } from 'expo-app/features/auth/hooks/useAuth';
import { useSpeedUnit } from 'expo-app/features/auth/hooks/useSpeedUnit';
import { useTemperatureUnit } from 'expo-app/features/auth/hooks/useTemperatureUnit';
import { useWeightUnit } from 'expo-app/features/auth/hooks/useWeightUnit';
import { useSeasonSuggestionsPrefs } from 'expo-app/features/packs/atoms/seasonSuggestionsAtoms';
import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme';
import { useTranslation } from 'expo-app/lib/hooks/useTranslation';
import { testIds } from 'expo-app/lib/testIds';
import ImageCacheManager from 'expo-app/lib/utils/ImageCacheManager';
import Constants from 'expo-constants';
import { useRouter } from 'expo-router';
Expand All @@ -37,6 +41,9 @@ export default function SettingsScreen() {

const router = useRouter();
const { announcementSeen, setAnnouncementSeen, opened, setOpened } = useSeasonSuggestionsPrefs();
const { unit: weightUnit, setWeightUnit } = useWeightUnit();
const { unit: temperatureUnit, setTemperatureUnit } = useTemperatureUnit();
const { unit: speedUnit, setSpeedUnit } = useSpeedUnit();

const isApple = isAppleIntelligenceAvailable();
const isDownloading = modelStatus === 'downloading';
Expand Down Expand Up @@ -100,6 +107,64 @@ export default function SettingsScreen() {
style={Platform.OS === 'ios' ? 'light' : colorScheme === 'dark' ? 'light' : 'dark'}
/>

<View>
<Text variant="subhead" className="mb-3">
{t('settings.displayUnits')}
</Text>
<View className="rounded-xl border border-border bg-card">
<View className="flex-row items-center justify-between p-4">
<View className="flex-1">
<Text className="font-medium">Weight</Text>

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Internationalize the main unit labels for consistency.

The subtitles now use t() for i18n, but the main labels ("Weight", "Temperature", "Wind & Distance") are still hardcoded English. This creates an inconsistency within the Display Units section.

🌐 Complete the i18n integration
               <View className="flex-1">
-                <Text className="font-medium">Weight</Text>
+                <Text className="font-medium">{t('settings.weight')}</Text>
                 <Text variant="footnote" className="mt-0.5 text-muted-foreground">
               <View className="flex-1">
-                <Text className="font-medium">Temperature</Text>
+                <Text className="font-medium">{t('settings.temperature')}</Text>
                 <Text variant="footnote" className="mt-0.5 text-muted-foreground">
               <View className="flex-1">
-                <Text className="font-medium">Wind & Distance</Text>
+                <Text className="font-medium">{t('settings.windDistance')}</Text>
                 <Text variant="footnote" className="mt-0.5 text-muted-foreground">

Add corresponding keys to your translation file (e.g., apps/expo/lib/i18n/locales/en.json):

{
  "settings": {
    "weight": "Weight",
    "temperature": "Temperature",
    "windDistance": "Wind & Distance"
  }
}

Also applies to: 134-134, 151-151

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/expo/app/`(app)/settings/index.tsx at line 117, The main unit labels
"Weight", "Temperature", and "Wind & Distance" in the Display Units section are
hardcoded in English while their subtitles already use the t() function for
internationalization, creating inconsistency. Replace each of these three
hardcoded label strings with corresponding t() function calls to use the
translation keys from your i18n configuration, ensuring all user-facing text in
the Display Units section is properly internationalized.

<Text variant="footnote" className="mt-0.5 text-muted-foreground">
{t('settings.weightSubtitle')}
</Text>
</View>
<View className="w-28">
<SegmentedControl
testID={testIds.settings.weightUnitControl}
values={['kg', 'lb']}
selectedIndex={weightUnit === 'kg' ? 0 : 1}
onIndexChange={(index) => setWeightUnit(index === 0 ? 'kg' : 'lb')}
/>
</View>
</View>
<View className="h-px bg-border mx-4" />
<View className="flex-row items-center justify-between p-4">
<View className="flex-1">
<Text className="font-medium">Temperature</Text>
<Text variant="footnote" className="mt-0.5 text-muted-foreground">
{t('settings.temperatureSubtitle')}
</Text>
</View>
<View className="w-28">
<SegmentedControl
testID={testIds.settings.temperatureUnitControl}
values={['°C', '°F']}
selectedIndex={temperatureUnit === 'C' ? 0 : 1}
onIndexChange={(index) => setTemperatureUnit(index === 0 ? 'C' : 'F')}
/>
</View>
</View>
<View className="h-px bg-border mx-4" />
<View className="flex-row items-center justify-between p-4">
<View className="flex-1">
<Text className="font-medium">Wind & Distance</Text>
<Text variant="footnote" className="mt-0.5 text-muted-foreground">
{t('settings.windDistanceSubtitle')}
</Text>
</View>
<View className="w-36">
<SegmentedControl
testID={testIds.settings.speedUnitControl}
values={['km/h', 'mph']}
selectedIndex={speedUnit === 'kmh' ? 0 : 1}
onIndexChange={(index) => setSpeedUnit(index === 0 ? 'kmh' : 'mph')}
/>
</View>
</View>
</View>
</View>
Comment on lines +110 to +166

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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add internationalization for new UI strings to maintain consistency.

The new "Display Units" section hardcodes English strings ("Display Units", "Weight", "Temperature", etc.), while the rest of the Settings screen uses the t() translation system (e.g., line 182: t('ai.modelManagement')). This creates an i18n gap.

Add translation keys and use t():

🌐 Suggested i18n integration
         <View>
-          <Text variant="subhead" className="mb-3">
-            Display Units
-          </Text>
+          <Text variant="subhead" className="mb-3">
+            {t('settings.displayUnits')}
+          </Text>
           <View className="rounded-xl border border-border bg-card">
             <View className="flex-row items-center justify-between p-4">
               <View className="flex-1">
-                <Text className="font-medium">Weight</Text>
+                <Text className="font-medium">{t('settings.weight')}</Text>
-                <Text variant="footnote" className="mt-0.5 text-muted-foreground">
-                  Shown on pack totals and items
-                </Text>
+                <Text variant="footnote" className="mt-0.5 text-muted-foreground">
+                  {t('settings.weightDescription')}
+                </Text>
               </View>
               {/* ... */}
             </View>
             {/* ... */}
             <View className="flex-row items-center justify-between p-4">
               <View className="flex-1">
-                <Text className="font-medium">Temperature</Text>
+                <Text className="font-medium">{t('settings.temperature')}</Text>
-                <Text variant="footnote" className="mt-0.5 text-muted-foreground">
-                  Shown in weather forecasts
-                </Text>
+                <Text variant="footnote" className="mt-0.5 text-muted-foreground">
+                  {t('settings.temperatureDescription')}
+                </Text>
               </View>
               {/* ... */}
             </View>
           </View>
         </View>

Add corresponding keys to your translation files (e.g., en.json):

{
  "settings": {
    "displayUnits": "Display Units",
    "weight": "Weight",
    "weightDescription": "Shown on pack totals and items",
    "temperature": "Temperature",
    "temperatureDescription": "Shown in weather forecasts"
  }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/expo/app/`(app)/settings/index.tsx around lines 107 - 178, The Display
Units section contains hardcoded English strings that should use the translation
system like the rest of the Settings screen. Replace all hardcoded strings
including "Display Units", "Weight", "Shown on pack totals and items",
"Temperature", and "Shown in weather forecasts" with corresponding t() function
calls (e.g., t('settings.displayUnits'), t('settings.weight'), etc.). Update
each Text component in the Display Units View to use the translation function
instead of literal strings, ensuring consistency with the i18n pattern used
elsewhere in the file.


<View>
<Text variant="subhead" className="mb-3">
{t('ai.modelManagement')}
Expand Down
20 changes: 10 additions & 10 deletions apps/expo/app/(app)/weight-analysis/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { Text } from '@packrat/ui/nativewindui';
import { getAppBarOptions } from '@packrat/ui/src/app-bar';
import { userStore } from 'expo-app/features/auth/store';
import { useWeightUnit } from 'expo-app/features/auth/hooks/useWeightUnit';
import { usePackWeightAnalysis } from 'expo-app/features/packs/hooks/usePackWeightAnalysis';
import { cn } from 'expo-app/lib/cn';
import { useTranslation } from 'expo-app/lib/hooks/useTranslation';
Expand Down Expand Up @@ -43,9 +43,8 @@ export default function WeightAnalysisScreen() {
const packId = params.id;
const { t } = useTranslation();

const { data, items } = usePackWeightAnalysis(packId as string);

const preferredWeightUnit = userStore.preferredWeightUnit.peek() ?? 'g';
const { data, items, preferredUnit } = usePackWeightAnalysis(packId as string);
const { convertWeight } = useWeightUnit();

return (
<SafeAreaView className="flex-1" edges={['bottom']}>
Expand All @@ -59,22 +58,22 @@ export default function WeightAnalysisScreen() {
<View className="grid grid-cols-2 gap-3 p-4">
<WeightCard
title={t('packs.baseWeight')}
weight={`${data.baseWeight} g`}
weight={`${data.baseWeight} ${preferredUnit}`}
className="col-span-1"
/>
<WeightCard
title={t('packs.consumablesWeight')}
weight={`${data.consumableWeight} ${preferredWeightUnit}`}
weight={`${data.consumableWeight} ${preferredUnit}`}
className="col-span-1"
/>
<WeightCard
title={t('packs.wornWeight')}
weight={`${data.wornWeight} ${preferredWeightUnit}`}
weight={`${data.wornWeight} ${preferredUnit}`}
className="col-span-1"
/>
<WeightCard
title={t('packs.totalWeight')}
weight={`${data.totalWeight} ${preferredWeightUnit}`}
weight={`${data.totalWeight} ${preferredUnit}`}
className="col-span-1"
/>
</View>
Expand All @@ -97,7 +96,7 @@ export default function WeightAnalysisScreen() {
{category.name}
</Text>
<Text variant="subhead" className="text-muted-foreground">
{category.weight} {preferredWeightUnit}
{category.weight} {preferredUnit}
</Text>
</View>
</View>
Expand All @@ -123,7 +122,8 @@ export default function WeightAnalysisScreen() {
)}
</View>
<Text variant="subhead" className="text-muted-foreground">
{item.weight} {item.weightUnit}
{convertWeight({ weight: item.weight, fromUnit: item.weightUnit || 'g' })}{' '}
{preferredUnit}
</Text>
</View>
))}
Expand Down
Loading
Loading