From 0bb22ac4c72c4a23a7f0f67b140144906eb5f13b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 05:10:15 +0000 Subject: [PATCH 1/4] Initial plan From 40ee48e542b143f09d7efb1e915f24d79819128b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 05:20:35 +0000 Subject: [PATCH 2/4] feat: extend schema types for HeaderBar, BreadcrumbItem, ViewSwitcher, SidebarNav; add i18n for data-table pagination Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../src/renderers/complex/data-table.tsx | 52 +++++- .../src/renderers/navigation/header-bar.tsx | 80 ++++++++-- packages/i18n/src/locales/ar.ts | 2 + packages/i18n/src/locales/de.ts | 2 + packages/i18n/src/locales/en.ts | 2 + packages/i18n/src/locales/es.ts | 2 + packages/i18n/src/locales/fr.ts | 2 + packages/i18n/src/locales/ja.ts | 2 + packages/i18n/src/locales/ko.ts | 2 + packages/i18n/src/locales/pt.ts | 2 + packages/i18n/src/locales/ru.ts | 2 + packages/i18n/src/locales/zh.ts | 2 + packages/layout/src/SidebarNav.tsx | 148 +++++++++++++++--- packages/plugin-view/src/ViewSwitcher.tsx | 55 ++++++- packages/types/src/navigation.ts | 20 +++ packages/types/src/views.ts | 11 ++ packages/types/src/zod/navigation.zod.ts | 32 ++-- packages/types/src/zod/views.zod.ts | 5 + 18 files changed, 380 insertions(+), 43 deletions(-) diff --git a/packages/components/src/renderers/complex/data-table.tsx b/packages/components/src/renderers/complex/data-table.tsx index 3029dbf586..eac281db7e 100644 --- a/packages/components/src/renderers/complex/data-table.tsx +++ b/packages/components/src/renderers/complex/data-table.tsx @@ -11,6 +11,7 @@ import React, { useState, useMemo, useRef, useEffect } from 'react'; import { cn } from '../../lib/utils'; import { ComponentRegistry } from '@object-ui/core'; import type { DataTableSchema } from '@object-ui/types'; +import { useObjectTranslation } from '@object-ui/react'; import { Table, TableHeader, @@ -52,6 +53,50 @@ import { type SortDirection = 'asc' | 'desc' | null; +// Default English fallback translations for the data table +const TABLE_DEFAULT_TRANSLATIONS: Record = { + 'table.rowsPerPage': 'Rows per page', + 'table.pageInfo': 'Page {{current}} of {{total}}', + 'table.totalRecords': '{{count}} total', +}; + +/** + * Safe wrapper for useObjectTranslation that falls back to English defaults + * when I18nProvider is not available (e.g., standalone usage). + */ +function useTableTranslation() { + try { + const result = useObjectTranslation(); + const testValue = result.t('table.rowsPerPage'); + if (testValue === 'table.rowsPerPage') { + return { + t: (key: string, options?: Record) => { + let value = TABLE_DEFAULT_TRANSLATIONS[key] || key; + if (options) { + for (const [k, v] of Object.entries(options)) { + value = value.replace(`{{${k}}}`, String(v)); + } + } + return value; + }, + }; + } + return { t: result.t }; + } catch { + return { + t: (key: string, options?: Record) => { + let value = TABLE_DEFAULT_TRANSLATIONS[key] || key; + if (options) { + for (const [k, v] of Object.entries(options)) { + value = value.replace(`{{${k}}}`, String(v)); + } + } + return value; + }, + }; + } +} + /** * Enterprise-level data table component with Airtable-like features. * @@ -110,6 +155,9 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => { showAddRow = false, } = schema; + // i18n support for pagination labels + const { t } = useTableTranslation(); + // Ensure data is always an array – provider config objects or null/undefined // must not reach array operations like .filter() / .some() const data = Array.isArray(rawData) ? rawData : []; @@ -936,7 +984,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => { {pagination && sortedData.length > 0 && (
- Rows per page: + {t('table.rowsPerPage')}: + {schema.search.shortcut && ( + + {schema.search.shortcut} + + )} +
+ )} + {schema.actions?.map((action, idx) => ( + + ))} + {schema.rightContent && } +
), { namespace: 'ui', label: 'Header Bar', inputs: [ - { name: 'crumbs', type: 'array', label: 'Breadcrumbs' } + { name: 'crumbs', type: 'array', label: 'Breadcrumbs' }, + { name: 'search', type: 'object', label: 'Search Configuration' }, + { name: 'actions', type: 'array', label: 'Action Slots' }, + { name: 'rightContent', type: 'object', label: 'Right Content' }, ], defaultProps: { crumbs: [ diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index 475fb631d9..4328263f5d 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -73,6 +73,8 @@ const ar = { hideColumn: 'إخفاء العمود', freezeColumn: 'تجميد العمود', unfreezeColumn: 'إلغاء تجميد العمود', + pageInfo: 'صفحة {{current}} من {{total}}', + totalRecords: '{{count}} إجمالي', }, grid: { actions: 'إجراءات', diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index a73b29c51f..30985089a8 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -72,6 +72,8 @@ const de = { hideColumn: 'Spalte ausblenden', freezeColumn: 'Spalte fixieren', unfreezeColumn: 'Spalte lösen', + pageInfo: 'Seite {{current}} von {{total}}', + totalRecords: '{{count}} gesamt', }, grid: { actions: 'Aktionen', diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index 0ee5b89491..58b0c2761e 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -72,6 +72,8 @@ const en = { hideColumn: 'Hide column', freezeColumn: 'Freeze column', unfreezeColumn: 'Unfreeze column', + pageInfo: 'Page {{current}} of {{total}}', + totalRecords: '{{count}} total', }, grid: { actions: 'Actions', diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index 76c9d06b2d..d333a79221 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -72,6 +72,8 @@ const es = { hideColumn: 'Ocultar columna', freezeColumn: 'Fijar columna', unfreezeColumn: 'Desfijar columna', + pageInfo: 'Página {{current}} de {{total}}', + totalRecords: '{{count}} en total', }, grid: { actions: 'Acciones', diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index 9fab28b634..ea79f25d80 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -72,6 +72,8 @@ const fr = { hideColumn: 'Masquer la colonne', freezeColumn: 'Figer la colonne', unfreezeColumn: 'Libérer la colonne', + pageInfo: 'Page {{current}} sur {{total}}', + totalRecords: '{{count}} au total', }, grid: { actions: 'Actions', diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index 4cce1d872c..04ad3cdfe0 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -72,6 +72,8 @@ const ja = { hideColumn: '列を非表示', freezeColumn: '列を固定', unfreezeColumn: '列の固定を解除', + pageInfo: '{{total}}ページ中{{current}}ページ', + totalRecords: '合計{{count}}件', }, grid: { actions: 'アクション', diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index 95aa22db13..8cf9d6fc7a 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -72,6 +72,8 @@ const ko = { hideColumn: '열 숨기기', freezeColumn: '열 고정', unfreezeColumn: '열 고정 해제', + pageInfo: '{{total}} 페이지 중 {{current}} 페이지', + totalRecords: '총 {{count}}개', }, grid: { actions: '작업', diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index e6ade96e9e..0af4ce51fe 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -72,6 +72,8 @@ const pt = { hideColumn: 'Ocultar coluna', freezeColumn: 'Fixar coluna', unfreezeColumn: 'Soltar coluna', + pageInfo: 'Página {{current}} de {{total}}', + totalRecords: '{{count}} no total', }, grid: { actions: 'Ações', diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index 2f0eb7ec04..bb886acf4c 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -72,6 +72,8 @@ const ru = { hideColumn: 'Скрыть столбец', freezeColumn: 'Закрепить столбец', unfreezeColumn: 'Открепить столбец', + pageInfo: 'Страница {{current}} из {{total}}', + totalRecords: 'Всего {{count}}', }, grid: { actions: 'Действия', diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index e36bf6f001..f6b0df5c91 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -72,6 +72,8 @@ const zh = { hideColumn: '隐藏列', freezeColumn: '冻结列', unfreezeColumn: '取消冻结列', + pageInfo: '第 {{current}} 页,共 {{total}} 页', + totalRecords: '共 {{count}} 条', }, grid: { actions: '操作', diff --git a/packages/layout/src/SidebarNav.tsx b/packages/layout/src/SidebarNav.tsx index 70b0051b4d..d8201bf708 100644 --- a/packages/layout/src/SidebarNav.tsx +++ b/packages/layout/src/SidebarNav.tsx @@ -9,44 +9,154 @@ import { SidebarMenu, SidebarMenuItem, SidebarMenuButton, + SidebarMenuSub, + SidebarMenuSubItem, + SidebarMenuSubButton, + Badge, + Input, + Collapsible, + CollapsibleTrigger, + CollapsibleContent, } from '@object-ui/components'; +import { ChevronRight, Search } from 'lucide-react'; export interface NavItem { title: string; href: string; icon?: React.ComponentType<{ className?: string }>; + badge?: string | number; + badgeVariant?: 'default' | 'destructive' | 'outline'; + children?: NavItem[]; +} + +export interface NavGroup { + label: string; + items: NavItem[]; } export interface SidebarNavProps { - items: NavItem[]; + items: NavItem[] | NavGroup[]; title?: string; className?: string; collapsible?: "offcanvas" | "icon" | "none"; + searchEnabled?: boolean; +} + +function isNavGroup(item: NavItem | NavGroup): item is NavGroup { + return 'items' in item && !('href' in item); } -export function SidebarNav({ items, title = "Application", className, collapsible = "icon" }: SidebarNavProps) { +function NavItemRenderer({ item, pathname }: { item: NavItem; pathname: string }) { + if (item.children && item.children.length > 0) { + return ( + + + + + {item.icon && } + {item.title} + {item.badge != null && ( + + {item.badge} + + )} + + + + + + {item.children.map((child) => ( + + + + {child.icon && } + {child.title} + {child.badge != null && ( + + {child.badge} + + )} + + + + ))} + + + + + ); + } + + return ( + + + + {item.icon && } + {item.title} + {item.badge != null && ( + + {item.badge} + + )} + + + + ); +} + +export function SidebarNav({ items, title = "Application", className, collapsible = "icon", searchEnabled = false }: SidebarNavProps) { const location = useLocation(); + const [search, setSearch] = React.useState(''); + + const flatItems: Array<{ groupLabel?: string; items: NavItem[] }> = React.useMemo(() => { + if (items.length === 0) return []; + if (isNavGroup(items[0])) { + return (items as NavGroup[]).map(g => ({ groupLabel: g.label, items: g.items })); + } + return [{ items: items as NavItem[] }]; + }, [items]); + + const filteredGroups = React.useMemo(() => { + if (!search) return flatItems; + const lowerSearch = search.toLowerCase(); + return flatItems.map(group => ({ + ...group, + items: group.items.filter(item => + item.title.toLowerCase().includes(lowerSearch) || + item.children?.some(child => child.title.toLowerCase().includes(lowerSearch)) + ), + })).filter(group => group.items.length > 0); + }, [flatItems, search]); return ( - - {title} - - - {items.map((item) => ( - - - - {item.icon && } - {item.title} - - - - ))} - - - + {searchEnabled && ( +
+
+ + setSearch(e.target.value)} + className="pl-8 h-9" + /> +
+
+ )} + {filteredGroups.map((group, gIdx) => ( + + {group.groupLabel || title} + + + {group.items.map((item) => ( + + ))} + + + + ))}
); diff --git a/packages/plugin-view/src/ViewSwitcher.tsx b/packages/plugin-view/src/ViewSwitcher.tsx index 24ebb8c732..7ebd7683dc 100644 --- a/packages/plugin-view/src/ViewSwitcher.tsx +++ b/packages/plugin-view/src/ViewSwitcher.tsx @@ -32,6 +32,11 @@ import { LayoutGrid, List, Map, + Plus, + Share2, + Settings, + Copy, + Trash2, icons, type LucideIcon, } from 'lucide-react'; @@ -42,6 +47,8 @@ export type ViewSwitcherProps = { schema: ViewSwitcherSchema; className?: string; onViewChange?: (view: ViewType) => void; + onCreateView?: () => void; + onViewAction?: (action: string, view: ViewType) => void; [key: string]: any; }; @@ -155,10 +162,19 @@ function getInitialView(schema: ViewSwitcherSchema): ViewType | undefined { return schema.views?.[0]?.type; } +const DEFAULT_VIEW_ACTION_ICONS: Record = { + share: Share2, + settings: Settings, + duplicate: Copy, + delete: Trash2, +}; + export const ViewSwitcher: React.FC = ({ schema, className, onViewChange, + onCreateView, + onViewAction, ...props }) => { const storageKey = React.useMemo(() => { @@ -225,8 +241,42 @@ export const ViewSwitcher: React.FC = ({ const isVertical = position === 'left' || position === 'right'; const orientation = isVertical ? 'vertical' : 'horizontal'; + const viewActionButtons = schema.viewActions && schema.viewActions.length > 0 ? ( +
+ {schema.viewActions.map((action, idx) => { + const ActionIcon = action.icon + ? resolveIcon(action.icon) || DEFAULT_VIEW_ACTION_ICONS[action.type] + : DEFAULT_VIEW_ACTION_ICONS[action.type]; + return ( + + ); + })} +
+ ) : null; + + const createViewButton = schema.allowCreateView ? ( + + ) : null; + const switcher = ( -
+
{variant === 'dropdown' && ( {schema.search.shortcut && ( diff --git a/packages/layout/src/SidebarNav.tsx b/packages/layout/src/SidebarNav.tsx index d8201bf708..8c7e2d4072 100644 --- a/packages/layout/src/SidebarNav.tsx +++ b/packages/layout/src/SidebarNav.tsx @@ -40,6 +40,7 @@ export interface SidebarNavProps { className?: string; collapsible?: "offcanvas" | "icon" | "none"; searchEnabled?: boolean; + searchPlaceholder?: string; } function isNavGroup(item: NavItem | NavGroup): item is NavGroup { @@ -104,7 +105,7 @@ function NavItemRenderer({ item, pathname }: { item: NavItem; pathname: string } ); } -export function SidebarNav({ items, title = "Application", className, collapsible = "icon", searchEnabled = false }: SidebarNavProps) { +export function SidebarNav({ items, title = "Application", className, collapsible = "icon", searchEnabled = false, searchPlaceholder = "Search..." }: SidebarNavProps) { const location = useLocation(); const [search, setSearch] = React.useState(''); @@ -137,7 +138,7 @@ export function SidebarNav({ items, title = "Application", className, collapsibl setSearch(e.target.value)} className="pl-8 h-9" diff --git a/packages/plugin-view/src/ViewSwitcher.tsx b/packages/plugin-view/src/ViewSwitcher.tsx index 7ebd7683dc..8128b0f4a1 100644 --- a/packages/plugin-view/src/ViewSwitcher.tsx +++ b/packages/plugin-view/src/ViewSwitcher.tsx @@ -49,6 +49,7 @@ export type ViewSwitcherProps = { onViewChange?: (view: ViewType) => void; onCreateView?: () => void; onViewAction?: (action: string, view: ViewType) => void; + createViewLabel?: string; [key: string]: any; }; @@ -169,12 +170,20 @@ const DEFAULT_VIEW_ACTION_ICONS: Record = { delete: Trash2, }; +const DEFAULT_VIEW_ACTION_LABELS: Record = { + share: 'Share', + settings: 'Settings', + duplicate: 'Duplicate', + delete: 'Delete', +}; + export const ViewSwitcher: React.FC = ({ schema, className, onViewChange, onCreateView, onViewAction, + createViewLabel = 'Create view', ...props }) => { const storageKey = React.useMemo(() => { @@ -254,7 +263,7 @@ export const ViewSwitcher: React.FC = ({ variant="ghost" size="icon-sm" onClick={() => onViewAction?.(action.type, currentView!)} - title={action.type} + title={DEFAULT_VIEW_ACTION_LABELS[action.type] || action.type} > {ActionIcon ? : null} @@ -269,7 +278,7 @@ export const ViewSwitcher: React.FC = ({ variant="ghost" size="icon-sm" onClick={() => onCreateView?.()} - title="Create view" + title={createViewLabel} > From 72e8801792464cae896c1b4e4967f04fec5c29e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 05:49:40 +0000 Subject: [PATCH 4/4] feat(B4/B6): complete ListView toolbar i18n and ObjectGrid i18n - replace all hardcoded English labels with t() calls Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/i18n/src/locales/ar.ts | 17 +++++++ packages/i18n/src/locales/de.ts | 17 +++++++ packages/i18n/src/locales/en.ts | 17 +++++++ packages/i18n/src/locales/es.ts | 17 +++++++ packages/i18n/src/locales/fr.ts | 17 +++++++ packages/i18n/src/locales/ja.ts | 17 +++++++ packages/i18n/src/locales/ko.ts | 17 +++++++ packages/i18n/src/locales/pt.ts | 17 +++++++ packages/i18n/src/locales/ru.ts | 17 +++++++ packages/i18n/src/locales/zh.ts | 17 +++++++ packages/plugin-grid/src/ObjectGrid.tsx | 66 ++++++++++++++++++++++--- packages/plugin-list/src/ListView.tsx | 42 ++++++++++------ 12 files changed, 255 insertions(+), 23 deletions(-) diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index 4328263f5d..b099457845 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -106,6 +106,23 @@ const ar = { addRecord: 'إضافة سجل', tabs: 'علامات التبويب', allRecords: 'جميع السجلات', + search: 'بحث', + filter: 'تصفية', + filterRecords: 'تصفية السجلات', + sort: 'ترتيب', + sortRecords: 'ترتيب السجلات', + group: 'تجميع', + groupBy: 'تجميع حسب', + export: 'تصدير', + exportAs: 'تصدير كـ {{format}}', + color: 'لون', + rowColor: 'لون الصف', + colorByField: 'تلوين حسب الحقل', + clear: 'مسح', + none: 'لا شيء', + hideFields: 'إخفاء الحقول', + noItems: 'لم يتم العثور على عناصر', + noItemsMessage: 'لا توجد سجلات للعرض. حاول تعديل الفلاتر أو إضافة بيانات جديدة.', }, kanban: { addCard: 'إضافة بطاقة', diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index 30985089a8..ebb6c8b208 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -105,6 +105,23 @@ const de = { addRecord: 'Datensatz hinzufügen', tabs: 'Tabs', allRecords: 'Alle Datensätze', + search: 'Suche', + filter: 'Filtern', + filterRecords: 'Datensätze filtern', + sort: 'Sortieren', + sortRecords: 'Datensätze sortieren', + group: 'Gruppieren', + groupBy: 'Gruppieren nach', + export: 'Exportieren', + exportAs: 'Exportieren als {{format}}', + color: 'Farbe', + rowColor: 'Zeilenfarbe', + colorByField: 'Nach Feld einfärben', + clear: 'Löschen', + none: 'Keine', + hideFields: 'Felder ausblenden', + noItems: 'Keine Einträge gefunden', + noItemsMessage: 'Es gibt keine Datensätze. Versuchen Sie, die Filter anzupassen oder neue Daten hinzuzufügen.', }, kanban: { addCard: 'Karte hinzufügen', diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index 58b0c2761e..3aea702d4e 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -105,6 +105,23 @@ const en = { addRecord: 'Add record', tabs: 'Tabs', allRecords: 'All Records', + search: 'Search', + filter: 'Filter', + filterRecords: 'Filter Records', + sort: 'Sort', + sortRecords: 'Sort Records', + group: 'Group', + groupBy: 'Group By', + export: 'Export', + exportAs: 'Export as {{format}}', + color: 'Color', + rowColor: 'Row Color', + colorByField: 'Color by field', + clear: 'Clear', + none: 'None', + hideFields: 'Hide fields', + noItems: 'No items found', + noItemsMessage: 'There are no records to display. Try adjusting your filters or adding new data.', }, kanban: { addCard: 'Add card', diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index d333a79221..6201fdb567 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -105,6 +105,23 @@ const es = { addRecord: 'Agregar registro', tabs: 'Pestañas', allRecords: 'Todos los registros', + search: 'Buscar', + filter: 'Filtrar', + filterRecords: 'Filtrar registros', + sort: 'Ordenar', + sortRecords: 'Ordenar registros', + group: 'Agrupar', + groupBy: 'Agrupar por', + export: 'Exportar', + exportAs: 'Exportar como {{format}}', + color: 'Color', + rowColor: 'Color de fila', + colorByField: 'Colorear por campo', + clear: 'Borrar', + none: 'Ninguno', + hideFields: 'Ocultar campos', + noItems: 'No se encontraron elementos', + noItemsMessage: 'No hay registros para mostrar. Intente ajustar los filtros o agregar nuevos datos.', }, kanban: { addCard: 'Añadir tarjeta', diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index ea79f25d80..a2995db14e 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -105,6 +105,23 @@ const fr = { addRecord: 'Ajouter un enregistrement', tabs: 'Onglets', allRecords: 'Tous les enregistrements', + search: 'Rechercher', + filter: 'Filtrer', + filterRecords: 'Filtrer les enregistrements', + sort: 'Trier', + sortRecords: 'Trier les enregistrements', + group: 'Grouper', + groupBy: 'Grouper par', + export: 'Exporter', + exportAs: 'Exporter en {{format}}', + color: 'Couleur', + rowColor: 'Couleur de ligne', + colorByField: 'Colorer par champ', + clear: 'Effacer', + none: 'Aucun', + hideFields: 'Masquer les champs', + noItems: 'Aucun élément trouvé', + noItemsMessage: "Il n'y a aucun enregistrement à afficher. Essayez d'ajuster les filtres ou d'ajouter de nouvelles données.", }, kanban: { addCard: 'Ajouter une carte', diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index 04ad3cdfe0..1247aa9c95 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -105,6 +105,23 @@ const ja = { addRecord: 'レコードを追加', tabs: 'タブ', allRecords: 'すべてのレコード', + search: '検索', + filter: 'フィルター', + filterRecords: 'レコードをフィルター', + sort: '並べ替え', + sortRecords: 'レコードを並べ替え', + group: 'グループ', + groupBy: 'グループ化', + export: 'エクスポート', + exportAs: '{{format}}としてエクスポート', + color: '色', + rowColor: '行の色', + colorByField: 'フィールドで色分け', + clear: 'クリア', + none: 'なし', + hideFields: 'フィールドを非表示', + noItems: '項目が見つかりません', + noItemsMessage: '表示するレコードがありません。フィルターを調整するか、新しいデータを追加してください。', }, kanban: { addCard: 'カードを追加', diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index 8cf9d6fc7a..cd79e2a02a 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -105,6 +105,23 @@ const ko = { addRecord: '레코드 추가', tabs: '탭', allRecords: '전체 레코드', + search: '검색', + filter: '필터', + filterRecords: '레코드 필터', + sort: '정렬', + sortRecords: '레코드 정렬', + group: '그룹', + groupBy: '그룹 기준', + export: '내보내기', + exportAs: '{{format}}으로 내보내기', + color: '색상', + rowColor: '행 색상', + colorByField: '필드별 색상', + clear: '지우기', + none: '없음', + hideFields: '필드 숨기기', + noItems: '항목을 찾을 수 없습니다', + noItemsMessage: '표시할 레코드가 없습니다. 필터를 조정하거나 새 데이터를 추가해 보세요.', }, kanban: { addCard: '카드 추가', diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index 0af4ce51fe..1a3790bfd1 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -105,6 +105,23 @@ const pt = { addRecord: 'Adicionar registro', tabs: 'Abas', allRecords: 'Todos os registros', + search: 'Pesquisar', + filter: 'Filtrar', + filterRecords: 'Filtrar registros', + sort: 'Ordenar', + sortRecords: 'Ordenar registros', + group: 'Agrupar', + groupBy: 'Agrupar por', + export: 'Exportar', + exportAs: 'Exportar como {{format}}', + color: 'Cor', + rowColor: 'Cor da linha', + colorByField: 'Colorir por campo', + clear: 'Limpar', + none: 'Nenhum', + hideFields: 'Ocultar campos', + noItems: 'Nenhum item encontrado', + noItemsMessage: 'Não há registros para exibir. Tente ajustar os filtros ou adicionar novos dados.', }, kanban: { addCard: 'Adicionar cartão', diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index bb886acf4c..13731ce881 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -105,6 +105,23 @@ const ru = { addRecord: 'Добавить запись', tabs: 'Вкладки', allRecords: 'Все записи', + search: 'Поиск', + filter: 'Фильтр', + filterRecords: 'Фильтр записей', + sort: 'Сортировка', + sortRecords: 'Сортировка записей', + group: 'Группировка', + groupBy: 'Группировать по', + export: 'Экспорт', + exportAs: 'Экспорт в {{format}}', + color: 'Цвет', + rowColor: 'Цвет строки', + colorByField: 'Окраска по полю', + clear: 'Очистить', + none: 'Нет', + hideFields: 'Скрыть поля', + noItems: 'Элементы не найдены', + noItemsMessage: 'Нет записей для отображения. Попробуйте изменить фильтры или добавить новые данные.', }, kanban: { addCard: 'Добавить карточку', diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index f6b0df5c91..0dbf94e3da 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -105,6 +105,23 @@ const zh = { addRecord: '添加记录', tabs: '标签页', allRecords: '全部记录', + search: '搜索', + filter: '筛选', + filterRecords: '筛选记录', + sort: '排序', + sortRecords: '排序记录', + group: '分组', + groupBy: '分组依据', + export: '导出', + exportAs: '导出为 {{format}}', + color: '颜色', + rowColor: '行颜色', + colorByField: '按字段着色', + clear: '清除', + none: '无', + hideFields: '隐藏字段', + noItems: '未找到项目', + noItemsMessage: '没有可显示的记录。请尝试调整筛选条件或添加新数据。', }, kanban: { addCard: '添加卡片', diff --git a/packages/plugin-grid/src/ObjectGrid.tsx b/packages/plugin-grid/src/ObjectGrid.tsx index 1cdfab49b5..82b98a2579 100644 --- a/packages/plugin-grid/src/ObjectGrid.tsx +++ b/packages/plugin-grid/src/ObjectGrid.tsx @@ -23,7 +23,7 @@ import React, { useEffect, useState, useCallback, useMemo } from 'react'; import type { ObjectGridSchema, DataSource, ListColumn, ViewData } from '@object-ui/types'; -import { SchemaRenderer, useDataScope, useNavigationOverlay, useAction } from '@object-ui/react'; +import { SchemaRenderer, useDataScope, useNavigationOverlay, useAction, useObjectTranslation } from '@object-ui/react'; import { getCellRenderer, formatCurrency, formatCompactCurrency, formatDate, formatPercent } from '@object-ui/fields'; import { Badge, Button, NavigationOverlay, @@ -39,6 +39,57 @@ import { useColumnSummary } from './useColumnSummary'; import { RowActionMenu, formatActionLabel } from './components/RowActionMenu'; import { BulkActionBar } from './components/BulkActionBar'; +// Default English fallback translations for the grid +const GRID_DEFAULT_TRANSLATIONS: Record = { + 'grid.actions': 'Actions', + 'grid.edit': 'Edit', + 'grid.delete': 'Delete', + 'grid.export': 'Export', + 'grid.exportAs': 'Export as {{format}}', + 'grid.loading': 'Loading grid...', + 'grid.errorLoading': 'Error loading grid', + 'grid.pullToRefresh': 'Pull to refresh', + 'grid.refreshing': 'Refreshing…', + 'grid.openRecord': 'Open record', +}; + +/** + * Safe wrapper for useObjectTranslation that falls back to English defaults + * when I18nProvider is not available (e.g., standalone usage). + */ +function useGridTranslation() { + try { + const result = useObjectTranslation(); + const testValue = result.t('grid.actions'); + if (testValue === 'grid.actions') { + return { + t: (key: string, options?: Record) => { + let value = GRID_DEFAULT_TRANSLATIONS[key] || key; + if (options) { + for (const [k, v] of Object.entries(options)) { + value = value.replace(`{{${k}}}`, String(v)); + } + } + return value; + }, + }; + } + return { t: result.t }; + } catch { + return { + t: (key: string, options?: Record) => { + let value = GRID_DEFAULT_TRANSLATIONS[key] || key; + if (options) { + for (const [k, v] of Object.entries(options)) { + value = value.replace(`{{${k}}}`, String(v)); + } + } + return value; + }, + }; + } +} + export interface ObjectGridProps { schema: ObjectGridSchema; dataSource?: DataSource; @@ -126,6 +177,7 @@ export const ObjectGrid: React.FC = ({ const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const { t } = useGridTranslation(); const [objectSchema, setObjectSchema] = useState(null); const [useCardView, setUseCardView] = useState(false); const [refreshKey, setRefreshKey] = useState(0); @@ -756,7 +808,7 @@ export const ObjectGrid: React.FC = ({ if (error) { return (
-

Error loading grid

+

{t('grid.errorLoading')}

{error.message}

); @@ -782,7 +834,7 @@ export const ObjectGrid: React.FC = ({ return (
-

Loading grid...

+

{t('grid.loading')}

); } @@ -820,7 +872,7 @@ export const ObjectGrid: React.FC = ({ const columnsWithActions = (hasActions || hasRowActions) ? [ ...persistedColumns, { - header: 'Actions', + header: t('grid.actions'), accessorKey: '_actions', cell: (_value: any, row: any) => ( = ({ className="h-7 px-2 text-muted-foreground hover:text-primary text-xs" > - Export + {t('grid.export')} @@ -1191,7 +1243,7 @@ export const ObjectGrid: React.FC = ({ onClick={() => handleExport(format)} > - Export as {format.toUpperCase()} + {t('grid.exportAs', { format: format.toUpperCase() })} ))}
@@ -1357,7 +1409,7 @@ export const ObjectGrid: React.FC = ({ className="flex items-center justify-center text-xs text-muted-foreground" style={{ height: pullDistance }} > - {isRefreshing ? 'Refreshing…' : 'Pull to refresh'} + {isRefreshing ? t('grid.refreshing') : t('grid.pullToRefresh')}
)} {gridToolbar} diff --git a/packages/plugin-list/src/ListView.tsx b/packages/plugin-list/src/ListView.tsx index 0b492b7352..6d972969c6 100644 --- a/packages/plugin-list/src/ListView.tsx +++ b/packages/plugin-list/src/ListView.tsx @@ -196,8 +196,18 @@ const LIST_DEFAULT_TRANSLATIONS: Record = { 'list.noItemsMessage': 'There are no records to display. Try adjusting your filters or adding new data.', 'list.search': 'Search', 'list.filter': 'Filter', + 'list.filterRecords': 'Filter Records', 'list.sort': 'Sort', + 'list.sortRecords': 'Sort Records', + 'list.group': 'Group', + 'list.groupBy': 'Group By', 'list.export': 'Export', + 'list.exportAs': 'Export as {{format}}', + 'list.color': 'Color', + 'list.rowColor': 'Row Color', + 'list.colorByField': 'Color by field', + 'list.clear': 'Clear', + 'list.none': 'None', 'list.hideFields': 'Hide fields', 'list.showAll': 'Show all', 'list.pullToRefresh': 'Pull to refresh', @@ -1113,7 +1123,7 @@ export const ListView: React.FC = ({ )} > - Filter + {t('list.filter')} {hasFilters && ( {currentFilters.conditions?.length || 0} @@ -1124,7 +1134,7 @@ export const ListView: React.FC = ({
-

Filter Records

+

{t('list.filterRecords')}

= ({ )} > - Group + {t('list.group')} {groupingConfig && groupingConfig.fields?.length > 0 && ( {groupingConfig.fields.length} @@ -1163,10 +1173,10 @@ export const ListView: React.FC = ({
-

Group By

+

{t('list.groupBy')}

{groupingConfig && ( )}
@@ -1212,7 +1222,7 @@ export const ListView: React.FC = ({ )} > - Sort + {t('list.sort')} {currentSort.length > 0 && ( {currentSort.length} @@ -1223,7 +1233,7 @@ export const ListView: React.FC = ({
-

Sort Records

+

{t('list.sortRecords')}

= ({ )} > - Color + {t('list.color')}
-

Row Color

+

{t('list.rowColor')}

{rowColorConfig && ( )}
- + handleSearchChange(e.target.value)} className="pl-7 h-8 text-xs"