mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
feat(native): internationalize tab bar labels and improve recently-viewed row
- Wrap tab bar trigger labels and stack screen titles with `useLingui()` / `t` so they translate correctly across all 6 supported locales - Swap `IconDeviceTv` → `IconDeviceTvOld` in the recently-viewed badge to match the icon used elsewhere in the app - Show the type icon inside the pill badge and display a department label (Actor, Director, Writer, etc.) for person results instead of the generic "Person" / "TV" label - Suppress the subtitle row for person results (department is shown in the badge instead) - Merge the separate iOS/Android `headerTitleStyle` variables in `TabStack` into a single shared style - Mark `packages/i18n/src/po/*` as readonly/excluded in VS Code settings alongside `routeTree.gen.ts` - Extract and compile updated PO/TS catalogs for all locales (de, en, es, fr, it, pt)
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import { useLingui } from "@lingui/react/macro";
|
||||
|
||||
import { TabStack } from "@/components/navigation/tab-stack";
|
||||
|
||||
export default function ExploreLayout() {
|
||||
return <TabStack title="Explore" />;
|
||||
const { t } = useLingui();
|
||||
return <TabStack title={t`Explore`} />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useLingui } from "@lingui/react/macro";
|
||||
|
||||
import { TabStack } from "@/components/navigation/tab-stack";
|
||||
|
||||
export default function HomeLayout() {
|
||||
return <TabStack title="Home" />;
|
||||
const { t } = useLingui();
|
||||
return <TabStack title={t`Home`} />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useLingui } from "@lingui/react/macro";
|
||||
|
||||
import { TabStack } from "@/components/navigation/tab-stack";
|
||||
|
||||
export default function SearchLayout() {
|
||||
return <TabStack />;
|
||||
const { t } = useLingui();
|
||||
return <TabStack title={t`Search`} />;
|
||||
}
|
||||
|
||||
@@ -68,13 +68,7 @@ export default function SearchScreen() {
|
||||
const keyExtractor = useCallback((item: SearchResultItem) => `${item.type}-${item.id}`, []);
|
||||
|
||||
return (
|
||||
<View className="bg-background flex-1">
|
||||
<Stack.Header
|
||||
transparent={false}
|
||||
style={{ backgroundColor: "#000" }}
|
||||
largeStyle={{ backgroundColor: "#000" }}
|
||||
/>
|
||||
<Stack.Screen.Title>Search</Stack.Screen.Title>
|
||||
<View collapsable={false} className="bg-background flex-1">
|
||||
<Stack.SearchBar
|
||||
placeholder={t`Search movies, shows, people...`}
|
||||
onChangeText={(e) => setQuery(e.nativeEvent.text)}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useLingui } from "@lingui/react/macro";
|
||||
|
||||
import { TabStack } from "@/components/navigation/tab-stack";
|
||||
|
||||
export default function SettingsLayout() {
|
||||
return <TabStack title="Settings" />;
|
||||
const { t } = useLingui();
|
||||
return <TabStack title={t`Settings`} />;
|
||||
}
|
||||
|
||||
@@ -58,8 +58,8 @@ import { activateLocale, type SupportedLocale } from "@sofa/i18n";
|
||||
import { LOCALE_INFO } from "@sofa/i18n/locales";
|
||||
|
||||
const settingsContentContainerStyle = {
|
||||
paddingTop: 8,
|
||||
paddingBottom: 32,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 24,
|
||||
paddingHorizontal: 16,
|
||||
};
|
||||
|
||||
@@ -535,17 +535,17 @@ export default function SettingsScreen() {
|
||||
<Animated.View entering={FadeInDown.duration(300).delay(450)} className="mt-3 items-center">
|
||||
<Pressable
|
||||
onPress={() => Linking.openURL("https://github.com/jakejarvis/sofa")}
|
||||
className="flex-row items-center gap-1.5 active:opacity-70"
|
||||
className="flex-row items-center gap-1 active:opacity-70"
|
||||
>
|
||||
<ScaledIcon icon={IconBrandGithub} size={14} color={mutedFgColor} />
|
||||
<Text className="text-muted-foreground text-xs">GitHub</Text>
|
||||
<Text className="text-muted-foreground text-xs">jakejarvis/sofa</Text>
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
|
||||
{/* TMDB Attribution */}
|
||||
<Animated.View
|
||||
entering={FadeInDown.duration(300).delay(500)}
|
||||
className="mt-4 items-center gap-2"
|
||||
className="mt-5 items-center gap-2"
|
||||
>
|
||||
<Pressable
|
||||
onPress={() => Linking.openURL("https://www.themoviedb.org/")}
|
||||
@@ -553,8 +553,8 @@ export default function SettingsScreen() {
|
||||
>
|
||||
<TmdbLogo height={12} />
|
||||
<Text
|
||||
maxFontSizeMultiplier={1.0}
|
||||
className="text-muted-foreground text-center text-xs leading-relaxed"
|
||||
maxFontSizeMultiplier={1.2}
|
||||
className="text-muted-foreground text-center text-[10px] leading-relaxed"
|
||||
>
|
||||
<Trans>This product uses the TMDB API but is not endorsed or certified by TMDB.</Trans>
|
||||
</Text>
|
||||
|
||||
@@ -38,14 +38,9 @@ enableFreeze(true);
|
||||
initialize();
|
||||
const localeReady = initLocale();
|
||||
|
||||
export const unstable_settings = {
|
||||
initialRouteName: "(tabs)",
|
||||
};
|
||||
|
||||
const changePasswordOptions =
|
||||
process.env.EXPO_OS === "ios"
|
||||
? {
|
||||
headerShown: true,
|
||||
presentation: "formSheet" as const,
|
||||
sheetAllowedDetents: "fitToContents" as const,
|
||||
sheetGrabberVisible: true,
|
||||
@@ -54,7 +49,6 @@ const changePasswordOptions =
|
||||
headerBlurEffect: "none" as const,
|
||||
}
|
||||
: {
|
||||
headerShown: true,
|
||||
presentation: "modal" as const,
|
||||
headerLargeTitle: false,
|
||||
headerTransparent: false,
|
||||
@@ -89,7 +83,7 @@ function AppContent() {
|
||||
// distinct ID, but only when the resolved state is actually enabled
|
||||
// (respects both ATT result and the user's settings override).
|
||||
if (enabled && posthog) {
|
||||
const advertisingId = await getAdvertisingId();
|
||||
const advertisingId = getAdvertisingId();
|
||||
if (advertisingId) {
|
||||
posthog.identify(advertisingId);
|
||||
}
|
||||
@@ -132,13 +126,12 @@ function AppContent() {
|
||||
<ServerUnreachableBanner />
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
contentStyle,
|
||||
animation: "slide_from_right",
|
||||
headerShown: false,
|
||||
}}
|
||||
>
|
||||
<Stack.Protected guard={!session}>
|
||||
<Stack.Screen name="(auth)" />
|
||||
<Stack.Screen name="(auth)" options={{ navigationBarHidden: true, animation: "fade" }} />
|
||||
</Stack.Protected>
|
||||
|
||||
<Stack.Protected guard={!!session}>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useLingui } from "@lingui/react/macro";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { NativeTabs } from "expo-router/unstable-native-tabs";
|
||||
import { useCSSVariable } from "uniwind";
|
||||
|
||||
export function NativeTabBar({ showSettingsBadge }: { showSettingsBadge: boolean }) {
|
||||
const { t } = useLingui();
|
||||
const mutedFgColor = useCSSVariable("--color-muted-foreground") as string;
|
||||
const primaryColor = useCSSVariable("--color-primary") as string;
|
||||
|
||||
@@ -23,20 +25,20 @@ export function NativeTabBar({ showSettingsBadge }: { showSettingsBadge: boolean
|
||||
})}
|
||||
>
|
||||
<NativeTabs.Trigger name="(home)" disableTransparentOnScrollEdge>
|
||||
<NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
|
||||
<NativeTabs.Trigger.Label>{t`Home`}</NativeTabs.Trigger.Label>
|
||||
<NativeTabs.Trigger.Icon sf="house.fill" md="home" />
|
||||
</NativeTabs.Trigger>
|
||||
<NativeTabs.Trigger name="(explore)" disableTransparentOnScrollEdge>
|
||||
<NativeTabs.Trigger.Label>Explore</NativeTabs.Trigger.Label>
|
||||
<NativeTabs.Trigger.Label>{t`Explore`}</NativeTabs.Trigger.Label>
|
||||
<NativeTabs.Trigger.Icon sf="safari" md="explore" />
|
||||
</NativeTabs.Trigger>
|
||||
<NativeTabs.Trigger name="(settings)" disableTransparentOnScrollEdge>
|
||||
<NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
|
||||
<NativeTabs.Trigger.Label>{t`Settings`}</NativeTabs.Trigger.Label>
|
||||
<NativeTabs.Trigger.Icon sf="gear" md="settings" />
|
||||
{showSettingsBadge ? <NativeTabs.Trigger.Badge>!</NativeTabs.Trigger.Badge> : null}
|
||||
</NativeTabs.Trigger>
|
||||
<NativeTabs.Trigger name="(search)" role="search" disableTransparentOnScrollEdge>
|
||||
<NativeTabs.Trigger.Label>Search</NativeTabs.Trigger.Label>
|
||||
<NativeTabs.Trigger.Label>{t`Search`}</NativeTabs.Trigger.Label>
|
||||
<NativeTabs.Trigger.Icon sf="magnifyingglass" md="search" />
|
||||
</NativeTabs.Trigger>
|
||||
</NativeTabs>
|
||||
|
||||
@@ -8,11 +8,8 @@ export function TabStack({ title, children }: { title?: string; children?: React
|
||||
const contentStyle = useResolveClassNames("bg-background");
|
||||
const tintColor = useCSSVariable("--color-primary") as string;
|
||||
const backgroundColor = useCSSVariable("--color-background") as string;
|
||||
const iosHeaderLargeTitleStyle = useResolveClassNames("font-display text-foreground");
|
||||
const iosHeaderTitleStyle = useResolveClassNames("font-display text-foreground text-lg");
|
||||
const androidHeaderTitleStyle = useResolveClassNames(
|
||||
"text-foreground font-sans text-lg font-semibold",
|
||||
);
|
||||
const headerTitleStyle = useResolveClassNames("font-display text-foreground text-xl");
|
||||
const headerLargeTitleStyle = useResolveClassNames("font-display text-foreground");
|
||||
|
||||
if (process.env.EXPO_OS === "ios") {
|
||||
return (
|
||||
@@ -41,8 +38,8 @@ export function TabStack({ title, children }: { title?: string; children?: React
|
||||
/>
|
||||
<Stack.Screen.Title
|
||||
large
|
||||
style={iosHeaderTitleStyle as Record<string, unknown>}
|
||||
largeStyle={iosHeaderLargeTitleStyle as Record<string, unknown>}
|
||||
style={headerTitleStyle as Record<string, unknown>}
|
||||
largeStyle={headerLargeTitleStyle as Record<string, unknown>}
|
||||
>
|
||||
{title}
|
||||
</Stack.Screen.Title>
|
||||
@@ -71,7 +68,7 @@ export function TabStack({ title, children }: { title?: string; children?: React
|
||||
shadowColor: "transparent",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen.Title style={androidHeaderTitleStyle as Record<string, unknown>}>
|
||||
<Stack.Screen.Title style={headerTitleStyle as Record<string, unknown>}>
|
||||
{title}
|
||||
</Stack.Screen.Title>
|
||||
</Stack.Screen>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useLingui } from "@lingui/react/macro";
|
||||
import { IconDeviceTv, IconMovie, IconUser } from "@tabler/icons-react-native";
|
||||
import { IconDeviceTvOld, IconMovie, IconUser } from "@tabler/icons-react-native";
|
||||
import { View } from "react-native";
|
||||
import { useCSSVariable } from "uniwind";
|
||||
|
||||
@@ -9,23 +9,29 @@ import type { RecentlyViewedItem } from "@/lib/recently-viewed";
|
||||
|
||||
const TypeIcon = {
|
||||
movie: IconMovie,
|
||||
tv: IconDeviceTv,
|
||||
tv: IconDeviceTvOld,
|
||||
person: IconUser,
|
||||
} as const;
|
||||
|
||||
function getTypeLabel(t: (strings: TemplateStringsArray, ...values: unknown[]) => string) {
|
||||
return {
|
||||
movie: t`Movie`,
|
||||
tv: t`TV`,
|
||||
person: t`Person`,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function RecentlyViewedRowContent({ item }: { item: RecentlyViewedItem }) {
|
||||
const { t } = useLingui();
|
||||
const mutedForeground = useCSSVariable("--color-muted-foreground") as string;
|
||||
const Icon = TypeIcon[item.type];
|
||||
|
||||
const typeLabel: Record<string, string> = {
|
||||
movie: t`Movie`,
|
||||
tv: t`TV`,
|
||||
person: t`Person`,
|
||||
};
|
||||
|
||||
const departmentLabels: Record<string, string> = {
|
||||
Acting: t`Actor`,
|
||||
Directing: t`Director`,
|
||||
Writing: t`Writer`,
|
||||
Production: t`Producer`,
|
||||
Editing: t`Editor`,
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="flex-row items-center">
|
||||
<View
|
||||
@@ -56,12 +62,15 @@ export function RecentlyViewedRowContent({ item }: { item: RecentlyViewedItem })
|
||||
{item.title}
|
||||
</Text>
|
||||
<View className="mt-1 flex-row items-center gap-2">
|
||||
<View className="bg-secondary rounded-full px-2 py-0.5">
|
||||
<View className="bg-secondary flex-row items-center gap-1 rounded-full px-2 py-0.5">
|
||||
<Icon size={12} color={mutedForeground} />
|
||||
<Text maxFontSizeMultiplier={1.0} className="text-muted-foreground text-xs">
|
||||
{getTypeLabel(t)[item.type]}
|
||||
{item.type === "person"
|
||||
? departmentLabels[item.subtitle ?? ""]
|
||||
: typeLabel[item.type]}
|
||||
</Text>
|
||||
</View>
|
||||
{item.subtitle ? (
|
||||
{item.type !== "person" && item.subtitle ? (
|
||||
<Text className="text-muted-foreground text-xs">{item.subtitle}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user