Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions components/batch/BatchActionBar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import { View, Text, Pressable } from "react-native";
import { Trash2, Edit3, X } from "lucide-react-native";
import { useTranslation } from "react-i18next";
import { cn } from "~/lib/utils";
import { useThemeColors } from "~/lib/theme-colors";

Expand All @@ -26,6 +27,7 @@ export function BatchActionBar({
onClearSelection,
className,
}: BatchActionBarProps) {
const { t } = useTranslation();
const { accent } = useThemeColors();
if (selectedCount === 0) return null;

Expand All @@ -52,7 +54,7 @@ export function BatchActionBar({
className="flex-row items-center rounded-lg bg-primary/10 px-3 py-2"
>
<Edit3 size={14} color={accent} />
<Text className="ml-1.5 text-xs font-semibold text-primary">Edit</Text>
<Text className="ml-1.5 text-xs font-semibold text-primary">{t("common.edit")}</Text>
</Pressable>
)}
{onBatchDelete && (
Expand All @@ -61,7 +63,7 @@ export function BatchActionBar({
className="flex-row items-center rounded-lg bg-destructive/10 px-3 py-2"
>
<Trash2 size={14} color="#ef4444" />
<Text className="ml-1.5 text-xs font-semibold text-destructive">Delete</Text>
<Text className="ml-1.5 text-xs font-semibold text-destructive">{t("common.delete")}</Text>
</Pressable>
)}
</View>
Expand Down
6 changes: 4 additions & 2 deletions components/common/UndoSnackbar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import { View, Text, TouchableOpacity } from "react-native";
import { useTranslation } from "react-i18next";

export interface UndoSnackbarProps {
message: string;
Expand All @@ -16,6 +17,7 @@ export function UndoSnackbar({
visible,
testID = "undo-snackbar",
}: UndoSnackbarProps) {
const { t } = useTranslation();
const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const [show, setShow] = React.useState(visible);

Expand Down Expand Up @@ -51,10 +53,10 @@ export function UndoSnackbar({
<TouchableOpacity
testID={`${testID}-undo`}
onPress={onUndo}
accessibilityLabel="Undo"
accessibilityLabel={t("common.undo")}
accessibilityRole="button"
>
<Text className="ml-3 text-sm font-bold text-primary">Undo</Text>
<Text className="ml-3 text-sm font-bold text-primary">{t("common.undo")}</Text>
</TouchableOpacity>
</View>
);
Expand Down
6 changes: 4 additions & 2 deletions components/query/QueryBuilder.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useCallback } from "react";
import { View, Text, Pressable, ScrollView } from "react-native";
import { Plus, Trash2, ToggleLeft } from "lucide-react-native";
import { useTranslation } from "react-i18next";
import { cn } from "~/lib/utils";
import { useThemeColors } from "~/lib/theme-colors";
import type { FieldDefinition } from "~/components/renderers/types";
Expand Down Expand Up @@ -44,6 +45,7 @@ export function QueryBuilder({
onClear,
className,
}: QueryBuilderProps) {
const { t } = useTranslation();
const { accent } = useThemeColors();
const handleAdd = useCallback(() => {
const firstField = fields[0]?.name ?? "";
Expand All @@ -67,7 +69,7 @@ export function QueryBuilder({
{root.filters.length > 0 && (
<Pressable onPress={onClear} className="flex-row items-center rounded-lg px-2 py-1">
<Trash2 size={14} color="#ef4444" />
<Text className="ml-1 text-xs text-destructive">Clear</Text>
<Text className="ml-1 text-xs text-destructive">{t("common.clear")}</Text>
</Pressable>
)}
</View>
Expand Down Expand Up @@ -98,7 +100,7 @@ export function QueryBuilder({
className="mt-3 flex-row items-center self-start rounded-lg border border-dashed border-border px-3 py-2"
>
<Plus size={14} color="#64748b" />
<Text className="ml-1.5 text-xs font-medium text-muted-foreground">Add filter</Text>
<Text className="ml-1.5 text-xs font-medium text-muted-foreground">{t("filter.addFilter")}</Text>
</Pressable>
</View>
);
Expand Down
6 changes: 4 additions & 2 deletions components/renderers/SwipeableRow.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useRef } from "react";
import { View, Text, Pressable, Animated } from "react-native";
import { Swipeable } from "react-native-gesture-handler";
import { useTranslation } from "react-i18next";
import { Edit, Trash2 } from "lucide-react-native";

/* ------------------------------------------------------------------ */
Expand Down Expand Up @@ -44,6 +45,7 @@ export function SwipeableRow({
onEdit,
onDelete,
}: SwipeableRowProps) {
const { t } = useTranslation();
const swipeableRef = useRef<Swipeable>(null);

const hasActions = !!(onEdit || onDelete);
Expand Down Expand Up @@ -81,7 +83,7 @@ export function SwipeableRow({
style={{ width: ACTION_WIDTH }}
>
<Edit size={20} color="#ffffff" />
<Text className="mt-1 text-xs font-medium text-white">Edit</Text>
<Text className="mt-1 text-xs font-medium text-white">{t("common.edit")}</Text>
</Pressable>
)}

Expand All @@ -97,7 +99,7 @@ export function SwipeableRow({
style={{ width: ACTION_WIDTH }}
>
<Trash2 size={20} color="#ffffff" />
<Text className="mt-1 text-xs font-medium text-white">Delete</Text>
<Text className="mt-1 text-xs font-medium text-white">{t("common.delete")}</Text>
</Pressable>
)}
</Animated.View>
Expand Down
18 changes: 10 additions & 8 deletions components/renderers/fields/FileField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "lucide-react-native";
import type { FieldDefinition } from "../types";
import type { FileUploadResult } from "~/hooks/useFileUpload";
import { useTranslation } from "react-i18next";
import { useToast } from "~/components/ui/Toast";
import { useThemeColors } from "~/lib/theme-colors";

Expand Down Expand Up @@ -81,6 +82,7 @@ export function FileField({
onShare,
error,
}: FileFieldProps) {
const { t } = useTranslation();
const [isUploading, setIsUploading] = useState(false);
const { toastError } = useToast();
const { accent } = useThemeColors();
Expand All @@ -96,12 +98,12 @@ export function FileField({
onChange?.(result);
}
} catch {
toastError("Could not upload the file. Please try again.");
toastError(t("fields.uploadFailed"));
} finally {
setIsUploading(false);
}
},
[onChange, toastError],
[onChange, toastError, t],
);

/* ---- Read-only ---- */
Expand All @@ -116,7 +118,7 @@ export function FileField({
<View className="flex-row items-center gap-2 rounded-lg border border-border bg-card px-3 py-2">
<FileText size={16} color="#64748b" />
<Text className="flex-1 text-sm text-foreground" numberOfLines={1}>
{fileInfo.name ?? fileInfo.id ?? "File"}
{fileInfo.name ?? fileInfo.id ?? t("fields.file")}
</Text>
{onDownload && fileInfo.id && (
<Pressable onPress={() => onDownload(fileInfo.id!, fileInfo.name ?? "file")}>
Expand All @@ -140,7 +142,7 @@ export function FileField({
<View className="flex-row items-center gap-2 rounded-lg border border-border bg-card px-3 py-2">
<FileText size={16} color="#64748b" />
<Text className="flex-1 text-sm text-foreground" numberOfLines={1}>
{fileInfo.name ?? "Attached file"}
{fileInfo.name ?? t("fields.attachedFile")}
</Text>
<Pressable onPress={() => onChange?.(null)}>
<X size={16} color="#ef4444" />
Expand All @@ -152,7 +154,7 @@ export function FileField({
{isUploading ? (
<View className="items-center py-4">
<ActivityIndicator size="small" color={accent} />
<Text className="mt-2 text-xs text-muted-foreground">Uploading…</Text>
<Text className="mt-2 text-xs text-muted-foreground">{t("fields.uploading")}</Text>
</View>
) : (
<View className="flex-row flex-wrap gap-2">
Expand All @@ -162,7 +164,7 @@ export function FileField({
onPress={() => handleUpload(onPickImage)}
>
<ImageIcon size={14} color="#64748b" />
<Text className="text-xs font-medium text-foreground">Gallery</Text>
<Text className="text-xs font-medium text-foreground">{t("fields.gallery")}</Text>
</Pressable>
)}
{(field.type === "image" || field.type === "file") && onCapturePhoto && (
Expand All @@ -171,7 +173,7 @@ export function FileField({
onPress={() => handleUpload(onCapturePhoto)}
>
<Camera size={14} color="#64748b" />
<Text className="text-xs font-medium text-foreground">Camera</Text>
<Text className="text-xs font-medium text-foreground">{t("fields.camera")}</Text>
</Pressable>
)}
{onPickDocument && (
Expand All @@ -180,7 +182,7 @@ export function FileField({
onPress={() => handleUpload(onPickDocument)}
>
<Upload size={14} color="#64748b" />
<Text className="text-xs font-medium text-foreground">File</Text>
<Text className="text-xs font-medium text-foreground">{t("fields.file")}</Text>
</Pressable>
)}
</View>
Expand Down
8 changes: 5 additions & 3 deletions components/ui/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ChevronDown,
} from "lucide-react-native";
import * as Haptics from "expo-haptics";
import { useTranslation } from "react-i18next";
import { cn } from "~/lib/utils";
import { formatDate, formatDateTime } from "~/lib/formatting";

Expand Down Expand Up @@ -96,6 +97,7 @@ export function DatePicker({
className,
error,
}: DatePickerProps) {
const { t } = useTranslation();
const [open, setOpen] = React.useState(false);

const selected = React.useMemo(() => parseDateValue(value), [value]);
Expand Down Expand Up @@ -295,7 +297,7 @@ export function DatePicker({
}}
className="rounded-lg px-3 py-2 active:bg-accent"
>
<Text className="text-sm font-medium text-muted-foreground">Clear</Text>
<Text className="text-sm font-medium text-muted-foreground">{t("common.clear")}</Text>
</Pressable>
<View className="flex-row gap-2">
<Pressable
Expand All @@ -314,11 +316,11 @@ export function DatePicker({
}}
className="rounded-lg px-3 py-2 active:bg-accent"
>
<Text className="text-sm font-medium text-primary">Today</Text>
<Text className="text-sm font-medium text-primary">{t("common.today")}</Text>
</Pressable>
{mode !== "date" && (
<Pressable onPress={confirm} className="rounded-lg bg-primary px-4 py-2">
<Text className="text-sm font-semibold text-primary-foreground">Done</Text>
<Text className="text-sm font-semibold text-primary-foreground">{t("common.done")}</Text>
</Pressable>
)}
</View>
Expand Down
4 changes: 3 additions & 1 deletion components/ui/MultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import { Modal, Pressable, ScrollView, Text, View } from "react-native";
import { ChevronDown, Check, X } from "lucide-react-native";
import * as Haptics from "expo-haptics";
import { useTranslation } from "react-i18next";
import { cn } from "~/lib/utils";
import type { SelectOption } from "~/components/renderers/types";

Expand Down Expand Up @@ -31,6 +32,7 @@ export function MultiSelect({
className,
error,
}: MultiSelectProps) {
const { t } = useTranslation();
const [open, setOpen] = React.useState(false);
const selectedSet = React.useMemo(() => new Set(value), [value]);
const selectedOptions = options.filter((o) => selectedSet.has(String(o.value)));
Expand Down Expand Up @@ -109,7 +111,7 @@ export function MultiSelect({
})}
</ScrollView>
<Pressable onPress={() => setOpen(false)} className="mt-1 items-center rounded-lg bg-primary py-2.5">
<Text className="text-sm font-semibold text-primary-foreground">Done</Text>
<Text className="text-sm font-semibold text-primary-foreground">{t("common.done")}</Text>
</Pressable>
</Pressable>
</Pressable>
Expand Down
4 changes: 3 additions & 1 deletion components/ui/Reasoning.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState } from "react";
import { View, Text, Pressable } from "react-native";
import { ChevronRight, ChevronDown, Brain } from "lucide-react-native";
import { useTranslation } from "react-i18next";
import { cn } from "~/lib/utils";

/**
Expand All @@ -19,6 +20,7 @@ export function Reasoning({
reasoning: string;
className?: string;
}) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
if (!reasoning || reasoning.trim() === "") return null;
return (
Expand All @@ -31,7 +33,7 @@ export function Reasoning({
accessibilityState={{ expanded: open }}
>
<Brain size={13} color="#64748b" />
<Text className="flex-1 text-xs font-medium text-muted-foreground">Reasoning</Text>
<Text className="flex-1 text-xs font-medium text-muted-foreground">{t("ai.reasoning")}</Text>
{open ? (
<ChevronDown size={14} color="#94a3b8" />
) : (
Expand Down
16 changes: 9 additions & 7 deletions components/views/SaveViewDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState } from "react";
import { View, Text, TextInput, Pressable, Modal } from "react-native";
import { X, Save } from "lucide-react-native";
import { useTranslation } from "react-i18next";
import { cn } from "~/lib/utils";
import type { SaveViewInput } from "~/hooks/useViewStorage";

Expand All @@ -27,6 +28,7 @@ export function SaveViewDialog({
onSave,
initialValues,
}: SaveViewDialogProps) {
const { t } = useTranslation();
const [name, setName] = useState(initialValues?.name ?? "");
const [visibility, setVisibility] = useState<"private" | "shared">(
initialValues?.visibility ?? "private",
Expand Down Expand Up @@ -56,25 +58,25 @@ export function SaveViewDialog({
<View className="w-full max-w-sm rounded-2xl bg-card p-5 shadow-lg">
{/* Header */}
<View className="mb-4 flex-row items-center justify-between">
<Text className="text-lg font-semibold text-foreground">Save View</Text>
<Text className="text-lg font-semibold text-foreground">{t("views.saveView")}</Text>
<Pressable onPress={onClose} className="rounded-full p-1">
<X size={20} color="#64748b" />
</Pressable>
</View>

{/* Name input */}
<Text className="mb-1 text-xs font-medium text-muted-foreground">View Name</Text>
<Text className="mb-1 text-xs font-medium text-muted-foreground">{t("views.viewName")}</Text>
<TextInput
className="mb-4 rounded-xl border border-input bg-background px-4 py-3 text-base text-foreground"
value={name}
onChangeText={setName}
placeholder="e.g. My Active Leads"
placeholder={t("views.namePlaceholder")}
placeholderTextColor="#94a3b8"
autoFocus
/>

{/* Visibility toggle */}
<Text className="mb-2 text-xs font-medium text-muted-foreground">Visibility</Text>
<Text className="mb-2 text-xs font-medium text-muted-foreground">{t("views.visibility")}</Text>
<View className="mb-5 flex-row gap-2">
<Pressable
onPress={() => setVisibility("private")}
Expand All @@ -91,7 +93,7 @@ export function SaveViewDialog({
visibility === "private" ? "text-primary" : "text-muted-foreground",
)}
>
Private
{t("views.private")}
</Text>
</Pressable>
<Pressable
Expand All @@ -109,7 +111,7 @@ export function SaveViewDialog({
visibility === "shared" ? "text-primary" : "text-muted-foreground",
)}
>
Shared
{t("views.shared")}
</Text>
</Pressable>
</View>
Expand All @@ -125,7 +127,7 @@ export function SaveViewDialog({
>
<Save size={16} color="#ffffff" />
<Text className="ml-2 font-semibold text-primary-foreground">
{isSaving ? "Saving…" : "Save View"}
{isSaving ? t("common.saving") : t("views.saveView")}
</Text>
</Pressable>
</View>
Expand Down
7 changes: 7 additions & 0 deletions jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,10 @@ const mockAppState = {
addEventListener: jest.fn(() => ({ remove: jest.fn() })),
};
jest.mock("react-native/Libraries/AppState/AppState", () => mockAppState);

/* ---- i18next ---- */
// Initialize the shared i18n instance so components' `t()` calls resolve to
// real (English) strings in tests instead of returning raw keys. Mirrors the
// app, where importing a screen pulls in `~/lib/i18n`; tests render components
// in isolation, so without this the translation layer is never set up.
require("~/lib/i18n");
Loading
Loading