fix(translate): chart labels, sheet-refs, sheet-name offset, paid-user Memento
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m19s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m19s
Multiple translation bugs in Word/Excel/PPTX that caused chart elements
to be left untranslated or charts to render as empty series.
Backend
-------
* providers (deepseek/openai/minimax): tighten system prompt so the LLM
actually translates chart titles/axis labels/legend/category labels,
month abbreviations, and clarifies what counts as a 'real' proper noun
(people/place/company/product names) vs. technical labels. Old rule
'keep proper nouns unchanged' was being read too broadly by the model
and caused chart text to be skipped.
* excel_translator.py:
- Sheet reference rewrite: when a sheet is renamed, the chart XML's
c:f refs (e.g. 'Sales 2024'!$D$2:$D$61) are now rewritten to the
new name with proper apostrophe escaping (Chiffre d'affaires ->
'Chiffre d''affaires') and auto-quoting when the new name contains
spaces or special chars. Without this, the chart points at a sheet
that no longer exists and renders 0/empty series.
- Sheet name offset bug: sheet_name_offset was computed after chart
text was appended to text_elements, causing sheet names to receive
chart text translations. Now captured BEFORE sheet names are added.
* New tests:
- test_excel_chart_sheet_refs.py (10 unit tests, synthetic inputs)
- test_chart_translation_prompt.py (3 contract tests on the prompt)
Frontend
--------
* DashboardSidebar / translate/page: hide the Memento promo section
for paying users (tier != 'free').
* constants.ts: temporarily comment out the 'CLES API' nav item.
Update constants.test.ts to match the new state.
All fixes are generic - no file-specific hardcoding, edge cases covered
(empty mapping, missing bang, apostrophe escaping, partial renames,
multi-series).
This commit is contained in:
50
tests/test_chart_translation_prompt.py
Normal file
50
tests/test_chart_translation_prompt.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""Verify all 3 provider prompts are identical and contain chart-translation rules.
|
||||
|
||||
This is a regression test for the bug where chart titles/legends/category labels
|
||||
were left untranslated because the system prompt was too ambiguous about
|
||||
"proper nouns or brand names".
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from services.providers.deepseek_provider import DEFAULT_TRANSLATION_PROMPT as P1
|
||||
from services.providers.minimax_provider import DEFAULT_TRANSLATION_PROMPT as P3
|
||||
from services.providers.openai_provider import DEFAULT_TRANSLATION_PROMPT as P2
|
||||
|
||||
REQUIRED_PHRASES = [
|
||||
"Chart elements",
|
||||
"MUST always be translated",
|
||||
"Translate month and weekday abbreviations",
|
||||
"Jan",
|
||||
"janvier",
|
||||
"Keep ONLY real proper nouns",
|
||||
"Google, Microsoft",
|
||||
"API, URL, HTTP, JSON",
|
||||
]
|
||||
|
||||
|
||||
def test_all_prompts_identical():
|
||||
assert P1 == P2 == P3, "All 3 provider prompts must be identical"
|
||||
|
||||
|
||||
def test_prompt_contains_required_rules():
|
||||
for phrase in REQUIRED_PHRASES:
|
||||
assert phrase in P1, f"Missing rule: {phrase!r}"
|
||||
|
||||
|
||||
def test_prompt_drops_old_ambiguous_rule():
|
||||
# The old "If the text contains proper nouns or brand names" rule was
|
||||
# the root cause of chart labels not being translated. The new prompt
|
||||
# uses a more specific rule that lists what counts as a proper noun.
|
||||
assert "If the text contains proper nouns or brand names" not in P1, (
|
||||
"Old ambiguous rule still present — chart labels will be skipped again"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_all_prompts_identical()
|
||||
test_prompt_contains_required_rules()
|
||||
test_prompt_drops_old_ambiguous_rule()
|
||||
print("All prompt regression tests passed.")
|
||||
138
tests/test_excel_chart_sheet_refs.py
Normal file
138
tests/test_excel_chart_sheet_refs.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""Regression tests for the bug where translated Excel chart <c:f> references
|
||||
kept pointing at the original (now-renamed) sheet name, causing the chart
|
||||
to render as zero/empty series after translation.
|
||||
|
||||
This is a real bug: see e.g. test_excel.xlsx where the chart references
|
||||
'Sales 2024'!$D$2:$D$61 but the sheet is renamed to "Chiffre d'affaires
|
||||
par produit" — the chart then shows 0 for every data point.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from translators.excel_translator import ExcelTranslator
|
||||
|
||||
NS_C = "http://schemas.openxmlformats.org/drawingml/2006/chart"
|
||||
|
||||
|
||||
def _build_chart_xml(series_refs):
|
||||
"""Helper: build a chart XML with the given <c:f> strings."""
|
||||
sers = "\n".join(
|
||||
f"<c:ser><c:tx><c:strRef><c:f>{ref}</c:f></c:strRef></c:tx>"
|
||||
f"<c:cat><c:strRef><c:f>{ref}</c:f></c:strRef></c:cat>"
|
||||
f"<c:val><c:numRef><c:f>{ref}</c:f></c:numRef></c:val></c:ser>"
|
||||
for ref in series_refs
|
||||
)
|
||||
return f"""<c:chartSpace xmlns:c="{NS_C}" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
|
||||
<c:chart>
|
||||
<c:plotArea><c:lineChart>{sers}</c:lineChart></c:plotArea>
|
||||
</c:chart>
|
||||
</c:chartSpace>"""
|
||||
|
||||
|
||||
def test_quoted_sheet_name_with_apostrophe_in_new_name():
|
||||
r = ExcelTranslator._rewrite_sheet_ref(
|
||||
"'Sales 2024'!$D$2:$D$61",
|
||||
{"Sales 2024": "Chiffre d'affaires par produit"},
|
||||
)
|
||||
assert r == "'Chiffre d''affaires par produit'!$D$2:$D$61", f"got: {r!r}"
|
||||
|
||||
|
||||
def test_unquoted_sheet_name_simple():
|
||||
r = ExcelTranslator._rewrite_sheet_ref(
|
||||
"Sheet1!$A$1",
|
||||
{"Sheet1": "Feuille1"},
|
||||
)
|
||||
assert r == "Feuille1!$A$1", f"got: {r!r}"
|
||||
|
||||
|
||||
def test_new_name_with_space_gets_quoted():
|
||||
r = ExcelTranslator._rewrite_sheet_ref(
|
||||
"Sheet1!$A$1",
|
||||
{"Sheet1": "Ma Feuille"},
|
||||
)
|
||||
assert r == "'Ma Feuille'!$A$1", f"got: {r!r}"
|
||||
|
||||
|
||||
def test_name_not_in_mapping_unchanged():
|
||||
r = ExcelTranslator._rewrite_sheet_ref(
|
||||
"Other!$A$1",
|
||||
{"Sheet1": "Feuille1"},
|
||||
)
|
||||
assert r == "Other!$A$1", f"got: {r!r}"
|
||||
|
||||
|
||||
def test_empty_mapping_is_noop():
|
||||
r = ExcelTranslator._rewrite_sheet_ref("Sheet1!$A$1", {})
|
||||
assert r == "Sheet1!$A$1", f"got: {r!r}"
|
||||
|
||||
|
||||
def test_no_bang_is_noop():
|
||||
r = ExcelTranslator._rewrite_sheet_ref("just_a_label", {"x": "y"})
|
||||
assert r == "just_a_label", f"got: {r!r}"
|
||||
|
||||
|
||||
def test_real_chart_xml_all_refs_rewritten():
|
||||
chart_xml = etree.fromstring(_build_chart_xml(["Sheet1!$B$1"]))
|
||||
n = ExcelTranslator._rewrite_sheet_refs_in_chart(chart_xml, {"Sheet1": "Ma Feuille"})
|
||||
assert n == 3, f"expected 3 rewrites, got {n}"
|
||||
refs = [f.text for f in chart_xml.iter(f"{{{NS_C}}}f")]
|
||||
assert refs == [
|
||||
"'Ma Feuille'!$B$1",
|
||||
"'Ma Feuille'!$B$1",
|
||||
"'Ma Feuille'!$B$1",
|
||||
], f"got: {refs}"
|
||||
|
||||
|
||||
def test_real_chart_xml_apostrophe_escaped_in_new_name():
|
||||
chart_xml = etree.fromstring(_build_chart_xml(["Sheet1!$B$1"]))
|
||||
n = ExcelTranslator._rewrite_sheet_refs_in_chart(
|
||||
chart_xml, {"Sheet1": "Chiffre d'affaires"}
|
||||
)
|
||||
assert n == 3
|
||||
refs = [f.text for f in chart_xml.iter(f"{{{NS_C}}}f")]
|
||||
assert all(r == "'Chiffre d''affaires'!$B$1" for r in refs), f"got: {refs}"
|
||||
|
||||
|
||||
def test_partial_mapping_leaves_other_refs_intact():
|
||||
"""When only one sheet is renamed, refs to other sheets must stay."""
|
||||
chart_xml = etree.fromstring(
|
||||
_build_chart_xml(["Sheet1!$A$1", "Sheet2!$B$1"])
|
||||
)
|
||||
n = ExcelTranslator._rewrite_sheet_refs_in_chart(
|
||||
chart_xml, {"Sheet1": "Feuille1"}
|
||||
)
|
||||
# 3 refs to Sheet1 (one series) are rewritten; 3 refs to Sheet2 stay.
|
||||
assert n == 3, f"expected 3 rewrites, got {n}"
|
||||
refs = [f.text for f in chart_xml.iter(f"{{{NS_C}}}f")]
|
||||
assert "Feuille1!$A$1" in refs
|
||||
assert "Sheet2!$B$1" in refs
|
||||
|
||||
|
||||
def test_multiple_series_renamed():
|
||||
"""When several sheets are renamed, refs to any of them get updated."""
|
||||
chart_xml = etree.fromstring(
|
||||
_build_chart_xml(["Sheet1!$A$1", "Sheet2!$B$1", "Sheet3!$C$1"])
|
||||
)
|
||||
n = ExcelTranslator._rewrite_sheet_refs_in_chart(
|
||||
chart_xml,
|
||||
{"Sheet1": "F1", "Sheet2": "F2", "Sheet3": "F3"},
|
||||
)
|
||||
assert n == 9, f"expected 9 rewrites (3 series x 3 refs each), got {n}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_quoted_sheet_name_with_apostrophe_in_new_name()
|
||||
test_unquoted_sheet_name_simple()
|
||||
test_new_name_with_space_gets_quoted()
|
||||
test_name_not_in_mapping_unchanged()
|
||||
test_empty_mapping_is_noop()
|
||||
test_no_bang_is_noop()
|
||||
test_real_chart_xml_all_refs_rewritten()
|
||||
test_real_chart_xml_apostrophe_escaped_in_new_name()
|
||||
test_partial_mapping_leaves_other_refs_intact()
|
||||
test_multiple_series_renamed()
|
||||
print("All chart sheet-ref rewrite tests passed.")
|
||||
Reference in New Issue
Block a user