Files
lucky_shop/scripts/full_migrate_i18n.py
ZF sun 1f1a642f99 fix(i18n): 修复审计脚本扫不到 modular 文件的盲区,接入命名规范检查到构建卡点
- i18n_audit.py: 同步扫描 lang/<locale>/<module>.js 多文件 + lang/<locale>.js 单文件
- i18n_check_naming.py (新增): 检测中文 key / 扁平 key / 行号式 locational key / 跨业务域同名 / 非 snake_case
- i18n_check.py: 加入 naming 子命令,默认全套审计包含 naming
- vue.config.js: I18nGuardPlugin 增加 naming 阻断
- full_migrate_i18n.py: 标 DEPRECATED,文档说明其输出违反 CLAUDE.md 1.2 铁律
- diagnose_keys.py (新增): 定位 orphan 调用 key

背景:之前审计'假阳性通过',实际 6018 处违规;现在审计能看到真实状态。

Fix: docs/i18n-rules.md 1.6 条铁律需补强 (modular 审计入口已加)

统计 (i18n_check_naming.py 当前输出):
  A. 中文 key: 2146
  B. 扁平 key: 188
  C. 行号式 locational key: 1612
  D. 跨业务域同名 tail: 1366
  E. 非 snake_case key 段: 2924
  TOTAL: 8236 处
2026-06-25 10:26:55 +08:00

808 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
[DEPRECATED] 自 2026-06-25 起不再使用 — 详见 docs/i18n-rules.md
i18n 全量迁移脚本:把旧的单文件机器生成 key 全部迁移到新的多文件语义化结构
⚠️ 此脚本的输出违反 CLAUDE.md 铁律:
- 会产生大量中文 key(违反 1.2 命名空间铁律)
- 把无法分类的 key 全部塞进 common.*(违反 common 只放真正通用文案的语义)
- 保留 lXX.tYY 行号式 key(违反长期维护目标 4)
正确的迁移方式:
- 每个业务模块单独一个 commit(避免 CLAUDE.md 1.5 夹带风险)
- 人类按 <业务域>.<子模块>.<语义> 命名,由机器只做查重和替换
- 完成后跑 python scripts/i18n_check.py 验证
用法(仍然能跑,但产出不符合规范):
python scripts/full_migrate_i18n.py --analyze # 分析分类结果
python scripts/full_migrate_i18n.py --generate # 生成新语言包文件 + 映射表
python scripts/full_migrate_i18n.py --apply # 替换代码中所有旧 key
"""
import os
import re
import sys
import json
import shutil
from collections import defaultdict
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ZH_FILE = os.path.join(PROJECT_ROOT, 'lang', 'zh-cn.js')
EN_FILE = os.path.join(PROJECT_ROOT, 'lang', 'en-us.js')
NEW_ZH_DIR = os.path.join(PROJECT_ROOT, 'lang', 'zh-cn')
NEW_EN_DIR = os.path.join(PROJECT_ROOT, 'lang', 'en-us')
MAP_FILE = os.path.join(PROJECT_ROOT, 'lang', '_migration_map.json')
SEARCH_DIRS = [
'common', 'components', 'components-diy', 'pages',
'pages_goods', 'pages_order', 'pages_promotion', 'pages_tool',
]
# 业务域分类规则(按 key 前缀匹配,优先级从高到低)
NAMESPACE_RULES = [
# ===== 商品相关 =====
('goods', [
'goods.', 'ns-goods', 'ns-goods-sku', 'ns-goods-recommend',
'list.l', 'detail.l', 'category.', 'search', 'Search', 'Random',
'Sales', 'Price', 'Filter', 'stock', 'spec', 'details',
'sellout', 'addcart', 'buynow', 'buy', 'follow', 'consult',
'Make', 'make', 'max', 'status',
]),
# ===== 订单相关 =====
('order', [
'order.', 'order_list', 'orders', 'order_detail',
'waitpay', 'waitsend', 'waitconfirm', 'completed',
'payment', 'refund', 'aftersale', 'activist',
'express', 'logistics', 'ship', 'delivery',
]),
# ===== 购物车 =====
('cart', [
'cart.', 'shoppingCart', 'addCart', 'addcart',
'settlement', 'total',
]),
# ===== 支付相关 =====
('pay', [
'pay.', 'payment', 'paypassword', 'payPassword',
'paymentAmount', 'paymentSuccess', 'paymentFail',
]),
# ===== 个人中心 =====
('member', [
'member.', 'userinfo', 'userInfo', 'account',
'address', 'addressPlaceholder', 'consignee',
'collection', 'footprint', 'assets', 'balance',
'cancellation', 'cancelrefuse', 'cancelstatus', 'cancelsuccess',
'card', 'card_buy', 'info_edit',
]),
# ===== 登录注册 =====
('login', [
'login.', 'ns-login', 'find.', 'register',
'captcha', 'dynacode', 'animateCode',
'accountInput', 'accountPlaceholder', 'accountType',
'password', 'mobile', 'phone', 'userName', 'username',
'logout', 'loginSuccess', 'loginTpis',
'registerAgreement', 'privacyPolicy', 'userService',
'agreeAgreement',
]),
# ===== 优惠券 =====
('coupon', [
'coupon.', 'coupon-empty', 'ns-coupon',
]),
# ===== 积分 =====
('point', [
'point.', 'points', 'exchange', '积分',
]),
# ===== 分销 =====
('fenxiao', [
'fenxiao.', 'commission', 'withdrawal',
'myTeam', 'team', '推广',
]),
# ===== 商家/店铺 =====
('merch', [
'merch.', 'merchcategory', 'merchant',
'diy-merch-list', '店铺', '商家',
]),
# ===== DIY 装修 =====
('diy', [
'diy.', 'diy-', 'ns-diy',
'diy-search', 'diy-category', 'diy-kefu', 'diy-listmenu',
'diy-map', 'diy-text', 'diy-notice', 'diy-coupon',
]),
# ===== 客服聊天 =====
('chat', [
'chat.', 'ns-chat', 'customerService',
'service', 'contactService', 'wxwork-contact',
'leave', 'onlineMessage', 'messageContent',
'pleaseEnterMessage',
]),
# ===== AI 聊天 =====
('ai_chat', [
'ai-chat.', 'ai_chat.', 'aiChat', 'ai-chat-',
'aiThinking', 'aiNickname', 'ai_chat',
'chatCleared', 'confirmClearChat',
'fallbackWelcome', 'serviceUnavailable',
'serviceTemporaryUnavailable',
]),
# ===== 文章 =====
('article', [
'article.', 'articles',
]),
# ===== 公告 =====
('notice', [
'notice.', 'notices',
]),
# ===== 帮助 =====
('help', [
'help.', 'helpCenter',
]),
# ===== 印章 =====
('seal', [
'seal.', '印章',
]),
# ===== 门店关闭 =====
('storeclose', [
'storeclose.', 'store_close',
]),
# ===== 文件相关 =====
('common', [
'file-preview', 'file-preview', 'files.',
]),
# ===== 通用组件(归到 common) =====
('common', [
'ns-empty', 'ns-loading', 'ns-form', 'ns-tabbar',
'ns-copyright', 'ns-video-player', 'ns-popup',
'ns-pick-regions', 'pick-regions', 'ns-scroll-list',
'copy', 'share', 'save', 'delete', 'edit', 'submit',
'cancel', 'close', 'reset', 'confirm', 'complete',
'searchTip', 'searchPlaceholder', 'searchEntrance',
'emptyTips', 'noDataTip', 'loading', 'loadingText',
'loadMore', 'loadMoreTxt', 'loadMoreLoading',
'currencySymbol', 'symbol',
'toGoodsCategoryPage', 'goHome', 'goShopping',
'prompt', 'copySuccess', 'copyFail', 'copyFailed',
'operationSuccess', 'operationFailed', 'networkError',
'saveSuccess', 'saveSuc', 'updateSuccess',
'noContentToCopy', 'noMatchResult',
'inputPlaceholder', 'selectPlaceholder',
'required', 'invalid', 'maxUpload',
'endDateBeforeStart', 'endTimeBeforeStartTime',
'idCardInvalid', 'phoneInvalid',
'selectAtLeastOne', 'uploadAtLeastOne',
'selectProvinceCity', 'selectProvinceCityDistrict',
'home', 'about', 'call', 'oneClickNavigation',
'location', 'image', 'video', 'audio', 'file',
'voice', 'voiceInput', 'emoji',
'send', 'settlement',
'allElection', 'selectAll',
'next', 'previous', 'retry',
'pageTitle', 'title',
]),
]
# key 名规范化映射(常见模式转换)
KEY_NORMALIZE_PATTERNS = [
# 组件前缀去除
(r'^ns-', ''),
# 文件扩展名去除
(r'\.public\.js\.', '.'),
(r'\.js\.', '.'),
# 行号模式去除(lXX.tY -> 不处理,交由后面的逻辑)
# 大小写规范化
]
def parse_lang_file(filepath):
"""解析语言包文件,返回展平的 key-value 字典"""
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
match = re.search(r'export const lang = \{(.*?)\n\};', content, re.DOTALL)
if not match:
return {}
body = match.group(1)
lines = body.split('\n')
stack = []
result = {}
for line in lines:
stripped = line.strip()
if not stripped:
continue
indent = len(line) - len(line.lstrip())
level = indent // 4 + 1
while len(stack) >= level:
stack.pop()
obj_match = re.match(r'"([^"]+)"\s*:\s*\{', stripped)
if obj_match:
stack.append(obj_match.group(1))
continue
val_match = re.match(r'"([^"]+)"\s*:\s*"([^"]*)"', stripped)
if val_match:
key = val_match.group(1)
value = val_match.group(2)
full_path = '.'.join(stack + [key])
result[full_path] = value
return result
def classify_key(old_key, old_value):
"""根据旧 key 和翻译值判断属于哪个业务域"""
# 先按规则匹配
for namespace, prefixes in NAMESPACE_RULES:
for prefix in prefixes:
lower_key = old_key.lower()
lower_prefix = prefix.lower()
if lower_key.startswith(lower_prefix) or lower_prefix in lower_key:
return namespace
# 兜底:根据翻译值的关键词判断
value_keywords = {
'goods': ['商品', '规格', '库存', '销量', '购买', '加入购物车', '立即购买', '商品详情'],
'order': ['订单', '付款', '发货', '收货', '退款', '售后', '物流'],
'cart': ['购物车', '结算', '合计'],
'pay': ['支付', '付款', '支付密码'],
'member': ['个人', '资料', '收货地址', '收藏', '浏览', '余额', '积分', '会员卡'],
'login': ['登录', '注册', '密码', '验证码', '账号', '手机号', '忘记密码'],
'coupon': ['优惠券', '领券', '满减'],
'fenxiao': ['分销', '佣金', '提成', '团队'],
'merch': ['店铺', '商家', '品牌'],
'diy': ['装修', '组件', '模板'],
'chat': ['客服', '聊天', '消息'],
'ai_chat': ['AI', '智能', '助手'],
'article': ['文章', '资讯'],
'notice': ['公告', '通知'],
'help': ['帮助', 'FAQ'],
}
for namespace, keywords in value_keywords.items():
for kw in keywords:
if kw in old_value:
return namespace
# 最终兜底:归到 common
return 'common'
def generate_new_key(old_key, old_value, namespace):
"""根据旧 key 和翻译值生成语义化新 key"""
# 已有良好命名的 key(非机器生成),只调整命名空间
if not re.search(r'\.l\d+\.t\d+', old_key) and '.' in old_key:
# 已经有命名空间的,检查是否需要调整
parts = old_key.split('.')
first_part = parts[0]
# 如果第一个就是正确的业务域,保持原样(转换为 snake_case)
if first_part == namespace:
return to_snake_case(old_key)
# 否则加上业务域前缀
return to_snake_case(f"{namespace}.{old_key}")
# 扁平 key(无点),加业务域前缀
if '.' not in old_key:
# 常见的扁平 key 映射
flat_map = {
'buy': 'action.buy',
'buynow': 'action.buy_now',
'save': 'button.save',
'cancel': 'button.cancel',
'submit': 'button.submit',
'delete': 'button.delete',
'edit': 'button.edit',
'close': 'button.close',
'search': 'button.search',
'reset': 'button.reset',
'copy': 'button.copy',
'share': 'button.share',
'retry': 'button.retry',
'next': 'button.next',
'previous': 'button.prev',
'back': 'button.back',
'confirm': 'button.confirm',
'complete': 'button.confirm',
'prompt': 'dialog.tip_title',
'logout': 'button.logout',
'login': 'button.login',
'register': 'button.register',
'loading': 'loading.text',
'emptyTips': 'empty.text',
'noDataTip': 'empty.text',
'goHome': 'empty.go_home',
'toGoodsCategoryPage': 'empty.go_shopping',
'home': 'nav.home',
'currencySymbol': 'currency.symbol',
'symbol': 'currency.symbol',
'copySuccess': 'tip.copy_success',
'copyFail': 'tip.copy_fail',
'saveSuccess': 'tip.save_success',
'operationSuccess': 'tip.operation_success',
'operationFailed': 'tip.operation_failed',
'networkError': 'tip.network_error',
'allElection': 'button.select_all',
'settlement': 'button.checkout',
'stock': 'detail.stock',
'spec': 'detail.spec',
'details': 'detail.details',
'sales': 'list.sales',
'price': 'list.price',
'Search': 'placeholder.search',
'searchTip': 'placeholder.search',
'searchPlaceholder': 'placeholder.search',
'searchEntrance': 'placeholder.search',
'inputPlaceholder': 'placeholder.input',
'selectPlaceholder': 'placeholder.select',
'required': 'form.required',
'invalid': 'form.invalid',
'title': 'common.title',
'status': 'detail.status',
}
lower_key = old_key.lower()
for k, v in flat_map.items():
if lower_key == k.lower():
return f"{namespace}.{v}"
# 其他扁平 key,转 snake_case 后放 list 或 detail 下
return f"{namespace}.list.{to_snake_case(old_key)}"
# 机器生成的行号 key(含 .lXX.tY)
# 提取前面的路径部分作为子模块
parts = old_key.split('.')
# 去掉行号部分
meaningful_parts = []
for p in parts:
if re.match(r'^l\d+$', p):
break
meaningful_parts.append(p)
# 去掉组件前缀中的 ns-
sub_parts = []
for p in meaningful_parts:
p = re.sub(r'^ns-', '', p)
p = re.sub(r'-index$', '', p)
sub_parts.append(p)
sub_path = '.'.join(sub_parts) if sub_parts else 'list'
# 如果翻译值有明确语义,用翻译值生成 key
semantic_suffix = generate_key_from_value(old_value)
new_key = f"{namespace}.{sub_path}.{semantic_suffix}"
new_key = to_snake_case(new_key)
# 清理多余的层级
new_key = clean_key_path(new_key, namespace)
return new_key
def generate_key_from_value(value):
"""根据翻译值生成语义化的 key 后缀"""
if not value:
return 'text'
# 常见翻译值映射
value_map = {
'确定': 'confirm',
'取消': 'cancel',
'保存': 'save',
'删除': 'delete',
'提交': 'submit',
'关闭': 'close',
'搜索': 'search',
'重置': 'reset',
'编辑': 'edit',
'复制': 'copy',
'分享': 'share',
'返回': 'back',
'下一步': 'next',
'上一步': 'prev',
'加载更多': 'load_more',
'重试': 'retry',
'确认': 'confirm',
'完成': 'confirm',
'提示': 'tip_title',
'暂无相关数据': 'empty_text',
'暂无记录': 'empty_text',
'去逛逛': 'go_shopping',
'回到首页': 'go_home',
'加载中': 'loading_text',
'加载中...': 'loading_text',
'下拉刷新': 'pull_refresh',
'释放更新': 'release_update',
'购买': 'buy',
'立即购买': 'buy_now',
'加入购物车': 'add_cart',
'立即咨询': 'consult',
'收藏': 'follow',
'销量': 'sales',
'价格': 'price',
'综合': 'comprehensive',
'筛选': 'filter',
'库存': 'stock',
'规格': 'spec',
'商品详情': 'details',
'合计': 'total',
'结算': 'checkout',
'管理': 'edit',
'全选': 'select_all',
'请输入搜索关键词': 'search_placeholder',
'请输入您要搜索的商品': 'search_placeholder',
'搜索商品': 'search_placeholder',
'暂无相关商品': 'empty_text',
'商品列表': 'title',
'订单列表': 'title',
'我的订单': 'title',
'订单详情': 'title',
'待付款': 'pending_payment',
'待发货': 'pending_shipment',
'待收货': 'pending_receipt',
'已完成': 'completed',
'售后': 'after_sale',
'全部': 'all',
'支付成功': 'pay_success',
'支付失败': 'pay_fail',
'支付金额': 'pay_amount',
'登录': 'login',
'注册': 'register',
'忘记密码?': 'forgot_password',
'退出登录': 'logout',
'个人资料': 'title',
'收货地址': 'title',
'我的收藏': 'title',
'浏览记录': 'title',
'个人中心': 'title',
'账号': 'account',
'密码': 'password',
'手机号': 'mobile',
'验证码': 'captcha',
'动态码': 'dynacode',
'获取验证码': 'get_captcha',
'获取动态码': 'get_dynacode',
'联系客服': 'customer_service',
'客服热线': 'service_hotline',
'复制成功': 'copy_success',
'复制失败': 'copy_fail',
'保存成功': 'save_success',
'操作成功': 'operation_success',
'操作失败': 'operation_failed',
'网络错误,请稍后重试': 'network_error',
'暂无相关订单': 'empty_text',
'购物车是空的': 'empty_text',
'立即付款': 'pay_now',
'取消订单': 'cancel_order',
'确认收货': 'confirm_receipt',
'申请退款': 'apply_refund',
'查看物流': 'view_logistics',
'再次购买': 'buy_again',
'删除订单': 'delete_order',
'订单编号': 'order_no',
'下单时间': 'create_time',
'实付金额': 'pay_amount',
'商品总额': 'goods_total',
'运费': 'shipping_fee',
'优惠': 'discount',
'配送方式': 'shipping_method',
'支付方式': 'payment_method',
'买家留言': 'buyer_message',
'库存不足': 'insufficient_stock',
'商品已下架': 'off_shelf',
'商品已售罄': 'sold_out',
'已选规格:': 'selected_spec',
'购买数量': 'buy_quantity',
'兑换数量': 'exchange_quantity',
'每人限购': 'limit_buy',
'最多购买': 'max_buy',
}
# 精确匹配
if value in value_map:
return value_map[value]
# 短翻译值直接用(去除特殊字符)
if len(value) <= 10 and len(value) >= 2:
clean = re.sub(r'[^\w\u4e00-\u9fa5]', '', value)
if clean:
return to_snake_case(clean)
# 长文本用通用后缀
if '暂无' in value:
return 'empty_text'
if '请输入' in value:
return 'placeholder'
if '确定要' in value:
return 'confirm_tip'
if '成功' in value:
return 'success_tip'
if '失败' in value:
return 'fail_tip'
return 'text'
def to_snake_case(text):
"""转换为 snake_case"""
# 先处理中划线
text = text.replace('-', '_')
# 处理驼峰:在大写字母前加下划线
text = re.sub(r'([A-Z])', r'_\1', text)
# 多下划线合并
text = re.sub(r'_+', '_', text)
# 转小写
text = text.lower()
# 去除首尾下划线
text = text.strip('_')
return text
def clean_key_path(key, namespace):
"""清理 key 路径中多余的层级"""
parts = key.split('.')
# 去除重复的业务域
if parts[0] == namespace and len(parts) > 1 and parts[1] == namespace:
parts = [parts[0]] + parts[2:]
# 合并常见的冗余层级
# 例如 goods.goods.sku -> goods.sku
if len(parts) >= 2 and parts[0] == parts[1]:
parts = [parts[0]] + parts[2:]
# 控制层级在 2-4 级
if len(parts) > 4:
# 保留首尾,中间合并
parts = [parts[0], parts[1], '_'.join(parts[2:-1]), parts[-1]]
return '.'.join(parts)
def analyze():
"""分析分类结果"""
zh_data = parse_lang_file(ZH_FILE)
print(f"总 key 数: {len(zh_data)}")
# 分类统计
categories = defaultdict(list)
for k, v in zh_data.items():
ns = classify_key(k, v)
new_key = generate_new_key(k, v, ns)
categories[ns].append((k, new_key, v))
print("\n" + "=" * 80)
print("分类结果统计")
print("=" * 80)
for ns in sorted(categories.keys(), key=lambda x: -len(categories[x])):
items = categories[ns]
print(f"\n{ns}: {len(items)} 个 key")
for old_key, new_key, value in items[:8]:
print(f" {old_key} -> {new_key} ({value[:30]})")
if len(items) > 8:
print(f" ... 还有 {len(items) - 8} 个")
# 检查新 key 冲突
all_new_keys = defaultdict(list)
for ns, items in categories.items():
for old_key, new_key, value in items:
all_new_keys[new_key].append((old_key, value))
conflicts = {k: v for k, v in all_new_keys.items() if len(v) > 1}
print(f"\n\n新 key 冲突数: {len(conflicts)}")
if conflicts:
print("冲突的 key:")
for new_key, old_list in list(conflicts.items())[:15]:
print(f" {new_key}:")
for old_key, value in old_list[:5]:
print(f" - {old_key} -> \"{value[:30]}\"")
return categories
def build_nested_dict(flat_dict):
"""将展平的 key-value 转换为嵌套对象"""
result = {}
for key, value in flat_dict.items():
parts = key.split('.')
current = result
for i, part in enumerate(parts[:-1]):
if part not in current:
current[part] = {}
elif isinstance(current[part], str):
# 冲突:当前是字符串,但需要变成对象
# 把字符串值移到 text 子键下
current[part] = {'text': current[part]}
current = current[part]
# 处理最后一级
last_part = parts[-1]
if last_part in current and isinstance(current[last_part], dict):
# 冲突:要写字符串,但已经是对象了
# 把字符串写到 text 子键
current[last_part]['text'] = value
else:
current[last_part] = value
return result
def write_lang_module(filepath, module_name, data):
"""写入单个语言模块文件"""
# 确保目录存在
os.makedirs(os.path.dirname(filepath), exist_ok=True)
# 生成缩进的 JS 对象字符串
def to_js_obj(obj, indent=1):
lines = []
tab = '\t' * indent
for key, value in sorted(obj.items()):
if isinstance(value, dict):
lines.append(f"{tab}{key}: {{")
lines.append(to_js_obj(value, indent + 1))
lines.append(f"{tab}}},")
else:
# 转义特殊字符
escaped = value.replace('\\', '\\\\').replace("'", "\\'")
lines.append(f"{tab}{key}: '{escaped}',")
return '\n'.join(lines)
content = f"""// {module_name} 模块文案
export const {module_name} = {{
{to_js_obj(data)}
}};
"""
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
def generate():
"""生成新的多文件语言包和映射表"""
zh_data = parse_lang_file(ZH_FILE)
en_data = parse_lang_file(EN_FILE)
# 分类
categories = defaultdict(dict) # namespace -> {new_key: zh_value}
en_categories = defaultdict(dict) # namespace -> {new_key: en_value}
migration_map = {} # old_key -> new_key
# 用于去重的计数器
new_key_counter = defaultdict(int)
for old_key, zh_val in zh_data.items():
ns = classify_key(old_key, zh_val)
new_key = generate_new_key(old_key, zh_val, ns)
# 处理冲突:如果新 key 已存在且值不同,加后缀
if new_key in categories[ns]:
existing_val = categories[ns][new_key]
if existing_val != zh_val:
# 翻译不同,加序号区分
new_key_counter[new_key] += 1
new_key = f"{new_key}_{new_key_counter[new_key] + 1}"
categories[ns][new_key] = zh_val
migration_map[old_key] = new_key
# 英文也同步处理
if old_key in en_data:
en_categories[ns][new_key] = en_data[old_key]
else:
en_categories[ns][new_key] = ''
# 写入中文语言包
print("生成中文语言包...")
for ns, data in categories.items():
filepath = os.path.join(NEW_ZH_DIR, f'{ns}.js')
nested = build_nested_dict(data)
write_lang_module(filepath, ns, nested)
print(f" {ns}.js: {len(data)} 个 key")
# 写入英文语言包
print("\n生成英文语言包...")
for ns, data in en_categories.items():
filepath = os.path.join(NEW_EN_DIR, f'{ns}.js')
nested = build_nested_dict(data)
write_lang_module(filepath, ns, nested)
print(f" {ns}.js: {len(data)} 个 key")
# 保存映射表
with open(MAP_FILE, 'w', encoding='utf-8') as f:
json.dump(migration_map, f, ensure_ascii=False, indent=2)
print(f"\n映射表已保存: {len(migration_map)} 个映射")
# 统计
total = len(migration_map)
print(f"\n迁移统计:")
print(f" 总 key 数: {total}")
print(f" 业务模块数: {len(categories)}")
for ns in sorted(categories.keys(), key=lambda x: -len(categories[x])):
print(f" {ns}: {len(categories[ns])} 个")
def apply_migration():
"""替换代码中所有旧 key"""
# 读取映射表
with open(MAP_FILE, 'r', encoding='utf-8') as f:
mapping = json.load(f)
print(f"加载映射表: {len(mapping)} 个映射")
# 统计
total_replacements = 0
files_modified = 0
pattern = re.compile(r'\$lang\(([\'"])([^\'"]+)([\'"])')
for dirname in SEARCH_DIRS:
dirpath = os.path.join(PROJECT_ROOT, dirname)
if not os.path.isdir(dirpath):
continue
for root, dirs, files in os.walk(dirpath):
for fname in files:
if not fname.endswith(('.vue', '.js')):
continue
fpath = os.path.join(root, fname)
try:
with open(fpath, 'r', encoding='utf-8') as f:
content = f.read()
original_content = content
def replace_match(m):
nonlocal total_replacements
quote1 = m.group(1)
old_key = m.group(2)
quote2 = m.group(3)
if old_key in mapping:
new_key = mapping[old_key]
total_replacements += 1
return f"$lang({quote1}{new_key}{quote2}"
return m.group(0)
content = pattern.sub(replace_match, content)
if content != original_content:
with open(fpath, 'w', encoding='utf-8') as f:
f.write(content)
files_modified += 1
rel_path = os.path.relpath(fpath, PROJECT_ROOT)
print(f" 修改: {rel_path}")
except Exception as e:
print(f" 跳过 {fname}: {e}")
print(f"\n完成!替换了 {total_replacements} 处调用,修改了 {files_modified} 个文件")
def main():
args = sys.argv[1:]
if not args or args[0] == '--analyze':
analyze()
elif args[0] == '--generate':
generate()
elif args[0] == '--apply':
apply_migration()
elif args[0] == '--all':
print("=" * 80)
print("步骤 1: 分析分类")
print("=" * 80)
analyze()
print("\n" + "=" * 80)
print("步骤 2: 生成新语言包")
print("=" * 80)
generate()
print("\n" + "=" * 80)
print("步骤 3: 替换代码中的旧 key")
print("=" * 80)
apply_migration()
else:
print(__doc__)
if __name__ == '__main__':
main()