From d4864938f42570ef71ce4ffb64c5014ec2454059 Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Thu, 11 Jun 2026 18:20:02 +0500 Subject: [PATCH] feat(i18n): localize empty / load-error states across renderers + screens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- app/(app)/[appName]/index.tsx | 8 +++-- app/(app)/packages.tsx | 6 ++-- app/(app)/page/[id].tsx | 4 ++- app/flows/index.tsx | 14 ++++---- components/renderers/CalendarViewRenderer.tsx | 4 ++- components/renderers/ChartViewRenderer.tsx | 15 ++++++--- .../renderers/DashboardViewRenderer.tsx | 12 ++++--- components/renderers/GalleryViewRenderer.tsx | 8 +++-- components/renderers/GanttViewRenderer.tsx | 8 +++-- components/renderers/ImageGallery.tsx | 6 ++-- components/renderers/KanbanViewRenderer.tsx | 4 ++- components/renderers/MapViewRenderer.tsx | 6 ++-- components/renderers/PageRenderer.tsx | 4 ++- components/renderers/ReportRenderer.tsx | 8 +++-- components/renderers/TimelineViewRenderer.tsx | 6 ++-- locales/ar.json | 33 +++++++++++++++++++ locales/en.json | 33 +++++++++++++++++++ locales/zh.json | 33 +++++++++++++++++++ 18 files changed, 173 insertions(+), 39 deletions(-) diff --git a/app/(app)/[appName]/index.tsx b/app/(app)/[appName]/index.tsx index 399bce3..608a83b 100644 --- a/app/(app)/[appName]/index.tsx +++ b/app/(app)/[appName]/index.tsx @@ -1,6 +1,7 @@ import { View, Text, ScrollView, Linking } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { useLocalSearchParams, useRouter } from "expo-router"; +import { useTranslation } from "react-i18next"; import { Inbox, ChevronRight, AlertCircle } from "lucide-react-native"; import { PressableCard } from "~/components/ui/PressableCard"; import { EmptyState } from "~/components/ui/EmptyState"; @@ -20,6 +21,7 @@ import { useThemeColors } from "~/lib/theme-colors"; export default function AppHomeScreen() { const { appName } = useLocalSearchParams<{ appName: string }>(); const router = useRouter(); + const { t } = useTranslation(); const { accent } = useThemeColors(); const { app, isLoading, error } = useApp(appName); @@ -116,7 +118,7 @@ export default function AppHomeScreen() { @@ -124,8 +126,8 @@ export default function AppHomeScreen() { ) : ( diff --git a/app/(app)/packages.tsx b/app/(app)/packages.tsx index fd92933..7dbec7e 100644 --- a/app/(app)/packages.tsx +++ b/app/(app)/packages.tsx @@ -68,7 +68,7 @@ export default function PackagesScreen() { ) : ( diff --git a/app/(app)/page/[id].tsx b/app/(app)/page/[id].tsx index 30f4b7f..d37ee15 100644 --- a/app/(app)/page/[id].tsx +++ b/app/(app)/page/[id].tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from "react"; import { SafeAreaView } from "react-native-safe-area-context"; import { useLocalSearchParams } from "expo-router"; +import { useTranslation } from "react-i18next"; import { useClient } from "@objectstack/client-react"; import { AlertCircle } from "lucide-react-native"; import { ScreenHeader } from "~/components/common/ScreenHeader"; @@ -20,6 +21,7 @@ import { */ export default function SDUIPageScreen() { const { id } = useLocalSearchParams<{ id: string }>(); + const { t } = useTranslation(); const client = useClient(); const [schema, setSchema] = useState(null); const [isLoading, setIsLoading] = useState(true); @@ -64,7 +66,7 @@ export default function SDUIPageScreen() { ) : schema ? ( diff --git a/app/flows/index.tsx b/app/flows/index.tsx index a50e2d0..94ee5fa 100644 --- a/app/flows/index.tsx +++ b/app/flows/index.tsx @@ -1,6 +1,7 @@ import { View, Text, ScrollView } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { useRouter } from "expo-router"; +import { useTranslation } from "react-i18next"; import { Workflow } from "lucide-react-native"; import { ScreenHeader } from "~/components/common/ScreenHeader"; import { PressableCard } from "~/components/ui/PressableCard"; @@ -46,6 +47,7 @@ function FlowCard({ flow, onPress }: { flow: FlowDefinition; onPress: () => void */ export default function FlowsScreen() { const router = useRouter(); + const { t } = useTranslation(); const { data: flows, isLoading, error, refetch, isRefetching } = useFlows(); const count = flows?.length ?? 0; @@ -53,8 +55,8 @@ export default function FlowsScreen() { return ( 0 ? `${count} flow${count === 1 ? "" : "s"}` : undefined} + title={t("empty.flowsHeader")} + subtitle={count > 0 ? t("empty.flowCount", { count }) : undefined} /> {isLoading ? ( @@ -62,17 +64,17 @@ export default function FlowsScreen() { void refetch()} actionLoading={isRefetching} /> ) : count === 0 ? ( ) : ( diff --git a/components/renderers/CalendarViewRenderer.tsx b/components/renderers/CalendarViewRenderer.tsx index 371d1d4..18b2e3d 100644 --- a/components/renderers/CalendarViewRenderer.tsx +++ b/components/renderers/CalendarViewRenderer.tsx @@ -6,6 +6,7 @@ import { Pressable, } from "react-native"; import { ChevronLeft, ChevronRight } from "lucide-react-native"; +import { useTranslation } from "react-i18next"; import { useThemeColors } from "~/lib/theme-colors"; import { Skeleton } from "~/components/ui/Skeleton"; import { cn } from "~/lib/utils"; @@ -113,6 +114,7 @@ export function CalendarViewRenderer({ initialYear, initialMonth, }: CalendarViewRendererProps) { + const { t } = useTranslation(); const { accent } = useThemeColors(); const now = new Date(); const [year, setYear] = useState(initialYear ?? now.getFullYear()); @@ -257,7 +259,7 @@ export function CalendarViewRenderer({ Events for {selectedDate} {selectedEvents.length === 0 ? ( - No events + {t("empty.calendarEvents")} ) : ( selectedEvents.map((ev) => ( - No data available + {emptyLabel} ); } @@ -244,6 +250,7 @@ export function ChartViewRenderer({ error, chartHeight = 220, }: ChartViewRendererProps) { + const { t } = useTranslation(); if (isLoading) { return ( @@ -277,7 +284,7 @@ export function ChartViewRenderer({ ); @@ -295,7 +302,7 @@ export function ChartViewRenderer({ - {renderChart(chartType, data, chartHeight)} + {renderChart(chartType, data, chartHeight, t("empty.noDataAvailable"))} diff --git a/components/renderers/DashboardViewRenderer.tsx b/components/renderers/DashboardViewRenderer.tsx index 0da4dd3..79b2901 100644 --- a/components/renderers/DashboardViewRenderer.tsx +++ b/components/renderers/DashboardViewRenderer.tsx @@ -11,6 +11,7 @@ import { import { Card, CardHeader, CardTitle, CardContent } from "~/components/ui/Card"; import { Skeleton } from "~/components/ui/Skeleton"; import { WidgetChart } from "./charts/WidgetChart"; +import { useTranslation } from "react-i18next"; import { formatByPattern, formatCurrency, formatNumber } from "~/lib/formatting"; import { useThemeColors } from "~/lib/theme-colors"; import type { DashboardMeta, DashboardWidgetMeta } from "./types"; @@ -185,6 +186,7 @@ function ListWidget({ widget: DashboardWidgetMeta; data?: WidgetDataPayload; }) { + const { t } = useTranslation(); const { accent } = useThemeColors(); const records = data?.records ?? []; @@ -202,7 +204,7 @@ function ListWidget({ {data?.isLoading ? ( ) : records.length === 0 ? ( - No data + {t("empty.noData")} ) : ( {records.slice(0, 5).map((rec, idx) => { @@ -243,6 +245,7 @@ function ChartWidget({ widget: DashboardWidgetMeta; data?: WidgetDataPayload; }) { + const { t } = useTranslation(); const { accent } = useThemeColors(); const chartType = String(widget.chartConfig?.type ?? widget.type ?? "bar"); const colors = Array.isArray(widget.chartConfig?.colors) @@ -275,7 +278,7 @@ function ChartWidget({ - No data to chart + {t("empty.noDataToChart")} )} @@ -344,6 +347,7 @@ export function DashboardViewRenderer({ isLoading = false, onWidgetPress: _onWidgetPress, }: DashboardViewRendererProps) { + const { t } = useTranslation(); const { width: screenWidth } = useWindowDimensions(); const numColumns = screenWidth > SINGLE_COLUMN_MAX ? 2 : 1; @@ -357,9 +361,9 @@ export function DashboardViewRenderer({ - No Dashboard + {t("empty.dashboardTitle")} - No dashboard widgets have been configured. + {t("empty.dashboardDesc")} ); diff --git a/components/renderers/GalleryViewRenderer.tsx b/components/renderers/GalleryViewRenderer.tsx index f45f2eb..f98e4bf 100644 --- a/components/renderers/GalleryViewRenderer.tsx +++ b/components/renderers/GalleryViewRenderer.tsx @@ -6,6 +6,7 @@ import { cn } from "~/lib/utils"; import { EmptyState } from "~/components/common/EmptyState"; import { Skeleton } from "~/components/ui/Skeleton"; import { Image as ImageIcon } from "lucide-react-native"; +import { useTranslation } from "react-i18next"; /* ------------------------------------------------------------------ */ /* Props */ @@ -92,8 +93,9 @@ export function GalleryViewRenderer({ aspectRatio = 1, isLoading, onCardPress, - emptyMessage = "No items to display", + emptyMessage, }: GalleryViewRendererProps) { + const { t } = useTranslation(); const renderCard = useCallback( ({ item }: { item: Record }) => { const uri = resolveImageUri(item, imageField); @@ -160,8 +162,8 @@ export function GalleryViewRenderer({ return ( } - title="No Items" - description={emptyMessage} + title={t("empty.galleryTitle")} + description={emptyMessage ?? t("empty.noData")} /> ); } diff --git a/components/renderers/GanttViewRenderer.tsx b/components/renderers/GanttViewRenderer.tsx index be7f42b..1a2e66b 100644 --- a/components/renderers/GanttViewRenderer.tsx +++ b/components/renderers/GanttViewRenderer.tsx @@ -2,6 +2,7 @@ import React, { useMemo, useCallback } from "react"; import { View, Text, Pressable, ScrollView } from "react-native"; import { EmptyState } from "~/components/common/EmptyState"; import { GanttChartSquare } from "lucide-react-native"; +import { useTranslation } from "react-i18next"; import { Skeleton } from "~/components/ui/Skeleton"; /* ------------------------------------------------------------------ */ @@ -153,8 +154,9 @@ export function GanttViewRenderer({ colorField, isLoading, onTaskPress, - emptyMessage = "No scheduled items", + emptyMessage, }: GanttViewRendererProps) { + const { t } = useTranslation(); const tasks = useMemo( () => buildGanttTasks(records, { labelField, startField, endField, colorField }), [records, labelField, startField, endField, colorField], @@ -202,8 +204,8 @@ export function GanttViewRenderer({ return ( } - title="Nothing Scheduled" - description={emptyMessage} + title={t("empty.ganttTitle")} + description={emptyMessage ?? t("empty.noData")} /> ); } diff --git a/components/renderers/ImageGallery.tsx b/components/renderers/ImageGallery.tsx index 58cdd12..c5b7bde 100644 --- a/components/renderers/ImageGallery.tsx +++ b/components/renderers/ImageGallery.tsx @@ -9,6 +9,7 @@ import { useWindowDimensions, } from "react-native"; import { X, Download, Share2, Image as ImageIcon } from "lucide-react-native"; +import { useTranslation } from "react-i18next"; import { Skeleton } from "~/components/ui/Skeleton"; import { EmptyState } from "~/components/ui/EmptyState"; @@ -52,6 +53,7 @@ export function ImageGallery({ onDownload, onShare, }: ImageGalleryProps) { + const { t } = useTranslation(); const { width: screenWidth } = useWindowDimensions(); const [selectedIndex, setSelectedIndex] = useState(null); @@ -94,8 +96,8 @@ export function ImageGallery({ return ( ); } diff --git a/components/renderers/KanbanViewRenderer.tsx b/components/renderers/KanbanViewRenderer.tsx index 1325455..76bd41d 100644 --- a/components/renderers/KanbanViewRenderer.tsx +++ b/components/renderers/KanbanViewRenderer.tsx @@ -7,6 +7,7 @@ import { Pressable, } from "react-native"; import { GripVertical, Plus } from "lucide-react-native"; +import { useTranslation } from "react-i18next"; import { Skeleton } from "~/components/ui/Skeleton"; import type { FieldDefinition } from "./types"; @@ -139,6 +140,7 @@ function KanbanColumnView({ onCardPress?: (record: Record) => void; onAddCard?: () => void; }) { + const { t } = useTranslation(); return ( {/* Column header */} @@ -176,7 +178,7 @@ function KanbanColumnView({ )} showsVerticalScrollIndicator={false} ListEmptyComponent={ - No items + {t("empty.kanbanItems")} } /> diff --git a/components/renderers/MapViewRenderer.tsx b/components/renderers/MapViewRenderer.tsx index d41f3cc..a4c8eb1 100644 --- a/components/renderers/MapViewRenderer.tsx +++ b/components/renderers/MapViewRenderer.tsx @@ -1,6 +1,7 @@ import React from "react"; import { View, Text, ScrollView, Pressable } from "react-native"; import { MapPin, Navigation } from "lucide-react-native"; +import { useTranslation } from "react-i18next"; import { useThemeColors } from "~/lib/theme-colors"; import { Card, CardContent } from "~/components/ui/Card"; import { ListSkeleton } from "~/components/ui/ListSkeleton"; @@ -96,6 +97,7 @@ export function MapViewRenderer({ isLoading = false, onMarkerPress, }: MapViewRendererProps) { + const { t } = useTranslation(); const { accent } = useThemeColors(); if (isLoading) { return ( @@ -109,9 +111,9 @@ export function MapViewRenderer({ return ( - No Locations + {t("empty.mapTitle")} - No records with location data were found. + {t("empty.mapDesc")} ); diff --git a/components/renderers/PageRenderer.tsx b/components/renderers/PageRenderer.tsx index fc768f7..8d2032d 100644 --- a/components/renderers/PageRenderer.tsx +++ b/components/renderers/PageRenderer.tsx @@ -1,6 +1,7 @@ import React from "react"; import { View, Text, ScrollView } from "react-native"; import { AlertCircle } from "lucide-react-native"; +import { useTranslation } from "react-i18next"; import { resolvePageSchema, type PageSchema, @@ -243,6 +244,7 @@ export function PageRenderer({ error, renderComponent, }: PageRendererProps) { + const { t } = useTranslation(); if (isLoading) { return ( @@ -270,7 +272,7 @@ export function PageRenderer({ ); diff --git a/components/renderers/ReportRenderer.tsx b/components/renderers/ReportRenderer.tsx index df529b6..4e454e1 100644 --- a/components/renderers/ReportRenderer.tsx +++ b/components/renderers/ReportRenderer.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from "react"; import { View, Text, ScrollView } from "react-native"; import { FileText, AlertCircle } from "lucide-react-native"; +import { useTranslation } from "react-i18next"; import { useThemeColors } from "~/lib/theme-colors"; import { Card, CardHeader, CardTitle, CardContent } from "~/components/ui/Card"; import { Skeleton } from "~/components/ui/Skeleton"; @@ -308,6 +309,7 @@ export function ReportRenderer({ isLoading, error, }: ReportRendererProps) { + const { t } = useTranslation(); const { accent } = useThemeColors(); if (isLoading) { return ( @@ -338,7 +340,7 @@ export function ReportRenderer({ ); @@ -348,8 +350,8 @@ export function ReportRenderer({ return ( ); } diff --git a/components/renderers/TimelineViewRenderer.tsx b/components/renderers/TimelineViewRenderer.tsx index 725cc9e..18cb9c5 100644 --- a/components/renderers/TimelineViewRenderer.tsx +++ b/components/renderers/TimelineViewRenderer.tsx @@ -1,6 +1,7 @@ import React from "react"; import { View, Text, ScrollView, Pressable } from "react-native"; import { Circle, CheckCircle2, AlertCircle, Clock, User, FileText } from "lucide-react-native"; +import { useTranslation } from "react-i18next"; import { ListSkeleton } from "~/components/ui/ListSkeleton"; /* ------------------------------------------------------------------ */ @@ -98,6 +99,7 @@ export function TimelineViewRenderer({ isLoading = false, onEntryPress, }: TimelineViewRendererProps) { + const { t } = useTranslation(); if (isLoading) { return ( @@ -110,9 +112,9 @@ export function TimelineViewRenderer({ return ( - No Activity + {t("empty.timelineTitle")} - No timeline entries found. + {t("empty.timelineDesc")} ); diff --git a/locales/ar.json b/locales/ar.json index 5377e9c..81e9f5d 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -228,6 +228,39 @@ "private": "خاص", "shared": "مُشترك" }, + "empty": { + "noData": "لا توجد بيانات", + "noDataToChart": "لا توجد بيانات للرسم", + "noDataAvailable": "لا توجد بيانات متاحة", + "dashboardTitle": "لا توجد لوحة معلومات", + "dashboardDesc": "لم يتم تكوين أي عناصر للوحة المعلومات.", + "mapTitle": "لا توجد مواقع", + "mapDesc": "لم يتم العثور على سجلات تحتوي على بيانات موقع.", + "timelineTitle": "لا يوجد نشاط", + "timelineDesc": "لم يتم العثور على إدخالات في المخطط الزمني.", + "calendarEvents": "لا توجد أحداث", + "kanbanItems": "لا توجد عناصر", + "ganttTitle": "لا يوجد جدول", + "galleryTitle": "لا توجد عناصر", + "imagesTitle": "لا توجد صور", + "imagesDesc": "لا توجد صور لعرضها بعد.", + "reportTitle": "لا توجد بيانات تقرير", + "reportDesc": "لا يوجد ما يُبلَّغ عنه للتحديد الحالي.", + "loadPage": "تعذّر تحميل الصفحة", + "loadReport": "تعذّر تحميل التقرير", + "loadChart": "تعذّر تحميل الرسم البياني", + "loadApp": "تعذّر تحميل التطبيق", + "loadPackages": "تعذّر تحميل الحزم", + "loadFlows": "تعذّر تحميل التدفقات", + "packagesTitle": "لا توجد حزم", + "packagesDesc": "لم يتم تثبيت أي حزم بعد.", + "appNavTitle": "لا يوجد تنقل", + "appNavDesc": "لم ينشر هذا التطبيق قائمة تنقل بعد.", + "flowsHeader": "تدفقات الأتمتة", + "flowsTitle": "لا توجد تدفقات مُعرّفة", + "flowsDesc": "لا توجد تدفقات أتمتة على هذا الخادم بعد.", + "flowCount": "{{count}} تدفقات" + }, "nav": { "home": "الرئيسية", "search": "بحث", diff --git a/locales/en.json b/locales/en.json index d66d7de..81b01da 100644 --- a/locales/en.json +++ b/locales/en.json @@ -224,6 +224,39 @@ "private": "Private", "shared": "Shared" }, + "empty": { + "noData": "No data", + "noDataToChart": "No data to chart", + "noDataAvailable": "No data available", + "dashboardTitle": "No Dashboard", + "dashboardDesc": "No dashboard widgets have been configured.", + "mapTitle": "No Locations", + "mapDesc": "No records with location data were found.", + "timelineTitle": "No Activity", + "timelineDesc": "No timeline entries found.", + "calendarEvents": "No events", + "kanbanItems": "No items", + "ganttTitle": "Nothing Scheduled", + "galleryTitle": "No Items", + "imagesTitle": "No Images", + "imagesDesc": "There are no images to display yet.", + "reportTitle": "No Report Data", + "reportDesc": "There's nothing to report for the current selection.", + "loadPage": "Couldn't Load Page", + "loadReport": "Couldn't Load Report", + "loadChart": "Couldn't Load Chart", + "loadApp": "Couldn't Load App", + "loadPackages": "Couldn't Load Packages", + "loadFlows": "Couldn't load flows", + "packagesTitle": "No Packages", + "packagesDesc": "No packages are installed yet.", + "appNavTitle": "No Navigation", + "appNavDesc": "This app hasn't published a navigation menu yet.", + "flowsHeader": "Automation Flows", + "flowsTitle": "No flows defined", + "flowsDesc": "This server has no automation flows yet.", + "flowCount": "{{count}} flows" + }, "nav": { "home": "Home", "search": "Search", diff --git a/locales/zh.json b/locales/zh.json index fa9308d..265865b 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -223,6 +223,39 @@ "private": "私有", "shared": "共享" }, + "empty": { + "noData": "暂无数据", + "noDataToChart": "暂无可绘制的数据", + "noDataAvailable": "暂无数据", + "dashboardTitle": "暂无仪表盘", + "dashboardDesc": "尚未配置任何仪表盘组件。", + "mapTitle": "暂无位置", + "mapDesc": "未找到带位置信息的记录。", + "timelineTitle": "暂无动态", + "timelineDesc": "未找到时间线条目。", + "calendarEvents": "暂无事件", + "kanbanItems": "暂无条目", + "ganttTitle": "暂无排期", + "galleryTitle": "暂无项目", + "imagesTitle": "暂无图片", + "imagesDesc": "暂时没有可显示的图片。", + "reportTitle": "暂无报表数据", + "reportDesc": "当前选择没有可报告的内容。", + "loadPage": "无法加载页面", + "loadReport": "无法加载报表", + "loadChart": "无法加载图表", + "loadApp": "无法加载应用", + "loadPackages": "无法加载应用包", + "loadFlows": "无法加载流程", + "packagesTitle": "暂无应用包", + "packagesDesc": "尚未安装任何应用包。", + "appNavTitle": "暂无导航", + "appNavDesc": "此应用尚未发布导航菜单。", + "flowsHeader": "自动化流程", + "flowsTitle": "未定义流程", + "flowsDesc": "此服务器还没有自动化流程。", + "flowCount": "{{count}} 个流程" + }, "nav": { "home": "首页", "search": "搜索",