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": "搜索",