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",
"error": "Fehler beim Hinzufügen des Textpaars zum Ordner"
},
"autoSave": "Autom. Speichern"
"autoSave": "Autom. Speichern",
"customLanguage": "oder Sprache eingeben..."
},
"dictionary": {
"title": "Wörterbuch",

View File

@@ -400,6 +400,7 @@
"auto": "Auto",
"generateIPA": "generate ipa",
"translateInto": "translate into",
"customLanguage": "or type language...",
"chinese": "Chinese",
"english": "English",
"french": "French",

View File

@@ -402,7 +402,8 @@
"success": "Paire de texte ajoutée 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": {
"title": "Dictionnaire",
@@ -508,15 +509,6 @@
"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": "Suivre",
"following": "Abonné",

View File

@@ -402,7 +402,8 @@
"success": "Coppia di testo aggiunta alla cartella",
"error": "Impossibile aggiungere coppia di testo alla cartella"
},
"autoSave": "Salvataggio Automatico"
"autoSave": "Salvataggio Automatico",
"customLanguage": "o digita lingua..."
},
"dictionary": {
"title": "Dizionario",
@@ -508,15 +509,6 @@
"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": "Segui",
"following": "Stai seguendo",

View File

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

View File

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

View File

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

View File

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

View File

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