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 */}
-
-
@@ -456,15 +457,15 @@ export const ExportModal = ({
{t('exportAll')}
-
+
setFormat('txt')} className={`flex items-center gap-3 p-3 rounded-lg border transition-all ${format === 'txt' ? 'bg-indigo-50 border-indigo-500 text-indigo-700 dark:bg-indigo-500/20 dark:border-indigo-500/50 dark:text-indigo-300' : 'border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800'}`}>
-
Text Files (.txt)
-
Standard caption files
+
{t('textFilesTxt')}
+
{t('standardCaptionFiles')}
setFormat('json')} className={`flex items-center gap-3 p-3 rounded-lg border transition-all ${format === 'json' ? 'bg-indigo-50 border-indigo-500 text-indigo-700 dark:bg-indigo-500/20 dark:border-indigo-500/50 dark:text-indigo-300' : 'border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800'}`}>
@@ -472,16 +473,16 @@ export const ExportModal = ({
{format === 'json' && }
-
JSON Files (.json)
-
Sidecar JSON format
+
{t('jsonFilesJson')}
+
{t('sidecarJsonFormat')}
- Cancel
- { onExport(format); onClose(); }} className="bg-zinc-900 dark:bg-white text-white dark:text-black px-6 py-2 rounded-lg font-bold text-sm">Export
+ {t('cancel')}
+ { onExport(format); onClose(); }} className="bg-zinc-900 dark:bg-white text-white dark:text-black px-6 py-2 rounded-lg font-bold text-sm">{t('export')}
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')}
- Reload View
+ {t('reloadView')}
);
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 */}
-
+
setFormat('txt')}
@@ -53,8 +55,8 @@ export const ExportView: React.FC = ({ onExport, totalImages })
- Text Files
- Standard .txt caption files side-by-side with images.
+ {t('textFiles')}
+ {t('textFilesDesc')}
{format === 'txt' &&
}
@@ -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 => (
- setSelectedBucket(type)}
- className={`px-4 py-2 rounded-md text-sm font-medium transition-all flex items-center gap-2 ${selectedBucket === type ? 'bg-indigo-600 text-white shadow' : 'text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800'}`}
- >
-
- {type}
- {type !== 'all' && {buckets[type]}}
-
- ))}
+ {(['all', 'landscape', 'portrait', 'square'] as const).map(type => {
+ const labelMap = { all: t('bucketAll'), landscape: t('landscape'), portrait: t('portrait'), square: t('square') };
+ return (
+ setSelectedBucket(type)}
+ className={`px-4 py-2 rounded-md text-sm font-medium transition-all flex items-center gap-2 ${selectedBucket === type ? 'bg-indigo-600 text-white shadow' : 'text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800'}`}
+ >
+
+ {labelMap[type]}
+ {type !== 'all' && {buckets[type]}}
+
+ );
+ })}
@@ -90,10 +95,10 @@ export const PreprocessView: React.FC
= ({ projects, onUpda
{/* Actions */}
- Smart Resize
+ {t('smartResize')}
- Next: Tagging
+ {t('nextTagging')}
@@ -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')}
startCropping(img.id)}
className="bg-white text-zinc-900 px-3 py-1.5 rounded-full text-xs font-bold flex items-center gap-1 hover:scale-105 transition-transform"
>
- Crop
+ {t('cropImage')}
@@ -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: ()
- { if (imgRef.current) setCrop({ x: 0, y: 0, w: imgRef.current.width, h: imgRef.current.height }) }} className="text-xs font-bold text-zinc-400 hover:text-white">Full
+ { if (imgRef.current) setCrop({ x: 0, y: 0, w: imgRef.current.width, h: imgRef.current.height }) }} className="text-xs font-bold text-zinc-400 hover:text-white">{t('full')}
{ if (imgRef.current) { const s = Math.min(imgRef.current.width, imgRef.current.height); setCrop({ x: 0, y: 0, w: s, h: s }) } }} className="text-xs font-bold text-zinc-400 hover:text-white">1:1
- Cancel
- Save Crop
+ {t('cancel')}
+ {t('saveCrop')}
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 */}
- Next: Export
+ {t('nextExport')}
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';