This commit is contained in:
243
src/app/translator/page.tsx
Normal file
243
src/app/translator/page.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
"use client";
|
||||
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import Button from "@/components/Button";
|
||||
import IconClick from "@/components/IconClick";
|
||||
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
||||
import IMAGES from "@/config/images";
|
||||
import { getTTSAudioUrl } from "@/utils";
|
||||
|
||||
export default function Home() {
|
||||
const [ipaEnabled, setIPAEnabled] = useState(true);
|
||||
const [voicesData, setVoicesData] = useState<{
|
||||
locale: string,
|
||||
short_name: string
|
||||
}[] | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [targetLang, setTargetLang] = useState('Italian');
|
||||
|
||||
const [sourceText, setSourceText] = useState('');
|
||||
const [targetText, setTargetText] = useState('');
|
||||
const [sourceIPA, setSourceIPA] = useState('');
|
||||
const [targetIPA, setTargetIPA] = useState('');
|
||||
const [sourceLocale, setSourceLocale] = useState<string | null>(null);
|
||||
const [targetLocale, setTargetLocale] = useState<string | null>(null);
|
||||
|
||||
const [translating, setTranslating] = useState(false);
|
||||
const { playAudio } = useAudioPlayer();
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/list_of_voices.json')
|
||||
.then(res => res.json())
|
||||
.then(setVoicesData)
|
||||
.catch(() => setVoicesData(null))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
if (loading) return <div>加载中...</div>;
|
||||
if (!voicesData) return <div>加载失败</div>;
|
||||
|
||||
const tl = ['English', 'Italian', 'Japanese'];
|
||||
|
||||
const inputLanguage = () => {
|
||||
const lang = prompt('Input a language.')?.trim();
|
||||
if (lang) {
|
||||
setTargetLang(lang);
|
||||
}
|
||||
}
|
||||
|
||||
const translate = () => {
|
||||
if (translating) return;
|
||||
if (sourceText.length === 0) return;
|
||||
|
||||
setTranslating(true);
|
||||
|
||||
setTargetText('');
|
||||
setSourceLocale(null);
|
||||
setTargetLocale(null);
|
||||
setSourceIPA('');
|
||||
setTargetIPA('');
|
||||
|
||||
const params = new URLSearchParams({
|
||||
text: sourceText,
|
||||
target: targetLang
|
||||
})
|
||||
fetch(`/api/translate?${params}`)
|
||||
.then(res => res.json())
|
||||
.then(obj => {
|
||||
setSourceLocale(obj.source_locale);
|
||||
setTargetLocale(obj.target_locale);
|
||||
setTargetText(obj.target_text);
|
||||
|
||||
if (ipaEnabled) {
|
||||
const params = new URLSearchParams({
|
||||
text: sourceText
|
||||
});
|
||||
fetch(`/api/ipa?${params}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
setSourceIPA(data.ipa);
|
||||
}).catch(e => {
|
||||
console.error(e);
|
||||
setSourceIPA('');
|
||||
})
|
||||
const params2 = new URLSearchParams({
|
||||
text: obj.target_text
|
||||
});
|
||||
fetch(`/api/ipa?${params2}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
setTargetIPA(data.ipa);
|
||||
}).catch(e => {
|
||||
console.error(e);
|
||||
setTargetIPA('');
|
||||
})
|
||||
}
|
||||
}).catch(r => {
|
||||
console.error(r);
|
||||
setSourceLocale('');
|
||||
setTargetLocale('');
|
||||
setTargetText('');
|
||||
}).finally(() => setTranslating(false));
|
||||
}
|
||||
|
||||
const handleInputChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setSourceText(e.target.value.trim());
|
||||
setTargetText('');
|
||||
setSourceLocale(null);
|
||||
setTargetLocale(null);
|
||||
setSourceIPA('');
|
||||
setTargetIPA('');
|
||||
}
|
||||
|
||||
const readSource = async () => {
|
||||
if (sourceText.length === 0) return;
|
||||
|
||||
if (sourceIPA.length === 0 && ipaEnabled) {
|
||||
const params = new URLSearchParams({
|
||||
text: sourceText
|
||||
});
|
||||
fetch(`/api/ipa?${params}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
setSourceIPA(data.ipa);
|
||||
}).catch(e => {
|
||||
console.error(e);
|
||||
setSourceIPA('');
|
||||
})
|
||||
}
|
||||
|
||||
if (!sourceLocale) {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
text: sourceText.slice(0, 30)
|
||||
});
|
||||
const res = await fetch(`/api/locale?${params}`);
|
||||
const info = await res.json();
|
||||
setSourceLocale(info.locale);
|
||||
|
||||
const voice = voicesData.find(v => v.locale.startsWith(info.locale));
|
||||
if (!voice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = await getTTSAudioUrl(sourceText, voice.short_name);
|
||||
await playAudio(url);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setSourceLocale(null);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const voice = voicesData.find(v => v.locale.startsWith(sourceLocale!));
|
||||
if (!voice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = await getTTSAudioUrl(sourceText, voice.short_name);
|
||||
await playAudio(url);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
const readTarget = async () => {
|
||||
if (targetText.length === 0) return;
|
||||
|
||||
if (targetIPA.length === 0 && ipaEnabled) {
|
||||
const params = new URLSearchParams({
|
||||
text: targetText
|
||||
});
|
||||
fetch(`/api/ipa?${params}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
setTargetIPA(data.ipa);
|
||||
}).catch(e => {
|
||||
console.error(e);
|
||||
setTargetIPA('');
|
||||
})
|
||||
}
|
||||
|
||||
const voice = voicesData.find(v => v.locale.startsWith(targetLocale!));
|
||||
if (!voice) return;
|
||||
|
||||
const url = await getTTSAudioUrl(targetText, voice.short_name);
|
||||
await playAudio(url);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-screen flex flex-col md:flex-row md:justify-between gap-2 p-2">
|
||||
<div className="card1 w-full md:w-1/2 flex flex-col-reverse gap-2">
|
||||
<div className="textarea1 border-1 border-gray-200 rounded-2xl w-full h-64 p-2">
|
||||
<textarea onChange={handleInputChange} className="resize-none h-8/12 w-full focus:outline-0"></textarea>
|
||||
<div className="ipa w-full h-2/12 overflow-auto text-gray-600">
|
||||
{sourceIPA}
|
||||
</div>
|
||||
<div className="h-2/12 w-full flex justify-end items-center">
|
||||
<IconClick onClick={async () => {
|
||||
if (sourceText.length !== 0)
|
||||
await navigator.clipboard.writeText(sourceText);
|
||||
}} src={IMAGES.copy_all} alt="copy"></IconClick>
|
||||
<IconClick onClick={readSource} src={IMAGES.play_arrow} alt="play"></IconClick>
|
||||
</div>
|
||||
</div>
|
||||
<div className="option1 w-full flex flex-row justify-between items-center">
|
||||
<span>detect language</span>
|
||||
<Button label="generate ipa" selected={ipaEnabled} onClick={() => setIPAEnabled(!ipaEnabled)}></Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card2 w-full md:w-1/2 flex flex-col-reverse gap-2">
|
||||
<div className="textarea2 bg-gray-100 rounded-2xl w-full h-64 p-2">
|
||||
<div className="h-8/12 w-full">{
|
||||
targetText
|
||||
}</div>
|
||||
<div className="ipa w-full h-2/12 overflow-auto text-gray-600">
|
||||
{targetIPA}
|
||||
</div>
|
||||
<div className="h-2/12 w-full flex justify-end items-center">
|
||||
<IconClick onClick={async () => {
|
||||
if (targetText.length !== 0)
|
||||
await navigator.clipboard.writeText(targetText);
|
||||
}} src={IMAGES.copy_all} alt="copy"></IconClick>
|
||||
<IconClick onClick={readTarget} src={IMAGES.play_arrow} alt="play"></IconClick>
|
||||
</div>
|
||||
</div>
|
||||
<div className="option2 w-full flex gap-1 items-center flex-wrap">
|
||||
<span>translate into</span>
|
||||
<Button onClick={() => { setTargetLang('English') }} label="English" selected={targetLang === 'English'}></Button>
|
||||
<Button onClick={() => { setTargetLang('Italian') }} label="Italian" selected={targetLang === 'Italian'}></Button>
|
||||
<Button onClick={() => { setTargetLang('Japanese') }} label="Japanese" selected={targetLang === 'Japanese'}></Button>
|
||||
<Button onClick={inputLanguage} label={'Other' + (tl.includes(targetLang) ? '' : ': ' + targetLang)} selected={!(tl.includes(targetLang))}></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="button-area w-screen flex justify-center items-center">
|
||||
<button onClick={translate} className={`text-xl font-extrabold border rounded-4xl p-3 border-gray-200 h-16 ${translating ? 'bg-gray-200' : 'bg-white hover:bg-gray-200 hover:cursor-pointer'}`}>
|
||||
{translating ? 'translating...' : 'translate'}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user