2 # SPDX-License-Identifier: GPL-2.0
5 This script helps track the translation status of the documentation
6 in different locales, e.g., zh_CN. More specially, it uses `git log`
7 commit to find the latest english commit from the translation commit
8 (order by author date) and the latest english commits from HEAD. If
9 differences occur, report the file and commits that need to be updated.
11 The usage is as follows:
12 - ./scripts/checktransupdate.py -l zh_CN
13 This will print all the files that need to be updated or translated in the zh_CN locale.
14 - ./scripts/checktransupdate.py Documentation/translations/zh_CN/dev-tools/testing-overview.rst
15 This will only print the status of the specified file.
17 The output is something like:
18 Documentation/dev-tools/kfence.rst
19 No translation in the locale of zh_CN
21 Documentation/translations/zh_CN/dev-tools/testing-overview.rst
22 commit 42fb9cfd5b18 ("Documentation: dev-tools: Add link to RV docs")
23 1 commits needs resolving in total
29 from argparse
import ArgumentParser
, ArgumentTypeError
, BooleanOptionalAction
30 from datetime
import datetime
33 def get_origin_path(file_path
):
34 """Get the origin path from the translation path"""
35 paths
= file_path
.split("/")
36 tidx
= paths
.index("translations")
38 opaths
+= paths
[tidx
+ 2 :]
39 return "/".join(opaths
)
42 def get_latest_commit_from(file_path
, commit
):
43 """Get the latest commit from the specified commit for the specified file"""
44 command
= f
"git log --pretty=format:%H%n%aD%n%cD%n%n%B {commit} -1 -- {file_path}"
45 logging
.debug(command
)
46 pipe
= os
.popen(command
)
48 result
= result
.split("\n")
52 logging
.debug("Result: %s", result
[0])
56 "author_date": datetime
.strptime(result
[1], "%a, %d %b %Y %H:%M:%S %z"),
57 "commit_date": datetime
.strptime(result
[2], "%a, %d %b %Y %H:%M:%S %z"),
58 "message": result
[4:],
62 def get_origin_from_trans(origin_path
, t_from_head
):
63 """Get the latest origin commit from the translation commit"""
64 o_from_t
= get_latest_commit_from(origin_path
, t_from_head
["hash"])
65 while o_from_t
is not None and o_from_t
["author_date"] > t_from_head
["author_date"]:
66 o_from_t
= get_latest_commit_from(origin_path
, o_from_t
["hash"] + "^")
67 if o_from_t
is not None:
68 logging
.debug("tracked origin commit id: %s", o_from_t
["hash"])
72 def get_commits_count_between(opath
, commit1
, commit2
):
73 """Get the commits count between two commits for the specified file"""
74 command
= f
"git log --pretty=format:%H {commit1}...{commit2} -- {opath}"
75 logging
.debug(command
)
76 pipe
= os
.popen(command
)
77 result
= pipe
.read().split("\n")
78 # filter out empty lines
79 result
= list(filter(lambda x
: x
!= "", result
))
83 def pretty_output(commit
):
84 """Pretty print the commit message"""
85 command
= f
"git log --pretty='format:%h (\"%s\")' -1 {commit}"
86 logging
.debug(command
)
87 pipe
= os
.popen(command
)
91 def valid_commit(commit
):
92 """Check if the commit is valid or not"""
93 msg
= pretty_output(commit
)
94 return "Merge tag" not in msg
96 def check_per_file(file_path
):
97 """Check the translation status for the specified file"""
98 opath
= get_origin_path(file_path
)
100 if not os
.path
.isfile(opath
):
101 logging
.error("Cannot find the origin path for {file_path}")
104 o_from_head
= get_latest_commit_from(opath
, "HEAD")
105 t_from_head
= get_latest_commit_from(file_path
, "HEAD")
107 if o_from_head
is None or t_from_head
is None:
108 logging
.error("Cannot find the latest commit for %s", file_path
)
111 o_from_t
= get_origin_from_trans(opath
, t_from_head
)
114 logging
.error("Error: Cannot find the latest origin commit for %s", file_path
)
117 if o_from_head
["hash"] == o_from_t
["hash"]:
118 logging
.debug("No update needed for %s", file_path
)
120 logging
.info(file_path
)
121 commits
= get_commits_count_between(
122 opath
, o_from_t
["hash"], o_from_head
["hash"]
125 for commit
in commits
:
126 if valid_commit(commit
):
127 logging
.info("commit %s", pretty_output(commit
))
129 logging
.info("%d commits needs resolving in total\n", count
)
132 def valid_locales(locale
):
133 """Check if the locale is valid or not"""
134 script_path
= os
.path
.dirname(os
.path
.abspath(__file__
))
135 linux_path
= os
.path
.join(script_path
, "..")
136 if not os
.path
.isdir(f
"{linux_path}/Documentation/translations/{locale}"):
137 raise ArgumentTypeError("Invalid locale: {locale}")
141 def list_files_with_excluding_folders(folder
, exclude_folders
, include_suffix
):
142 """List all files with the specified suffix in the folder and its subfolders"""
148 # filter out the exclude folders
149 if os
.path
.basename(pwd
) in exclude_folders
:
151 # list all files and folders
152 for item
in os
.listdir(pwd
):
153 ab_item
= os
.path
.join(pwd
, item
)
154 if os
.path
.isdir(ab_item
):
155 stack
.append(ab_item
)
157 if ab_item
.endswith(include_suffix
):
158 files
.append(ab_item
)
163 class DmesgFormatter(logging
.Formatter
):
164 """Custom dmesg logging formatter"""
165 def format(self
, record
):
166 timestamp
= time
.time()
167 formatted_time
= f
"[{timestamp:>10.6f}]"
168 log_message
= f
"{formatted_time} {record.getMessage()}"
172 def config_logging(log_level
, log_file
="checktransupdate.log"):
173 """configure logging based on the log level"""
174 # set up the root logger
175 logger
= logging
.getLogger()
176 logger
.setLevel(log_level
)
178 # Create console handler
179 console_handler
= logging
.StreamHandler()
180 console_handler
.setLevel(log_level
)
182 # Create file handler
183 file_handler
= logging
.FileHandler(log_file
)
184 file_handler
.setLevel(log_level
)
186 # Create formatter and add it to the handlers
187 formatter
= DmesgFormatter()
188 console_handler
.setFormatter(formatter
)
189 file_handler
.setFormatter(formatter
)
191 # Add the handler to the logger
192 logger
.addHandler(console_handler
)
193 logger
.addHandler(file_handler
)
197 """Main function of the script"""
198 script_path
= os
.path
.dirname(os
.path
.abspath(__file__
))
199 linux_path
= os
.path
.join(script_path
, "..")
201 parser
= ArgumentParser(description
="Check the translation update")
207 help="Locale to check when files are not specified",
211 "--print-missing-translations",
212 action
=BooleanOptionalAction
,
214 help="Print files that do not have translations",
220 choices
=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
221 help='Set the logging level')
225 default
='checktransupdate.log',
226 help='Set the logging file (default: checktransupdate.log)')
229 "files", nargs
="*", help="Files to check, if not specified, check all files"
231 args
= parser
.parse_args()
233 # Configure logging based on the --log argument
234 log_level
= getattr(logging
, args
.log
.upper(), logging
.INFO
)
235 config_logging(log_level
)
237 # Get files related to linux path
240 offical_files
= list_files_with_excluding_folders(
241 os
.path
.join(linux_path
, "Documentation"), ["translations", "output"], "rst"
244 for file in offical_files
:
245 # split the path into parts
246 path_parts
= file.split(os
.sep
)
247 # find the index of the "Documentation" directory
248 kindex
= path_parts
.index("Documentation")
249 # insert the translations and locale after the Documentation directory
250 new_path_parts
= path_parts
[:kindex
+ 1] + ["translations", args
.locale
] \
251 + path_parts
[kindex
+ 1 :]
252 # join the path parts back together
253 new_file
= os
.sep
.join(new_path_parts
)
254 if os
.path
.isfile(new_file
):
255 files
.append(new_file
)
257 if args
.print_missing_translations
:
258 logging
.info(os
.path
.relpath(os
.path
.abspath(file), linux_path
))
259 logging
.info("No translation in the locale of %s\n", args
.locale
)
261 files
= list(map(lambda x
: os
.path
.relpath(os
.path
.abspath(x
), linux_path
), files
))
263 # cd to linux root directory
270 if __name__
== "__main__":