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:
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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é",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -411,7 +411,8 @@
|
|||||||
"success": "テキストペアがフォルダーに追加されました",
|
"success": "テキストペアがフォルダーに追加されました",
|
||||||
"error": "テキストペアをフォルダーに追加できませんでした"
|
"error": "テキストペアをフォルダーに追加できませんでした"
|
||||||
},
|
},
|
||||||
"autoSave": "自動保存"
|
"autoSave": "自動保存",
|
||||||
|
"customLanguage": "または言語を入力..."
|
||||||
},
|
},
|
||||||
"dictionary": {
|
"dictionary": {
|
||||||
"title": "辞書",
|
"title": "辞書",
|
||||||
|
|||||||
@@ -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": "팔로잉",
|
||||||
|
|||||||
@@ -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": "ئەگىشىۋاتىدۇ",
|
||||||
|
|||||||
@@ -424,7 +424,8 @@
|
|||||||
"success": "文本对已添加到文件夹",
|
"success": "文本对已添加到文件夹",
|
||||||
"error": "添加文本对到文件夹失败"
|
"error": "添加文本对到文件夹失败"
|
||||||
},
|
},
|
||||||
"autoSave": "自动保存"
|
"autoSave": "自动保存",
|
||||||
|
"customLanguage": "或输入语言..."
|
||||||
},
|
},
|
||||||
"dictionary": {
|
"dictionary": {
|
||||||
"title": "词典",
|
"title": "词典",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user