diff --git a/.changeset/localize-loading-messages.md b/.changeset/localize-loading-messages.md new file mode 100644 index 0000000..384c542 --- /dev/null +++ b/.changeset/localize-loading-messages.md @@ -0,0 +1,5 @@ +--- +'@ioai/lerobot-studio': patch +--- + +Localize hardcoded English UI strings (en/zh/ja): the episode loading status messages ("Fetching frame data for episode N...", "Loading episode N...", "Reading metadata...", "Processing charts...", "Initializing data loader..."), the "No features available" empty state, the filter "clear search" accessible label, the sample cover image alt text and the image panel load errors. diff --git a/src/react/components/PanelTitle.tsx b/src/react/components/PanelTitle.tsx index 20fbcb1..0424e01 100644 --- a/src/react/components/PanelTitle.tsx +++ b/src/react/components/PanelTitle.tsx @@ -142,7 +142,7 @@ export const PanelTitle: React.FC = (props) => { {features.length === 0 ? (
- No features available + {t('common.noFeaturesAvailable')}
) : ( features.map((key) => { @@ -175,7 +175,7 @@ export const PanelTitle: React.FC = (props) => { {features.length === 0 ? (
- No features available + {t('common.noFeaturesAvailable')}
) : ( features.map((key) => { diff --git a/src/react/components/open/SampleDatasetCard.tsx b/src/react/components/open/SampleDatasetCard.tsx index 49c07f3..1928064 100644 --- a/src/react/components/open/SampleDatasetCard.tsx +++ b/src/react/components/open/SampleDatasetCard.tsx @@ -95,7 +95,7 @@ export const SampleDatasetCard: React.FC = ({ {fallbackImageUrl ? ( Default Cover ) : ( diff --git a/src/react/components/panels/ChartPanel/NestedHierarchicalFilterList.tsx b/src/react/components/panels/ChartPanel/NestedHierarchicalFilterList.tsx index 5df3a1c..8633e6a 100644 --- a/src/react/components/panels/ChartPanel/NestedHierarchicalFilterList.tsx +++ b/src/react/components/panels/ChartPanel/NestedHierarchicalFilterList.tsx @@ -1,4 +1,5 @@ import { memo, useEffect, useMemo, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; import { Search, X } from 'lucide-react'; import { Input } from '@/ui'; import { Button } from '@/ui'; @@ -89,6 +90,7 @@ export const NestedHierarchicalFilterList = memo(function NestedHierarchicalFilt getItemColor, className, }: NestedHierarchicalFilterListProps) { + const { t } = useTranslation(); const allItemIds = useMemo(() => getAllFeatureFilterItemIds(featureNodes), [featureNodes]); const filteredNodes = useMemo( () => filterFeatureNodes(featureNodes, searchValue), @@ -139,7 +141,7 @@ export const NestedHierarchicalFilterList = memo(function NestedHierarchicalFilt e.stopPropagation(); onSearchChange(''); }} - aria-label="clear-search" + aria-label={t('common.clearSearch')} > diff --git a/src/react/components/panels/Common/filters/HierarchicalFilterList.tsx b/src/react/components/panels/Common/filters/HierarchicalFilterList.tsx index 59d005b..8713759 100644 --- a/src/react/components/panels/Common/filters/HierarchicalFilterList.tsx +++ b/src/react/components/panels/Common/filters/HierarchicalFilterList.tsx @@ -1,4 +1,5 @@ import { memo, useEffect, useMemo, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; import { Search, X } from 'lucide-react'; import { Input } from '@/ui'; import { Button } from '@/ui'; @@ -72,6 +73,7 @@ export const HierarchicalFilterList = memo(function HierarchicalFilterList({ getGroupColor, className, }: HierarchicalFilterListProps) { + const { t } = useTranslation(); const allItemIds = useMemo( () => groups.flatMap((group) => group.items.map((item) => item.id)), [groups], @@ -121,7 +123,7 @@ export const HierarchicalFilterList = memo(function HierarchicalFilterList({ e.stopPropagation(); onSearchChange(''); }} - aria-label="clear-search" + aria-label={t('common.clearSearch')} > diff --git a/src/react/components/panels/ImagePanel/ImagePanel.tsx b/src/react/components/panels/ImagePanel/ImagePanel.tsx index b8613da..c8c1981 100644 --- a/src/react/components/panels/ImagePanel/ImagePanel.tsx +++ b/src/react/components/panels/ImagePanel/ImagePanel.tsx @@ -23,6 +23,8 @@ interface ImagePanelProps { */ export const ImagePanel: React.FC = ({ params }) => { const { t } = useTranslation(); + const tRef = useRef(t); + tRef.current = t; const { info, dataLoader, imageService, subscribeFrameIndex } = useLeRobotData(); const { selectedEpisodeIndex } = useLeRobotSelection(); @@ -116,7 +118,7 @@ export const ImagePanel: React.FC = ({ params }) => { const pathResult = loader.getEpisodeDataPath(episode); if (!pathResult) { if (mountedRef.current) { - setError('Cannot determine parquet file path'); + setError(tRef.current('panels.image.noParquetPath')); setIsLoading(false); } continue; @@ -189,7 +191,7 @@ export const ImagePanel: React.FC = ({ params }) => { selectedEpisodeRef.current === episode && featureKeyRef.current === key; if (isStillCurrent) { - setError(e instanceof Error ? e.message : 'Failed to load image'); + setError(e instanceof Error ? e.message : tRef.current('panels.image.loadFailed')); setIsLoading(false); } } finally { diff --git a/src/react/contexts/LeRobotProvider.tsx b/src/react/contexts/LeRobotProvider.tsx index 8050956..10ad7fe 100644 --- a/src/react/contexts/LeRobotProvider.tsx +++ b/src/react/contexts/LeRobotProvider.tsx @@ -13,6 +13,7 @@ import type { NumericalColumnMap } from '@/platform'; import type { DataSource } from '@/platform'; import type { ValidationReport } from '@/core'; import { DirectoryDataSource } from '@/platform'; +import { useTranslation } from '../i18n/reactI18nextCompat'; import { useLoading } from './LoadingContext'; import { LeRobotContext, @@ -125,6 +126,10 @@ export const LeRobotDataProvider: React.FC<{ children: React.ReactNode }> = ({ c const [lastValidationReport, setLastValidationReport] = useState(null); const [healthDialogOpen, setHealthDialogOpen] = useState(false); + const { t } = useTranslation(); + // Keep t out of callback deps so a locale switch never re-triggers dataset loading. + const tRef = useRef(t); + tRef.current = t; const { upsertTask, completeTask, @@ -367,7 +372,11 @@ export const LeRobotDataProvider: React.FC<{ children: React.ReactNode }> = ({ c setIsLoading(true); setError(null); const taskId = 'lerobot-init'; - upsertTask({ id: taskId, phase: 'index', message: 'Initializing data loader...' }); + upsertTask({ + id: taskId, + phase: 'index', + message: tRef.current('loading.message.initLoader'), + }); let loader: LeRobotDataLoader | null = null; try { @@ -420,7 +429,11 @@ export const LeRobotDataProvider: React.FC<{ children: React.ReactNode }> = ({ c await import('../../platform/services/LeRobotDataLoader'); loader = new LeRobotDataLoaderCtor(source); - upsertTask({ id: taskId, phase: 'read', message: 'Reading metadata...' }); + upsertTask({ + id: taskId, + phase: 'read', + message: tRef.current('loading.message.readMetadata'), + }); const lerobotInfo = await loader.initialize(); if (generation !== initGenerationRef.current) { await loader.dispose().catch(() => {}); @@ -482,7 +495,11 @@ export const LeRobotDataProvider: React.FC<{ children: React.ReactNode }> = ({ c setIsPlaying(false); setError(null); const taskId = 'lerobot-select-episode'; - upsertTask({ id: taskId, phase: 'read', message: `Loading episode ${index}...` }); + upsertTask({ + id: taskId, + phase: 'read', + message: tRef.current('loading.message.loadEpisode', { index }), + }); setSelectedEpisodeIndex(index); setCurrentFrames([]); @@ -514,13 +531,17 @@ export const LeRobotDataProvider: React.FC<{ children: React.ReactNode }> = ({ c upsertTask({ id: taskId, phase: 'read', - message: `Fetching frame data for episode ${index}...`, + message: tRef.current('loading.message.fetchFrames', { index }), }); const numericalData = await dataLoader.loadAllNumericalData(index, eagerColumns); if (loadingEpisodeRef.current !== index) return false; - upsertTask({ id: taskId, phase: 'render', message: 'Processing charts...' }); + upsertTask({ + id: taskId, + phase: 'render', + message: tRef.current('loading.message.processCharts'), + }); const frames = buildPlaybackFrames(numericalData, info.fps ?? 30); setCurrentFrames(frames); const subtaskSource = await dataLoader.loadEpisodeSubtaskSource(index); diff --git a/src/react/locales/en.json b/src/react/locales/en.json index d47080c..b3d9d53 100644 --- a/src/react/locales/en.json +++ b/src/react/locales/en.json @@ -9,6 +9,7 @@ "copy": "Copy", "copied": "Copied", "close": "Close", + "noFeaturesAvailable": "No features available", "clearSearch": "Clear search", "loading": "Loading...", "addPanel": "Add Panel", @@ -51,6 +52,13 @@ "render": "Rendering panels...", "ready": "Ready", "error": "Load Failed" + }, + "message": { + "initLoader": "Initializing data loader...", + "readMetadata": "Reading metadata...", + "loadEpisode": "Loading episode {{index}}...", + "fetchFrames": "Fetching frame data for episode {{index}}...", + "processCharts": "Processing charts..." } }, "errors": { @@ -511,6 +519,8 @@ } }, "image": { + "noParquetPath": "Cannot determine parquet file path", + "loadFailed": "Failed to load image", "title": "Image Panel", "placeholder": "Waiting for image file...", "debug": { @@ -608,6 +618,7 @@ } }, "welcome": { + "defaultCover": "Default cover", "title": "Open Data", "subtitle": "Select local file/folder or enter remote URL", "quickStart": "Quick Start", diff --git a/src/react/locales/ja.json b/src/react/locales/ja.json index 7053334..f634944 100644 --- a/src/react/locales/ja.json +++ b/src/react/locales/ja.json @@ -9,6 +9,7 @@ "copy": "コピー", "copied": "コピーしました", "close": "閉じる", + "noFeaturesAvailable": "利用可能な特徴量がありません", "clearSearch": "検索をクリア", "loading": "読み込み中...", "addPanel": "パネルを追加", @@ -51,6 +52,13 @@ "render": "パネルを描画中...", "ready": "準備完了", "error": "読み込み失敗" + }, + "message": { + "initLoader": "データローダーを初期化中...", + "readMetadata": "メタデータを読み込み中...", + "loadEpisode": "エピソード {{index}} を読み込み中...", + "fetchFrames": "エピソード {{index}} のフレームデータを取得中...", + "processCharts": "チャートを処理中..." } }, "errors": { @@ -515,6 +523,8 @@ } }, "image": { + "noParquetPath": "Parquet ファイルのパスを特定できません", + "loadFailed": "画像の読み込みに失敗しました", "title": "画像パネル", "placeholder": "画像ファイルを待機中...", "debug": { @@ -610,6 +620,7 @@ } }, "welcome": { + "defaultCover": "デフォルトカバー", "title": "データを開く", "subtitle": "ローカルファイル/フォルダを選択するか、リモート URL を入力", "quickStart": "クイックスタート", diff --git a/src/react/locales/zh.json b/src/react/locales/zh.json index aca99d6..36ccaa4 100644 --- a/src/react/locales/zh.json +++ b/src/react/locales/zh.json @@ -10,6 +10,7 @@ "copy": "复制", "copied": "已复制", "close": "关闭", + "noFeaturesAvailable": "没有可用的特征", "clearSearch": "清空搜索", "loading": "加载中...", "addPanel": "添加面板", @@ -51,6 +52,13 @@ "render": "正在渲染面板...", "ready": "加载完成", "error": "加载失败" + }, + "message": { + "initLoader": "正在初始化数据加载器...", + "readMetadata": "正在读取元数据...", + "loadEpisode": "正在加载 Episode {{index}}...", + "fetchFrames": "正在获取 Episode {{index}} 的帧数据...", + "processCharts": "正在处理图表..." } }, "errors": { @@ -511,6 +519,8 @@ } }, "image": { + "noParquetPath": "无法确定 Parquet 文件路径", + "loadFailed": "图像加载失败", "title": "图像面板", "placeholder": "等待加载图像文件...", "debug": { @@ -608,6 +618,7 @@ } }, "welcome": { + "defaultCover": "默认封面", "title": "浏览数据", "subtitle": "选择本地文件、文件夹或输入远程 URL", "quickStart": "快速开始",