Skip to content

Commit 73c1ebb

Browse files
committed
ci: タイムスタンプだけの更新を取り込まない
1 parent ebdf588 commit 73c1ebb

3 files changed

Lines changed: 154 additions & 1 deletion

File tree

‎.github/workflows/update.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ jobs:
4949
sudo apt-get update -y && sudo apt-get install gettext -y
5050
pip install --upgrade pip
5151
pip install -r requirements.txt -r cpython/Doc/requirements.txt
52+
- name: Test translation tooling
53+
run: python -B -m unittest discover -s tests
5254
- name: Update translations
5355
run: |
5456
sh scripts/update.sh
@@ -58,6 +60,8 @@ jobs:
5860
- name: Wrap catalog message files
5961
run: |
6062
powrap --modified
63+
- name: Ignore timestamp-only changes
64+
run: python scripts/translations.py restore-timestamp-only .
6165
- name: Commit and push changes
6266
if: github.repository == 'python/python-docs-ja'
6367
run: |
@@ -113,6 +117,8 @@ jobs:
113117
- name: Wrap catalog message files
114118
run: |
115119
powrap --modified -C "${{ matrix.branch }}"
120+
- name: Ignore timestamp-only changes
121+
run: python "${{ env.CPYTHON_BRANCH }}/scripts/translations.py" restore-timestamp-only "${{ matrix.branch }}"
116122

117123
- name: Commit and push changes
118124
if: github.repository == 'python/python-docs-ja'

‎scripts/translations.py‎

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,55 @@
44
import configparser
55
import os
66
from pathlib import Path, PurePosixPath
7+
import re
78
import shutil
89
import subprocess
910

1011

12+
def without_po_timestamps(content):
13+
"""Ignore date fields in the PO header, preserving every other byte."""
14+
header = re.search(
15+
rb'^msgid ""\r?\nmsgstr "[^\r\n]*"\r?\n(?:"[^\r\n]*"\r?\n)*',
16+
content, re.MULTILINE,
17+
)
18+
if header is None:
19+
return content
20+
normalized = re.sub(
21+
rb'^"(?:POT-Creation-Date|PO-Revision-Date):[^"\r\n]*\\n"\r?\n',
22+
b'', header.group(), flags=re.MULTILINE,
23+
)
24+
return content[:header.start()] + normalized + content[header.end():]
25+
26+
27+
def restore_timestamp_only_changes(root):
28+
"""Restore PO files whose unstaged changes only affect header dates.
29+
30+
Run after wrapping, so formatting differences in downloaded or merged files
31+
do not prevent timestamp-only changes from being recognized.
32+
"""
33+
result = subprocess.run(
34+
["git", "-C", str(root), "diff", "--name-only", "-z", "--", "*.po"],
35+
check=True, stdout=subprocess.PIPE,
36+
)
37+
for name in result.stdout.split(b"\0"):
38+
if not name:
39+
continue
40+
path = os.fsdecode(name)
41+
target = root / path
42+
if not target.is_file():
43+
continue
44+
original = subprocess.run(
45+
["git", "-C", str(root), "show", f":{path}"],
46+
check=True, stdout=subprocess.PIPE,
47+
).stdout
48+
if (
49+
without_po_timestamps(target.read_bytes())
50+
== without_po_timestamps(original)
51+
):
52+
target.write_bytes(original)
53+
print(f"Ignored timestamp-only change: {path}")
54+
55+
1156
def active_catalogs(config_path):
1257
config = configparser.ConfigParser(interpolation=None)
1358
with config_path.open(encoding="utf-8") as stream:
@@ -89,11 +134,15 @@ def main():
89134
merge_parser = commands.add_parser("merge")
90135
merge_parser.add_argument("source", type=Path)
91136
merge_parser.add_argument("destination", type=Path)
137+
restore_parser = commands.add_parser("restore-timestamp-only")
138+
restore_parser.add_argument("root", type=Path)
92139
args = parser.parse_args()
93140
if args.command == "sync":
94141
sync(args.source, args.destination, args.language)
95-
else:
142+
elif args.command == "merge":
96143
merge(args.source, args.destination)
144+
else:
145+
restore_timestamp_only_changes(args.root)
97146

98147

99148
if __name__ == "__main__":

‎tests/test_translations.py‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,21 @@ def write_po(root, name, translation):
3131
return path
3232

3333

34+
DATED_PO = (
35+
'# Catalog\n'
36+
'msgid ""\n'
37+
'msgstr ""\n'
38+
'"Content-Type: text/plain; charset=UTF-8\\n"\n'
39+
'"POT-Creation-Date: 2026-09-29 18:58+0000\\n"\n'
40+
'"PO-Revision-Date: 2025-09-16 00:01+0000\\n"\n'
41+
'"Last-Translator: Translator\\n"\n'
42+
'\n'
43+
'#: example.rst:10\n'
44+
'msgid "example"\n'
45+
'msgstr "翻訳"\n'
46+
).encode('utf-8')
47+
48+
3449
class TranslationTests(unittest.TestCase):
3550
def setUp(self):
3651
temporary = tempfile.TemporaryDirectory()
@@ -49,6 +64,89 @@ def track(self):
4964
["git", "-C", str(self.destination), "add", "."], check=True
5065
)
5166

67+
def restore_catalog(self, before, incoming):
68+
target = self.destination / "example.po"
69+
target.write_bytes(before)
70+
self.track()
71+
target.write_bytes(incoming)
72+
translations.restore_timestamp_only_changes(self.destination)
73+
return target
74+
75+
def test_restore_timestamp_only_changes(self):
76+
for newline in (b'\n', b'\r\n'):
77+
before = DATED_PO.replace(b'\n', newline)
78+
changes = [
79+
before,
80+
before.replace(b'2026-09-29', b'2026-10-19'),
81+
before.replace(b'2025-09-16', b'2026-10-20'),
82+
before.replace(b'2026-09-29', b'2026-10-19').replace(
83+
b'2025-09-16', b'2026-10-20'
84+
),
85+
]
86+
for incoming in changes:
87+
with self.subTest(newline=newline, incoming=incoming):
88+
target = self.restore_catalog(before, incoming)
89+
self.assertEqual(target.read_bytes(), before)
90+
91+
def test_restore_keeps_other_changes_and_their_new_timestamps(self):
92+
updated = DATED_PO.replace(b'2026-09-29', b'2026-10-19')
93+
replacements = [
94+
('msgstr "翻訳"', 'msgstr "新しい訳"'),
95+
('msgid "example"', 'msgid "changed"'),
96+
('#: example.rst:10', '#: example.rst:20'),
97+
('# Catalog', '# Updated comment'),
98+
('msgid "example"', '#, fuzzy\nmsgid "example"'),
99+
('Last-Translator: Translator', 'Last-Translator: New Translator'),
100+
('msgstr "翻訳"', 'msgstr ""\n"翻訳"'),
101+
]
102+
for old, new in replacements:
103+
with self.subTest(change=new):
104+
incoming = updated.replace(old.encode(), new.encode())
105+
target = self.restore_catalog(DATED_PO, incoming)
106+
self.assertEqual(target.read_bytes(), incoming)
107+
108+
def test_restore_does_not_ignore_timestamp_text_in_translation(self):
109+
before = DATED_PO.replace(
110+
'msgstr "翻訳"\n'.encode(),
111+
b'msgstr ""\n"POT-Creation-Date: 2026-09-29 18:58+0000\\n"\n',
112+
)
113+
incoming = before.replace(b'2026-09-29', b'2026-10-19')
114+
target = self.restore_catalog(before, incoming)
115+
self.assertEqual(target.read_bytes(), incoming)
116+
117+
def test_restore_preserves_new_files_and_deletions(self):
118+
deleted = self.destination / "deleted.po"
119+
deleted.write_bytes(DATED_PO)
120+
self.track()
121+
deleted.unlink()
122+
new = self.destination / "new.po"
123+
new.write_bytes(DATED_PO)
124+
translations.restore_timestamp_only_changes(self.destination)
125+
self.assertFalse(deleted.exists())
126+
self.assertEqual(new.read_bytes(), DATED_PO)
127+
128+
def test_restore_command_handles_multiple_files_and_explicit_root(self):
129+
catalogs = ["unchanged.po", "translated.po", "library/with space.po"]
130+
for path in catalogs:
131+
target = self.destination / path
132+
target.parent.mkdir(parents=True, exist_ok=True)
133+
target.write_bytes(DATED_PO)
134+
self.track()
135+
updated = DATED_PO.replace(b'2026-09-29', b'2026-10-19')
136+
translated = updated.replace('翻訳'.encode(), '新しい訳'.encode())
137+
(self.destination / "unchanged.po").write_bytes(updated)
138+
(self.destination / "translated.po").write_bytes(translated)
139+
(self.destination / "library/with space.po").write_bytes(updated)
140+
subprocess.run(
141+
[sys.executable, translations.__file__, "restore-timestamp-only",
142+
str(self.destination)],
143+
cwd=self.source, check=True, stdout=subprocess.PIPE,
144+
)
145+
146+
self.assertEqual((self.destination / "unchanged.po").read_bytes(), DATED_PO)
147+
self.assertEqual((self.destination / "translated.po").read_bytes(), translated)
148+
self.assertEqual((self.destination / "library/with space.po").read_bytes(), DATED_PO)
149+
52150
def test_sync_removes_old_path_and_preserves_untracked_files(self):
53151
catalogs = ["builtins/functions.po", "library/builtins.po", "bugs.po"]
54152
write_config(self.source, catalogs)

0 commit comments

Comments
 (0)