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
22 changes: 12 additions & 10 deletions mobile/app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { NativeTabs } from 'expo-router/unstable-native-tabs';
import { useWindowDimensions } from 'react-native';
import { useTranslation } from 'react-i18next';

import { alpha, colors } from '@/theme';

export const tabLabelFontSize = (fontScale: number) => 12 / Math.max(fontScale, 1);

export default function TabsLayout() {
const { fontScale } = useWindowDimensions();
const { t } = useTranslation();
const fontSize = tabLabelFontSize(fontScale);
const labelStyle = {
default: {
Expand All @@ -33,25 +35,25 @@ export default function TabsLayout() {
labelStyle={labelStyle}
labelVisibilityMode="labeled"
tintColor={colors.action}>
<NativeTabs.Trigger accessibilityLabel="Today" name="today" testID="tab-today">
<NativeTabs.Trigger accessibilityLabel={t('tabs.today')} name="today" testID="tab-today">
<NativeTabs.Trigger.Icon md="calendar_today" sf="calendar" />
<NativeTabs.Trigger.Label>Today</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Label>{t('tabs.today')}</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger accessibilityLabel="Pantry" name="pantry" testID="tab-pantry">
<NativeTabs.Trigger accessibilityLabel={t('tabs.pantry')} name="pantry" testID="tab-pantry">
<NativeTabs.Trigger.Icon md="inventory_2" sf="cabinet" />
<NativeTabs.Trigger.Label>Pantry</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Label>{t('tabs.pantry')}</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger accessibilityLabel="Rescue" name="rescue" testID="tab-rescue">
<NativeTabs.Trigger accessibilityLabel={t('tabs.rescue')} name="rescue" testID="tab-rescue">
<NativeTabs.Trigger.Icon md="restaurant" sf="fork.knife" />
<NativeTabs.Trigger.Label>Rescue</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Label>{t('tabs.rescue')}</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger accessibilityLabel="Insights" name="insights" testID="tab-insights">
<NativeTabs.Trigger accessibilityLabel={t('tabs.insights')} name="insights" testID="tab-insights">
<NativeTabs.Trigger.Icon md="insights" sf={{ default: 'chart.bar', selected: 'chart.bar.fill' }} />
<NativeTabs.Trigger.Label>Insights</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Label>{t('tabs.insights')}</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger accessibilityLabel="Settings" name="settings" testID="tab-settings">
<NativeTabs.Trigger accessibilityLabel={t('tabs.settings')} name="settings" testID="tab-settings">
<NativeTabs.Trigger.Icon md="settings" sf={{ default: 'gearshape', selected: 'gearshape.fill' }} />
<NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Label>{t('tabs.settings')}</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
</NativeTabs>
);
Expand Down
78 changes: 51 additions & 27 deletions mobile/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { useNetworkState } from 'expo-network';
import { Redirect, Stack, ThemeProvider, usePathname } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { I18nextProvider } from 'react-i18next';
import { GestureHandlerRootView } from 'react-native-gesture-handler';

import { initializeNotifications } from '@/services/notifications';
import { AppErrorBoundary } from '@/application/AppErrorBoundary';
import { OfflineStatusBanner } from '@/application/OfflineStatusBanner';
import { getFreshLoopDatabase } from '@/data/database/expo-sqlite';
import { SettingsRepository } from '@/data/repositories/settings';
import i18next, { initI18n } from '@/i18n';
import { alpha, colors, useFreshLoopFonts } from '@/theme';

export { AppErrorBoundary as ErrorBoundary };
Expand All @@ -33,41 +37,61 @@ export default function RootLayout() {
useFreshLoopFonts();
const pathname = usePathname();
const networkState = useNetworkState();
const [i18nReady, setI18nReady] = useState(false);

useEffect(() => {
void initializeNotifications().catch(() => undefined);
}, []);

useEffect(() => {
let cancelled = false;
void (async () => {
try {
const db = await getFreshLoopDatabase();
const store = new SettingsRepository(db);
await initI18n(store);
} catch {
await initI18n();
}
if (!cancelled) setI18nReady(true);
})();
return () => { cancelled = true; };
}, []);

if (pathname === '/explore') {
return <Redirect href="/today" />;
}

return (
<GestureHandlerRootView style={{ flex: 1 }}>
<ThemeProvider value={freshLoopNavigationTheme}>
<StatusBar style="dark" />
<OfflineStatusBanner
isConnected={networkState.isConnected}
isInternetReachable={networkState.isInternetReachable}
/>
<Stack
screenOptions={{
contentStyle: { backgroundColor: colors.canvas },
headerBackButtonDisplayMode: 'minimal',
headerStyle: { backgroundColor: colors.canvas },
headerTintColor: colors.text,
headerTitleStyle: { fontFamily: 'Manrope-Bold', fontWeight: '700' },
}}>
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="item/add" options={{ title: 'Add item' }} />
<Stack.Screen name="item/scan" options={{ title: 'Add item' }} />
<Stack.Screen name="item/[id]" options={{ title: 'Item detail' }} />
<Stack.Screen name="item/[id]/edit" options={{ title: 'Edit item' }} />
<Stack.Screen name="item/gallery" options={{ title: 'Component gallery' }} />
<Stack.Screen name="recipe/[id]" options={{ title: 'Recipe detail' }} />
</Stack>
</ThemeProvider>
</GestureHandlerRootView>
<I18nextProvider i18n={i18next}>
<GestureHandlerRootView style={{ flex: 1 }}>
<ThemeProvider value={freshLoopNavigationTheme}>
<StatusBar style="dark" />
{i18nReady ? (
<OfflineStatusBanner
isConnected={networkState.isConnected}
isInternetReachable={networkState.isInternetReachable}
/>
) : null}
<Stack
screenOptions={{
contentStyle: { backgroundColor: colors.canvas },
headerBackButtonDisplayMode: 'minimal',
headerStyle: { backgroundColor: colors.canvas },
headerTintColor: colors.text,
headerTitleStyle: { fontFamily: 'Manrope-Bold', fontWeight: '700' },
}}>
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="item/add" options={{ title: 'Add item' }} />
<Stack.Screen name="item/scan" options={{ title: 'Add item' }} />
<Stack.Screen name="item/[id]" options={{ title: 'Item detail' }} />
<Stack.Screen name="item/[id]/edit" options={{ title: 'Edit item' }} />
<Stack.Screen name="item/gallery" options={{ title: 'Component gallery' }} />
<Stack.Screen name="recipe/[id]" options={{ title: 'Recipe detail' }} />
</Stack>
</ThemeProvider>
</GestureHandlerRootView>
</I18nextProvider>
);
}
1 change: 1 addition & 0 deletions mobile/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module.exports = {
preset: 'jest-expo',
testPathIgnorePatterns: ['/node_modules/', '/android/', '/ios/'],
setupFiles: ['./jest.setup.ts'],
};
30 changes: 30 additions & 0 deletions mobile/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';

import en from './src/i18n/locales/en.json';

void i18next.use(initReactI18next).init({
compatibilityJSON: 'v4',
resources: { en: { translation: en } },
lng: 'en',
fallbackLng: 'en',
interpolation: { escapeValue: false },
returnNull: false,
returnEmptyString: false,
});

// Global mock for react-i18next to avoid wrapping every test in I18nextProvider.
// useTranslation() will use the pre-initialised i18next instance above.
// Jest requires `mock` prefix for out-of-scope references in jest.mock factories.
const mockI18next = i18next;
jest.mock('react-i18next', () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const actual = jest.requireActual('react-i18next');
return {
...actual,
useTranslation: () => ({
t: (key: string, opts?: Record<string, unknown>) => mockI18next.t(key, opts ?? {}),
i18n: { language: 'en', changeLanguage: jest.fn() },
}),
};
});
Loading