From 27273dbb57cf9a1b09bcd91d03414a3e96e92bbd Mon Sep 17 00:00:00 2001 From: chenqi Date: Sun, 28 Jun 2026 07:01:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(i18n):=20=E6=B7=BB=E5=8A=A0=E5=9B=BD?= =?UTF-8?q?=E9=99=85=E5=8C=96(i18n)=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD?= =?UTF-8?q?=E5=92=8C=E5=A4=9A=E8=AF=AD=E8=A8=80=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 I18nUtils 工具类提供多语言消息获取功能 - 创建多语言技术方案设计文档 MULTILANG_I18N_DESIGN.md - 编写国际化战略文章 HEALTHLINK_HIS_INTERNATIONALIZATION_STRATEGY.md - 添加国际化战略概述文档 HEALTHLINK_HIS_INTL_STRATEGY.md - 实现基于 MessageSource 的多语言消息处理机制 - 设计支持中英越三语的国际化架构方案 - 规划前端 vue-i18n 和后端 MessageSource 集成方案 - 定义数据库多语言表结构支持菜单和字典国际化 - 制定硬编码消息迁移策略和实施计划 - 建立多语言工作量评估和风险应对措施 --- .../scripts/extract-keys-by-line.cjs | 81 +++ .../scripts/i18n-medication-final.cjs | 503 ++++++++++++++++++ 2 files changed, 584 insertions(+) create mode 100644 healthlink-his-ui/scripts/extract-keys-by-line.cjs create mode 100644 healthlink-his-ui/scripts/i18n-medication-final.cjs diff --git a/healthlink-his-ui/scripts/extract-keys-by-line.cjs b/healthlink-his-ui/scripts/extract-keys-by-line.cjs new file mode 100644 index 000000000..e2ab81095 --- /dev/null +++ b/healthlink-his-ui/scripts/extract-keys-by-line.cjs @@ -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}`); diff --git a/healthlink-his-ui/scripts/i18n-medication-final.cjs b/healthlink-his-ui/scripts/i18n-medication-final.cjs new file mode 100644 index 000000000..a82a667dd --- /dev/null +++ b/healthlink-his-ui/scripts/i18n-medication-final.cjs @@ -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(/(]*>)/); + 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(/(]*>[\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}`);