Skip to content

Commit ca1efe3

Browse files
committed
Preserve translation formatting in code-block reviews
1 parent 646ea7e commit ca1efe3

3 files changed

Lines changed: 30 additions & 47 deletions

File tree

‎AGENTS.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- 用語集の `translation_ja` を採用する訳語の基準とし、`pos` と `notes` で品詞や適用する文脈を確認してください。訳語が空欄の項目は、既存訳から用例を探してください。
66
- 重要な用語は既存 PO から用例を検索してください。検索対象は `.tx/config` に載っている現行 PO を基本とし、未翻訳・fuzzy・obsolete のエントリーを訳例として採用しないでください。
77
- 明示された用語の決定を優先し、文体は対象節の既存訳に合わせてください。
8+
- 英語の文頭の大文字をそのまま引き継がず、一般概念と識別子を文脈に応じて区別してください。例えば数学的な set は「集合」、Python の型を指す Set objects は「``set`` オブジェクト」とします。識別子の大文字・小文字は実際の綴りに合わせてください。
89
- 関数・メソッドの説明では、直前のシグネチャも確認し、一般用語と仮引数名を区別してください。仮引数名は翻訳せず、原文の識別子を保持してください。原文でマークアップされていなくても仮引数名を指す場合は、ダブルバッククォートで囲んでください。
910
- 既存訳が食い違う場合は黙って統一せず、レビュー資料に候補と判断理由を記載してください。
1011
- 参照数を原文に合わせるために `:mod:`、`:class:`、`:func:`、`:meth:`、`:const:` などのコード要素の参照を外す場合は、名前をダブルバッククォートで囲むインラインリテラルにしてください。関数名・メソッド名には `()` を付けてください。
@@ -15,3 +16,5 @@
1516
- 理由・条件などの説明は、主語と述語の間に挟まず、文頭に置くことを検討してください。
1617
- 読点は意味のまとまりを区切るために使い、主語の直後に機械的に打たないでください。
1718
- 読点で読みやすくしようとする前に、語順を見直してください。
19+
- 翻訳文自体に句点の後で改行を入れてください。長い文では読点の後でも改行し、意味のまとまりを読み取りやすくしてください。改行直後に不要なスペースを残さないでください。
20+
- reStructuredText のインラインマークアップやコード例の途中は改行せず、コードブロックのインデントや文末の「。 ::」など、構文上必要なまとまりは保持してください。

‎scripts/po_review.py‎

Lines changed: 4 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -58,44 +58,11 @@ def escape(text):
5858
return re.sub(r"([\\`*_\[\]<>#|~])", r"\\\1", text.replace("&", "&amp;"))
5959

6060

61-
INLINE_RST = re.compile(
62-
r"(?<![\\`])(?::[\w.-]+(?::[\w.-]+)*:)?"
63-
r"(?P<ticks>`{1,2})(?!`)(?:\\.|[^`\\\n])+(?P=ticks)(?!`)"
64-
)
65-
66-
67-
def inline_text(text):
68-
# Preserve the whole reST token in a Markdown code span, including its
69-
# role and backticks. This avoids backslash noise in the Markdown source.
70-
parts = []
71-
start = 0
72-
for match in INLINE_RST.finditer(text):
73-
parts.append(escape(text[start:match.start()]))
74-
token = match.group()
75-
fence = "`" * (1 + max(len(run) for run in re.findall(r"`+", token)))
76-
parts.append(f"{fence} {token} {fence}")
77-
start = match.end()
78-
parts.append(escape(text[start:]))
79-
result = "".join(parts)
80-
# Prevent prose lines from becoming Markdown lists or thematic breaks.
81-
return re.sub(r"^(\s*)([-+]|\d+[.)])(?=\s|$)",
82-
lambda match: match[1] + match[2][:-1] + "\\" + match[2][-1], result)
83-
84-
85-
def paragraph(text):
86-
return "\n".join(
87-
"> " + inline_text(line) + (" " if line else "")
88-
for line in text.split("\n")
89-
)
90-
91-
9261
def field(label, text):
93-
# Keep indentation and alignment in code examples, while allowing prose
94-
# to wrap naturally in the Markdown viewer.
95-
if text.startswith((">>> ", ".. code-block::", ".. code::")):
96-
fence = "`" * max(3, 1 + max((len(run) for run in re.findall(r"`+", text)), default=0))
97-
return f"**{label}**\n\n{fence}text\n{text}\n{fence}\n"
98-
return f"**{label}**\n\n{paragraph(text)}\n"
62+
# Preserve PO text verbatim, including line breaks and reST markup.
63+
# Embedded backticks must not be able to close the Markdown fence.
64+
fence = "`" * max(3, 1 + max((len(run) for run in re.findall(r"`+", text)), default=0))
65+
return f"**{label}**\n\n{fence}rst\n{text}\n{fence}\n"
9966

10067

10168
def translation(entry):

‎tests/test_po_review.py‎

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ def test_plural_fuzzy_and_empty_translations(self):
4646
msgstr_plural={0: "ファイル", 1: ""})
4747
text = po_review.render_entry("test.po", old, new, 1, "test.po")
4848
self.assertIn("Many files", text)
49-
self.assertIn(r"msgstr\[1\]: 複数のファイル", text)
50-
self.assertIn(r"msgstr\[1\]: (未翻訳)", text)
49+
self.assertIn("msgstr[1]: 複数のファイル", text)
50+
self.assertIn("msgstr[1]: (未翻訳)", text)
5151
self.assertIn("fuzzy(要確認) → 未翻訳", text)
5252
new.msgstr_plural[1] = old.msgstr_plural[1]
5353
self.assertNotEqual(po_review.signature(old), po_review.signature(new))
@@ -63,17 +63,18 @@ def test_new_removed_and_obsolete_entries(self):
6363

6464
def test_markdown_and_html_are_literal(self):
6565
text = ':func:`open` <tag> [link](url) **bold**\n\n```\nnext'
66-
result = po_review.paragraph(text)
67-
self.assertIn(r"`` :func:`open` `` \<tag\> \[link\](url) \*\*bold\*\*", result)
68-
self.assertIn("> \n> \\`\\`\\`", result)
69-
self.assertEqual("> &amp;copy; ", po_review.paragraph("&copy;"))
66+
self.assertEqual(
67+
"**変更後**\n\n````rst\n" + text + "\n````\n",
68+
po_review.field("変更後", text),
69+
)
70+
self.assertEqual("**訳文**\n\n```rst\n&copy;\n```\n",
71+
po_review.field("訳文", "&copy;"))
7072

7173
def test_rst_roles_and_literals_do_not_need_backslash_escapes(self):
7274
text = ':ref:`tut-object` と :py:class:`表示名 <module.Class>`、``a_b``。'
73-
result = po_review.paragraph(text)
75+
result = po_review.field("訳文", text)
7476
self.assertEqual(
75-
'> `` :ref:`tut-object` `` と `` :py:class:`表示名 <module.Class>` ``、'
76-
'``` ``a_b`` ```。 ', result,
77+
'**訳文**\n\n```rst\n' + text + '\n```\n', result,
7778
)
7879
self.assertNotIn("\\", result)
7980
self.assertEqual("../../tutorial/classes.rst:422 (source)",
@@ -82,7 +83,19 @@ def test_rst_roles_and_literals_do_not_need_backslash_escapes(self):
8283
def test_code_examples_keep_indentation_and_cannot_close_fence(self):
8384
code = '>>> if True:\n... print("````")\n````'
8485
result = po_review.field("原文", code)
85-
self.assertIn("`````text\n" + code + "\n`````", result)
86+
self.assertIn("`````rst\n" + code + "\n`````", result)
87+
88+
def test_prose_line_breaks_and_spaces_are_preserved(self):
89+
for text in (
90+
'一文目です。 続く長い文、さらに続きます。',
91+
'一文目です。\n続く長い文、\nさらに続きます。 ::\n',
92+
' 先頭の空白。\n末尾の空白。 ',
93+
):
94+
with self.subTest(text=text):
95+
self.assertEqual(
96+
'**訳文**\n\n```rst\n' + text + '\n```\n',
97+
po_review.field("訳文", text),
98+
)
8699

87100

88101
class GitReportTests(unittest.TestCase):

0 commit comments

Comments
 (0)