2d59a3e636
- Svepte 51 hårdkodade strängar -> t() över konto, sök/bläddra, sparade recept, kök, planera, skanna, byt måltid, inköp, minne, cooking, api-fel (+ locLabel/ kategorier via nycklar). 47 nya nycklar, översatta till alla 12 språk. - NY: apps/mobile/scripts/i18n-check.mjs (i18n:check) – fäller bygget om ett språk saknar en nyckel, en använd nyckel saknas, eller det finns hårdkodad UI-text. Wire:ad i CI (.github/workflows/ci.yml) efter typecheck. => 'lägg till språk' blir: kör vakten, den listar exakt vad som fattas. Aldrig mer leta för hand. - Prettier-fixade 5 filer från tidigare leveranser så format:check blir grön. - submit-recipe fri-text-payload + firebase dev-stubs markerade // i18n-ignore. Co-Authored-By: Claude <noreply@anthropic.com>
132 lines
5.2 KiB
TypeScript
132 lines
5.2 KiB
TypeScript
import { useEffect, useState } from "react";
|
||
import { Stack } from "expo-router";
|
||
import { StatusBar } from "expo-status-bar";
|
||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||
import { QueryClient } from "@tanstack/react-query";
|
||
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
|
||
import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister";
|
||
import { useAuth } from "@/lib/auth";
|
||
import { AnalyticsProvider } from "@/lib/analytics";
|
||
import { BRAND } from "@/lib/brand";
|
||
import { t, useI18nVersion } from "@/lib/i18n";
|
||
import { colors } from "@/lib/theme";
|
||
import { LoadingView } from "@/components/ui";
|
||
|
||
/**
|
||
* Rotlayout: React Query med AsyncStorage-persistens för Smart Cache/offline
|
||
* (spec §44: cachelagrade recept, lager och inköpslista fungerar offline).
|
||
*/
|
||
|
||
const queryClient = new QueryClient({
|
||
defaultOptions: {
|
||
queries: {
|
||
staleTime: 30_000,
|
||
gcTime: 7 * 24 * 3600_000, // behåll cache en vecka för offline-läge
|
||
retry: 1,
|
||
},
|
||
},
|
||
});
|
||
|
||
const persister = createAsyncStoragePersister({
|
||
storage: AsyncStorage,
|
||
key: `${BRAND.slug}-query-cache-v2`,
|
||
});
|
||
|
||
export default function RootLayout() {
|
||
const hydrate = useAuth((s) => s.hydrate);
|
||
const hydrated = useAuth((s) => s.hydrated);
|
||
const [ready, setReady] = useState(false);
|
||
// Språkbyte utan omstart (i18n M4): versionen bumpar vid setLocale och
|
||
// key={} re-monterar hela trädet så att alla t()-strängar byts direkt.
|
||
const i18nVersion = useI18nVersion();
|
||
|
||
useEffect(() => {
|
||
void hydrate().then(() => setReady(true));
|
||
}, [hydrate]);
|
||
|
||
if (!hydrated || !ready) return <LoadingView />;
|
||
|
||
return (
|
||
<PersistQueryClientProvider
|
||
key={`i18n-${i18nVersion}`}
|
||
client={queryClient}
|
||
persistOptions={{ persister }}
|
||
>
|
||
<AnalyticsProvider>
|
||
<StatusBar style="dark" />
|
||
<Stack
|
||
screenOptions={{
|
||
headerStyle: { backgroundColor: colors.background },
|
||
headerTintColor: colors.text,
|
||
headerShadowVisible: false,
|
||
// Chevron utan textetikett – tar bort "tillbaka"-labeln som annars
|
||
// blinkar/hoppar när den mäts om under push-övergången.
|
||
headerBackButtonDisplayMode: "minimal",
|
||
contentStyle: { backgroundColor: colors.background },
|
||
fullScreenGestureEnabled: true,
|
||
}}
|
||
>
|
||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||
<Stack.Screen name="(auth)/login" options={{ headerShown: false }} />
|
||
<Stack.Screen name="(auth)/register" options={{ headerShown: false }} />
|
||
<Stack.Screen name="(auth)/forgot-password" options={{ headerShown: false }} />
|
||
<Stack.Screen name="(auth)/reset-password" options={{ headerShown: false }} />
|
||
<Stack.Screen name="(auth)/verify-email" options={{ headerShown: false }} />
|
||
<Stack.Screen name="onboarding" options={{ headerShown: false, gestureEnabled: false }} />
|
||
<Stack.Screen name="recipe/[id]" options={{ title: "" }} />
|
||
<Stack.Screen
|
||
name="cooking/[id]"
|
||
options={{ title: "", presentation: "fullScreenModal" }}
|
||
/>
|
||
<Stack.Screen
|
||
name="scan-review/[jobId]"
|
||
options={{ title: t("scan.review.title"), fullScreenGestureEnabled: false }}
|
||
/>
|
||
<Stack.Screen
|
||
name="swap-meal/[entryId]"
|
||
options={{ presentation: "modal", title: t("nav.swap") }}
|
||
/>
|
||
<Stack.Screen
|
||
name="meal-review/[jobId]"
|
||
options={{ title: t("scan.meal.title"), fullScreenGestureEnabled: false }}
|
||
/>
|
||
<Stack.Screen name="reconciliation" options={{ title: t("reconciliation.title") }} />
|
||
<Stack.Screen
|
||
name="scan-diff-review/[jobId]"
|
||
options={{ title: t("scan.review.title"), fullScreenGestureEnabled: false }}
|
||
/>
|
||
<Stack.Screen
|
||
name="shopping"
|
||
options={{ title: t("shopping.title"), presentation: "modal" }}
|
||
/>
|
||
<Stack.Screen
|
||
name="meal-boxes"
|
||
options={{ title: t("mealbox.title"), presentation: "modal" }}
|
||
/>
|
||
<Stack.Screen
|
||
name="household"
|
||
options={{ title: t("home.household"), presentation: "modal" }}
|
||
/>
|
||
<Stack.Screen name="memory" options={{ title: t("memory.title") }} />
|
||
<Stack.Screen name="saved-recipes" options={{ title: t("nav.savedRecipes") }} />
|
||
<Stack.Screen name="recipes" options={{ title: t("nav.browseRecipes") }} />
|
||
<Stack.Screen name="kitchen" options={{ title: t("nav.kitchen") }} />
|
||
<Stack.Screen
|
||
name="profile"
|
||
options={{ title: t("profile.title"), presentation: "modal" }}
|
||
/>
|
||
<Stack.Screen
|
||
name="paywall"
|
||
options={{ title: t("paywall.title"), presentation: "modal" }}
|
||
/>
|
||
<Stack.Screen
|
||
name="log-meal"
|
||
options={{ title: t("myday.logMeal"), presentation: "modal" }}
|
||
/>
|
||
<Stack.Screen name="barcode" options={{ title: t("nav.barcode") }} />
|
||
</Stack>
|
||
</AnalyticsProvider>
|
||
</PersistQueryClientProvider>
|
||
);
|
||
}
|