From 2222ac455275eb7dbfdf8a6d59253739273faa04 Mon Sep 17 00:00:00 2001 From: free sponge Date: Sat, 30 May 2026 01:04:25 +0000 Subject: [PATCH] Add missing i18n translations for all UI components - Add 58 new translation keys to all locale files (en.json, zh.json, ja.json) - Update ExportView, PreprocessView, ReviewView, GlobalFallbacks, Lightbox, AppLayout, AppModals to use t() for all hardcoded strings - Add Japanese language button to settings modal - Update theme button labels to use translations - Add Japanese (ja) to Language type and AppSettings type - Add all new keys to legacy i18n.ts with full Japanese translations - All three locale files now have 209 identical keys Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/App.tsx | 2 +- src/components/AppLayout.tsx | 24 +- src/components/AppModals.tsx | 19 +- src/components/GlobalFallbacks.tsx | 13 +- src/components/Lightbox.tsx | 4 +- src/components/workflow/ExportView.tsx | 24 +- src/components/workflow/PreprocessView.tsx | 42 +-- src/components/workflow/ReviewView.tsx | 34 ++- src/locales/en.json | 60 +++- src/locales/ja.json | 60 +++- src/locales/zh.json | 60 +++- src/types.ts | 2 +- src/utils/i18n.ts | 333 ++++++++++++++++++++- 13 files changed, 597 insertions(+), 80 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 79e6006..0d593e4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -313,7 +313,7 @@ const App: React.FC = () => { {isOffline && (
- {t('offlineWarning') || 'You are currently offline. Local features are still available, but AI generation is disabled.'} + {t('offlineWarning')}
)} diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index 6eecfd7..0c391e0 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -126,7 +126,7 @@ export const Sidebar = ({
-
+
@@ -187,14 +187,14 @@ export const Inspector = ({ ) : ( setIsRenaming(true)} > {activeImage.file.name} )}
- {inspectorProjectName || 'Unknown'} + {inspectorProjectName || t('noCaption')}
@@ -225,11 +225,11 @@ export const Inspector = ({ className="w-full h-48 p-3 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-950 text-sm font-mono focus:ring-2 focus:ring-indigo-500 focus:border-transparent resize-none text-zinc-800 dark:text-zinc-200" value={activeImage.caption} onChange={(e) => onUpdateCaption(e.target.value)} - placeholder="Caption..." + placeholder={t('captionPlaceholder')} />
- - {activeImage.caption.length} chars + + {activeImage.caption.length} {t('chars')}
@@ -292,12 +292,12 @@ export const SmartToolbar = ({
{/* View Filters */}
- - + - @@ -307,8 +307,8 @@ export const SmartToolbar = ({ {/* View Mode Toggle */}
- - + + {/* Grid Columns Slider */} {viewMode === 'grid' && ( @@ -376,7 +376,7 @@ export const SmartToolbar = ({ {t('batchEdit')} - diff --git a/src/components/AppModals.tsx b/src/components/AppModals.tsx index e818533..91efb45 100644 --- a/src/components/AppModals.tsx +++ b/src/components/AppModals.tsx @@ -146,13 +146,14 @@ export const SettingsModal = ({
+
- - + +
@@ -456,15 +457,15 @@ export const ExportModal = ({

{t('exportAll')}

- +
- - + +
diff --git a/src/components/GlobalFallbacks.tsx b/src/components/GlobalFallbacks.tsx index 8b6e4cf..049c4b6 100644 --- a/src/components/GlobalFallbacks.tsx +++ b/src/components/GlobalFallbacks.tsx @@ -1,14 +1,16 @@ import React from 'react'; import { AlertTriangle, RotateCcw } from 'lucide-react'; import { FallbackProps } from 'react-error-boundary'; +import { useTranslation } from 'react-i18next'; export function GlobalErrorFallback({ error, resetErrorBoundary }: FallbackProps) { + const { t } = useTranslation(); return (
-

Something went wrong

+

{t('somethingWentWrong')}

{error instanceof Error ? error.message : String(error)}

@@ -17,25 +19,26 @@ export function GlobalErrorFallback({ error, resetErrorBoundary }: FallbackProps className="flex items-center gap-2 px-6 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg transition-colors shadow-sm" > - Try Again + {t('tryAgain')}
); } export function ViewErrorFallback({ resetErrorBoundary }: FallbackProps) { + const { t } = useTranslation(); return (
-

View Error

+

{t('viewError')}

- Failed to render this section. The rest of the app should still work. + {t('viewErrorDesc')}

); diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index 622a89f..19f31c0 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import { X, ChevronLeft, ChevronRight, ZoomIn, ZoomOut } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; import { TagImage } from '../types'; interface LightboxProps { @@ -15,6 +16,7 @@ interface LightboxProps { export const Lightbox: React.FC = ({ isOpen, onClose, image, onNext, onPrev, hasNext, hasPrev }) => { + const { t } = useTranslation(); const [scale, setScale] = useState(1); const [position, setPosition] = useState({ x: 0, y: 0 }); const [isDragging, setIsDragging] = useState(false); @@ -112,7 +114,7 @@ export const Lightbox: React.FC = ({ {/* Caption Overlay */}

- {image.caption || No caption} + {image.caption || {t('noCaption')}}

diff --git a/src/components/workflow/ExportView.tsx b/src/components/workflow/ExportView.tsx index b821f67..daa9044 100644 --- a/src/components/workflow/ExportView.tsx +++ b/src/components/workflow/ExportView.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Download, FileText, Code, CheckCircle2 } from '../Icons'; interface ExportViewProps { @@ -7,6 +8,7 @@ interface ExportViewProps { } export const ExportView: React.FC = ({ onExport, totalImages }) => { + const { t } = useTranslation(); const [format, setFormat] = useState<'txt' | 'json'>('txt'); const [isExporting, setIsExporting] = useState(false); @@ -23,8 +25,8 @@ export const ExportView: React.FC = ({ onExport, totalImages })
-

Export Dataset

-

Choose your preferred format and download your training data.

+

{t('exportDatasetTitle')}

+

{t('exportDatasetDesc')}

@@ -34,14 +36,14 @@ export const ExportView: React.FC = ({ onExport, totalImages })
-
Ready to Export
-
{totalImages} images and captions prepared.
+
{t('readyToExport')}
+
{t('imagesPrepared', { count: totalImages })}
{/* Format Selection */}
- +
@@ -68,8 +70,8 @@ export const ExportView: React.FC = ({ onExport, totalImages })
-
Kohya JSON
-
Single metadata.json file compatible with Kohya-ss scripts.
+
{t('kohyaJson')}
+
{t('kohyaJsonDesc')}
{format === 'json' &&
}
@@ -86,9 +88,9 @@ export const ExportView: React.FC = ({ onExport, totalImages }) }`} > {isExporting ? ( - <>Processing... + <>{t('exportProcessing')} ) : ( - <> Download Dataset + <> {t('downloadDataset')} )}
diff --git a/src/components/workflow/PreprocessView.tsx b/src/components/workflow/PreprocessView.tsx index 412440d..66b696b 100644 --- a/src/components/workflow/PreprocessView.tsx +++ b/src/components/workflow/PreprocessView.tsx @@ -1,4 +1,5 @@ import React, { useState, useMemo, useRef, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { TagImage, Project } from '../../types'; import { LayoutTemplate, Scissors, Maximize2, Move @@ -19,6 +20,7 @@ const getBucket = (width: number, height: number) => { }; export const PreprocessView: React.FC = ({ projects, onUpdateImage, onNext }) => { + const { t } = useTranslation(); const [selectedBucket, setSelectedBucket] = useState<'all' | 'landscape' | 'portrait' | 'square'>('all'); const [croppingId, setCroppingId] = useState(null); // Image ID being cropped const [imageDims, setImageDims] = useState>({}); @@ -72,17 +74,20 @@ export const PreprocessView: React.FC = ({ projects, onUpda {/* Toolbar */}
- {(['all', 'landscape', 'portrait', 'square'] as const).map(type => ( - - ))} + {(['all', 'landscape', 'portrait', 'square'] as const).map(type => { + const labelMap = { all: t('bucketAll'), landscape: t('landscape'), portrait: t('portrait'), square: t('square') }; + return ( + + ); + })}
@@ -90,10 +95,10 @@ export const PreprocessView: React.FC = ({ projects, onUpda {/* Actions */}
@@ -109,13 +114,13 @@ export const PreprocessView: React.FC = ({ projects, onUpda
- {dims ? `${dims.w}x${dims.h}` : 'Loading...'} + {dims ? `${dims.w}x${dims.h}` : t('loading')}
@@ -142,6 +147,7 @@ export const PreprocessView: React.FC = ({ projects, onUpda // --- Simple Crop Editor Component (Inline) --- const CropEditor = ({ image, onClose, onSave }: { image?: TagImage, onClose: () => void, onSave: (f: File) => void }) => { + const { t } = useTranslation(); const imgRef = useRef(null); const [crop, setCrop] = useState({ x: 0, y: 0, w: 0, h: 0 }); // Percentages? Or Pixels. Let's use Pixels relative to displayed image. const containerRef = useRef(null); @@ -272,12 +278,12 @@ const CropEditor = ({ image, onClose, onSave }: { image?: TagImage, onClose: ()
- +
- - + +
diff --git a/src/components/workflow/ReviewView.tsx b/src/components/workflow/ReviewView.tsx index ac62845..fcff9aa 100644 --- a/src/components/workflow/ReviewView.tsx +++ b/src/components/workflow/ReviewView.tsx @@ -1,4 +1,5 @@ import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import { Project, TagImage } from '../../types'; import { AlertCircle, CheckCircle2, FileText, ImageIcon, Tags, ArrowRight } from '../Icons'; @@ -8,6 +9,7 @@ interface ReviewViewProps { } export const ReviewView: React.FC = ({ projects, onNext }) => { + const { t } = useTranslation(); // --- Statistics --- const stats = useMemo(() => { let totalImages = 0; @@ -39,17 +41,17 @@ export const ReviewView: React.FC = ({ projects, onNext }) => { projects.forEach(p => { p.images.forEach(img => { if (!img.caption) { - list.push({ projectId: p.id, img, issue: 'Missing caption' }); + list.push({ projectId: p.id, img, issue: t('missingCaption') }); } else if (img.caption.length < 10) { - list.push({ projectId: p.id, img, issue: 'Caption too short' }); + list.push({ projectId: p.id, img, issue: t('captionTooShort') }); } if (img.status === 'error') { - list.push({ projectId: p.id, img, issue: img.errorMsg || 'Processing error' }); + list.push({ projectId: p.id, img, issue: img.errorMsg || t('processingError') }); } }); }); return list; - }, [projects]); + }, [projects, t]); return (
@@ -57,8 +59,8 @@ export const ReviewView: React.FC = ({ projects, onNext }) => { {/* Header */}
-

Project Review

-

Review your dataset statistics and health before exporting.

+

{t('projectReview')}

+

{t('reviewDesc')}

{/* Stats Grid */} @@ -68,7 +70,7 @@ export const ReviewView: React.FC = ({ projects, onNext }) => {
{stats.totalImages}
-
Total Images
+
{t('totalImages')}
@@ -76,7 +78,7 @@ export const ReviewView: React.FC = ({ projects, onNext }) => {
{stats.totalCaptions}
-
Captioned
+
{t('captioned')}
@@ -84,7 +86,7 @@ export const ReviewView: React.FC = ({ projects, onNext }) => {
{stats.missingCaptions}
-
Missing Tags
+
{t('missingTags')}
@@ -92,7 +94,7 @@ export const ReviewView: React.FC = ({ projects, onNext }) => {
{stats.avgTags}
-
Avg Tags/Img
+
{t('avgTagsPerImage')}
@@ -101,10 +103,10 @@ export const ReviewView: React.FC = ({ projects, onNext }) => {

- Review Items + {t('reviewItems')}

0 ? 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400' : 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'}`}> - {issues.length} Issues Found + {t('issuesFound', { count: issues.length })}
@@ -113,8 +115,8 @@ export const ReviewView: React.FC = ({ projects, onNext }) => {
-

All Good!

-

No content issues detected. You are ready to export.

+

{t('allGood')}

+

{t('noIssuesDesc')}

) : (
@@ -128,7 +130,7 @@ export const ReviewView: React.FC = ({ projects, onNext }) => { {item.img.file.name} {item.issue}
-

{item.img.caption || "No caption"}

+

{item.img.caption || t('noCaption')}

))} @@ -139,7 +141,7 @@ export const ReviewView: React.FC = ({ projects, onNext }) => { {/* Actions */}
diff --git a/src/locales/en.json b/src/locales/en.json index 7d0fa08..ef598db 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -149,5 +149,63 @@ "cleanLabel": "Clean", "cols": "Cols", "offlineWarning": "You are currently offline. Local features are still available, but AI generation is disabled.", - "selectAnImage": "Select an image to edit details" + "selectAnImage": "Select an image to edit details", + "exportDatasetTitle": "Export Dataset", + "exportDatasetDesc": "Choose your preferred format and download your training data.", + "readyToExport": "Ready to Export", + "imagesPrepared": "{count} images and captions prepared.", + "selectFormat": "Select Format", + "textFiles": "Text Files", + "textFilesDesc": "Standard .txt caption files side-by-side with images.", + "kohyaJson": "Kohya JSON", + "kohyaJsonDesc": "Single metadata.json file compatible with Kohya-ss scripts.", + "exportProcessing": "Processing...", + "downloadDataset": "Download Dataset", + "landscape": "Landscape", + "portrait": "Portrait", + "square": "Square", + "smartResize": "Smart Resize", + "nextTagging": "Next: Tagging", + "cropImage": "Crop", + "loading": "Loading...", + "full": "Full", + "cancel": "Cancel", + "saveCrop": "Save Crop", + "projectReview": "Project Review", + "reviewDesc": "Review your dataset statistics and health before exporting.", + "totalImages": "Total Images", + "captioned": "Captioned", + "missingTags": "Missing Tags", + "avgTagsPerImage": "Avg Tags/Img", + "reviewItems": "Review Items", + "issuesFound": "{count} Issues Found", + "allGood": "All Good!", + "noIssuesDesc": "No content issues detected. You are ready to export.", + "missingCaption": "Missing caption", + "captionTooShort": "Caption too short", + "processingError": "Processing error", + "noCaption": "No caption", + "nextExport": "Next: Export", + "format": "Format", + "textFilesTxt": "Text Files (.txt)", + "standardCaptionFiles": "Standard caption files", + "jsonFilesJson": "JSON Files (.json)", + "sidecarJsonFormat": "Sidecar JSON format", + "somethingWentWrong": "Something went wrong", + "tryAgain": "Try Again", + "viewError": "View Error", + "viewErrorDesc": "Failed to render this section. The rest of the app should still work.", + "reloadView": "Reload View", + "clickToRename": "Click to rename", + "captionPlaceholder": "Caption...", + "saveTxt": "Save .txt", + "chars": "chars", + "allItems": "All Items", + "pending": "Pending", + "completed": "Completed", + "gridView": "Grid View", + "listView": "List View", + "saved": "Saved", + "bucketAll": "All", + "cleanTagsRegex": "Clean Tags (Regex)" } \ No newline at end of file diff --git a/src/locales/ja.json b/src/locales/ja.json index 2018263..21fab51 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -149,5 +149,63 @@ "cleanLabel": "クリーン", "cols": "段数", "offlineWarning": "オフラインです。AI 自動生成は使用できません。", - "selectAnImage": "画像を選択して詳細を編集します" + "selectAnImage": "画像を選択して詳細を編集します", + "exportDatasetTitle": "データセットのエクスポート", + "exportDatasetDesc": "お好みのフォーマットを選択してトレーニングデータをダウンロードしてください。", + "readyToExport": "エクスポート準備完了", + "imagesPrepared": "{count} 枚の画像とキャプションが準備されました。", + "selectFormat": "フォーマットを選択", + "textFiles": "テキストファイル", + "textFilesDesc": "画像と対になる標準的な .txt キャプションファイル。", + "kohyaJson": "Kohya JSON", + "kohyaJsonDesc": "Kohya-ss スクリプト互換の metadata.json ファイル。", + "exportProcessing": "処理中...", + "downloadDataset": "データセットをダウンロード", + "landscape": "横向き", + "portrait": "縦向き", + "square": "正方形", + "smartResize": "スマートリサイズ", + "nextTagging": "次へ:タグ付け", + "cropImage": "切り抜き", + "loading": "読み込み中...", + "full": "全体", + "cancel": "キャンセル", + "saveCrop": "切り抜きを保存", + "projectReview": "プロジェクトレビュー", + "reviewDesc": "エクスポート前にデータセットの統計と健全性を確認してください。", + "totalImages": "総画像数", + "captioned": "キャプション済み", + "missingTags": "タグ不足", + "avgTagsPerImage": "平均タグ数/画像", + "reviewItems": "レビュー項目", + "issuesFound": "{count} 件の問題が見つかりました", + "allGood": "問題なし!", + "noIssuesDesc": "コンテンツの問題は検出されませんでした。エクスポートの準備ができています。", + "missingCaption": "キャプションなし", + "captionTooShort": "キャプションが短すぎます", + "processingError": "処理エラー", + "noCaption": "キャプションなし", + "nextExport": "次へ:エクスポート", + "format": "フォーマット", + "textFilesTxt": "テキストファイル (.txt)", + "standardCaptionFiles": "標準キャプションファイル", + "jsonFilesJson": "JSON ファイル (.json)", + "sidecarJsonFormat": "サイドカー JSON 形式", + "somethingWentWrong": "問題が発生しました", + "tryAgain": "再試行", + "viewError": "表示エラー", + "viewErrorDesc": "このセクションのレンダリングに失敗しました。アプリの他の部分は引き続き動作するはずです。", + "reloadView": "ビューを再読み込み", + "clickToRename": "クリックしてリネーム", + "captionPlaceholder": "キャプション...", + "saveTxt": ".txt を保存", + "chars": "文字", + "allItems": "すべての項目", + "pending": "未処理", + "completed": "完了済み", + "gridView": "グリッドビュー", + "listView": "リストビュー", + "saved": "保存済み", + "bucketAll": "すべて", + "cleanTagsRegex": "タグクリーニング (正規表現)" } \ No newline at end of file diff --git a/src/locales/zh.json b/src/locales/zh.json index 901d1ba..df7117d 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -149,5 +149,63 @@ "triggerWord": "触发词", "triggerWordPlaceholder": "例如:miku_hatsune", "offlineWarning": "您当前处于离线状态。本地功能依然可用,但大模型自动打标功能已禁用。", - "selectAnImage": "请在左边列表选择一张图像以编辑详情" + "selectAnImage": "请在左边列表选择一张图像以编辑详情", + "exportDatasetTitle": "导出数据集", + "exportDatasetDesc": "选择您偏好的格式并下载训练数据。", + "readyToExport": "准备导出", + "imagesPrepared": "已准备好 {count} 张图片及其标注。", + "selectFormat": "选择格式", + "textFiles": "文本文件", + "textFilesDesc": "标准 .txt 标注文件,与图片一一对应。", + "kohyaJson": "Kohya JSON", + "kohyaJsonDesc": "兼容 Kohya-ss 脚本的单个 metadata.json 文件。", + "exportProcessing": "处理中...", + "downloadDataset": "下载数据集", + "landscape": "横向", + "portrait": "纵向", + "square": "方形", + "smartResize": "智能缩放", + "nextTagging": "下一步:打标", + "cropImage": "裁剪", + "loading": "加载中...", + "full": "全部", + "cancel": "取消", + "saveCrop": "保存裁剪", + "projectReview": "项目审查", + "reviewDesc": "在导出之前,检查数据集的统计信息和健康状态。", + "totalImages": "总图片数", + "captioned": "已标注", + "missingTags": "缺少标签", + "avgTagsPerImage": "平均标签数/图", + "reviewItems": "审查项", + "issuesFound": "发现 {count} 个问题", + "allGood": "一切正常!", + "noIssuesDesc": "未检测到内容问题。您可以开始导出了。", + "missingCaption": "缺少标注", + "captionTooShort": "标注过短", + "processingError": "处理错误", + "noCaption": "无标注", + "nextExport": "下一步:导出", + "format": "格式", + "textFilesTxt": "文本文件 (.txt)", + "standardCaptionFiles": "标准标注文件", + "jsonFilesJson": "JSON 文件 (.json)", + "sidecarJsonFormat": "附带 JSON 格式", + "somethingWentWrong": "出了点问题", + "tryAgain": "重试", + "viewError": "视图错误", + "viewErrorDesc": "此部分渲染失败。应用的其余部分应仍可正常使用。", + "reloadView": "重新加载视图", + "clickToRename": "点击重命名", + "captionPlaceholder": "标注内容...", + "saveTxt": "保存 .txt", + "chars": "字符", + "allItems": "所有项", + "pending": "待处理", + "completed": "已完成", + "gridView": "网格视图", + "listView": "列表视图", + "saved": "已保存", + "bucketAll": "全部", + "cleanTagsRegex": "标签清洗 (正则)" } \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 1dc2fc9..45e1cf8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -27,7 +27,7 @@ export interface PromptTemplate { } export interface AppSettings { - language: 'en' | 'zh'; + language: 'en' | 'zh' | 'ja'; theme: 'light' | 'dark'; // New theme setting viewMode: 'grid' | 'list'; // New view mode setting protocol: AiProtocol; diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts index 224eaf3..bca70b0 100644 --- a/src/utils/i18n.ts +++ b/src/utils/i18n.ts @@ -151,7 +151,67 @@ export const translations = { startSelected: "Start Selected", processingSelected: "Processing {count} items...", cleanLabel: "Clean", - cols: "Cols" + cols: "Cols", + offlineWarning: "You are currently offline. Local features are still available, but AI generation is disabled.", + selectAnImage: "Select an image to edit details", + exportDatasetTitle: "Export Dataset", + exportDatasetDesc: "Choose your preferred format and download your training data.", + readyToExport: "Ready to Export", + imagesPrepared: "{count} images and captions prepared.", + selectFormat: "Select Format", + textFiles: "Text Files", + textFilesDesc: "Standard .txt caption files side-by-side with images.", + kohyaJson: "Kohya JSON", + kohyaJsonDesc: "Single metadata.json file compatible with Kohya-ss scripts.", + exportProcessing: "Processing...", + downloadDataset: "Download Dataset", + landscape: "Landscape", + portrait: "Portrait", + square: "Square", + smartResize: "Smart Resize", + nextTagging: "Next: Tagging", + cropImage: "Crop", + loading: "Loading...", + full: "Full", + cancel: "Cancel", + saveCrop: "Save Crop", + projectReview: "Project Review", + reviewDesc: "Review your dataset statistics and health before exporting.", + totalImages: "Total Images", + captioned: "Captioned", + missingTags: "Missing Tags", + avgTagsPerImage: "Avg Tags/Img", + reviewItems: "Review Items", + issuesFound: "{count} Issues Found", + allGood: "All Good!", + noIssuesDesc: "No content issues detected. You are ready to export.", + missingCaption: "Missing caption", + captionTooShort: "Caption too short", + processingError: "Processing error", + noCaption: "No caption", + nextExport: "Next: Export", + format: "Format", + textFilesTxt: "Text Files (.txt)", + standardCaptionFiles: "Standard caption files", + jsonFilesJson: "JSON Files (.json)", + sidecarJsonFormat: "Sidecar JSON format", + somethingWentWrong: "Something went wrong", + tryAgain: "Try Again", + viewError: "View Error", + viewErrorDesc: "Failed to render this section. The rest of the app should still work.", + reloadView: "Reload View", + clickToRename: "Click to rename", + captionPlaceholder: "Caption...", + saveTxt: "Save .txt", + chars: "chars", + allItems: "All Items", + pending: "Pending", + completed: "Completed", + gridView: "Grid View", + listView: "List View", + saved: "Saved", + bucketAll: "All", + cleanTagsRegex: "Clean Tags (Regex)" }, zh: { appTitle: "LoRA 标注助手", @@ -304,8 +364,275 @@ export const translations = { // Sidebar projects: "项目列表", triggerWord: "触发词", - triggerWordPlaceholder: "例如:miku_hatsune" + triggerWordPlaceholder: "例如:miku_hatsune", + offlineWarning: "您当前处于离线状态。本地功能依然可用,但大模型自动打标功能已禁用。", + selectAnImage: "请在左边列表选择一张图像以编辑详情", + exportDatasetTitle: "导出数据集", + exportDatasetDesc: "选择您偏好的格式并下载训练数据。", + readyToExport: "准备导出", + imagesPrepared: "已准备好 {count} 张图片及其标注。", + selectFormat: "选择格式", + textFiles: "文本文件", + textFilesDesc: "标准 .txt 标注文件,与图片一一对应。", + kohyaJson: "Kohya JSON", + kohyaJsonDesc: "兼容 Kohya-ss 脚本的单个 metadata.json 文件。", + exportProcessing: "处理中...", + downloadDataset: "下载数据集", + landscape: "横向", + portrait: "纵向", + square: "方形", + smartResize: "智能缩放", + nextTagging: "下一步:打标", + cropImage: "裁剪", + loading: "加载中...", + full: "全部", + cancel: "取消", + saveCrop: "保存裁剪", + projectReview: "项目审查", + reviewDesc: "在导出之前,检查数据集的统计信息和健康状态。", + totalImages: "总图片数", + captioned: "已标注", + missingTags: "缺少标签", + avgTagsPerImage: "平均标签数/图", + reviewItems: "审查项", + issuesFound: "发现 {count} 个问题", + allGood: "一切正常!", + noIssuesDesc: "未检测到内容问题。您可以开始导出了。", + missingCaption: "缺少标注", + captionTooShort: "标注过短", + processingError: "处理错误", + noCaption: "无标注", + nextExport: "下一步:导出", + format: "格式", + textFilesTxt: "文本文件 (.txt)", + standardCaptionFiles: "标准标注文件", + jsonFilesJson: "JSON 文件 (.json)", + sidecarJsonFormat: "附带 JSON 格式", + somethingWentWrong: "出了点问题", + tryAgain: "重试", + viewError: "视图错误", + viewErrorDesc: "此部分渲染失败。应用的其余部分应仍可正常使用。", + reloadView: "重新加载视图", + clickToRename: "点击重命名", + captionPlaceholder: "标注内容...", + saveTxt: "保存 .txt", + chars: "字符", + allItems: "所有项", + pending: "待处理", + completed: "已完成", + gridView: "网格视图", + listView: "列表视图", + saved: "已保存", + bucketAll: "全部", + cleanTagsRegex: "标签清洗 (正则)" + }, + ja: { + appTitle: "Tag Master", + import: "インポート", + exportAll: "すべてエクスポート", + exportFailed: "エクスポートに失敗しました。もう一度お試しください。", + dashboard: "ダッシュボード", + spaces: "スペース", + settings: "設定", + processing: "処理中", + progress: "進捗", + pause: "一時停止", + resume: "再開", + stop: "停止", + startAll: "すべて開始", + startProject: "プロジェクトを開始", + startSelected: "選択を開始", + batchEdit: "一括編集", + retry: "再試行", + reset: "リセット", + clearDone: "完了をクリア", + inspector: "インスペクター", + regen: "再生成", + wait: "待機中", + captionContent: "キャプション内容", + generationFailed: "生成に失敗", + prev: "前へ", + next: "次へ", + remove: "削除", + providerName: "プロバイダー名", + modelName: "モデル名", + apiKey: "APIキー", + concurrency: "同時実行数", + testing: "接続をテスト中...", + connectionSuccess: "接続成功!", + connectionFailed: "接続失敗", + blockedWords: "ブロックワード", + promptPreset: "プロンプトプリセット", + activePrompt: "現在のプロンプト", + saveAs: "名前を付けて保存...", + close: "閉じる", + apiProvider: "APIプロバイダー", + googleGemini: "Google Gemini", + openaiCompatible: "OpenAI 互換", + apiEndpoint: "APIエンドポイント", + googleDefault: "Google Generative AI (デフォルト)", + securityNote: "セキュリティ:APIキーはブラウザのローカルに難読化して保存されます。共有デバイスでの使用は避けてください。", + test: "テスト", + fetchModels: "モデル取得", + fetchingModels: "取得中...", + fetchModelsSuccess: "{count} 個のモデルを取得しました", + fetchModelsFailed: "取得失敗。URL/キーを確認してください。", + selectModel: "モデルを選択...", + advancedHeaders: "詳細:カスタムヘッダー", + advancedHeadersDesc: "APIリクエストにカスタムHTTPヘッダーを追加します。", + loadPreset: "プリセットを読み込む", + presetOpenAI: "OpenAI プロキシ (Referer)", + presetCors: "CORS / Origin", + presetAuth: "カスタム認証", + presetRelay: "リレーステーションデフォルト", + startHeader: "新しいヘッダーを追加", + headerKeyPlaceholder: "キー (例: X-Proxy-Auth)", + headerValuePlaceholder: "値", + done: "完了", + dropHere: "フォルダまたは画像をここにドロップ", + workspaceEmpty: "ワークスペースが空です", + browseFiles: "ファイルを参照", + deleteProjectConfirm: "このプロジェクトグループ全体を削除しますか?", + clearAllConfirm: "すべてのプロジェクトと画像をクリアしますか?", + resetConfirm: "表示中の画像のキャプションとステータスをリセットしますか?", + clearDoneConfirm: "完了した画像をすべて削除しますか?", + deleteSelectedConfirm: "選択した {count} 枚の画像を削除しますか?", + batchTitle: "一括操作", + batchSubtitle: "現在のビューに表示されているすべての画像のキャプションを変更します。", + findReplace: "検索と置換", + prependAppend: "先頭/末尾に追加", + smartTags: "スマートタグ", + addTags: "タグを追加", + removeTags: "タグを削除", + addTagsPlaceholder: "tag1, tag2 (自動重複排除)", + removeTagsPlaceholder: "tag1, tag2 (完全一致)", + target: "対象範囲", + scopeAll: "すべての表示項目", + scopeSelected: "選択のみ", + find: "検索テキスト...", + replaceWith: "置換テキスト...", + preview: "変更をプレビュー", + apply: "変更を適用", + prefix: "先頭に追加", + suffix: "末尾に追加", + noImages: "一括編集可能な画像がありません。", + modified: "変更済み", + language: "言語", + theme: "テーマ", + themeDark: "ダーク", + themeLight: "ライト", + searchPlaceholder: "ファイルまたはタグを検索...", + noResults: "一致する画像が見つかりません。", + selectAll: "すべて選択", + deselectAll: "選択解除", + selected: "選択済み", + deleteSelected: "選択を削除", + move: "移動/グループ", + moveTo: "移動先...", + merge: "マージ", + mergeTo: "マージ先...", + moveTitle: "アイテムを移動", + mergeTitle: "プロジェクトをマージ", + targetProject: "対象プロジェクト", + newProject: "新規プロジェクトを作成...", + newProjectName: "新規プロジェクト名", + moveDesc: "選択した {count} 枚の画像を別のプロジェクトに移動します。", + mergeDesc: "'{name}' を別のプロジェクトにマージします。元のプロジェクトは削除されます。", + confirmMove: "移動", + confirmMerge: "マージ", + tutorial: "ガイド", + tutWelcomeTitle: "Tag Master へようこそ", + tutWelcomeDesc: "AIモデルトレーニングデータセット準備のための強力なツールです。画像データセットの整理、キャプション付け、エクスポートが簡単にできます。", + tutImportTitle: "フォルダをドラッグ&ドロップ", + tutImportDesc: "フォルダ(サブディレクトリ含む)をアプリにドラッグするだけ。フォルダ名に基づいて自動的にプロジェクトに整理されます。", + tutSelectTitle: "効率的な選択", + tutSelectDesc: "クリックで選択。Shift+クリックで範囲選択。画像上でドラッグして素早く複数選択できます。", + tutTagTitle: "AI 自動キャプション", + tutTagDesc: "設定でAPIキーを入力。「すべて開始」で一括キャプション生成、またはインスペクターで個別に編集できます。", + tutExportTitle: "ワンクリックエクスポート", + tutExportDesc: "ZIPファイルとしてエクスポート。LoRAトレーニングに最適なフォーマット:画像とテキストファイルのペアがフォルダ別に整理されます。", + skip: "スキップ", + finish: "始める", + cleanTitle: "タグクリーニングルール (正規表現)", + cleanDescription: "ルールは生成時に自動的に適用されます。既存のタグに対して手動で実行することもできます。", + cleanTip: "ヒント: /pattern/flags で高度な正規表現を使用できます (例: /\\b(girl|boy)\\b/gi)。デフォルトは大文字小文字を区別しないグローバルマッチです。", + pattern: "パターン (正規表現)", + activeRules: "有効なルール", + noRules: "ルールが定義されていません", + liveTest: "ライブテスト", + result: "結果", + add: "追加", + cleanSelected: "選択をクリーニング", + cleanAll: "すべてクリーニング", + confirmCleanSelected: "選択した {count} 枚の画像にルールを適用しますか?", + confirmCleanAll: "すべての {count} 枚の表示画像に {rules} 個のルールを適用しますか?この操作は簡単に元に戻せません。", + projects: "プロジェクト", + triggerWord: "トリガーワード", + triggerWordPlaceholder: "例: miku", + processingSelected: "処理中...", + cleanLabel: "クリーン", + cols: "段数", + offlineWarning: "オフラインです。AI 自動生成は使用できません。", + selectAnImage: "画像を選択して詳細を編集します", + exportDatasetTitle: "データセットのエクスポート", + exportDatasetDesc: "お好みのフォーマットを選択してトレーニングデータをダウンロードしてください。", + readyToExport: "エクスポート準備完了", + imagesPrepared: "{count} 枚の画像とキャプションが準備されました。", + selectFormat: "フォーマットを選択", + textFiles: "テキストファイル", + textFilesDesc: "画像と対になる標準的な .txt キャプションファイル。", + kohyaJson: "Kohya JSON", + kohyaJsonDesc: "Kohya-ss スクリプト互換の metadata.json ファイル。", + exportProcessing: "処理中...", + downloadDataset: "データセットをダウンロード", + landscape: "横向き", + portrait: "縦向き", + square: "正方形", + smartResize: "スマートリサイズ", + nextTagging: "次へ:タグ付け", + cropImage: "切り抜き", + loading: "読み込み中...", + full: "全体", + cancel: "キャンセル", + saveCrop: "切り抜きを保存", + projectReview: "プロジェクトレビュー", + reviewDesc: "エクスポート前にデータセットの統計と健全性を確認してください。", + totalImages: "総画像数", + captioned: "キャプション済み", + missingTags: "タグ不足", + avgTagsPerImage: "平均タグ数/画像", + reviewItems: "レビュー項目", + issuesFound: "{count} 件の問題が見つかりました", + allGood: "問題なし!", + noIssuesDesc: "コンテンツの問題は検出されませんでした。エクスポートの準備ができています。", + missingCaption: "キャプションなし", + captionTooShort: "キャプションが短すぎます", + processingError: "処理エラー", + noCaption: "キャプションなし", + nextExport: "次へ:エクスポート", + format: "フォーマット", + textFilesTxt: "テキストファイル (.txt)", + standardCaptionFiles: "標準キャプションファイル", + jsonFilesJson: "JSON ファイル (.json)", + sidecarJsonFormat: "サイドカー JSON 形式", + somethingWentWrong: "問題が発生しました", + tryAgain: "再試行", + viewError: "表示エラー", + viewErrorDesc: "このセクションのレンダリングに失敗しました。アプリの他の部分は引き続き動作するはずです。", + reloadView: "ビューを再読み込み", + clickToRename: "クリックしてリネーム", + captionPlaceholder: "キャプション...", + saveTxt: ".txt を保存", + chars: "文字", + allItems: "すべての項目", + pending: "未処理", + completed: "完了済み", + gridView: "グリッドビュー", + listView: "リストビュー", + saved: "保存済み", + bucketAll: "すべて", + cleanTagsRegex: "タグクリーニング (正規表現)" } }; -export type Language = 'en' | 'zh'; +export type Language = 'en' | 'zh' | 'ja';