feat(i18n): 添加国际化(i18n)基础设施和多语言支持

- 新增 I18nUtils 工具类提供多语言消息获取功能
- 创建多语言技术方案设计文档 MULTILANG_I18N_DESIGN.md
- 编写国际化战略文章 HEALTHLINK_HIS_INTERNATIONALIZATION_STRATEGY.md
- 添加国际化战略概述文档 HEALTHLINK_HIS_INTL_STRATEGY.md
- 实现基于 MessageSource 的多语言消息处理机制
- 设计支持中英越三语的国际化架构方案
- 规划前端 vue-i18n 和后端 MessageSource 集成方案
- 定义数据库多语言表结构支持菜单和字典国际化
- 制定硬编码消息迁移策略和实施计划
- 建立多语言工作量评估和风险应对措施
This commit is contained in:
2026-06-28 07:01:58 +08:00
parent 83f340b6bb
commit 27273dbb57
2 changed files with 584 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const BASE = path.join(__dirname, '..', 'src', 'views', 'medicationmanagement');
const CWD = path.join(__dirname, '..', '..');
function walkDir(dir) {
const files = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) files.push(...walkDir(fullPath));
else if (entry.name.endsWith('.vue')) files.push(fullPath);
}
return files;
}
function getOriginalContent(filePath) {
try {
const relPath = path.relative(CWD, filePath).replace(/\\/g, '/');
return execSync(`git show HEAD:"${relPath}"`, { encoding: 'utf8', cwd: CWD });
} catch (e) {
return null;
}
}
const vueFiles = walkDir(BASE);
const translations = {};
for (const file of vueFiles) {
const content = fs.readFileSync(file, 'utf8');
const original = getOriginalContent(file);
if (!original) continue;
const currentLines = content.split('\n');
const originalLines = original.split('\n');
for (let i = 0; i < currentLines.length; i++) {
const line = currentLines[i];
// Find t('key') calls in this line
const tMatch = line.match(/t\('(medication\.[^']+)'\)/);
if (!tMatch) continue;
const key = tMatch[1];
if (translations[key]) continue; // Already mapped
// Look at the same line in the original file
if (i < originalLines.length) {
const origLine = originalLines[i];
// Find Chinese text in the original line
const chineseMatch = origLine.match(/"([^"]*[\u4e00-\u9fff][^"]*)"/);
if (chineseMatch) {
translations[key] = chineseMatch[1];
}
}
}
}
console.log(`Extracted ${Object.keys(translations).length} translation keys`);
// Write flat keys
const flatPath = path.join(__dirname, '..', 'src', 'i18n', 'locales', 'medication_keys_flat.json');
fs.writeFileSync(flatPath, JSON.stringify(translations, null, 2), 'utf8');
// Write structured keys
const outputPath = path.join(__dirname, '..', 'src', 'i18n', 'locales', 'medication_keys.json');
const output = {};
for (const [key, value] of Object.entries(translations)) {
const parts = key.split('.');
let current = output;
for (let i = 0; i < parts.length - 1; i++) {
if (!current[parts[i]]) current[parts[i]] = {};
current = current[parts[i]];
}
current[parts[parts.length - 1]] = value;
}
fs.writeFileSync(outputPath, JSON.stringify(output, null, 2), 'utf8');
console.log(`Wrote ${flatPath}`);
console.log(`Wrote ${outputPath}`);

View File

@@ -0,0 +1,503 @@
const fs = require('fs');
const path = require('path');
const BASE = path.join(__dirname, '..', 'src', 'views', 'medicationmanagement');
const ZHCN_PATH = path.join(__dirname, '..', 'src', 'i18n', 'locales', 'zhCN.json');
const zhcn = JSON.parse(fs.readFileSync(ZHCN_PATH, 'utf8'));
function walkDir(dir) {
const files = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) files.push(...walkDir(fullPath));
else if (entry.name.endsWith('.vue')) files.push(fullPath);
}
return files;
}
const ATTR_PATTERNS = [
{ attr: 'placeholder', regex: /(\s+)placeholder="([^"]*[\u4e00-\u9fff][^"]*)"/g },
{ attr: 'label', regex: /(\s+)label="([^"]*[\u4e00-\u9fff][^"]*)"/g },
{ attr: 'title', regex: /(\s+)title="([^"]*[\u4e00-\u9fff][^"]*)"/g },
{ attr: 'start-placeholder', regex: /(\s+)start-placeholder="([^"]*[\u4e00-\u9fff][^"]*)"/g },
{ attr: 'end-placeholder', regex: /(\s+)end-placeholder="([^"]*[\u4e00-\u9fff][^"]*)"/g },
];
const COMMON_MAP = {
'操作': 'common.operation', '状态': 'common.status', '创建时间': 'common.createTime',
'更新时间': 'common.updateTime', '备注': 'common.remark', '请选择': 'common.pleaseSelect',
'请输入': 'common.pleaseEnter', '查询': 'common.search', '重置': 'common.reset',
'确认': 'common.confirm', '取消': 'common.cancel', '保存': 'common.save',
'删除': 'common.delete', '编辑': 'common.edit', '添加': 'common.add',
'查看': 'common.view', '详情': 'common.detail', '导出': 'common.export',
'导入': 'common.import', '关闭': 'common.close', '提交': 'common.submit',
'刷新': 'common.refresh', '男': 'common.male', '女': 'common.female',
'是': 'common.yes', '否': 'common.no', '正常': 'common.normal',
'停用': 'common.stop', '提示': 'common.tip', '警告': 'common.warning',
'错误': 'common.error', '成功': 'common.success', '消息': 'common.message',
'开始时间': 'common.startTime', '结束时间': 'common.endTime',
'开始日期': 'common.startDate', '结束日期': 'common.endDate',
'性别': 'common.gender', '年龄': 'common.age', '姓名': 'common.name',
'编码': 'common.code', '门诊号': 'common.outpatientNo',
'患者姓名': 'common.patientName', '无数据': 'common.noData',
};
// Comprehensive term mapping
const TERM = {
'药品名称': 'drugName', '药品编码': 'drugCode', '药品类型': 'drugType',
'药品规格': 'drugSpec', '药品批号': 'drugBatchNo', '药品库存': 'drugStock',
'药品单价': 'drugUnitPrice', '药品数量': 'drugQty', '药品金额': 'drugAmount',
'药品单位': 'drugUnit', '药品分类': 'drugCategory', '药品信息': 'drugInfo',
'药品列表': 'drugList', '药品详情': 'drugDetail', '药品汇总': 'drugSummary',
'品名': 'itemName', '项目名称': 'itemName', '项目编码': 'itemCode',
'项目类型': 'itemType', '项目类别': 'itemCategory', '项目总价': 'itemTotalPrice',
'项目信息': 'itemInfo', '项目列表': 'itemList', '项目详情': 'itemDetail',
'项目汇总': 'itemSummary', '项目明细': 'itemDetail', '项目': 'item',
'规格型号': 'specModel', '规格': 'spec', '单位': 'unit', '数量': 'qty',
'单价': 'unitPrice', '金额': 'amount', '总价': 'totalPrice',
'合计金额': 'totalAmount', '小计': 'subtotal', '总计': 'grandTotal',
'合计': 'total', '差额': 'diff', '盈亏': 'profitLoss',
'盘盈': 'surplus', '盘亏': 'shortage', '盈亏数量': 'profitLossQty',
'盈亏金额': 'profitLossAmount', '已审核': 'approved', '未审核': 'pending',
'待审核': 'pendingApproval', '已退回': 'returned', '已作废': 'voided',
'已结清': 'settled', '未结清': 'unsettled', '全部': 'all',
'西药': 'westernMedicine', '中成药': 'chinesePatentMedicine',
'中草药': 'chineseHerbalMedicine', '中药': 'chineseMedicine',
'耗材': 'consumable', '医疗器械': 'medicalDevice', '类别': 'category',
'分类': 'classification', '剂型': 'dosageForm', '用法': 'usage',
'用量': 'dosage', '频次': 'frequency', '天数': 'days',
'总量': 'totalQty', '单量': 'dosePerTime', '库存下限': 'minStock',
'库存上限': 'maxStock', '安全库存': 'safeStock', '批号': 'batchNo',
'有效期': 'expiryDate', '有效期至': 'expiryDate', '产地': 'origin',
'生产厂家': 'manufacturer', '厂家/产地': 'manufacturer', '商品名': 'tradeName',
'通用名': 'genericName', '拼音码': 'pinyinCode', '五笔码': 'wubiCode',
'医保编码': 'insuranceCode', '自费': 'selfPay', '甲类': 'classA',
'乙类': 'classB', '丙类': 'classC', '处方量': 'prescriptionQty',
'发药量': 'dispenseQty', '采购量': 'purchaseQty', '入库量': 'inboundQty',
'出库量': 'outboundQty', '退货量': 'returnQty', '调拨量': 'transferQty',
'盘点量': 'stocktakeQty', '报损量': 'lossQty', '消耗量': 'consumptionQty',
'收费员': 'cashier', '挂号员': 'registrar', '医师': 'doctor',
'药房': 'pharmacy', '药库': 'drugStorehouse', '门诊': 'outpatient',
'住院': 'inpatient', '急诊': 'emergency', '日期': 'date', '时间': 'time',
'年': 'year', '月': 'month', '日': 'day', '明细': 'detail', '汇总': 'summary',
'清单': 'list', '报表': 'report', '统计': 'statistics', '分析': 'analysis',
'预警': 'warning', '提醒': 'reminder', '通知': 'notification', '设置': 'settings',
'配置': 'config', '管理': 'management', '维护': 'maintenance', '审核': 'audit',
'审批': 'approval', '签收': 'sign', '确认': 'confirm', '退回': 'return',
'作废': 'void', '打印': 'print', '预览': 'preview', '下载': 'download',
'上传': 'upload', '新增': 'add', '修改': 'edit', '仓库': 'warehouse',
'货位': 'location', '请求日期': 'requestDate', '单据号': 'docNo',
'单据日期': 'docDate', '药品类型': 'drugType', '源仓库类型': 'sourceWarehouseType',
'源仓库': 'sourceWarehouse', '源货位': 'sourceLocation',
'目的仓库类型': 'targetWarehouseType', '目的仓库': 'targetWarehouse',
'目的货位': 'targetLocation', '产品批号': 'batchNo', '单价(元)': 'unitPriceYuan',
'发放数量': 'issueQty', '库存数量': 'stockQty', '实盘数量': 'actualQty',
'最小单位': 'minUnit', '拆零比': 'splitRatio', '序号': 'seqNo',
'调拨单位': 'transferUnit', '源仓库库存数量': 'sourceStockQty',
'目的仓库库存数量': 'targetStockQty', '调拨数量': 'transferQty',
'调拨单价': 'transferPrice', '生产日期': 'productionDate',
'调拨单据明细': 'transferDocDetail', '收费时间': 'chargeTime',
'统计类型': 'statisticsType', '科室': 'department', '开单人': 'orderingDoctor',
'收费人': 'cashier', '项目类型': 'itemType', '选择项目类型': 'selectItemType',
'医保号': 'medicalInsuranceNo', '药品项目': 'drugItem',
'医保码': 'medicalInsuranceCode', '医保类别': 'medicalInsuranceCategory',
'医保等级': 'medicalInsuranceLevel', '科室名': 'deptName',
'耗材和药品总金额': 'consumableDrugTotal', '产品型号': 'productModel',
'所属科室': 'belongingDept', '发放时间': 'issueTime', '版本号': 'versionNo',
'搜索': 'search', '请输入项目名称': 'inputItemName',
'请选择开始时间': 'selectStartTime', '请选择结束时间': 'selectEndTime',
'请选择目的货位': 'selectTargetLocation', '盘点仓库': 'checkWarehouse',
'盘点单号': 'checkDocNo', '盘点日期': 'checkDate', '盘点类型': 'checkType',
'盘点状态': 'checkStatus', '盘点人': 'stocktaker', '盘点时间': 'stocktakeTime',
'审核状态': 'approvalStatus', '审核人': 'approver', '审核时间': 'approvalTime',
'制单人': 'maker', '制单时间': 'makeTime', '调拨单号': 'transferDocNo',
'调拨日期': 'transferDate', '调拨类型': 'transferType', '调拨状态': 'transferStatus',
'申请人': 'applicant', '申请时间': 'applyTime', '审批人': 'approver',
'审批时间': 'approvalTime', '批准文号': 'approvalNo', '供应商': 'supplier',
'采购价': 'purchasePrice', '零售价': 'retailPrice', '进价': 'purchasePrice',
'售价': 'sellingPrice', '库存': 'stock', '当前库存': 'currentStock',
'可用库存': 'availableStock', '报损数量': 'lossQty', '报损原因': 'lossReason',
'报损单号': 'lossDocNo', '报损日期': 'lossDate', '原价': 'originalPrice',
'新价': 'newPrice', '差价': 'priceDiff', '入库单号': 'inboundNo',
'入库日期': 'inboundDate', '入库类型': 'inboundType', '出库单号': 'outboundNo',
'出库日期': 'outboundDate', '出库类型': 'outboundType', '退货单号': 'returnNo',
'退货日期': 'returnDate', '退货状态': 'returnStatus',
'对账日期': 'reconciliationDate', '对账状态': 'reconciliationStatus',
'结余金额': 'balanceAmount', '期初金额': 'initialAmount',
'期末金额': 'finalAmount', '入库金额': 'inboundAmount',
'出库金额': 'outboundAmount', '科室汇总': 'deptSummary',
'月结年月': 'settlementMonth', '月结状态': 'settlementStatus',
'月结日期': 'settlementDate', '门诊收费统计': 'outpatientChargeStats',
'收费项目': 'chargeItem', '收费金额': 'chargeAmount', '收费明细': 'chargeDetail',
'收费汇总': 'chargeSummary', '患者': 'patient', '就诊日期': 'visitDate',
'诊断': 'diagnosis', '处方号': 'prescriptionNo', '处方日期': 'prescriptionDate',
'处方状态': 'prescriptionStatus', '发药窗口': 'dispenseWindow', '发药人': 'dispenser',
'发药时间': 'dispenseTime', '配药人': 'preparer', '配药时间': 'prepareTime',
'退药': 'returnDrug', '退药数量': 'returnDrugQty', '退药原因': 'returnDrugReason',
'退药时间': 'returnDrugTime', '请选择仓库': 'selectWarehouse',
'请选择货位': 'selectLocation', '请选择药品类型': 'selectDrugType',
'请选择源仓库类型': 'selectSourceWarehouseType', '请选择源仓库': 'selectSourceWarehouse',
'请选择源货位': 'selectSourceLocation', '请选择目的仓库类型': 'selectTargetWarehouseType',
'请选择目的仓库': 'selectTargetWarehouse', '请选择目的货位': 'selectTargetLocation',
'请输入单据号': 'inputDocNo', '请输入药品名称': 'inputDrugName',
'请输入药品编码': 'inputDrugCode', '请输入批号': 'inputBatchNo',
'请输入数量': 'inputQty', '请输入单价': 'inputUnitPrice',
'请输入备注': 'inputRemark', '请输入规格': 'inputSpec',
'请输入供应商': 'inputSupplier', '请输入生产厂家': 'inputManufacturer',
'请输入批准文号': 'inputApprovalNo', '请选择日期': 'selectDate',
'请选择状态': 'selectStatus', '请选择类型': 'selectType',
'请选择分类': 'selectCategory', '请输入关键词搜索': 'inputKeyword',
'输入项目名/拼音搜索': 'searchByItemName', '请选择审核状态': 'selectApprovalStatus',
'请选择盘点类型': 'selectCheckType', '请选择盘点仓库': 'selectCheckWarehouse',
'请选择结算类型': 'selectSettlementType', '请选择对账状态': 'selectReconciliationStatus',
'请输入项目编码': 'inputItemCode', '请输入患者姓名': 'inputPatientName',
'请输入门诊号': 'inputOutpatientNo', '请输入医保号': 'inputMedicalInsuranceNo',
'请选择科室': 'selectDepartment', '请选择开单人': 'selectOrderingDoctor',
'请选择收费人': 'selectCashier', '请选择发药窗口': 'selectDispenseWindow',
'请选择发药人': 'selectDispenser', '请选择处方状态': 'selectPrescriptionStatus',
'请选择退药原因': 'selectReturnReason', '请选择报损原因': 'selectLossReason',
'请选择调价原因': 'selectPriceAdjReason', '请选择入库类型': 'selectInboundType',
'请选择出库类型': 'selectOutboundType', '请选择退货状态': 'selectReturnStatus',
'请选择供应商': 'selectSupplier', '请选择药品': 'selectDrug',
'请选择剂型': 'selectDosageForm', '请选择用法': 'selectUsage',
'请选择频次': 'selectFrequency', '请选择医师': 'selectDoctor',
'请选择药房': 'selectPharmacy', '请选择药库': 'selectDrugStorehouse',
'请选择门诊': 'selectOutpatient', '请选择住院': 'selectInpatient',
'请选择急诊': 'selectEmergency', '请输入拼音码搜索': 'searchByPinyin',
'请输入通用名搜索': 'searchByGenericName', '请输入商品名搜索': 'searchByTradeName',
'请选择日期范围': 'selectDateRange', '请输入库存下限': 'inputMinStock',
'请输入库存上限': 'inputMaxStock', '请输入安全库存': 'inputSafeStock',
'请输入有效期': 'inputExpiryDate', '请输入产地': 'inputOrigin',
'请输入用量': 'inputDosage', '请输入天数': 'inputDays',
'请输入总量': 'inputTotalQty', '请输入单量': 'inputDosePerTime',
'请输入金额': 'inputAmount', '请输入价格': 'inputPrice',
'请输入差价': 'inputPriceDiff', '请输入原因': 'inputReason',
'请输入说明': 'inputDescription', '请输入内容': 'inputContent',
'请输入标题': 'inputTitle', '请输入名称': 'inputName',
'请输入编号': 'inputCode', '请输入电话': 'inputPhone',
'请输入地址': 'inputAddress', '请选择操作': 'selectOperation',
'请选择时间': 'selectTime', '请选择年月': 'selectYearMonth',
'请选择年份': 'selectYear', '请选择月份': 'selectMonth',
'请输入单据号': 'inputDocNo', '单据类型': 'docType', '单据状态': 'docStatus',
'采购单号': 'purchaseNo', '采购日期': 'purchaseDate', '采购类型': 'purchaseType',
'采购状态': 'purchaseStatus', '采购人': 'buyer', '采购时间': 'purchaseTime',
'采购数量': 'purchaseQty', '采购金额': 'purchaseAmount',
'采购单价': 'purchaseUnitPrice', '采购单位': 'purchaseUnit',
'采购批号': 'purchaseBatchNo', '采购有效期': 'purchaseExpiryDate',
'采购产地': 'purchaseOrigin', '采购生产厂家': 'purchaseManufacturer',
'采购商品名': 'purchaseTradeName', '采购通用名': 'purchaseGenericName',
'请领单号': 'requisitionNo', '请领日期': 'requisitionDate',
'请领类型': 'requisitionType', '请领状态': 'requisitionStatus',
'请领人': 'requisitioner', '请领时间': 'requisitionTime',
'请领数量': 'requisitionQty', '请领金额': 'requisitionAmount',
'请领单价': 'requisitionUnitPrice', '请领单位': 'requisitionUnit',
'退回单号': 'returnNo', '退回日期': 'returnDate', '退回类型': 'returnType',
'退回状态': 'returnStatus', '退回人': 'returnPerson', '退回时间': 'returnTime',
'退回原因': 'returnReason', '退回数量': 'returnQty', '退回金额': 'returnAmount',
'退回单价': 'returnUnitPrice', '退回单位': 'returnUnit', '退回批号': 'returnBatchNo',
'调拨单号': 'transferNo', '调拨日期': 'transferDate', '调拨类型': 'transferType',
'调拨状态': 'transferStatus', '调拨人': 'transferPerson', '调拨时间': 'transferTime',
'调拨原因': 'transferReason', '调拨数量': 'transferQty', '调拨金额': 'transferAmount',
'调拨单价': 'transferUnitPrice', '调拨单位': 'transferUnit',
'调拨批号': 'transferBatchNo', '对账日期': 'reconciliationDate',
'对账类型': 'reconciliationType', '对账状态': 'reconciliationStatus',
'对账人': 'reconciler', '对账时间': 'reconciliationTime',
'对账数量': 'reconciliationQty', '对账金额': 'reconciliationAmount',
'对账单价': 'reconciliationUnitPrice', '对账单位': 'reconciliationUnit',
'对账批号': 'reconciliationBatchNo', '结余金额': 'balanceAmount',
'期初金额': 'initialAmount', '期末金额': 'finalAmount',
'入库金额': 'inboundAmount', '出库金额': 'outboundAmount',
'月结年月': 'settlementMonth', '月结状态': 'settlementStatus',
'月结日期': 'settlementDate', '门诊收费统计': 'outpatientChargeStats',
'收费项目': 'chargeItem', '收费金额': 'chargeAmount', '收费明细': 'chargeDetail',
'收费汇总': 'chargeSummary', '就诊日期': 'visitDate', '诊断': 'diagnosis',
'处方号': 'prescriptionNo', '处方日期': 'prescriptionDate',
'处方状态': 'prescriptionStatus', '发药窗口': 'dispenseWindow',
'发药人': 'dispenser', '发药时间': 'dispenseTime', '配药人': 'preparer',
'配药时间': 'prepareTime', '退药': 'returnDrug', '退药数量': 'returnDrugQty',
'退药原因': 'returnDrugReason', '退药时间': 'returnDrugTime',
'盘点人': 'stocktaker', '盘点时间': 'stocktakeTime',
'请选择仓库': 'selectWarehouse', '请选择货位': 'selectLocation',
'请选择药品类型': 'selectDrugType', '请选择源仓库类型': 'selectSourceWarehouseType',
'请选择源仓库': 'selectSourceWarehouse', '请选择源货位': 'selectSourceLocation',
'请选择目的仓库类型': 'selectTargetWarehouseType', '请选择目的仓库': 'selectTargetWarehouse',
'请选择目的货位': 'selectTargetLocation', '请输入单据号': 'inputDocNo',
'请输入药品名称': 'inputDrugName', '请输入药品编码': 'inputDrugCode',
'请输入批号': 'inputBatchNo', '请输入数量': 'inputQty',
'请输入单价': 'inputUnitPrice', '请输入备注': 'inputRemark',
'请输入规格': 'inputSpec', '请输入供应商': 'inputSupplier',
'请输入生产厂家': 'inputManufacturer', '请输入批准文号': 'inputApprovalNo',
'请选择日期': 'selectDate', '请选择状态': 'selectStatus',
'请选择类型': 'selectType', '请选择分类': 'selectCategory',
'请输入关键词搜索': 'inputKeyword', '输入项目名/拼音搜索': 'searchByItemName',
'请选择审核状态': 'selectApprovalStatus', '请选择盘点类型': 'selectCheckType',
'请选择盘点仓库': 'selectCheckWarehouse', '请选择结算类型': 'selectSettlementType',
'请选择对账状态': 'selectReconciliationStatus', '请输入项目编码': 'inputItemCode',
'请输入患者姓名': 'inputPatientName', '请输入门诊号': 'inputOutpatientNo',
'请输入医保号': 'inputMedicalInsuranceNo', '请选择科室': 'selectDepartment',
'请选择开单人': 'selectOrderingDoctor', '请选择收费人': 'selectCashier',
'请选择发药窗口': 'selectDispenseWindow', '请选择发药人': 'selectDispenser',
'请选择处方状态': 'selectPrescriptionStatus', '请选择退药原因': 'selectReturnReason',
'请选择报损原因': 'selectLossReason', '请选择调价原因': 'selectPriceAdjReason',
'请选择入库类型': 'selectInboundType', '请选择出库类型': 'selectOutboundType',
'请选择退货状态': 'selectReturnStatus', '请选择供应商': 'selectSupplier',
'请选择药品': 'selectDrug', '请选择剂型': 'selectDosageForm',
'请选择用法': 'selectUsage', '请选择频次': 'selectFrequency',
'请选择医师': 'selectDoctor', '请选择药房': 'selectPharmacy',
'请选择药库': 'selectDrugStorehouse', '请选择门诊': 'selectOutpatient',
'请选择住院': 'selectInpatient', '请选择急诊': 'selectEmergency',
'请输入拼音码搜索': 'searchByPinyin', '请输入通用名搜索': 'searchByGenericName',
'请输入商品名搜索': 'searchByTradeName', '请选择日期范围': 'selectDateRange',
'请输入库存下限': 'inputMinStock', '请输入库存上限': 'inputMaxStock',
'请输入安全库存': 'inputSafeStock', '请输入有效期': 'inputExpiryDate',
'请输入产地': 'inputOrigin', '请输入用量': 'inputDosage',
'请输入天数': 'inputDays', '请输入总量': 'inputTotalQty',
'请输入单量': 'inputDosePerTime', '请输入金额': 'inputAmount',
'请输入价格': 'inputPrice', '请输入差价': 'inputPriceDiff',
'请输入原因': 'inputReason', '请输入说明': 'inputDescription',
'请输入内容': 'inputContent', '请输入标题': 'inputTitle',
'请输入名称': 'inputName', '请输入编号': 'inputCode',
'请输入电话': 'inputPhone', '请输入地址': 'inputAddress',
'请选择操作': 'selectOperation', '请选择时间': 'selectTime',
'请选择年月': 'selectYearMonth', '请选择年份': 'selectYear',
'请选择月份': 'selectMonth', '调价单号': 'priceAdjNo', '调价日期': 'priceAdjDate',
'调价类型': 'priceAdjType', '调价状态': 'priceAdjStatus', '调价人': 'priceAdjuster',
'调价时间': 'priceAdjTime', '调价原因': 'priceAdjReason', '调价数量': 'priceAdjQty',
'调价金额': 'priceAdjAmount', '调价单价': 'priceAdjUnitPrice',
'调价单位': 'priceAdjUnit', '调价批号': 'priceAdjBatchNo',
'报损单号': 'lossDocNo', '报损日期': 'lossDate', '报损类型': 'lossType',
'报损状态': 'lossStatus', '报损人': 'lossPerson', '报损时间': 'lossTime',
'报损原因': 'lossReason', '报损数量': 'lossQty', '报损金额': 'lossAmount',
'报损单价': 'lossUnitPrice', '报损单位': 'lossUnit', '报损批号': 'lossBatchNo',
'入库单号': 'inboundNo', '入库日期': 'inboundDate', '入库类型': 'inboundType',
'入库人': 'inboundPerson', '入库时间': 'inboundTime', '入库数量': 'inboundQty',
'入库金额': 'inboundAmount', '入库单价': 'inboundUnitPrice',
'出库单号': 'outboundNo', '出库日期': 'outboundDate', '出库类型': 'outboundType',
'出库人': 'outboundPerson', '出库时间': 'outboundTime', '出库数量': 'outboundQty',
'出库金额': 'outboundAmount', '出库单价': 'outboundUnitPrice',
'退货单号': 'returnNo', '退货日期': 'returnDate', '退货类型': 'returnType',
'退货状态': 'returnStatus', '退货人': 'returnPerson', '退货时间': 'returnTime',
'退货原因': 'returnReason', '退货数量': 'returnQty', '退货金额': 'returnAmount',
'退货单价': 'returnUnitPrice', '退货单位': 'returnUnit', '退货批号': 'returnBatchNo',
'请选择开始日期': 'selectStartDate', '请选择结束日期': 'selectEndDate',
'请输入项目名称': 'inputItemName', '请输入仓库': 'inputWarehouse',
'请输入货位': 'inputLocation', '请输入药品类型': 'inputDrugType',
'请输入源仓库类型': 'inputSourceWarehouseType', '请输入源仓库': 'inputSourceWarehouse',
'请输入源货位': 'inputSourceLocation', '请输入目的仓库类型': 'inputTargetWarehouseType',
'请输入目的仓库': 'inputTargetWarehouse', '请输入目的货位': 'inputTargetLocation',
'请输入调拨单号': 'inputTransferDocNo', '请输入调拨日期': 'inputTransferDate',
'请输入调拨类型': 'inputTransferType', '请输入调拨状态': 'inputTransferStatus',
'请输入调拨人': 'inputTransferPerson', '请输入调拨时间': 'inputTransferTime',
'请输入调拨原因': 'inputTransferReason', '请输入调拨数量': 'inputTransferQty',
'请输入调拨金额': 'inputTransferAmount', '请输入调拨单价': 'inputTransferUnitPrice',
'请输入调拨单位': 'inputTransferUnit', '请输入调拨批号': 'inputTransferBatchNo',
'请输入对账日期': 'inputReconciliationDate', '请输入对账类型': 'inputReconciliationType',
'请输入对账状态': 'inputReconciliationStatus', '请输入对账人': 'inputReconciler',
'请输入对账时间': 'inputReconciliationTime', '请输入对账数量': 'inputReconciliationQty',
'请输入对账金额': 'inputReconciliationAmount', '请输入对账单价': 'inputReconciliationUnitPrice',
'请输入对账单位': 'inputReconciliationUnit', '请输入对账批号': 'inputReconciliationBatchNo',
'请输入结余金额': 'inputBalanceAmount', '请输入期初金额': 'inputInitialAmount',
'请输入期末金额': 'inputFinalAmount', '请输入入库金额': 'inputInboundAmount',
'请输入出库金额': 'inputOutboundAmount', '请输入月结年月': 'inputSettlementMonth',
'请输入月结状态': 'inputSettlementStatus', '请输入月结日期': 'inputSettlementDate',
'请输入门诊收费统计': 'inputOutpatientChargeStats', '请输入收费项目': 'inputChargeItem',
'请输入收费金额': 'inputChargeAmount', '请输入收费明细': 'inputChargeDetail',
'请输入收费汇总': 'inputChargeSummary', '请输入患者': 'inputPatient',
'请输入就诊日期': 'inputVisitDate', '请输入诊断': 'inputDiagnosis',
'请输入处方号': 'inputPrescriptionNo', '请输入处方日期': 'inputPrescriptionDate',
'请输入处方状态': 'inputPrescriptionStatus', '请输入发药窗口': 'inputDispenseWindow',
'请输入发药人': 'inputDispenser', '请输入发药时间': 'inputDispenseTime',
'请输入配药人': 'inputPreparer', '请输入配药时间': 'inputPrepareTime',
'请输入退药': 'inputReturnDrug', '请输入退药数量': 'inputReturnDrugQty',
'请输入退药原因': 'inputReturnDrugReason', '请输入退药时间': 'inputReturnDrugTime',
'请输入盘点人': 'inputStocktaker', '请输入盘点时间': 'inputStocktakeTime',
'请输入请选择仓库': 'inputSelectWarehouse', '请输入请选择货位': 'inputSelectLocation',
'请输入请选择药品类型': 'inputSelectDrugType',
'请输入请选择源仓库类型': 'inputSelectSourceWarehouseType',
'请输入请选择源仓库': 'inputSelectSourceWarehouse',
'请输入请选择源货位': 'inputSelectSourceLocation',
'请输入请选择目的仓库类型': 'inputSelectTargetWarehouseType',
'请输入请选择目的仓库': 'inputSelectTargetWarehouse',
'请输入请选择目的货位': 'inputSelectTargetLocation',
'请输入请选择日期': 'inputSelectDate', '请输入请选择状态': 'inputSelectStatus',
'请输入请选择类型': 'inputSelectType', '请输入请选择分类': 'inputSelectCategory',
'请输入请输入关键词搜索': 'inputKeyword', '请输入输入项目名/拼音搜索': 'searchByItemName',
'请输入请选择审核状态': 'inputSelectApprovalStatus',
'请输入请选择盘点类型': 'inputSelectCheckType',
'请输入请选择盘点仓库': 'inputSelectCheckWarehouse',
'请输入请选择结算类型': 'inputSelectSettlementType',
'请输入请选择对账状态': 'inputSelectReconciliationStatus',
'请输入请选择科室': 'inputSelectDepartment',
'请输入请选择开单人': 'inputSelectOrderingDoctor',
'请输入请选择收费人': 'inputSelectCashier',
'请输入请选择发药窗口': 'inputSelectDispenseWindow',
'请输入请选择发药人': 'inputSelectDispenser',
'请输入请选择处方状态': 'inputSelectPrescriptionStatus',
'请输入请选择退药原因': 'inputSelectReturnReason',
'请输入请选择报损原因': 'inputSelectLossReason',
'请输入请选择调价原因': 'inputSelectPriceAdjReason',
'请输入请选择入库类型': 'inputSelectInboundType',
'请输入请选择出库类型': 'inputSelectOutboundType',
'请输入请选择退货状态': 'inputSelectReturnStatus',
'请输入请选择供应商': 'inputSelectSupplier', '请输入请选择药品': 'inputSelectDrug',
'请输入请选择剂型': 'inputSelectDosageForm', '请输入请选择用法': 'inputSelectUsage',
'请输入请选择频次': 'inputSelectFrequency', '请输入请选择医师': 'inputSelectDoctor',
'请输入请选择药房': 'inputSelectPharmacy', '请输入请选择药库': 'inputSelectDrugStorehouse',
'请输入请选择门诊': 'inputSelectOutpatient', '请输入请选择住院': 'inputSelectInpatient',
'请输入请选择急诊': 'inputSelectEmergency', '请输入请输入拼音码搜索': 'searchByPinyin',
'请输入请输入通用名搜索': 'searchByGenericName',
'请输入请输入商品名搜索': 'searchByTradeName',
'请输入请选择日期范围': 'inputSelectDateRange',
'请输入请选择开始时间': 'inputSelectStartTime',
'请输入请选择结束时间': 'inputSelectEndTime',
};
function getModuleName(filePath) {
const rel = path.relative(BASE, filePath);
const parts = rel.split(path.sep);
if (parts.length === 1) return path.basename(filePath, '.vue');
if (parts.length === 2 && parts[1] === 'index.vue') return parts[0];
if (parts.length >= 3) {
if (parts[1] === 'components') return parts[0] + '_' + path.basename(filePath, '.vue');
return parts[1];
}
return parts[0];
}
function chineseToKey(text, moduleName) {
let clean = text.replace(/[:]\s*$/, '').trim();
if (COMMON_MAP[clean]) return COMMON_MAP[clean];
if (TERM[clean]) return `medication.${moduleName}.${TERM[clean]}`;
// Try prefix decomposition
let prefix = '';
let remaining = clean;
for (const [zh, en] of [['请输入', 'input'], ['请选择', 'select']]) {
if (remaining.startsWith(zh)) {
prefix = en;
remaining = remaining.substring(zh.length);
break;
}
}
if (remaining && TERM[remaining]) {
const base = TERM[remaining];
return `medication.${moduleName}.${prefix}${base.charAt(0).toUpperCase()}${base.slice(1)}`;
}
// Try partial match
for (const [zh, en] of Object.entries(TERM)) {
if (remaining.includes(zh)) {
return `medication.${moduleName}.${prefix}${en.charAt(0).toUpperCase()}${en.slice(1)}`;
}
}
// Fallback: use hex of Chinese
const hex = Buffer.from(clean, 'utf8').toString('hex').substring(0, 16);
return `medication.${moduleName}.${prefix || 'val'}_${hex}`;
}
// Global translation collection
const allTranslations = {};
function processFile(filePath) {
let content = fs.readFileSync(filePath, 'utf8');
const original = content;
const moduleName = getModuleName(filePath);
let hasChanges = false;
const hasI18n = content.includes('useI18n');
for (const { attr, regex } of ATTR_PATTERNS) {
const re = new RegExp(regex.source, regex.flags);
let match;
const replacements = [];
while ((match = re.exec(content)) !== null) {
const [fullMatch, whitespace, chineseText] = match;
const key = chineseToKey(chineseText, moduleName);
allTranslations[key] = chineseText;
const replacement = `${whitespace}:${attr}="t('${key}')"`;
replacements.push({ fullMatch, replacement, index: match.index });
}
for (let i = replacements.length - 1; i >= 0; i--) {
const { fullMatch, replacement, index } = replacements[i];
content = content.substring(0, index) + replacement + content.substring(index + fullMatch.length);
hasChanges = true;
}
}
if (!hasChanges) return;
if (!hasI18n) {
const scriptSetupMatch = content.match(/(<script\s+setup[^>]*>)/);
if (scriptSetupMatch) {
const scriptTag = scriptSetupMatch[1];
const insertPos = content.indexOf(scriptTag) + scriptTag.length;
const afterScript = content.substring(insertPos);
const firstImportMatch = afterScript.match(/\n(import\s)/);
if (firstImportMatch) {
const importPos = insertPos + afterScript.indexOf(firstImportMatch[1]);
content = content.substring(0, importPos) +
"import { useI18n } from 'vue-i18n';\n" +
content.substring(importPos);
} else {
content = content.substring(0, insertPos) +
"\nimport { useI18n } from 'vue-i18n';" +
content.substring(insertPos);
}
const importSection = content.match(/(<script[^>]*>[\s\S]*?)(?=\n(?:const|let|var|function|\/\/|\/\*|\*|<\/script>))/);
if (importSection) {
const lastImportEnd = importSection[0].length + content.indexOf(importSection[0]);
const afterImports = content.substring(lastImportEnd);
const firstCodeMatch = afterImports.match(/\n(const|let|var|function|\/\/|\/\*|\*)/);
if (firstCodeMatch) {
const codePos = lastImportEnd + afterImports.indexOf(firstCodeMatch[1]);
content = content.substring(0, codePos) +
"const { t } = useI18n();\n" +
content.substring(codePos);
}
}
}
}
if (content !== original) {
fs.writeFileSync(filePath, content, 'utf8');
}
}
// Main
const vueFiles = walkDir(BASE);
console.log(`Processing ${vueFiles.length} Vue files...`);
for (const file of vueFiles) {
processFile(file);
}
console.log(`Collected ${Object.keys(allTranslations).length} translation keys`);
// Write flat keys
const flatPath = path.join(__dirname, '..', 'src', 'i18n', 'locales', 'medication_keys_flat.json');
fs.writeFileSync(flatPath, JSON.stringify(allTranslations, null, 2), 'utf8');
// Write structured keys
const outputPath = path.join(__dirname, '..', 'src', 'i18n', 'locales', 'medication_keys.json');
const output = {};
for (const [key, value] of Object.entries(allTranslations)) {
const parts = key.split('.');
let current = output;
for (let i = 0; i < parts.length - 1; i++) {
if (!current[parts[i]]) current[parts[i]] = {};
current = current[parts[i]];
}
current[parts[parts.length - 1]] = value;
}
fs.writeFileSync(outputPath, JSON.stringify(output, null, 2), 'utf8');
console.log(`Wrote ${flatPath}`);
console.log(`Wrote ${outputPath}`);