export function validateModuleCode(code) { const value = String(code ?? '').trim() if (!value) return '编码不能为空' if (value.length < 2 || value.length > 50) return '编码长度需为 2-50 个字符' if (!/^[a-z][a-z0-9_]*$/.test(value)) { return '编码须以小写字母开头,且只能包含小写字母、数字和下划线' } return '' } export function getI18nNamespace(key) { return String(key || '').split('.', 1)[0] || 'common' } export function isValidI18nKey(key) { return /^[A-Za-z][A-Za-z0-9_.:-]{1,149}$/.test(String(key || '')) } export function buildModuleItemI18nKey(prefix, moduleCode, itemCode) { const normalizedModuleCode = String(moduleCode || '').trim() const normalizedItemCode = String(itemCode || '').trim() return normalizedModuleCode && normalizedItemCode ? `${prefix}.${normalizedModuleCode}.${normalizedItemCode}` : null } export function buildModuleI18nKey(moduleType, moduleCode) { const normalizedModuleCode = String(moduleCode || '').trim() if (!normalizedModuleCode) return null const prefix = moduleType === 'data' || moduleType === 'query' ? 'module' : 'menu' return `${prefix}.${normalizedModuleCode}` } export function syncModuleItemI18nKeys(rows, prefix, moduleCode, codeField) { let changed = false for (const row of rows || []) { const key = buildModuleItemI18nKey(prefix, moduleCode, row?.[codeField]) if (row.b_i18n === key) continue row.b_i18n = key changed = true } return changed } export function groupI18nRows(rows) { const groups = {} rows.forEach((item) => { const key = item.b_key groups[key] ||= { key, values: {}, records: {} } groups[key].values[item.b_locale] = item.b_value groups[key].records[item.b_locale] = item }) return groups } // 将同步字段生成的默认翻译条目并入透视行数组: // - 已存在同 b_key 的行则补全其空语言值(不覆盖用户已填内容); // - 否则新增一行;managedId 用于关联字段,使重命名字段时复用同一行(改名而非新增)。 // 返回新的数组,不修改入参。 export function mergeI18nDefaults(rows, entries) { const next = [...rows] const byKey = new Map(next.map((r) => [String(r.b_key), r])) const managedIds = new Set( next .map((r) => r._managedId) .filter(Boolean) .map(String), ) for (const e of entries || []) { if (!e.key) continue const existing = byKey.get(String(e.key)) if (existing) { // 已存在同键行:仅补全空值,保留用户已配置的翻译 if (existing[e.locale] == null || existing[e.locale] === '') existing[e.locale] = e.value continue } // 同键不存在,但该受管键已由其它行代表时不再新建,避免重复键 if (e.managedId && managedIds.has(String(e.managedId))) continue const row = { _id: 'i18n_' + Math.random().toString(36).slice(2, 10), b_key: e.key, _localeIds: {}, _managedId: e.managedId ? String(e.managedId) : undefined, _syncInjected: e.managedId ? true : undefined, [e.locale]: e.value, } next.push(row) byKey.set(String(e.key), row) if (e.managedId) managedIds.add(String(e.managedId)) } return next } // 合并同 b_key 的重复透视行,保证一个键仅一行。 // 返回新的数组,不修改入参。 export function consolidatePivotRows(rows) { const byKey = new Map() const merged = [] for (const row of rows) { const key = String(row.b_key || '') const hit = byKey.get(key) if (hit) { for (const [k, v] of Object.entries(row)) { if (k === '_id' || k === '_localeIds') continue // 仅当目标为空(null/undefined/空串)且来源非空时才补全,保留首个非空值 if ((hit[k] == null || hit[k] === '') && v != null) hit[k] = v } Object.assign(hit._localeIds || (hit._localeIds = {}), row._localeIds || {}) if (row._managedId && !hit._managedId) hit._managedId = row._managedId if (row._syncInjected && !hit._syncInjected) hit._syncInjected = row._syncInjected if (row._defaultValue != null && hit._defaultValue == null) hit._defaultValue = row._defaultValue if (row._sortType && !hit._sortType) hit._sortType = row._sortType } else { byKey.set(key, row) merged.push(row) } } return merged }