Skip to content

Commit 3380acf

Browse files
feat: 扫描未国际化的代码,写入 not_internationalized.txt 文件中
1 parent fd7b878 commit 3380acf

2 files changed

Lines changed: 429 additions & 0 deletions

File tree

apps/locales/i18n_automation.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
2. 扫描所有Python文件中的国际化代码
77
3. 合并已有翻译和新发现的内容
88
4. 生成排序后的翻译文件
9+
5. 扫描所有Python文件中未使用国际化功能的代码,并写入 `not_internationalized.txt` 文件中
910
"""
1011

1112
import re
@@ -38,6 +39,17 @@
3839
# 说明:因为有VCS,所以默认不备份。
3940
BACKUP_PO = False
4041

42+
# 配置5:是否扫描未国际化的代码?
43+
SCAN_NOT_INTERNATIONALIZED = True
44+
45+
# 配置6:忽略 `未国际化的代码` 数组
46+
NOT_INTERNATIONALIZED_IGNORE_ARRAYS = frozenset([
47+
"\n**********\n",
48+
"\n│",
49+
"***************",
50+
"<%s %s>",
51+
])
52+
4153

4254
class I18nAutomation:
4355
"""国际化自动化工具类"""
@@ -579,6 +591,158 @@ def write_po_files(self):
579591

580592
print("\n所有翻译文件已生成完成!!!")
581593

594+
def scan_non_i18n_strings(self):
595+
"""
596+
步骤5: 扫描 Python 文件中应该国际化但未国际化的字符串
597+
598+
Returns:
599+
包含未国际化字符串信息的列表
600+
"""
601+
print("\n" + "="*60)
602+
print("步骤5: 扫描未国际化的字符串")
603+
print("="*60)
604+
605+
non_i18n_list = []
606+
apps_dir = self.base_dir / 'apps'
607+
608+
# 匹配常见的需要国际化的字符串模式
609+
# 1. 用户可见的提示消息、错误消息等
610+
patterns = [
611+
# 匹配 raise Exception("...") 或 raise ValidationError("...") 等异常消息
612+
re.compile(r'\braise\s+\w+[\w\.]*\s*\(\s*r?["\']((?:[^"\'\\]|\\.)*?)["\']', re.MULTILINE | re.DOTALL),
613+
# 匹配 logger.error("..."), logger.warning("..."), logger.info("...") 等日志消息
614+
re.compile(r'\blogger\.(?:error|warning|info|debug|critical)\s*\(\s*r?["\']((?:[^"\'\\]|\\.)*?)["\']', re.MULTILINE | re.DOTALL),
615+
# 匹配 print("...")
616+
re.compile(r'\bprint\s*\(\s*r?["\']((?:[^"\'\\]|\\.)*?)["\']', re.MULTILINE | re.DOTALL),
617+
# 匹配 return "..." (常见于 API 响应消息)
618+
re.compile(r'\breturn\s+r?["\']((?:[^"\'\\]|\\.)*?)["\']', re.MULTILINE | re.DOTALL),
619+
]
620+
621+
# 排除模式:不应该被国际化的内容
622+
exclude_patterns = [
623+
re.compile(r'^[a-zA-Z0-9_\-\.\/\:\{\}]+$', re.MULTILINE), # 纯技术标识符、路径、JSON等
624+
re.compile(r'^\s*$'), # 空字符串
625+
re.compile(r'^[{}[\]:,.\-\s]+$'), # 纯标点符号
626+
]
627+
628+
# 递归查找所有 .py 文件
629+
py_files = list(apps_dir.rglob('*.py'))
630+
print(f"找到 {len(py_files)} 个 Python 文件")
631+
632+
scanned_count = 0
633+
for py_file in py_files:
634+
# 跳过 __pycache__、migrations 和 locales 目录
635+
if '__pycache__' in str(py_file) or 'migrations' in str(py_file) or 'locales' in str(py_file):
636+
continue
637+
638+
try:
639+
with open(py_file, 'r', encoding='utf-8') as f:
640+
content = f.read()
641+
642+
file_has_non_i18n = False
643+
for pattern in patterns:
644+
matches = pattern.finditer(content)
645+
for match in matches:
646+
msgid = match.group(1) if match.lastindex else None
647+
648+
if msgid is None:
649+
continue
650+
651+
msgid = self._unescape_string(msgid)
652+
653+
# 过滤掉不应该国际化的内容
654+
if not msgid or not msgid.strip():
655+
continue
656+
657+
should_exclude = False
658+
for exclude_pattern in exclude_patterns:
659+
if exclude_pattern.match(msgid):
660+
should_exclude = True
661+
break
662+
663+
if should_exclude:
664+
continue
665+
666+
# 检查该字符串是否已经被国际化(在已扫描的 i18n 列表中)
667+
if hasattr(self, 'scanned_i18n') and msgid in self.scanned_i18n:
668+
continue
669+
670+
# 记录未国际化的字符串
671+
relative_path = str(py_file.relative_to(self.base_dir)).replace("\\", "/")
672+
line_no = content[:match.start()].count('\n') + 1
673+
674+
if msgid not in NOT_INTERNATIONALIZED_IGNORE_ARRAYS:
675+
non_i18n_list.append({
676+
'file': relative_path,
677+
'line_no': line_no,
678+
'msgid': msgid
679+
})
680+
file_has_non_i18n = True
681+
682+
if file_has_non_i18n:
683+
scanned_count += 1
684+
685+
except Exception as e:
686+
print(f"处理文件 {py_file} 时出错: {e}")
687+
688+
print(f"扫描了 {scanned_count} 个文件")
689+
print(f"发现 {len(non_i18n_list)} 条未国际化的字符串")
690+
691+
self.non_i18n_strings = non_i18n_list
692+
return non_i18n_list
693+
694+
def write_not_internationalized_file(self):
695+
"""
696+
将未国际化的字符串写入 not_internationalized.txt 文件
697+
"""
698+
print("\n" + "="*60)
699+
print("步骤6: 写入 not_internationalized.txt 文件")
700+
print("="*60)
701+
702+
output_file = self.locales_dir / 'not_internationalized.txt'
703+
704+
if not hasattr(self, 'non_i18n_strings'):
705+
self.non_i18n_strings = []
706+
707+
# 按 msgid 分组,相同 msgid 的合并到一起
708+
msgid_groups = {}
709+
for item in self.non_i18n_strings:
710+
msgid = item['msgid']
711+
if msgid not in msgid_groups:
712+
msgid_groups[msgid] = []
713+
msgid_groups[msgid].append({
714+
'file': item['file'],
715+
'line_no': item['line_no']
716+
})
717+
718+
# 按 msgid 排序
719+
sorted_msgids = sorted(msgid_groups.keys())
720+
721+
# 生成文件内容
722+
lines = []
723+
for msgid in sorted_msgids:
724+
locations = msgid_groups[msgid]
725+
726+
# 对同一 msgid 的位置按文件路径和行号排序
727+
locations.sort(key=lambda x: (x['file'], x['line_no']))
728+
729+
# 添加所有位置信息
730+
for loc in locations:
731+
lines.append(f"#: {loc['file']}:{loc['line_no']}")
732+
733+
# 添加 msgid
734+
escaped_content = self._escape_string(msgid)
735+
lines.append(f'msgid "{escaped_content}"')
736+
lines.append('') # 空行分隔
737+
738+
content = '\n'.join(lines)
739+
740+
# 写入文件
741+
with open(output_file, 'w', encoding='utf-8') as f:
742+
f.write(content)
743+
744+
print(f"已写入 {len(sorted_msgids)} 条未国际化字符串到: {output_file}")
745+
582746
def generate_report(self):
583747
"""
584748
生成翻译报告
@@ -676,6 +840,13 @@ def run(self):
676840
# 步骤4: 写入文件
677841
self.write_po_files()
678842

843+
if SCAN_NOT_INTERNATIONALIZED:
844+
# 步骤5: 扫描未国际化的字符串
845+
self.scan_non_i18n_strings()
846+
847+
# 步骤6: 写入 not_internationalized.txt
848+
self.write_not_internationalized_file()
849+
679850
# 生成报告
680851
self.generate_report()
681852

0 commit comments

Comments
 (0)