Skip to content

Commit d486493

Browse files
os-zhuangclaude
andcommitted
feat(i18n): localize empty / load-error states across renderers + screens
P3 of the i18n sweep — the "No data" / "No X" / "Couldn't Load X" placeholders. New `empty` namespace covering: dashboard, map, timeline, calendar, kanban, chart, gantt, gallery, images, report empties; the load-error titles for page/report/chart/app/packages/flows; and the packages / app-navigation / flows screen empties (+ a flowCount subtitle key). Wired across all the view renderers (Dashboard/Map/Timeline/Calendar/Kanban/ Chart/Gantt/Gallery/ImageGallery/Report/Page) and the reachable screens (app navigation, packages, flows, SDUI page). ChartViewRenderer's `renderChart` helper takes the empty label as an arg since it isn't a component. All three locales at full key parity; tsc + lint clean, full suite 1337 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 19f3569 commit d486493

18 files changed

Lines changed: 173 additions & 39 deletions

‎app/(app)/[appName]/index.tsx‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { View, Text, ScrollView, Linking } from "react-native";
22
import { SafeAreaView } from "react-native-safe-area-context";
33
import { useLocalSearchParams, useRouter } from "expo-router";
4+
import { useTranslation } from "react-i18next";
45
import { Inbox, ChevronRight, AlertCircle } from "lucide-react-native";
56
import { PressableCard } from "~/components/ui/PressableCard";
67
import { EmptyState } from "~/components/ui/EmptyState";
@@ -20,6 +21,7 @@ import { useThemeColors } from "~/lib/theme-colors";
2021
export default function AppHomeScreen() {
2122
const { appName } = useLocalSearchParams<{ appName: string }>();
2223
const router = useRouter();
24+
const { t } = useTranslation();
2325
const { accent } = useThemeColors();
2426
const { app, isLoading, error } = useApp(appName);
2527

@@ -116,16 +118,16 @@ export default function AppHomeScreen() {
116118
<EmptyState
117119
icon={AlertCircle}
118120
variant="error"
119-
title="Couldn't Load App"
121+
title={t("empty.loadApp")}
120122
description={error.message}
121123
/>
122124
</View>
123125
) : navigation.length === 0 ? (
124126
<View className="pt-20">
125127
<EmptyState
126128
icon={Inbox}
127-
title="No Navigation"
128-
description="This app hasn't published a navigation menu yet."
129+
title={t("empty.appNavTitle")}
130+
description={t("empty.appNavDesc")}
129131
/>
130132
</View>
131133
) : (

‎app/(app)/packages.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export default function PackagesScreen() {
6868
<EmptyState
6969
icon={Package}
7070
variant="error"
71-
title="Couldn't Load Packages"
71+
title={t("empty.loadPackages")}
7272
description={error.message}
7373
actionLabel="Retry"
7474
onAction={refetch}
@@ -78,8 +78,8 @@ export default function PackagesScreen() {
7878
<View className="pt-24">
7979
<EmptyState
8080
icon={Package}
81-
title="No Packages"
82-
description="No packages are installed yet."
81+
title={t("empty.packagesTitle")}
82+
description={t("empty.packagesDesc")}
8383
/>
8484
</View>
8585
) : (

‎app/(app)/page/[id].tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useEffect, useState } from "react";
22
import { SafeAreaView } from "react-native-safe-area-context";
33
import { useLocalSearchParams } from "expo-router";
4+
import { useTranslation } from "react-i18next";
45
import { useClient } from "@objectstack/client-react";
56
import { AlertCircle } from "lucide-react-native";
67
import { ScreenHeader } from "~/components/common/ScreenHeader";
@@ -20,6 +21,7 @@ import {
2021
*/
2122
export default function SDUIPageScreen() {
2223
const { id } = useLocalSearchParams<{ id: string }>();
24+
const { t } = useTranslation();
2325
const client = useClient();
2426
const [schema, setSchema] = useState<PageSchema | null>(null);
2527
const [isLoading, setIsLoading] = useState(true);
@@ -64,7 +66,7 @@ export default function SDUIPageScreen() {
6466
<EmptyState
6567
icon={AlertCircle}
6668
variant="error"
67-
title="Couldn't Load Page"
69+
title={t("empty.loadPage")}
6870
description={error.message}
6971
/>
7072
) : schema ? (

‎app/flows/index.tsx‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { View, Text, ScrollView } from "react-native";
22
import { SafeAreaView } from "react-native-safe-area-context";
33
import { useRouter } from "expo-router";
4+
import { useTranslation } from "react-i18next";
45
import { Workflow } from "lucide-react-native";
56
import { ScreenHeader } from "~/components/common/ScreenHeader";
67
import { PressableCard } from "~/components/ui/PressableCard";
@@ -46,33 +47,34 @@ function FlowCard({ flow, onPress }: { flow: FlowDefinition; onPress: () => void
4647
*/
4748
export default function FlowsScreen() {
4849
const router = useRouter();
50+
const { t } = useTranslation();
4951
const { data: flows, isLoading, error, refetch, isRefetching } = useFlows();
5052

5153
const count = flows?.length ?? 0;
5254

5355
return (
5456
<SafeAreaView className="flex-1 bg-background" edges={["left", "right"]}>
5557
<ScreenHeader
56-
title="Automation Flows"
57-
subtitle={count > 0 ? `${count} flow${count === 1 ? "" : "s"}` : undefined}
58+
title={t("empty.flowsHeader")}
59+
subtitle={count > 0 ? t("empty.flowCount", { count }) : undefined}
5860
/>
5961
{isLoading ? (
6062
<ListSkeleton count={6} />
6163
) : error ? (
6264
<EmptyState
6365
icon={Workflow}
6466
variant="error"
65-
title="Couldn't load flows"
67+
title={t("empty.loadFlows")}
6668
description={error.message}
67-
actionLabel="Retry"
69+
actionLabel={t("common.retry")}
6870
onAction={() => void refetch()}
6971
actionLoading={isRefetching}
7072
/>
7173
) : count === 0 ? (
7274
<EmptyState
7375
icon={Workflow}
74-
title="No flows defined"
75-
description="This server has no automation flows yet."
76+
title={t("empty.flowsTitle")}
77+
description={t("empty.flowsDesc")}
7678
/>
7779
) : (
7880
<ScrollView className="flex-1" contentContainerClassName="px-4 pt-4 pb-8">

‎components/renderers/CalendarViewRenderer.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
Pressable,
77
} from "react-native";
88
import { ChevronLeft, ChevronRight } from "lucide-react-native";
9+
import { useTranslation } from "react-i18next";
910
import { useThemeColors } from "~/lib/theme-colors";
1011
import { Skeleton } from "~/components/ui/Skeleton";
1112
import { cn } from "~/lib/utils";
@@ -113,6 +114,7 @@ export function CalendarViewRenderer({
113114
initialYear,
114115
initialMonth,
115116
}: CalendarViewRendererProps) {
117+
const { t } = useTranslation();
116118
const { accent } = useThemeColors();
117119
const now = new Date();
118120
const [year, setYear] = useState(initialYear ?? now.getFullYear());
@@ -257,7 +259,7 @@ export function CalendarViewRenderer({
257259
Events for {selectedDate}
258260
</Text>
259261
{selectedEvents.length === 0 ? (
260-
<Text className="text-sm text-muted-foreground">No events</Text>
262+
<Text className="text-sm text-muted-foreground">{t("empty.calendarEvents")}</Text>
261263
) : (
262264
selectedEvents.map((ev) => (
263265
<Pressable

‎components/renderers/ChartViewRenderer.tsx‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useMemo } from "react";
22
import { View, Text, ScrollView } from "react-native";
33
import { BarChart3, TrendingUp, PieChart, Activity, AlertCircle } from "lucide-react-native";
4+
import { useTranslation } from "react-i18next";
45
import { useThemeColors } from "~/lib/theme-colors";
56
import { Card, CardHeader, CardTitle, CardContent } from "~/components/ui/Card";
67
import { Skeleton } from "~/components/ui/Skeleton";
@@ -202,12 +203,17 @@ function FunnelChartView({ data, height }: { data: AnalyticsDataPoint[]; height:
202203
/* Chart dispatcher */
203204
/* ------------------------------------------------------------------ */
204205

205-
function renderChart(type: ChartType, data: AnalyticsDataPoint[], height: number) {
206+
function renderChart(
207+
type: ChartType,
208+
data: AnalyticsDataPoint[],
209+
height: number,
210+
emptyLabel: string,
211+
) {
206212
if (data.length === 0) {
207213
return (
208214
<View className="items-center justify-center py-12">
209215
<BarChart3 size={40} color="#94a3b8" />
210-
<Text className="mt-3 text-sm text-muted-foreground">No data available</Text>
216+
<Text className="mt-3 text-sm text-muted-foreground">{emptyLabel}</Text>
211217
</View>
212218
);
213219
}
@@ -244,6 +250,7 @@ export function ChartViewRenderer({
244250
error,
245251
chartHeight = 220,
246252
}: ChartViewRendererProps) {
253+
const { t } = useTranslation();
247254
if (isLoading) {
248255
return (
249256
<ScrollView className="flex-1" contentContainerStyle={{ padding: 16 }}>
@@ -277,7 +284,7 @@ export function ChartViewRenderer({
277284
<EmptyState
278285
icon={AlertCircle}
279286
variant="error"
280-
title="Couldn't Load Chart"
287+
title={t("empty.loadChart")}
281288
description={error.message}
282289
/>
283290
);
@@ -295,7 +302,7 @@ export function ChartViewRenderer({
295302
</View>
296303
</CardHeader>
297304
<CardContent>
298-
{renderChart(chartType, data, chartHeight)}
305+
{renderChart(chartType, data, chartHeight, t("empty.noDataAvailable"))}
299306
</CardContent>
300307
</Card>
301308
</ScrollView>

‎components/renderers/DashboardViewRenderer.tsx‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { Card, CardHeader, CardTitle, CardContent } from "~/components/ui/Card";
1212
import { Skeleton } from "~/components/ui/Skeleton";
1313
import { WidgetChart } from "./charts/WidgetChart";
14+
import { useTranslation } from "react-i18next";
1415
import { formatByPattern, formatCurrency, formatNumber } from "~/lib/formatting";
1516
import { useThemeColors } from "~/lib/theme-colors";
1617
import type { DashboardMeta, DashboardWidgetMeta } from "./types";
@@ -185,6 +186,7 @@ function ListWidget({
185186
widget: DashboardWidgetMeta;
186187
data?: WidgetDataPayload;
187188
}) {
189+
const { t } = useTranslation();
188190
const { accent } = useThemeColors();
189191
const records = data?.records ?? [];
190192

@@ -202,7 +204,7 @@ function ListWidget({
202204
{data?.isLoading ? (
203205
<ActivityIndicator size="small" />
204206
) : records.length === 0 ? (
205-
<Text className="text-sm text-muted-foreground">No data</Text>
207+
<Text className="text-sm text-muted-foreground">{t("empty.noData")}</Text>
206208
) : (
207209
<View className="gap-2">
208210
{records.slice(0, 5).map((rec, idx) => {
@@ -243,6 +245,7 @@ function ChartWidget({
243245
widget: DashboardWidgetMeta;
244246
data?: WidgetDataPayload;
245247
}) {
248+
const { t } = useTranslation();
246249
const { accent } = useThemeColors();
247250
const chartType = String(widget.chartConfig?.type ?? widget.type ?? "bar");
248251
const colors = Array.isArray(widget.chartConfig?.colors)
@@ -275,7 +278,7 @@ function ChartWidget({
275278
<View className="items-center justify-center py-8">
276279
<BarChart3 size={48} color="#94a3b8" />
277280
<Text className="mt-3 text-sm text-muted-foreground">
278-
No data to chart
281+
{t("empty.noDataToChart")}
279282
</Text>
280283
</View>
281284
)}
@@ -344,6 +347,7 @@ export function DashboardViewRenderer({
344347
isLoading = false,
345348
onWidgetPress: _onWidgetPress,
346349
}: DashboardViewRendererProps) {
350+
const { t } = useTranslation();
347351
const { width: screenWidth } = useWindowDimensions();
348352
const numColumns = screenWidth > SINGLE_COLUMN_MAX ? 2 : 1;
349353

@@ -357,9 +361,9 @@ export function DashboardViewRenderer({
357361
<View className="h-20 w-20 items-center justify-center rounded-2xl bg-muted">
358362
<Activity size={40} color="#94a3b8" />
359363
</View>
360-
<Text className="mt-5 text-lg font-semibold text-foreground">No Dashboard</Text>
364+
<Text className="mt-5 text-lg font-semibold text-foreground">{t("empty.dashboardTitle")}</Text>
361365
<Text className="mt-2 text-center text-sm text-muted-foreground">
362-
No dashboard widgets have been configured.
366+
{t("empty.dashboardDesc")}
363367
</Text>
364368
</View>
365369
);

‎components/renderers/GalleryViewRenderer.tsx‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { cn } from "~/lib/utils";
66
import { EmptyState } from "~/components/common/EmptyState";
77
import { Skeleton } from "~/components/ui/Skeleton";
88
import { Image as ImageIcon } from "lucide-react-native";
9+
import { useTranslation } from "react-i18next";
910

1011
/* ------------------------------------------------------------------ */
1112
/* Props */
@@ -92,8 +93,9 @@ export function GalleryViewRenderer({
9293
aspectRatio = 1,
9394
isLoading,
9495
onCardPress,
95-
emptyMessage = "No items to display",
96+
emptyMessage,
9697
}: GalleryViewRendererProps) {
98+
const { t } = useTranslation();
9799
const renderCard = useCallback(
98100
({ item }: { item: Record<string, unknown> }) => {
99101
const uri = resolveImageUri(item, imageField);
@@ -160,8 +162,8 @@ export function GalleryViewRenderer({
160162
return (
161163
<EmptyState
162164
icon={<ImageIcon size={40} color="#94a3b8" />}
163-
title="No Items"
164-
description={emptyMessage}
165+
title={t("empty.galleryTitle")}
166+
description={emptyMessage ?? t("empty.noData")}
165167
/>
166168
);
167169
}

‎components/renderers/GanttViewRenderer.tsx‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useMemo, useCallback } from "react";
22
import { View, Text, Pressable, ScrollView } from "react-native";
33
import { EmptyState } from "~/components/common/EmptyState";
44
import { GanttChartSquare } from "lucide-react-native";
5+
import { useTranslation } from "react-i18next";
56
import { Skeleton } from "~/components/ui/Skeleton";
67

78
/* ------------------------------------------------------------------ */
@@ -153,8 +154,9 @@ export function GanttViewRenderer({
153154
colorField,
154155
isLoading,
155156
onTaskPress,
156-
emptyMessage = "No scheduled items",
157+
emptyMessage,
157158
}: GanttViewRendererProps) {
159+
const { t } = useTranslation();
158160
const tasks = useMemo(
159161
() => buildGanttTasks(records, { labelField, startField, endField, colorField }),
160162
[records, labelField, startField, endField, colorField],
@@ -202,8 +204,8 @@ export function GanttViewRenderer({
202204
return (
203205
<EmptyState
204206
icon={<GanttChartSquare size={40} color="#94a3b8" />}
205-
title="Nothing Scheduled"
206-
description={emptyMessage}
207+
title={t("empty.ganttTitle")}
208+
description={emptyMessage ?? t("empty.noData")}
207209
/>
208210
);
209211
}

‎components/renderers/ImageGallery.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
useWindowDimensions,
1010
} from "react-native";
1111
import { X, Download, Share2, Image as ImageIcon } from "lucide-react-native";
12+
import { useTranslation } from "react-i18next";
1213
import { Skeleton } from "~/components/ui/Skeleton";
1314
import { EmptyState } from "~/components/ui/EmptyState";
1415

@@ -52,6 +53,7 @@ export function ImageGallery({
5253
onDownload,
5354
onShare,
5455
}: ImageGalleryProps) {
56+
const { t } = useTranslation();
5557
const { width: screenWidth } = useWindowDimensions();
5658
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
5759

@@ -94,8 +96,8 @@ export function ImageGallery({
9496
return (
9597
<EmptyState
9698
icon={ImageIcon}
97-
title="No Images"
98-
description="There are no images to display yet."
99+
title={t("empty.imagesTitle")}
100+
description={t("empty.imagesDesc")}
99101
/>
100102
);
101103
}

0 commit comments

Comments
 (0)