feat(translator): add custom target language input

- Replace Select with Input for custom language entry
- Users can now type any target language they want
- Add i18n translations for all 8 languages
This commit is contained in:
2026-03-16 12:07:46 +08:00
parent f53fa5e2a1
commit 2f5ec1c0f0
9 changed files with 46 additions and 69 deletions

View File

@@ -373,7 +373,8 @@
"success": "Textpaar zum Ordner hinzugefügt", "success": "Textpaar zum Ordner hinzugefügt",
"error": "Fehler beim Hinzufügen des Textpaars zum Ordner" "error": "Fehler beim Hinzufügen des Textpaars zum Ordner"
}, },
"autoSave": "Autom. Speichern" "autoSave": "Autom. Speichern",
"customLanguage": "oder Sprache eingeben..."
}, },
"dictionary": { "dictionary": {
"title": "Wörterbuch", "title": "Wörterbuch",

View File

@@ -46,7 +46,7 @@
"unfavorite": "Unfavorite", "unfavorite": "Unfavorite",
"pleaseLogin": "Please login first" "pleaseLogin": "Please login first"
}, },
"folder_id": { "folder_id": {
"unauthorized": "You are not the owner of this folder", "unauthorized": "You are not the owner of this folder",
"back": "Back", "back": "Back",
"textPairs": "Text Pairs", "textPairs": "Text Pairs",
@@ -287,7 +287,7 @@
"favorites": "Favorites", "favorites": "Favorites",
"settings": "Settings" "settings": "Settings"
}, },
"ocr": { "ocr": {
"title": "OCR Vocabulary Extractor", "title": "OCR Vocabulary Extractor",
"description": "Upload vocabulary table screenshots from textbooks to extract word-definition pairs", "description": "Upload vocabulary table screenshots from textbooks to extract word-definition pairs",
"uploadSection": "Upload Image", "uploadSection": "Upload Image",
@@ -400,6 +400,7 @@
"auto": "Auto", "auto": "Auto",
"generateIPA": "generate ipa", "generateIPA": "generate ipa",
"translateInto": "translate into", "translateInto": "translate into",
"customLanguage": "or type language...",
"chinese": "Chinese", "chinese": "Chinese",
"english": "English", "english": "English",
"french": "French", "french": "French",

View File

@@ -402,7 +402,8 @@
"success": "Paire de texte ajoutée au dossier", "success": "Paire de texte ajoutée au dossier",
"error": "Échec de l'ajout de la paire de texte au dossier" "error": "Échec de l'ajout de la paire de texte au dossier"
}, },
"autoSave": "Sauvegarde automatique" "autoSave": "Sauvegarde automatique",
"customLanguage": "ou tapez la langue..."
}, },
"dictionary": { "dictionary": {
"title": "Dictionnaire", "title": "Dictionnaire",
@@ -508,15 +509,6 @@
"view": "Voir" "view": "Voir"
} }
}, },
"follow": {
"follow": "Suivre",
"following": "Abonnements",
"followers": "Abonnés",
"followersOf": "Abonnés de {username}",
"followingOf": "Abonnements de {username}",
"noFollowers": "Pas encore d'abonnés",
"noFollowing": "Ne suit personne"
},
"follow": { "follow": {
"follow": "Suivre", "follow": "Suivre",
"following": "Abonné", "following": "Abonné",

View File

@@ -402,7 +402,8 @@
"success": "Coppia di testo aggiunta alla cartella", "success": "Coppia di testo aggiunta alla cartella",
"error": "Impossibile aggiungere coppia di testo alla cartella" "error": "Impossibile aggiungere coppia di testo alla cartella"
}, },
"autoSave": "Salvataggio Automatico" "autoSave": "Salvataggio Automatico",
"customLanguage": "o digita lingua..."
}, },
"dictionary": { "dictionary": {
"title": "Dizionario", "title": "Dizionario",
@@ -508,15 +509,6 @@
"view": "Visualizza" "view": "Visualizza"
} }
}, },
"follow": {
"follow": "Segui",
"following": "Seguiti",
"followers": "Seguaci",
"followersOf": "Seguaci di {username}",
"followingOf": "Seguiti di {username}",
"noFollowers": "Nessun seguace ancora",
"noFollowing": "Non segui ancora nessuno"
},
"follow": { "follow": {
"follow": "Segui", "follow": "Segui",
"following": "Stai seguendo", "following": "Stai seguendo",

View File

@@ -411,7 +411,8 @@
"success": "テキストペアがフォルダーに追加されました", "success": "テキストペアがフォルダーに追加されました",
"error": "テキストペアをフォルダーに追加できませんでした" "error": "テキストペアをフォルダーに追加できませんでした"
}, },
"autoSave": "自動保存" "autoSave": "自動保存",
"customLanguage": "または言語を入力..."
}, },
"dictionary": { "dictionary": {
"title": "辞書", "title": "辞書",

View File

@@ -402,7 +402,8 @@
"success": "텍스트 쌍이 폴더에 추가됨", "success": "텍스트 쌍이 폴더에 추가됨",
"error": "폴더에 텍스트 쌍 추가 실패" "error": "폴더에 텍스트 쌍 추가 실패"
}, },
"autoSave": "자동 저장" "autoSave": "자동 저장",
"customLanguage": "또는 언어 입력..."
}, },
"dictionary": { "dictionary": {
"title": "사전", "title": "사전",
@@ -508,15 +509,6 @@
"view": "보기" "view": "보기"
} }
}, },
"follow": {
"follow": "팔로우",
"following": "팔로잉",
"followers": "팔로워",
"followersOf": "{username}의 팔로워",
"followingOf": "{username}의 팔로잉",
"noFollowers": "아직 팔로워가 없습니다",
"noFollowing": "아직 팔로잉하는 사람이 없습니다"
},
"follow": { "follow": {
"follow": "팔로우", "follow": "팔로우",
"following": "팔로잉", "following": "팔로잉",

View File

@@ -402,7 +402,8 @@
"success": "تېكىست جۈپى قىسقۇچقا قوشۇلدى", "success": "تېكىست جۈپى قىسقۇچقا قوشۇلدى",
"error": "تېكىست جۈپىنى قىسقۇچقا قوشۇش مەغلۇپ بولدى" "error": "تېكىست جۈپىنى قىسقۇچقا قوشۇش مەغلۇپ بولدى"
}, },
"autoSave": "ئاپتوماتىك ساقلاش" "autoSave": "ئاپتوماتىك ساقلاش",
"customLanguage": "ياكى تىل تىل كىرۇڭ..."
}, },
"dictionary": { "dictionary": {
"title": "لۇغەت", "title": "لۇغەت",
@@ -508,15 +509,6 @@
"view": "كۆرۈش" "view": "كۆرۈش"
} }
}, },
"follow": {
"follow": "ئەگىشىش",
"following": "ئەگىشىۋاتقانلار",
"followers": "ئەگەشكۈچىلەر",
"followersOf": "{username} نىڭ ئەگەشكۈچىلىرى",
"followingOf": "{username} نىڭ ئەگىشىۋاتقانلىرى",
"noFollowers": "تېخى ئەگەشكۈچى يوق",
"noFollowing": "تېخى ئەگىشىۋاتقان يوق"
},
"follow": { "follow": {
"follow": "ئەگىشىش", "follow": "ئەگىشىش",
"following": "ئەگىشىۋاتىدۇ", "following": "ئەگىشىۋاتىدۇ",

View File

@@ -424,7 +424,8 @@
"success": "文本对已添加到文件夹", "success": "文本对已添加到文件夹",
"error": "添加文本对到文件夹失败" "error": "添加文本对到文件夹失败"
}, },
"autoSave": "自动保存" "autoSave": "自动保存",
"customLanguage": "或输入语言..."
}, },
"dictionary": { "dictionary": {
"title": "词典", "title": "词典",

View File

@@ -1,8 +1,8 @@
"use client"; "use client";
import { LightButton, PrimaryButton, import { LightButton, PrimaryButton, IconClick } from "@/design-system/base/button";
IconClick } from "@/design-system/base/button";
import { Select } from "@/design-system/base/select"; import { Select } from "@/design-system/base/select";
import { Input } from "@/design-system/base/input";
import { Textarea } from "@/design-system/base/textarea"; import { Textarea } from "@/design-system/base/textarea";
import { IMAGES } from "@/config/images"; import { IMAGES } from "@/config/images";
import { useAudioPlayer } from "@/hooks/useAudioPlayer"; import { useAudioPlayer } from "@/hooks/useAudioPlayer";
@@ -46,6 +46,7 @@ export default function TranslatorPage() {
const taref = useRef<HTMLTextAreaElement>(null); const taref = useRef<HTMLTextAreaElement>(null);
const [sourceLanguage, setSourceLanguage] = useState<string>("Auto"); const [sourceLanguage, setSourceLanguage] = useState<string>("Auto");
const [targetLanguage, setTargetLanguage] = useState<string>("Chinese"); const [targetLanguage, setTargetLanguage] = useState<string>("Chinese");
const [customTargetLanguage, setCustomTargetLanguage] = useState<string>("");
const [translationResult, setTranslationResult] = useState<TSharedTranslationResult | null>(null); const [translationResult, setTranslationResult] = useState<TSharedTranslationResult | null>(null);
const [needIpa, setNeedIpa] = useState(true); const [needIpa, setNeedIpa] = useState(true);
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
@@ -93,18 +94,18 @@ export default function TranslatorPage() {
setProcessing(true); setProcessing(true);
const sourceText = taref.current.value; const sourceText = taref.current.value;
const effectiveTargetLanguage = customTargetLanguage.trim() || targetLanguage;
// 判断是否需要强制重新翻译 // 判断是否需要强制重新翻译
// 只有当源文本、源语言和目标语言都与上次相同时,才强制重新翻译
const forceRetranslate = const forceRetranslate =
lastTranslation?.sourceText === sourceText && lastTranslation?.sourceText === sourceText &&
lastTranslation?.sourceLanguage === sourceLanguage && lastTranslation?.sourceLanguage === sourceLanguage &&
lastTranslation?.targetLanguage === targetLanguage; lastTranslation?.targetLanguage === effectiveTargetLanguage;
try { try {
const result = await actionTranslateText({ const result = await actionTranslateText({
sourceText, sourceText,
targetLanguage, targetLanguage: effectiveTargetLanguage,
forceRetranslate, forceRetranslate,
needIpa, needIpa,
sourceLanguage: sourceLanguage === "Auto" ? undefined : sourceLanguage, sourceLanguage: sourceLanguage === "Auto" ? undefined : sourceLanguage,
@@ -115,7 +116,7 @@ export default function TranslatorPage() {
setLastTranslation({ setLastTranslation({
sourceText, sourceText,
sourceLanguage, sourceLanguage,
targetLanguage, targetLanguage: effectiveTargetLanguage,
}); });
} else { } else {
toast.error(result.message || "翻译失败,请重试"); toast.error(result.message || "翻译失败,请重试");
@@ -246,39 +247,43 @@ export default function TranslatorPage() {
<div className="option2 w-full flex gap-1 items-center overflow-x-auto"> <div className="option2 w-full flex gap-1 items-center overflow-x-auto">
<span className="shrink-0">{t("translateInto")}</span> <span className="shrink-0">{t("translateInto")}</span>
<LightButton <LightButton
selected={targetLanguage === "Chinese"} selected={!customTargetLanguage && targetLanguage === "Chinese"}
onClick={() => setTargetLanguage("Chinese")} onClick={() => {
setTargetLanguage("Chinese");
setCustomTargetLanguage("");
}}
className="shrink-0 hidden lg:inline-flex" className="shrink-0 hidden lg:inline-flex"
> >
{t("chinese")} {t("chinese")}
</LightButton> </LightButton>
<LightButton <LightButton
selected={targetLanguage === "English"} selected={!customTargetLanguage && targetLanguage === "English"}
onClick={() => setTargetLanguage("English")} onClick={() => {
setTargetLanguage("English");
setCustomTargetLanguage("");
}}
className="shrink-0 hidden lg:inline-flex" className="shrink-0 hidden lg:inline-flex"
> >
{t("english")} {t("english")}
</LightButton> </LightButton>
<LightButton <LightButton
selected={targetLanguage === "Japanese"} selected={!customTargetLanguage && targetLanguage === "Japanese"}
onClick={() => setTargetLanguage("Japanese")} onClick={() => {
setTargetLanguage("Japanese");
setCustomTargetLanguage("");
}}
className="shrink-0 hidden xl:inline-flex" className="shrink-0 hidden xl:inline-flex"
> >
{t("japanese")} {t("japanese")}
</LightButton> </LightButton>
<Select <Input
value={targetLanguage} variant="bordered"
onChange={(e) => setTargetLanguage(e.target.value)}
variant="light"
size="sm" size="sm"
className="w-auto min-w-[100px] shrink-0" value={customTargetLanguage}
> onChange={(e) => setCustomTargetLanguage(e.target.value)}
{TARGET_LANGUAGES.map((lang) => ( placeholder={t("customLanguage")}
<option key={lang.value} value={lang.value}> className="w-auto min-w-[120px] shrink-0"
{t(lang.labelKey)} />
</option>
))}
</Select>
</div> </div>
</div> </div>
</div> </div>