Free-Translator / app.py
mr-don88's picture
Update app.py
0400578 verified
Raw
History Blame Contribute Delete
15.2 kB
"""
Free Translator β€” Hugging Face Spaces
Google Translate (unofficial, no API key) + MyMemory fallback
Added: 70+ languages, light theme, better readability
"""
import json
import urllib.request
import urllib.parse
import urllib.error
import gradio as gr
# ─── Language list (70+ languages) ───────────────────────────────────────────
LANGUAGES = [
("Albanian", "sq"), ("Amharic", "am"), ("Arabic", "ar"), ("Armenian", "hy"),
("Azerbaijani", "az"), ("Basque", "eu"), ("Belarusian", "be"), ("Bengali", "bn"),
("Bosnian", "bs"), ("Bulgarian", "bg"), ("Burmese", "my"), ("Catalan", "ca"),
("Cebuano", "ceb"), ("Chinese (Simplified)", "zh-CN"), ("Chinese (Traditional)", "zh-TW"),
("Croatian", "hr"), ("Czech", "cs"), ("Danish", "da"), ("Dutch", "nl"),
("English", "en"), ("Esperanto", "eo"), ("Estonian", "et"), ("Finnish", "fi"),
("French", "fr"), ("Galician", "gl"), ("Georgian", "ka"), ("German", "de"),
("Greek", "el"), ("Gujarati", "gu"), ("Haitian Creole", "ht"), ("Hausa", "ha"),
("Hebrew", "he"), ("Hindi", "hi"), ("Hmong", "hmn"), ("Hungarian", "hu"),
("Icelandic", "is"), ("Igbo", "ig"), ("Indonesian", "id"), ("Irish", "ga"),
("Italian", "it"), ("Japanese", "ja"), ("Javanese", "jw"), ("Kannada", "kn"),
("Kazakh", "kk"), ("Khmer", "km"), ("Korean", "ko"), ("Kurdish", "ku"),
("Kyrgyz", "ky"), ("Lao", "lo"), ("Latin", "la"), ("Latvian", "lv"),
("Lithuanian", "lt"), ("Luxembourgish", "lb"), ("Macedonian", "mk"), ("Malagasy", "mg"),
("Malay", "ms"), ("Malayalam", "ml"), ("Maltese", "mt"), ("Maori", "mi"),
("Marathi", "mr"), ("Mongolian", "mn"), ("Nepali", "ne"), ("Norwegian", "no"),
("Pashto", "ps"), ("Persian (Farsi)", "fa"), ("Polish", "pl"), ("Portuguese", "pt"),
("Punjabi", "pa"), ("Romanian", "ro"), ("Russian", "ru"), ("Samoan", "sm"),
("Scottish Gaelic", "gd"), ("Serbian", "sr"), ("Sesotho", "st"), ("Shona", "sn"),
("Sindhi", "sd"), ("Sinhala", "si"), ("Slovak", "sk"), ("Slovenian", "sl"),
("Somali", "so"), ("Spanish", "es"), ("Sundanese", "su"), ("Swahili", "sw"),
("Swedish", "sv"), ("Tagalog (Filipino)", "tl"), ("Tajik", "tg"), ("Tamil", "ta"),
("Telugu", "te"), ("Thai", "th"), ("Turkish", "tr"), ("Ukrainian", "uk"),
("Urdu", "ur"), ("Uzbek", "uz"), ("Vietnamese", "vi"), ("Welsh", "cy"),
("Xhosa", "xh"), ("Yiddish", "yi"), ("Yoruba", "yo"), ("Zulu", "zu"),
]
# Sort alphabetically by language name
LANGUAGES = sorted(LANGUAGES, key=lambda x: x[0])
LANG_NAMES = [l[0] for l in LANGUAGES]
LANG_CODE = {l[0]: l[1] for l in LANGUAGES}
CHUNK_SIZE = 4500
UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
)
# ─── Translation engines (unchanged) ─────────────────────────────────────────
def google_translate(text: str, src: str, tgt: str) -> str:
url = "https://translate.googleapis.com/translate_a/single"
params = urllib.parse.urlencode({
"client": "gtx", "sl": src, "tl": tgt, "dt": "t", "q": text,
})
req = urllib.request.Request(f"{url}?{params}", headers={"User-Agent": UA})
with urllib.request.urlopen(req, timeout=15) as r:
data = json.loads(r.read().decode("utf-8"))
return "".join(seg[0] for seg in data[0] if seg[0])
def mymemory_translate(text: str, src: str, tgt: str) -> str:
url = "https://api.mymemory.translated.net/get"
params = urllib.parse.urlencode({"q": text, "langpair": f"{src}|{tgt}"})
req = urllib.request.Request(f"{url}?{params}", headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=15) as r:
data = json.loads(r.read().decode("utf-8"))
if data.get("responseStatus") == 200:
return data["responseData"]["translatedText"]
raise RuntimeError(data.get("responseDetails", "MyMemory error"))
def split_chunks(text: str, size: int) -> list[str]:
if len(text) <= size:
return [text]
chunks = []
while text:
if len(text) <= size:
chunks.append(text)
break
cut = size
for sep in ("\n\n", "\n", ". ", "。", " "):
idx = text.rfind(sep, 0, cut)
if idx != -1:
cut = idx + len(sep)
break
chunks.append(text[:cut])
text = text[cut:]
return chunks
def do_translate(text: str, src_name: str, tgt_name: str) -> str:
text = (text or "").strip()
if not text:
return ""
src = LANG_CODE.get(src_name, "en")
tgt = LANG_CODE.get(tgt_name, "vi")
if src == tgt:
return text
chunks = split_chunks(text, CHUNK_SIZE)
parts = []
for chunk in chunks:
try:
parts.append(google_translate(chunk, src, tgt))
except Exception:
try:
parts.append(mymemory_translate(chunk, src, tgt))
except Exception as e:
parts.append(f"[Lα»—i: {e}]")
return "".join(parts)
def swap_and_translate(src_name, tgt_name, src_text, tgt_text):
new_src_text = tgt_text
result = do_translate(new_src_text, tgt_name, src_name)
return tgt_name, src_name, new_src_text, result
def char_count(text: str) -> str:
n = len(text or "")
chunks = max(1, (n + CHUNK_SIZE - 1) // CHUNK_SIZE)
if n == 0:
return "0 kΓ½ tα»±"
if n > CHUNK_SIZE:
return f"{n:,} kΓ½ tα»± Β· {chunks} phαΊ§n"
return f"{n:,} kΓ½ tα»±"
# ─── Light Mode CSS (easy to read, not dark) ─────────────────────────────────
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,300;14..32,400;14..32,500;14..32,600;14..32,700&family=JetBrains+Mono:wght@400;500&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body, .gradio-container {
background: #f8fafc !important;
font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, sans-serif !important;
color: #0f172a !important;
}
/* header */
#header {
text-align: center;
padding: 48px 24px 32px;
position: relative;
}
#header h1 {
font-size: clamp(2.2rem, 6vw, 3.5rem) !important;
font-weight: 700 !important;
letter-spacing: -1px !important;
background: linear-gradient(135deg, #1e293b 0%, #3b82f6 50%, #0f172a 100%);
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
background-clip: text !important;
margin: 0 0 8px !important;
}
#header p {
color: #475569 !important;
font-family: 'JetBrains Mono', monospace !important;
font-size: 13px !important;
letter-spacing: 0.3px !important;
}
.badge-row {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 20px;
flex-wrap: wrap;
}
.badge {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
font-weight: 500;
padding: 5px 14px;
border-radius: 40px;
background: #ffffff;
box-shadow: 0 1px 2px rgba(0,0,0,0.03), 0 1px 3px rgba(0,0,0,0.05);
border: 1px solid #e2e8f0;
}
.badge-green { color: #15803d; border-left: 3px solid #22c55e; }
.badge-blue { color: #1e40af; border-left: 3px solid #3b82f6; }
.badge-purple { color: #6b21a5; border-left: 3px solid #a855f7; }
/* main card */
#main-panel {
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 28px;
padding: 28px;
margin: 0 auto 32px;
max-width: 1200px;
box-shadow: 0 8px 20px rgba(0,0,0,0.02), 0 2px 6px rgba(0,0,0,0.05);
transition: box-shadow 0.2s;
}
#main-panel:hover {
box-shadow: 0 20px 35px -12px rgba(0,0,0,0.08);
}
/* labels */
label, .label-wrap span {
font-family: 'JetBrains Mono', monospace !important;
font-size: 11px !important;
font-weight: 500 !important;
letter-spacing: 0.5px !important;
color: #334155 !important;
text-transform: uppercase !important;
margin-bottom: 8px !important;
}
/* textareas */
textarea, .scroll-hide {
background: #ffffff !important;
color: #0f172a !important;
border: 1.5px solid #e2e8f0 !important;
border-radius: 18px !important;
font-family: 'Inter', monospace !important;
font-size: 15px !important;
line-height: 1.6 !important;
padding: 16px 18px !important;
resize: vertical !important;
transition: all 0.2s ease !important;
box-shadow: 0 1px 2px rgba(0,0,0,0.02) !important;
}
textarea:focus {
border-color: #3b82f6 !important;
outline: none !important;
box-shadow: 0 0 0 3px rgba(59,130,246,0.15) !important;
}
textarea[readonly] {
background: #f9f9fc !important;
color: #1e293b !important;
border-color: #e2e8f0 !important;
}
/* dropdowns */
.wrap-inner, select, .multiselect {
background: #ffffff !important;
color: #0f172a !important;
border: 1.5px solid #e2e8f0 !important;
border-radius: 16px !important;
font-family: 'Inter', monospace !important;
font-size: 13px !important;
font-weight: 500 !important;
padding: 10px 14px !important;
}
.wrap-inner:hover, select:hover {
border-color: #94a3b8 !important;
}
/* buttons */
button, .btn {
font-family: 'Inter', sans-serif !important;
font-weight: 600 !important;
border-radius: 40px !important;
transition: all 0.2s ease !important;
cursor: pointer !important;
}
#translate-btn button, button.primary {
background: #0f172a !important;
color: white !important;
border: none !important;
padding: 12px 32px !important;
font-size: 14px !important;
box-shadow: 0 2px 5px rgba(0,0,0,0.05), 0 1px 2px rgba(0,0,0,0.03) !important;
}
#translate-btn button:hover, button.primary:hover {
background: #1e293b !important;
transform: translateY(-1px);
box-shadow: 0 10px 20px -8px rgba(15,23,42,0.2);
}
#swap-btn button, button.secondary {
background: #ffffff !important;
color: #1e293b !important;
border: 1.5px solid #e2e8f0 !important;
padding: 10px 18px !important;
font-size: 20px !important;
font-weight: 500 !important;
}
#swap-btn button:hover {
background: #f1f5f9 !important;
border-color: #cbd5e1 !important;
transform: rotate(180deg) scale(1.02);
}
#clear-btn button {
background: #ffffff !important;
color: #475569 !important;
border: 1.5px solid #e2e8f0 !important;
padding: 8px 20px !important;
font-size: 13px !important;
}
#clear-btn button:hover {
color: #dc2626 !important;
border-color: #fecaca !important;
background: #fef2f2 !important;
}
/* char counter */
#char-count {
font-family: 'JetBrains Mono', monospace !important;
font-size: 11px !important;
color: #475569 !important;
text-align: right !important;
margin-top: 8px;
}
/* divider */
.divider {
height: 1px;
background: #eef2ff;
margin: 20px 0;
}
/* footer */
#footer {
text-align: center;
padding: 0 0 40px;
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: #64748b;
letter-spacing: 0.3px;
}
/* scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: #f1f5f9; border-radius: 10px; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
/* responsive */
@media (max-width: 720px) {
#main-panel { padding: 18px; margin: 0 12px 24px; }
#header { padding: 32px 16px 20px; }
.badge { font-size: 9px; padding: 3px 10px; }
}
"""
# ─── Gradio UI ───────────────────────────────────────────────────────────────
def build_app():
with gr.Blocks(css=CSS, title="Free Translator", theme=gr.themes.Soft()) as demo:
gr.HTML("""
<div id="header">
<h1>🌍 Free Translator</h1>
<p>Google Translate (unofficial) Β· 70+ languages Β· No API key</p>
<div class="badge-row">
<span class="badge badge-green">βœ“ Free & unlimited</span>
<span class="badge badge-blue">⚑ Fast & reliable</span>
<span class="badge badge-purple">πŸ” MyMemory fallback</span>
</div>
</div>
""")
with gr.Group(elem_id="main-panel"):
with gr.Row(elem_id="lang-row"):
src_combo = gr.Dropdown(
choices=LANG_NAMES,
value="English",
label="FROM",
scale=2,
interactive=True,
)
swap_btn = gr.Button("⇄", elem_id="swap-btn", scale=0, min_width=70)
tgt_combo = gr.Dropdown(
choices=LANG_NAMES,
value="Vietnamese",
label="TO",
scale=2,
interactive=True,
)
gr.HTML('<div class="divider"></div>')
with gr.Row():
with gr.Column():
src_text = gr.Textbox(
label="INPUT",
placeholder="Paste or type text to translate...",
lines=14,
max_lines=30,
show_copy_button=True,
interactive=True,
)
char_lbl = gr.Markdown("0 kΓ½ tα»±", elem_id="char-count")
with gr.Column():
tgt_text = gr.Textbox(
label="TRANSLATION",
placeholder="Translation will appear here...",
lines=14,
max_lines=30,
interactive=True,
show_copy_button=True,
)
gr.HTML('<div class="divider"></div>')
with gr.Row():
clear_btn = gr.Button("βœ• Clear", elem_id="clear-btn", scale=0, min_width=120)
translate_btn = gr.Button("Translate β†’", elem_id="translate-btn", variant="primary", scale=0, min_width=180)
gr.HTML("""
<div id="footer">
Powered by Google Translate Β· MyMemory backup Β· Made with Gradio
</div>
""")
# Events
src_text.change(fn=char_count, inputs=src_text, outputs=char_lbl)
translate_btn.click(fn=do_translate, inputs=[src_text, src_combo, tgt_combo], outputs=tgt_text)
src_text.submit(fn=do_translate, inputs=[src_text, src_combo, tgt_combo], outputs=tgt_text)
swap_btn.click(fn=swap_and_translate, inputs=[src_combo, tgt_combo, src_text, tgt_text],
outputs=[src_combo, tgt_combo, src_text, tgt_text])
clear_btn.click(fn=lambda: ("", "", "0 kΓ½ tα»±"), outputs=[src_text, tgt_text, char_lbl])
return demo
if __name__ == "__main__":
app = build_app()
app.launch(server_name="0.0.0.0", server_port=7860, share=True)