FazBrowse GitHub Viewer
|
Trending
|
URL:
|
Home
Tools:
[Download Repo ZIP]
[View Raw Code]
[Original HTTPS Page]
python-docs-ja/scripts/po_review.py at 3.15 · python/python-docs-ja · GitHub
python
python-docs-ja
Repository navigation
Code
Issues
1
(1)
Pull requests
Actions
Security and quality
Insights
Expand file tree
Breadcrumbs
python-docs-ja
/
scripts
/
po_review.py
Copy path
More file actions
More file actions
Latest commit
History
History
History
235 lines (203 loc) · 9.13 KB
Breadcrumbs
python-docs-ja
/
scripts
/
po_review.py
Copy path
File metadata and controls
235 lines (203 loc) · 9.13 KB
Raw
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
"""Render local PO translation changes as a Markdown review report."""
import
argparse
import
hashlib
import
json
import
os
from
pathlib
import
Path
import
re
import
subprocess
import
sys
from
urllib
.
parse
import
quote
import
polib
def
git
(
root
,
*
args
):
return
subprocess
.
run
(
[
"git"
,
"-C"
,
str
(
root
),
*
args
],
check
=
True
,
stdout
=
subprocess
.
PIPE
,
stderr
=
subprocess
.
PIPE
,
).
stdout
def
entries
(
data
):
if
data
is
None
:
return
{}
encoding
=
polib
.
detect_encoding
(
data
)
catalog
=
polib
.
pofile
(
data
.
decode
(
encoding
),
encoding
=
encoding
)
result
=
{}
for
entry
in
catalog
:
if
entry
.
obsolete
or
not
entry
.
msgid
:
continue
key
=
(
entry
.
msgctxt
,
entry
.
msgid
)
if
key
in
result
:
raise
ValueError
(
f"Duplicate PO entry:
{
key
!r
}
"
)
result
[
key
]
=
entry
return
result
def
signature
(
entry
):
if
entry
is
None
:
return
None
return
(
entry
.
msgid_plural
,
entry
.
msgstr
,
entry
.
msgstr_plural
,
"fuzzy"
in
entry
.
flags
,
)
def
changes
(
before
,
after
):
# Keep document order; entries removed from a catalog follow surviving ones.
for
key
in
dict
.
fromkeys
([
*
after
,
*
before
]):
old
,
new
=
before
.
get
(
key
),
after
.
get
(
key
)
if
signature
(
old
)
!=
signature
(
new
):
yield
old
,
new
def
escape
(
text
):
# Ordinary punctuation such as dots, hyphens and parentheses needs no escape.
return
re
.
sub
(
r"([\\`*_\[\]<>#|~])"
,
r"\\\1"
,
text
.
replace
(
"&"
,
"&"
))
INLINE_RST
=
re
.
compile
(
r"(?<![\\`])(?::[\w.-]+(?::[\w.-]+)*:)?"
r"(?P<ticks>`{1,2})(?!`)(?:\\.|[^`\\\n])+(?P=ticks)(?!`)"
)
def
inline_text
(
text
):
# Preserve the whole reST token in a Markdown code span, including its
# role and backticks. This avoids backslash noise in the Markdown source.
parts
=
[]
start
=
0
for
match
in
INLINE_RST
.
finditer
(
text
):
parts
.
append
(
escape
(
text
[
start
:
match
.
start
()]))
token
=
match
.
group
()
fence
=
"`"
*
(
1
+
max
(
len
(
run
)
for
run
in
re
.
findall
(
r"`+"
,
token
)))
parts
.
append
(
f"
{
fence
}
{
token
}
{
fence
}
"
)
start
=
match
.
end
()
parts
.
append
(
escape
(
text
[
start
:]))
result
=
""
.
join
(
parts
)
# Prevent prose lines from becoming Markdown lists or thematic breaks.
return
re
.
sub
(
r"^(\s*)([-+]|\d+[.)])(?=\s|$)"
,
lambda
match
:
match
[
1
]
+
match
[
2
][:
-
1
]
+
"
\\
"
+
match
[
2
][
-
1
],
result
)
def
paragraph
(
text
):
return
"
\n
"
.
join
(
"> "
+
inline_text
(
line
)
+
(
" "
if
line
else
""
)
for
line
in
text
.
split
(
"
\n
"
)
)
def
field
(
label
,
text
):
# Keep indentation and alignment in code examples, while allowing prose
# to wrap naturally in the Markdown viewer.
if
text
.
startswith
((
">>> "
,
".. code-block::"
,
".. code::"
)):
fence
=
"`"
*
max
(
3
,
1
+
max
((
len
(
run
)
for
run
in
re
.
findall
(
r"`+"
,
text
)),
default
=
0
))
return
f"**
{
label
}
**
\n
\n
{
fence
}
text
\n
{
text
}
\n
{
fence
}
\n
"
return
f"**
{
label
}
**
\n
\n
{
paragraph
(
text
)
}
\n
"
def
translation
(
entry
):
if
entry
is
None
:
return
"(エントリーなし)"
if
entry
.
msgid_plural
:
return
"
\n
\n
"
.
join
(
f"msgstr[
{
index
}
]:
{
text
or
'(未翻訳)'
}
"
for
index
,
text
in
sorted
(
entry
.
msgstr_plural
.
items
())
)
or
"(未翻訳)"
return
entry
.
msgstr
or
"(未翻訳)"
def
status
(
entry
):
if
entry
is
None
:
return
"なし"
if
"fuzzy"
in
entry
.
flags
:
return
"fuzzy(要確認)"
return
"翻訳済み"
if
entry
.
translated
()
else
"未翻訳"
def
render_entry
(
path
,
old
,
new
,
number
,
link
):
entry
=
new
if
new
is
not
None
else
old
identity
=
json
.
dumps
([
path
,
entry
.
msgctxt
,
entry
.
msgid
],
ensure_ascii
=
False
)
identifier
=
hashlib
.
sha256
(
identity
.
encode
()).
hexdigest
()[:
12
]
kind
=
"追加"
if
old
is
None
else
"削除"
if
new
is
None
else
"変更"
lines
=
[
f"###
{
number
}
.
{
kind
}
·
{
identifier
}
"
,
""
]
if
new
is
not
None
:
lines
.
extend
([
f"[PO の該当箇所](
{
link
}
#L
{
new
.
linenum
}
)"
,
""
])
if
entry
.
occurrences
:
locations
=
", "
.
join
(
f"
{
name
}
:
{
line
}
"
if
line
else
name
for
name
,
line
in
entry
.
occurrences
)
lines
.
extend
([
"原文の位置: "
+
escape
(
locations
),
""
])
if
entry
.
msgctxt
is
not
None
:
lines
.
append
(
field
(
"文脈(msgctxt)"
,
entry
.
msgctxt
))
lines
.
append
(
field
(
"原文"
,
entry
.
msgid
))
if
entry
.
msgid_plural
:
lines
.
append
(
field
(
"原文(複数形)"
,
entry
.
msgid_plural
))
if
old
is
not
None
and
old
.
msgid_plural
!=
entry
.
msgid_plural
:
lines
.
append
(
field
(
"変更前の原文(複数形)"
,
old
.
msgid_plural
or
"(なし)"
))
lines
.
append
(
field
(
"変更前"
,
translation
(
old
)))
lines
.
append
(
field
(
"変更後"
,
translation
(
new
)))
lines
.
extend
([
f"状態:
{
status
(
old
)
}
→
{
status
(
new
)
}
"
,
""
])
if
entry
.
tcomment
:
lines
.
append
(
field
(
"翻訳者コメント"
,
entry
.
tcomment
))
if
entry
.
comment
:
lines
.
append
(
field
(
"抽出コメント"
,
entry
.
comment
))
return
"
\n
"
.
join
(
lines
)
def
report
(
root
,
*
,
cached
=
False
,
base
=
None
,
paths
=
(),
output_dir
=
None
):
diff_args
=
[
"diff"
,
"--no-ext-diff"
,
"--no-renames"
,
"--name-status"
,
"-z"
]
revision
=
None
if
cached
:
diff_args
.
append
(
"--cached"
)
comparison
=
"HEAD → ステージ済み(git diff --cached)"
elif
base
:
revision
=
git
(
root
,
"rev-parse"
,
"--verify"
,
"--end-of-options"
,
base
+
"^{commit}"
).
decode
().
strip
()
diff_args
.
append
(
revision
)
comparison
=
f"
{
base
}
(
{
revision
[:
12
]
}
) → 作業ツリー"
else
:
comparison
=
"インデックス → 作業ツリー(git diff)"
diff_args
.
extend
([
"--"
,
*
(
paths
or
(
"*.po"
,))])
records
=
git
(
root
,
*
diff_args
).
split
(
b"
\0
"
)
sections
,
deleted
=
[], []
count
=
0
for
offset
in
range
(
0
,
len
(
records
)
-
1
,
2
):
change
=
records
[
offset
].
decode
()
path
=
os
.
fsdecode
(
records
[
offset
+
1
])
if
not
path
.
endswith
(
".po"
):
continue
if
change
==
"D"
:
deleted
.
append
(
path
)
continue
if
change
not
in
(
"A"
,
"M"
):
raise
ValueError
(
f"Unsupported file change
{
change
}
:
{
path
}
"
)
old_ref
=
"HEAD"
if
cached
else
revision
or
""
before
=
entries
(
None
if
change
==
"A"
else
git
(
root
,
"show"
,
f"
{
old_ref
}
:
{
path
}
"
))
after
=
entries
(
git
(
root
,
"show"
,
f":
{
path
}
"
)
if
cached
else
(
root
/
path
).
read_bytes
())
modified
=
list
(
changes
(
before
,
after
))
if
not
modified
:
continue
link
=
quote
(
Path
(
os
.
path
.
relpath
(
root
/
path
,
output_dir
or
root
)).
as_posix
(),
safe
=
"/"
)
section
=
[
"## "
+
escape
(
path
),
""
]
for
old
,
new
in
modified
:
count
+=
1
section
.
append
(
render_entry
(
path
,
old
,
new
,
count
,
link
))
sections
.
append
(
"
\n
"
.
join
(
section
))
header
=
[
"# 翻訳レビュー"
,
""
,
"比較: "
+
escape
(
comparison
),
""
,
f"翻訳・状態の変更:
{
count
}
エントリー /
{
len
(
sections
)
}
ファイル。"
f"ファイル全体の削除:
{
len
(
deleted
)
}
件。"
,
""
,
"PO ヘッダー、折り返し、参照行番号だけの変更は省略しています。"
"原文が変わったエントリーは追加・削除として表示します。"
,
""
,
]
if
not
count
:
header
.
extend
([
"翻訳・状態の変更はありません。"
,
""
])
if
deleted
:
sections
.
append
(
"## 削除された PO ファイル
\n
\n
"
+
"
\n
"
.
join
(
"- "
+
escape
(
path
)
for
path
in
deleted
)
+
"
\n
"
)
return
"
\n
"
.
join
([
*
header
,
*
sections
]).
rstrip
()
+
"
\n
"
def
main
():
parser
=
argparse
.
ArgumentParser
(
description
=
__doc__
)
selection
=
parser
.
add_mutually_exclusive_group
()
selection
.
add_argument
(
"--cached"
,
action
=
"store_true"
,
help
=
"compare HEAD to the index"
)
selection
.
add_argument
(
"--base"
,
metavar
=
"REV"
,
help
=
"compare REV to the working tree, including staged changes"
)
parser
.
add_argument
(
"-o"
,
"--output"
,
type
=
Path
,
help
=
"write UTF-8 Markdown to this file (default: stdout)"
)
parser
.
add_argument
(
"paths"
,
nargs
=
"*"
,
help
=
"Git pathspecs relative to the repository root (default: all PO files)"
)
args
=
parser
.
parse_args
()
try
:
root
=
Path
(
os
.
fsdecode
(
git
(
Path
.
cwd
(),
"rev-parse"
,
"--show-toplevel"
)).
rstrip
(
"
\n
"
))
output
=
args
.
output
.
resolve
()
if
args
.
output
else
None
# A report must never overwrite a translation or another tracked file.
if
output
:
if
output
.
suffix
.
lower
()
!=
".md"
:
raise
ValueError
(
"Output must have a .md extension"
)
tracked
=
git
(
root
,
"ls-files"
,
"-z"
).
split
(
b"
\0
"
)
if
any
((
root
/
os
.
fsdecode
(
path
)).
resolve
()
==
output
for
path
in
tracked
if
path
):
raise
ValueError
(
f"Output is a tracked file:
{
output
}
"
)
markdown
=
report
(
root
,
cached
=
args
.
cached
,
base
=
args
.
base
,
paths
=
args
.
paths
,
output_dir
=
output
.
parent
if
output
else
root
)
if
output
:
output
.
write_text
(
markdown
,
encoding
=
"utf-8"
)
else
:
sys
.
stdout
.
write
(
markdown
)
except
subprocess
.
CalledProcessError
as
error
:
parser
.
exit
(
1
,
error
.
stderr
.
decode
(
errors
=
"replace"
))
except
(
OSError
,
ValueError
)
as
error
:
parser
.
exit
(
1
,
f"
{
error
}
\n
"
)
if
__name__
==
"__main__"
:
main
()
Back
|
FazBrowse Home
|
New Git URL