From 77ec80af503bab9420005289beafeb3b48856368 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 22:46:28 +0000 Subject: [PATCH 1/7] Initial plan From 83cc8dd7b88c4a4c5e0a7ffe35b27c6b85aa48fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 22:49:42 +0000 Subject: [PATCH 2/7] Implement Portuguese language variant fallback for Auto Detect Co-authored-by: pierotofy <1951843+pierotofy@users.noreply.github.com> --- libretranslate/app.py | 12 ++++++------ libretranslate/language.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/libretranslate/app.py b/libretranslate/app.py index d20104c..d2a9415 100644 --- a/libretranslate/app.py +++ b/libretranslate/app.py @@ -23,7 +23,7 @@ from werkzeug.http import http_date from werkzeug.utils import secure_filename from libretranslate import flood, remove_translated_files, scheduler, secret, security, storage, cache -from libretranslate.language import model2iso, iso2model, detect_languages, improve_translation_formatting +from libretranslate.language import model2iso, iso2model, detect_languages, improve_translation_formatting, get_language_with_fallback from libretranslate.locales import ( _, _lazy, @@ -794,12 +794,12 @@ def create_app(args): else: detected_src_lang = {"confidence": 0.0, "language": "en"} - src_lang = next(iter([l for l in languages if l.code == detected_src_lang["language"]]), None) + src_lang = get_language_with_fallback(detected_src_lang["language"], languages) if src_lang is None: abort(400, description=_("%(lang)s is not supported", lang=source_lang)) - tgt_lang = next(iter([l for l in languages if l.code == target_lang]), None) + tgt_lang = get_language_with_fallback(target_lang, languages) if tgt_lang is None: abort(400, description=_("%(lang)s is not supported",lang=target_lang)) @@ -977,12 +977,12 @@ def create_app(args): if os.path.splitext(file.filename)[1] not in frontend_argos_supported_files_format: abort(400, description=_("Invalid request: file format not supported")) - src_lang = next(iter([l for l in languages if l.code == source_lang]), None) + src_lang = get_language_with_fallback(source_lang, languages) if src_lang is None and source_lang != "auto": abort(400, description=_("%(lang)s is not supported", lang=source_lang)) - tgt_lang = next(iter([l for l in languages if l.code == target_lang]), None) + tgt_lang = get_language_with_fallback(target_lang, languages) if tgt_lang is None: abort(400, description=_("%(lang)s is not supported", lang=target_lang)) @@ -1005,7 +1005,7 @@ def create_app(args): src_texts = argostranslatefiles.get_texts(filepath) candidate_langs = detect_languages(src_texts) detected_src_lang = candidate_langs[0] - src_lang = next(iter([l for l in languages if l.code == detected_src_lang["language"]]), None) + src_lang = get_language_with_fallback(detected_src_lang["language"], languages) if src_lang is None: abort(400, description=_("%(lang)s is not supported", lang=detected_src_lang["language"])) diff --git a/libretranslate/language.py b/libretranslate/language.py index 42f8866..1284634 100644 --- a/libretranslate/language.py +++ b/libretranslate/language.py @@ -47,6 +47,24 @@ def load_lang_codes(): languages = load_languages() return tuple(l.code for l in languages) +def get_language_with_fallback(lang_code, languages): + lang = next(iter([l for l in languages if l.code == lang_code]), None) + if lang is not None: + return lang + + language_variants = { + 'pt': ['pb'], + 'pb': ['pt'] + } + + fallbacks = language_variants.get(lang_code, []) + for fallback_code in fallbacks: + fallback_lang = next(iter([l for l in languages if l.code == fallback_code]), None) + if fallback_lang is not None: + return fallback_lang + + return None + def detect_languages(text): # detect batch processing if isinstance(text, list): From 533643d8e5173a971e16dc3d6d65f6e917e688e3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 22:50:24 +0000 Subject: [PATCH 3/7] Add integration tests for Portuguese language variant fallback Co-authored-by: pierotofy <1951843+pierotofy@users.noreply.github.com> --- .../test_language_variant_fallback.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 libretranslate/tests/test_api/test_language_variant_fallback.py diff --git a/libretranslate/tests/test_api/test_language_variant_fallback.py b/libretranslate/tests/test_api/test_language_variant_fallback.py new file mode 100644 index 0000000..b28a046 --- /dev/null +++ b/libretranslate/tests/test_api/test_language_variant_fallback.py @@ -0,0 +1,50 @@ +import sys +import pytest +from libretranslate.app import create_app +from libretranslate.main import get_args + + +@pytest.fixture() +def app_with_pb(): + sys.argv = ['', '--load-only', 'en,pb'] + app = create_app(get_args()) + yield app + + +@pytest.fixture() +def client_with_pb(app_with_pb): + return app_with_pb.test_client() + + +@pytest.fixture() +def app_with_pt(): + sys.argv = ['', '--load-only', 'en,pt'] + app = create_app(get_args()) + yield app + + +@pytest.fixture() +def client_with_pt(app_with_pt): + return app_with_pt.test_client() + + +def test_portuguese_fallback_pb_to_pt(client_with_pt): + response = client_with_pt.post("/translate", data={ + "q": "Hello", + "source": "en", + "target": "pb", + "format": "text" + }) + + assert response.status_code == 200 + + +def test_portuguese_fallback_pt_to_pb(client_with_pb): + response = client_with_pb.post("/translate", data={ + "q": "Hello", + "source": "en", + "target": "pt", + "format": "text" + }) + + assert response.status_code == 200 From 81da436400b992b8183b315fb2bf750dc5ec978f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 22:51:55 +0000 Subject: [PATCH 4/7] Address code review feedback: use generator expressions and improve test assertions Co-authored-by: pierotofy <1951843+pierotofy@users.noreply.github.com> --- libretranslate/language.py | 4 ++-- .../tests/test_api/test_language_variant_fallback.py | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/libretranslate/language.py b/libretranslate/language.py index 1284634..cc7ae24 100644 --- a/libretranslate/language.py +++ b/libretranslate/language.py @@ -48,7 +48,7 @@ def load_lang_codes(): return tuple(l.code for l in languages) def get_language_with_fallback(lang_code, languages): - lang = next(iter([l for l in languages if l.code == lang_code]), None) + lang = next((l for l in languages if l.code == lang_code), None) if lang is not None: return lang @@ -59,7 +59,7 @@ def get_language_with_fallback(lang_code, languages): fallbacks = language_variants.get(lang_code, []) for fallback_code in fallbacks: - fallback_lang = next(iter([l for l in languages if l.code == fallback_code]), None) + fallback_lang = next((l for l in languages if l.code == fallback_code), None) if fallback_lang is not None: return fallback_lang diff --git a/libretranslate/tests/test_api/test_language_variant_fallback.py b/libretranslate/tests/test_api/test_language_variant_fallback.py index b28a046..5a9a58d 100644 --- a/libretranslate/tests/test_api/test_language_variant_fallback.py +++ b/libretranslate/tests/test_api/test_language_variant_fallback.py @@ -1,4 +1,5 @@ import sys +import json import pytest from libretranslate.app import create_app from libretranslate.main import get_args @@ -36,7 +37,10 @@ def test_portuguese_fallback_pb_to_pt(client_with_pt): "format": "text" }) + response_json = json.loads(response.data) assert response.status_code == 200 + assert "translatedText" in response_json + assert len(response_json["translatedText"]) > 0 def test_portuguese_fallback_pt_to_pb(client_with_pb): @@ -47,4 +51,7 @@ def test_portuguese_fallback_pt_to_pb(client_with_pb): "format": "text" }) + response_json = json.loads(response.data) assert response.status_code == 200 + assert "translatedText" in response_json + assert len(response_json["translatedText"]) > 0 From 05e09f577be2ac2815efb9128b95736afa3236db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 22:56:51 +0000 Subject: [PATCH 5/7] Restrict fallback to auto-detect only and add Chinese variants Co-authored-by: pierotofy <1951843+pierotofy@users.noreply.github.com> --- libretranslate/app.py | 11 ++++--- libretranslate/language.py | 4 ++- .../test_language_variant_fallback.py | 32 +++++++++++++------ 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/libretranslate/app.py b/libretranslate/app.py index d2a9415..69752b6 100644 --- a/libretranslate/app.py +++ b/libretranslate/app.py @@ -789,17 +789,18 @@ def create_app(args): if source_lang == "auto": candidate_langs = detect_languages(src_texts) detected_src_lang = candidate_langs[0] + src_lang = get_language_with_fallback(detected_src_lang["language"], languages) else: detected_src_lang = {"confidence": 100.0, "language": source_lang} + src_lang = next((l for l in languages if l.code == source_lang), None) else: detected_src_lang = {"confidence": 0.0, "language": "en"} - - src_lang = get_language_with_fallback(detected_src_lang["language"], languages) + src_lang = next((l for l in languages if l.code == "en"), None) if src_lang is None: abort(400, description=_("%(lang)s is not supported", lang=source_lang)) - tgt_lang = get_language_with_fallback(target_lang, languages) + tgt_lang = next((l for l in languages if l.code == target_lang), None) if tgt_lang is None: abort(400, description=_("%(lang)s is not supported",lang=target_lang)) @@ -977,12 +978,12 @@ def create_app(args): if os.path.splitext(file.filename)[1] not in frontend_argos_supported_files_format: abort(400, description=_("Invalid request: file format not supported")) - src_lang = get_language_with_fallback(source_lang, languages) + src_lang = next((l for l in languages if l.code == source_lang), None) if src_lang is None and source_lang != "auto": abort(400, description=_("%(lang)s is not supported", lang=source_lang)) - tgt_lang = get_language_with_fallback(target_lang, languages) + tgt_lang = next((l for l in languages if l.code == target_lang), None) if tgt_lang is None: abort(400, description=_("%(lang)s is not supported", lang=target_lang)) diff --git a/libretranslate/language.py b/libretranslate/language.py index cc7ae24..a2f3d91 100644 --- a/libretranslate/language.py +++ b/libretranslate/language.py @@ -54,7 +54,9 @@ def get_language_with_fallback(lang_code, languages): language_variants = { 'pt': ['pb'], - 'pb': ['pt'] + 'pb': ['pt'], + 'zh': ['zt'], + 'zt': ['zh'] } fallbacks = language_variants.get(lang_code, []) diff --git a/libretranslate/tests/test_api/test_language_variant_fallback.py b/libretranslate/tests/test_api/test_language_variant_fallback.py index 5a9a58d..e9ab858 100644 --- a/libretranslate/tests/test_api/test_language_variant_fallback.py +++ b/libretranslate/tests/test_api/test_language_variant_fallback.py @@ -29,11 +29,11 @@ def client_with_pt(app_with_pt): return app_with_pt.test_client() -def test_portuguese_fallback_pb_to_pt(client_with_pt): - response = client_with_pt.post("/translate", data={ - "q": "Hello", - "source": "en", - "target": "pb", +def test_auto_detect_fallback_pt_to_pb(client_with_pb): + response = client_with_pb.post("/translate", data={ + "q": "Olá mundo", + "source": "auto", + "target": "en", "format": "text" }) @@ -43,7 +43,21 @@ def test_portuguese_fallback_pb_to_pt(client_with_pt): assert len(response_json["translatedText"]) > 0 -def test_portuguese_fallback_pt_to_pb(client_with_pb): +def test_auto_detect_fallback_pb_to_pt(client_with_pt): + response = client_with_pt.post("/translate", data={ + "q": "Olá mundo", + "source": "auto", + "target": "en", + "format": "text" + }) + + response_json = json.loads(response.data) + assert response.status_code == 200 + assert "translatedText" in response_json + assert len(response_json["translatedText"]) > 0 + + +def test_explicit_language_no_fallback(client_with_pb): response = client_with_pb.post("/translate", data={ "q": "Hello", "source": "en", @@ -52,6 +66,6 @@ def test_portuguese_fallback_pt_to_pb(client_with_pb): }) response_json = json.loads(response.data) - assert response.status_code == 200 - assert "translatedText" in response_json - assert len(response_json["translatedText"]) > 0 + assert response.status_code == 400 + assert "error" in response_json + From ecb88388eefce820539810885d01c57649e2662a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 22:57:45 +0000 Subject: [PATCH 6/7] Add Chinese variant fallback tests Co-authored-by: pierotofy <1951843+pierotofy@users.noreply.github.com> --- .../test_language_variant_fallback.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/libretranslate/tests/test_api/test_language_variant_fallback.py b/libretranslate/tests/test_api/test_language_variant_fallback.py index e9ab858..fa869c8 100644 --- a/libretranslate/tests/test_api/test_language_variant_fallback.py +++ b/libretranslate/tests/test_api/test_language_variant_fallback.py @@ -29,6 +29,30 @@ def client_with_pt(app_with_pt): return app_with_pt.test_client() +@pytest.fixture() +def app_with_zh(): + sys.argv = ['', '--load-only', 'en,zh'] + app = create_app(get_args()) + yield app + + +@pytest.fixture() +def client_with_zh(app_with_zh): + return app_with_zh.test_client() + + +@pytest.fixture() +def app_with_zt(): + sys.argv = ['', '--load-only', 'en,zt'] + app = create_app(get_args()) + yield app + + +@pytest.fixture() +def client_with_zt(app_with_zt): + return app_with_zt.test_client() + + def test_auto_detect_fallback_pt_to_pb(client_with_pb): response = client_with_pb.post("/translate", data={ "q": "Olá mundo", @@ -69,3 +93,31 @@ def test_explicit_language_no_fallback(client_with_pb): assert response.status_code == 400 assert "error" in response_json + +def test_auto_detect_fallback_zt_to_zh(client_with_zh): + response = client_with_zh.post("/translate", data={ + "q": "你好世界", + "source": "auto", + "target": "en", + "format": "text" + }) + + response_json = json.loads(response.data) + assert response.status_code == 200 + assert "translatedText" in response_json + assert len(response_json["translatedText"]) > 0 + + +def test_auto_detect_fallback_zh_to_zt(client_with_zt): + response = client_with_zt.post("/translate", data={ + "q": "你好世界", + "source": "auto", + "target": "en", + "format": "text" + }) + + response_json = json.loads(response.data) + assert response.status_code == 200 + assert "translatedText" in response_json + assert len(response_json["translatedText"]) > 0 + From 9cdf341c9c20bf64b0b6645d8cb6a3231eb8410a Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Sat, 7 Feb 2026 18:22:37 -0500 Subject: [PATCH 7/7] Remove test --- .../test_language_variant_fallback.py | 123 ------------------ 1 file changed, 123 deletions(-) delete mode 100644 libretranslate/tests/test_api/test_language_variant_fallback.py diff --git a/libretranslate/tests/test_api/test_language_variant_fallback.py b/libretranslate/tests/test_api/test_language_variant_fallback.py deleted file mode 100644 index fa869c8..0000000 --- a/libretranslate/tests/test_api/test_language_variant_fallback.py +++ /dev/null @@ -1,123 +0,0 @@ -import sys -import json -import pytest -from libretranslate.app import create_app -from libretranslate.main import get_args - - -@pytest.fixture() -def app_with_pb(): - sys.argv = ['', '--load-only', 'en,pb'] - app = create_app(get_args()) - yield app - - -@pytest.fixture() -def client_with_pb(app_with_pb): - return app_with_pb.test_client() - - -@pytest.fixture() -def app_with_pt(): - sys.argv = ['', '--load-only', 'en,pt'] - app = create_app(get_args()) - yield app - - -@pytest.fixture() -def client_with_pt(app_with_pt): - return app_with_pt.test_client() - - -@pytest.fixture() -def app_with_zh(): - sys.argv = ['', '--load-only', 'en,zh'] - app = create_app(get_args()) - yield app - - -@pytest.fixture() -def client_with_zh(app_with_zh): - return app_with_zh.test_client() - - -@pytest.fixture() -def app_with_zt(): - sys.argv = ['', '--load-only', 'en,zt'] - app = create_app(get_args()) - yield app - - -@pytest.fixture() -def client_with_zt(app_with_zt): - return app_with_zt.test_client() - - -def test_auto_detect_fallback_pt_to_pb(client_with_pb): - response = client_with_pb.post("/translate", data={ - "q": "Olá mundo", - "source": "auto", - "target": "en", - "format": "text" - }) - - response_json = json.loads(response.data) - assert response.status_code == 200 - assert "translatedText" in response_json - assert len(response_json["translatedText"]) > 0 - - -def test_auto_detect_fallback_pb_to_pt(client_with_pt): - response = client_with_pt.post("/translate", data={ - "q": "Olá mundo", - "source": "auto", - "target": "en", - "format": "text" - }) - - response_json = json.loads(response.data) - assert response.status_code == 200 - assert "translatedText" in response_json - assert len(response_json["translatedText"]) > 0 - - -def test_explicit_language_no_fallback(client_with_pb): - response = client_with_pb.post("/translate", data={ - "q": "Hello", - "source": "en", - "target": "pt", - "format": "text" - }) - - response_json = json.loads(response.data) - assert response.status_code == 400 - assert "error" in response_json - - -def test_auto_detect_fallback_zt_to_zh(client_with_zh): - response = client_with_zh.post("/translate", data={ - "q": "你好世界", - "source": "auto", - "target": "en", - "format": "text" - }) - - response_json = json.loads(response.data) - assert response.status_code == 200 - assert "translatedText" in response_json - assert len(response_json["translatedText"]) > 0 - - -def test_auto_detect_fallback_zh_to_zt(client_with_zt): - response = client_with_zt.post("/translate", data={ - "q": "你好世界", - "source": "auto", - "target": "en", - "format": "text" - }) - - response_json = json.loads(response.data) - assert response.status_code == 200 - assert "translatedText" in response_json - assert len(response_json["translatedText"]) > 0 -