From 51516697f0642cb601749a0ec0b47e5d7077adbd Mon Sep 17 00:00:00 2001 From: tamnd <1218621+tamnd@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:09:21 +0700 Subject: [PATCH] Copy a code entry before asking whether a person signed it apply asked the human guard first and the code copy second. The block rule learning to read `def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):` was enough to show what that costs. Four segments left the memory, because a person's copy of a line of code is not a translation of it. The five entries in the corpus stayed exactly as they were, translated and not fuzzy and now accounted for by nothing, and S04 reported all five. S04 was right. An unmarked non-fuzzy string reads to a reviewer and to Sphinx as somebody's considered work, and there was nothing behind these. The guard was holding in place a mark that had lost its meaning, and there was no way to clear the finding short of a corpus-wide --refuzzy, which would have to be run again after the next classifier change. The guard exists to stop a machine translation landing on a person's work. Copying the msgid is not a machine translation, it is the source text, and P07 requires the entry to hold it byte for byte. The guard is not engaged here, and putting it first only meant a writer could leave its own checker with findings it had no way to clear. Measured before the swap: 5 entries in the corpus are human-marked and classified as code, and all 5 already hold the msgid byte for byte. The reordering changes no translated text and adds five provenance comments saying where the text came from. The guard is where it always was for everything that is not code, which is 81 964 of the 87 008 entries. --- pyproject.toml | 2 +- src/pydocvi/apply.py | 40 ++++++++++++++++++++++++++++------------ tests/test_apply.py | 32 +++++++++++++++++++++----------- uv.lock | 2 +- 4 files changed, 51 insertions(+), 25 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bfbf159..fceea1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-docs-vi-translator" -version = "0.1.7" +version = "0.1.8" description = "Translates the CPython documentation into Vietnamese gettext catalogs, with a deterministic audit over every string it writes" readme = "README.md" license = "MIT" diff --git a/src/pydocvi/apply.py b/src/pydocvi/apply.py index 8d73db2..d8dfeee 100644 --- a/src/pydocvi/apply.py +++ b/src/pydocvi/apply.py @@ -269,8 +269,8 @@ def apply_entry( pipeline failing until the memory caught up with the catalog. What matters is that no machine string ever lands on top of a human one, and none does. - A code entry is copied whatever the memory holds for it, which is a rule - about the memory being wrong rather than empty. ``P07`` says a doctest is + A code entry is copied before any of that is asked, whatever the memory + holds for it and whatever the file holds for it. ``P07`` says a doctest is byte-identical to its ``msgid``, and until this clause existed the only thing standing behind that was nothing having queued one: :func:`batch` filters non-prose out, so a segment for a code entry could only come from a run made @@ -281,14 +281,30 @@ def apply_entry( Refusing it here rather than pruning the memory is what makes the guarantee hold going forward. The classifier will get stricter again, that is the - direction it has moved twice, and each time it does the memory acquires - another handful of translations of things that turned out to be code. What - they are worth is decided by what the string is now, not by what some earlier - run thought when it asked. - - The human guard still comes first, so this cannot quietly replace a reviewed - string with the English. Getting past that is ``--refuzzy`` and nothing else, - which is the same answer as everywhere else in this module. + direction it has moved three times now, and each time it does both the memory + and the corpus acquire another handful of translations of things that turned + out to be code. What they are worth is decided by what the string is now, not + by what some earlier run thought when it asked. + + Which is why the copy is asked before the human guard and not after it. It + was after it first, on the reasoning that a reviewer's mark outranks + everything, and one classifier change was enough to show what that costs. The + block rule learned to read ``def f(pos1, pos2, /, pos_or_kwd, *, kwd1, + kwd2):``, four segments left the memory because a person's copy of a line of + code is not a translation of it, and the five entries in the corpus stayed + exactly where they were: translated, not fuzzy, and now accounted for by + nothing. ``S04`` reported all five, correctly, because that is what an + unmarked non-fuzzy string is. The guard was holding a string in place that no + longer had anything behind it. + + The guard exists to stop a machine translation landing on a person's work. + Copying the ``msgid`` is not a machine translation, it is the source text, + and ``P07`` requires it. So the guard is not engaged here and putting it + first only meant that a writer could leave its own checker with findings it + had no way to clear. Measured before the swap: 5 entries in the corpus are + human-marked and classified as code, and all 5 already hold the ``msgid`` + byte for byte. The reordering changes no translated text at all, and adds + five provenance comments saying where the text came from. An entry the memory has nothing for is either prose nobody has translated yet, which is left as upstream had it, or a no-op, which is copied through @@ -304,10 +320,10 @@ def apply_entry( ``refuzzy`` is the one way to get past that guard, and it is off by default. See :func:`_contradicted` for what it does and why it exists. """ - if existing is not None and is_human(existing) and not _contradicted(segment, refuzzy): - return existing if classify.classify(upstream.msgid).code: return _copied(upstream) + if existing is not None and is_human(existing) and not _contradicted(segment, refuzzy): + return existing if segment is None or not segment.msgstr: return _copied(upstream) if not _translatable(upstream) else _untranslated(upstream) if segment.source == "human": diff --git a/tests/test_apply.py b/tests/test_apply.py index 38ff39d..7a9af97 100644 --- a/tests/test_apply.py +++ b/tests/test_apply.py @@ -188,22 +188,32 @@ def test_a_segment_for_a_code_entry_is_refused(self) -> None: assert entry.msgstr == code assert entry.comments[-1] == "# pydocvi: passthrough=literal_block" - def test_refusing_it_does_not_reach_past_the_reviewer(self) -> None: - """The human guard comes first, so this cannot quietly replace a reviewed - string with the English. ``--refuzzy`` is the only way past it, here as - everywhere else in this module.""" + def test_the_copy_reaches_past_the_reviewer_too(self) -> None: + """This assertion was the other way round first, on the reasoning that a + reviewer's mark outranks everything. One classifier change was enough to + show what that costs: four segments left the memory, five entries stayed + in the corpus translated and not fuzzy with nothing behind them, and + ``S04`` reported all five and had no way to be cleared. + + The guard exists to stop a machine translation landing on a person's + work. The ``msgid`` is not a machine translation, it is the source text, + and ``P07`` requires the entry to hold it byte for byte.""" code = ">>> n # try it\nNameError" source = upstream(block(code.replace("\n", "\\n"))) existing = upstream(block(code.replace("\n", "\\n"), "đã duyệt")) out = apply.apply_catalog(source, existing, Memory(), stamp=STAMP)[0] - assert entry_of(out, code).msgstr == "đã duyệt" + entry = entry_of(out, code) + assert entry.msgstr == code + assert entry.comments[-1] == "# pydocvi: passthrough=doctest" - def test_refuzzy_reaches_it(self) -> None: - code = ">>> n # try it\nNameError" - source = upstream(block(code.replace("\n", "\\n"))) - existing = upstream(block(code.replace("\n", "\\n"), "đã duyệt")) - out = apply.apply_catalog(source, existing, Memory(), stamp=STAMP, refuzzy=True)[0] - assert entry_of(out, code).msgstr == code + def test_a_reviewed_prose_string_is_still_untouchable(self) -> None: + """What the reordering did not change, and the reason to write it down + next to what it did. The guard is where it always was for everything that + is not code, which is 81 964 of the 87 008 entries.""" + source = upstream(block("Return a sorted list.")) + existing = upstream(block("Return a sorted list.", "Trả về một danh sách.")) + out = apply.apply_catalog(source, existing, Memory(), stamp=STAMP)[0] + assert entry_of(out, "Return a sorted list.").msgstr == "Trả về một danh sách." def test_a_copy_is_not_counted_as_work_done(self) -> None: """Spec 12 §5: a no-op is neither translated nor outstanding. Folding diff --git a/uv.lock b/uv.lock index 590b218..b9177c3 100644 --- a/uv.lock +++ b/uv.lock @@ -678,7 +678,7 @@ wheels = [ [[package]] name = "python-docs-vi-translator" -version = "0.1.7" +version = "0.1.8" source = { editable = "." } dependencies = [ { name = "httpx" },