Files
learn-languages/src/app/translator/page.tsx

285 lines
8.6 KiB
TypeScript

"use client";
import { ChangeEvent, useState } from "react";
import LightButton from "@/components/buttons/LightButton";
import IconClick from "@/components/IconClick";
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
import IMAGES from "@/config/images";
import { getTTSAudioUrl } from "@/utils";
import { Navbar } from "@/components/Navbar";
import { VOICES } from "@/config/locales";
export default function Translator() {
const [ipaEnabled, setIPAEnabled] = useState(true);
const [targetLang, setTargetLang] = useState("Chinese");
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();
const tl = ["Chinese", "English", "Italian"];
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 = VOICES.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 = VOICES.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 = VOICES.find((v) => v.locale.startsWith(targetLocale!));
if (!voice) return;
const url = await getTTSAudioUrl(targetText, voice.short_name);
await playAudio(url);
URL.revokeObjectURL(url);
};
return (
<>
<Navbar></Navbar>
<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>
<LightButton
selected={ipaEnabled}
onClick={() => setIPAEnabled(!ipaEnabled)}
>
generate ipa
</LightButton>
</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>
<LightButton
onClick={() => {
setTargetLang("Chinese");
}}
selected={targetLang === "Chinese"}
>
Chinese
</LightButton>
<LightButton
onClick={() => {
setTargetLang("English");
}}
selected={targetLang === "English"}
>
English
</LightButton>
<LightButton
onClick={() => {
setTargetLang("Italian");
}}
selected={targetLang === "Italian"}
>
Italian
</LightButton>
<LightButton onClick={inputLanguage} selected={!tl.includes(targetLang)}>
{"Other" + (tl.includes(targetLang) ? "" : ": " + targetLang)}
</LightButton>
</div>
</div>
</div>
<div className="button-area w-screen flex justify-center items-center">
<button
onClick={translate}
className={`duration-150 ease-in 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>
</>
);
}