fix: 术语表上传/导入/导出全面修复+协同维护+离线保存
上传不触发问题: - 原生file控件改为隐藏,由【导入CSV】按钮触发(triggerGlossaryImport) - 折叠区和弹窗内都提供导入入口 - 导入后自动打开弹窗显示结果+提示导入条数 CSV解析鲁棒性: - 用FileReader读UTF-8文本,Papa.parse(字符串)替代File对象,加手动parseCsvText兜底 - transformHeader剥离BOM+规范化,修复带BOM的CSV首列名变成zh导致中文列丢失 - normalizeGlossaryRow支持3列(zh/en/id)、2列任意组合、旧src/dst格式 协同维护+离线保存: - loadGlossaryFromServer失败时从localStorage缓存回退 - saveGlossary先写localStorage再同步服务端,服务端失败提示"已保存到本机" - openGlossaryModal用getOrCreateInstance复用实例避免重复创建 文案: - glossaryHelp更新为三语说明+支持任意两列 - 新增glossaryImportBtn/glossaryImportNone/glossaryImportError/glossarySavedLocal Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -108,7 +108,7 @@
|
|||||||
"unlimitedPlaceholder": "留空为无限制",
|
"unlimitedPlaceholder": "留空为无限制",
|
||||||
"glossaryGenTitle": "术语表",
|
"glossaryGenTitle": "术语表",
|
||||||
"glossaryLabel": "术语表(全公司共享)",
|
"glossaryLabel": "术语表(全公司共享)",
|
||||||
"glossaryHelp": "选择一个或多个CSV文件。文件需包含'src'和'dst'两列标题,分别代表原文和译文。",
|
"glossaryHelp": "全公司共享的三语术语表(中文/English/Bahasa)。翻译时按目标语言自动取对应列。支持任意两列(如仅中英)。点击下方按钮导入CSV或查看编辑。",
|
||||||
"viewGlossaryBtn": "查看术语表",
|
"viewGlossaryBtn": "查看术语表",
|
||||||
"clearGlossaryBtn": "清空",
|
"clearGlossaryBtn": "清空",
|
||||||
"glossaryGenEnableLabel": "自动生成术语表",
|
"glossaryGenEnableLabel": "自动生成术语表",
|
||||||
@@ -186,7 +186,11 @@
|
|||||||
"glossaryUnsaved": "未保存",
|
"glossaryUnsaved": "未保存",
|
||||||
"glossarySaved": "术语表已保存,对所有用户生效。",
|
"glossarySaved": "术语表已保存,对所有用户生效。",
|
||||||
"glossarySaveFailed": "保存失败",
|
"glossarySaveFailed": "保存失败",
|
||||||
"glossaryImported": "已导入 {n} 条术语,请点击【保存】使其对所有用户生效。"
|
"glossaryImported": "已导入 {n} 条术语,请点击【保存】使其对所有用户生效。",
|
||||||
|
"glossaryImportBtn": "导入CSV",
|
||||||
|
"glossaryImportNone": "未能从CSV中解析出有效术语,请检查文件格式(表头应为 zh,en,id 或 src,dst)。",
|
||||||
|
"glossaryImportError": "导入失败",
|
||||||
|
"glossarySavedLocal": "已保存到本机(服务端同步失败,数据保留在本地,下次可重试保存)"
|
||||||
},
|
},
|
||||||
"en": {
|
"en": {
|
||||||
"pageTitle": "DocuTranslate - Interactive Document Translation",
|
"pageTitle": "DocuTranslate - Interactive Document Translation",
|
||||||
@@ -297,7 +301,7 @@
|
|||||||
"unlimitedPlaceholder": "Leave empty for unlimited",
|
"unlimitedPlaceholder": "Leave empty for unlimited",
|
||||||
"glossaryGenTitle": "Glossary",
|
"glossaryGenTitle": "Glossary",
|
||||||
"glossaryLabel": "Glossary (shared company-wide)",
|
"glossaryLabel": "Glossary (shared company-wide)",
|
||||||
"glossaryHelp": "Select one or more CSV files. Files must contain 'src' and 'dst' headers, representing source and destination terms.",
|
"glossaryHelp": "Company-wide shared trilingual glossary (中文/English/Bahasa). The target-language column is used automatically during translation. Any two columns (e.g. Chinese-English only) are supported. Use the buttons below to import CSV or view/edit.",
|
||||||
"viewGlossaryBtn": "View Glossary",
|
"viewGlossaryBtn": "View Glossary",
|
||||||
"clearGlossaryBtn": "Clear",
|
"clearGlossaryBtn": "Clear",
|
||||||
"glossaryGenEnableLabel": "Auto-generate Glossary",
|
"glossaryGenEnableLabel": "Auto-generate Glossary",
|
||||||
@@ -375,7 +379,11 @@
|
|||||||
"glossaryUnsaved": "Unsaved",
|
"glossaryUnsaved": "Unsaved",
|
||||||
"glossarySaved": "Glossary saved and applied to all users.",
|
"glossarySaved": "Glossary saved and applied to all users.",
|
||||||
"glossarySaveFailed": "Save failed",
|
"glossarySaveFailed": "Save failed",
|
||||||
"glossaryImported": "Imported {n} terms. Click [Save] to apply to all users."
|
"glossaryImported": "Imported {n} terms. Click [Save] to apply to all users.",
|
||||||
|
"glossaryImportBtn": "Import CSV",
|
||||||
|
"glossaryImportNone": "No valid terms parsed from the CSV. Check the format (headers should be zh,en,id or src,dst).",
|
||||||
|
"glossaryImportError": "Import failed",
|
||||||
|
"glossarySavedLocal": "Saved locally (server sync failed; data kept locally, retry save later)"
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"pageTitle": "DocuTranslate - Terjemahan Dokumen Interaktif",
|
"pageTitle": "DocuTranslate - Terjemahan Dokumen Interaktif",
|
||||||
@@ -486,7 +494,7 @@
|
|||||||
"unlimitedPlaceholder": "Kosongkan untuk tanpa batas",
|
"unlimitedPlaceholder": "Kosongkan untuk tanpa batas",
|
||||||
"glossaryGenTitle": "Glosarium",
|
"glossaryGenTitle": "Glosarium",
|
||||||
"glossaryLabel": "Glosarium (dibagikan ke seluruh perusahaan)",
|
"glossaryLabel": "Glosarium (dibagikan ke seluruh perusahaan)",
|
||||||
"glossaryHelp": "Pilih file CSV dengan kolom src dan dst.",
|
"glossaryHelp": "Glosarium trilingual dibagikan ke seluruh perusahaan (中文/English/Bahasa). Kolom bahasa target digunakan otomatis saat menerjemahkan. Mendukung dua kolom saja (mis. Chinese-English). Gunakan tombol di bawah untuk mengimpor CSV atau melihat/mengedit.",
|
||||||
"viewGlossaryBtn": "Lihat Glosarium",
|
"viewGlossaryBtn": "Lihat Glosarium",
|
||||||
"clearGlossaryBtn": "Hapus",
|
"clearGlossaryBtn": "Hapus",
|
||||||
"glossaryGenEnableLabel": "Buat Glosarium Otomatis",
|
"glossaryGenEnableLabel": "Buat Glosarium Otomatis",
|
||||||
@@ -564,6 +572,10 @@
|
|||||||
"glossaryUnsaved": "Belum disimpan",
|
"glossaryUnsaved": "Belum disimpan",
|
||||||
"glossarySaved": "Glosarium disimpan, berlaku untuk semua pengguna.",
|
"glossarySaved": "Glosarium disimpan, berlaku untuk semua pengguna.",
|
||||||
"glossarySaveFailed": "Gagal menyimpan",
|
"glossarySaveFailed": "Gagal menyimpan",
|
||||||
"glossaryImported": "Mengimpor {n} istilah. Klik [Simpan] untuk berlaku ke semua pengguna."
|
"glossaryImported": "Mengimpor {n} istilah. Klik [Simpan] untuk berlaku ke semua pengguna.",
|
||||||
|
"glossaryImportBtn": "Impor CSV",
|
||||||
|
"glossaryImportNone": "Tidak ada istilah valid yang diurai dari CSV. Periksa format (header seharusnya zh,en,id atau src,dst).",
|
||||||
|
"glossaryImportError": "Impor gagal",
|
||||||
|
"glossarySavedLocal": "Tersimpan secara lokal (sinkronisasi server gagal; data tetap lokal, coba simpan lagi nanti)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -684,21 +684,20 @@
|
|||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">{{ t('glossaryLabel') }}</label>
|
<label class="form-label">{{ t('glossaryLabel') }}</label>
|
||||||
<input class="form-control" type="file" @change="handleGlossaryFiles"
|
<div class="form-text mb-2">{{ t('glossaryHelp') }}</div>
|
||||||
multiple accept=".csv" ref="glossaryInput">
|
<div class="btn-group">
|
||||||
<div class="form-text">{{ t('glossaryHelp') }}</div>
|
|
||||||
<div class="btn-group mt-2">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-info"
|
<button type="button" class="btn btn-sm btn-outline-info"
|
||||||
@click="openGlossaryModal">
|
@click="openGlossaryModal">
|
||||||
<i class="bi bi-card-list me-1"></i><span>{{ t('viewGlossaryBtn') }} <span
|
<i class="bi bi-card-list me-1"></i><span>{{ t('viewGlossaryBtn') }} <span
|
||||||
v-if="glossaryCount">({{glossaryCount}})</span></span>
|
v-if="glossaryCount">({{glossaryCount}})</span></span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger"
|
<button type="button" class="btn btn-sm btn-outline-warning"
|
||||||
@click="clearGlossary">
|
@click="triggerGlossaryImport">
|
||||||
<i class="bi bi-trash me-1"></i><span>{{ t('clearGlossaryBtn')
|
<i class="bi bi-upload me-1"></i><span>{{ t('glossaryImportBtn') }}</span>
|
||||||
}}</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<input class="form-control d-none" type="file" @change="handleGlossaryFiles"
|
||||||
|
multiple accept=".csv" ref="glossaryInput">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -883,6 +882,9 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-warning" @click="triggerGlossaryImport">
|
||||||
|
<i class="bi bi-upload me-1"></i>{{ t('glossaryImportBtn') }}
|
||||||
|
</button>
|
||||||
<button type="button" class="btn btn-outline-secondary" @click="exportGlossaryCsv">
|
<button type="button" class="btn btn-outline-secondary" @click="exportGlossaryCsv">
|
||||||
<i class="bi bi-download me-1"></i>{{ t('glossaryExportCsv') }}
|
<i class="bi bi-download me-1"></i>{{ t('glossaryExportCsv') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -1390,25 +1392,38 @@
|
|||||||
glossaryRows.value.splice(i, 1);
|
glossaryRows.value.splice(i, 1);
|
||||||
glossaryDirty.value = true;
|
glossaryDirty.value = true;
|
||||||
};
|
};
|
||||||
|
const _normalizeGlossaryRowShape = (r) => ({
|
||||||
|
zh: String(r.zh||''), en: String(r.en||''), id: String(r.id||'')
|
||||||
|
});
|
||||||
const loadGlossaryFromServer = async () => {
|
const loadGlossaryFromServer = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/service/glossary');
|
const res = await fetch('/service/glossary');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
glossaryRows.value = (data && Array.isArray(data.rows)) ? data.rows.map(r => ({
|
const rows = (data && Array.isArray(data.rows)) ? data.rows.map(_normalizeGlossaryRowShape) : [];
|
||||||
zh: String(r.zh||''), en: String(r.en||''), id: String(r.id||'')
|
glossaryRows.value = rows;
|
||||||
})) : [];
|
|
||||||
glossaryDirty.value = false;
|
glossaryDirty.value = false;
|
||||||
|
// 镜像到 localStorage,离线/服务端不可用时回退使用
|
||||||
|
try { localStorage.setItem('glossary_cache', JSON.stringify(rows)); } catch (_) {}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load shared glossary', e);
|
console.error('Failed to load shared glossary, trying local cache', e);
|
||||||
|
// 离线回退:从 localStorage 读取上次缓存
|
||||||
|
try {
|
||||||
|
const cached = JSON.parse(localStorage.getItem('glossary_cache') || '[]');
|
||||||
|
if (Array.isArray(cached) && cached.length) {
|
||||||
|
glossaryRows.value = cached.map(_normalizeGlossaryRowShape);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const saveGlossary = async () => {
|
const saveGlossary = async () => {
|
||||||
if (glossarySaving.value) return;
|
if (glossarySaving.value) return;
|
||||||
glossarySaving.value = true;
|
glossarySaving.value = true;
|
||||||
|
const rows = glossaryRows.value
|
||||||
|
.map(r => ({zh:(r.zh||'').trim(), en:(r.en||'').trim(), id:(r.id||'').trim()}))
|
||||||
|
.filter(r => r.zh || r.en || r.id);
|
||||||
|
// 先写本地缓存(离线保存),保证数据不丢
|
||||||
|
try { localStorage.setItem('glossary_cache', JSON.stringify(rows)); } catch (_) {}
|
||||||
try {
|
try {
|
||||||
const rows = glossaryRows.value
|
|
||||||
.map(r => ({zh:(r.zh||'').trim(), en:(r.en||'').trim(), id:(r.id||'').trim()}))
|
|
||||||
.filter(r => r.zh || r.en || r.id);
|
|
||||||
const res = await fetch('/service/glossary', {
|
const res = await fetch('/service/glossary', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
@@ -1416,13 +1431,13 @@
|
|||||||
});
|
});
|
||||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
glossaryRows.value = (data.rows || []).map(r => ({
|
glossaryRows.value = (data.rows || []).map(_normalizeGlossaryRowShape);
|
||||||
zh: String(r.zh||''), en: String(r.en||''), id: String(r.id||'')
|
|
||||||
}));
|
|
||||||
glossaryDirty.value = false;
|
glossaryDirty.value = false;
|
||||||
alert(t('glossarySaved'));
|
alert(t('glossarySaved'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(t('glossarySaveFailed') + ': ' + e.message);
|
// 服务端保存失败时,本地缓存已保存,标记未同步
|
||||||
|
glossaryDirty.value = true;
|
||||||
|
alert(t('glossarySavedLocal') + ': ' + e.message);
|
||||||
} finally {
|
} finally {
|
||||||
glossarySaving.value = false;
|
glossarySaving.value = false;
|
||||||
}
|
}
|
||||||
@@ -1449,34 +1464,104 @@
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 触发隐藏文件选择框(由按钮调用,避免原生file控件样式不一致)
|
||||||
|
const triggerGlossaryImport = () => {
|
||||||
|
if (glossaryInput.value) glossaryInput.value.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 手动 CSV 解析兜底:当 PapaParse 不可用或解析异常时使用
|
||||||
|
const parseCsvText = (text) => {
|
||||||
|
text = text.replace(/^/, ''); // 剥离 BOM
|
||||||
|
const lines = text.split(/\r?\n/).filter(l => l.length);
|
||||||
|
if (!lines.length) return [];
|
||||||
|
const parseLine = (line) => {
|
||||||
|
const out = []; let cur = '', inQ = false;
|
||||||
|
for (let i = 0; i < line.length; i++) {
|
||||||
|
const c = line[i];
|
||||||
|
if (inQ) {
|
||||||
|
if (c === '"' && line[i+1] === '"') { cur += '"'; i++; }
|
||||||
|
else if (c === '"') inQ = false;
|
||||||
|
else cur += c;
|
||||||
|
} else {
|
||||||
|
if (c === '"') inQ = true;
|
||||||
|
else if (c === ',') { out.push(cur); cur = ''; }
|
||||||
|
else cur += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push(cur);
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
const headers = parseLine(lines[0]).map(h => h.trim().toLowerCase());
|
||||||
|
const rows = [];
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const cells = parseLine(lines[i]);
|
||||||
|
const obj = {};
|
||||||
|
headers.forEach((h, idx) => { obj[h] = (cells[idx] || '').trim(); });
|
||||||
|
rows.push(obj);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 把解析出的行(可能含 zh/en/id/src/dst 等键)规范化为三列
|
||||||
|
const normalizeGlossaryRow = (r) => {
|
||||||
|
const row = {zh:'', en:'', id:''};
|
||||||
|
const has = (...keys) => keys.some(k => k in r && String(r[k]||'').trim());
|
||||||
|
if (has('zh','en','id')) {
|
||||||
|
row.zh = (r.zh||'').trim(); row.en = (r.en||'').trim(); row.id = (r.id||'').trim();
|
||||||
|
} else if (has('src','dst')) {
|
||||||
|
// 旧 2 列或任意 src/dst:src→en(英文),dst→zh(中文)
|
||||||
|
row.zh = (r.dst||'').trim(); row.en = (r.src||'').trim();
|
||||||
|
} else {
|
||||||
|
// 纯 2 列中文-英文(无表头或表头不规范):按 [中, 英] 推断
|
||||||
|
const vals = Object.values(r).map(v => String(v||'').trim()).filter(Boolean);
|
||||||
|
if (vals.length >= 2) { row.zh = vals[0]; row.en = vals[1]; }
|
||||||
|
else if (vals.length === 1) { row.zh = vals[0]; }
|
||||||
|
}
|
||||||
|
return (row.zh || row.en || row.id) ? row : null;
|
||||||
|
};
|
||||||
|
|
||||||
const handleGlossaryFiles = (e) => {
|
const handleGlossaryFiles = (e) => {
|
||||||
const files = e.target.files;
|
const files = e.target.files;
|
||||||
if (!files.length) return;
|
if (!files || !files.length) return;
|
||||||
|
let processed = 0;
|
||||||
|
const totalFiles = files.length;
|
||||||
Array.from(files).forEach(f => {
|
Array.from(files).forEach(f => {
|
||||||
Papa.parse(f, {
|
const reader = new FileReader();
|
||||||
header: true, skipEmptyLines: true,
|
reader.onload = (ev) => {
|
||||||
// 剥离 BOM + 规范化表头(小写、去空白),兼容 Excel 另存的 CSV
|
try {
|
||||||
transformHeader: (h) => String(h||'').replace(/^/, '').trim().toLowerCase(),
|
const text = ev.target.result || '';
|
||||||
complete: (res) => {
|
// 优先用 PapaParse,失败则用手动解析
|
||||||
if (!res.data) return;
|
let parsed = null;
|
||||||
|
if (window.Papa) {
|
||||||
|
const res = Papa.parse(text, {
|
||||||
|
header: true, skipEmptyLines: true,
|
||||||
|
transformHeader: (h) => String(h||'').replace(/^/, '').trim().toLowerCase()
|
||||||
|
});
|
||||||
|
parsed = res.data;
|
||||||
|
}
|
||||||
|
if (!parsed) parsed = parseCsvText(text);
|
||||||
let added = 0;
|
let added = 0;
|
||||||
res.data.forEach(r => {
|
parsed.forEach(r => {
|
||||||
const row = {zh:'', en:'', id:''};
|
const row = normalizeGlossaryRow(r);
|
||||||
if ('zh' in r || 'en' in r || 'id' in r) {
|
if (row) { glossaryRows.value.push(row); added++; }
|
||||||
row.zh = (r.zh||'').trim(); row.en = (r.en||'').trim(); row.id = (r.id||'').trim();
|
|
||||||
} else if ('src' in r && 'dst' in r) {
|
|
||||||
// 旧 2 列:src 视为英文、dst 视为中文(最常见英译中场景)
|
|
||||||
row.zh = (r.dst||'').trim(); row.en = (r.src||'').trim();
|
|
||||||
} else { return; }
|
|
||||||
if (row.zh || row.en || row.id) {
|
|
||||||
glossaryRows.value.push(row);
|
|
||||||
glossaryDirty.value = true;
|
|
||||||
added++;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
if (added) alert(t('glossaryImported').replace('{n}', added));
|
glossaryDirty.value = true;
|
||||||
|
processed++;
|
||||||
|
if (processed === totalFiles) {
|
||||||
|
openGlossaryModal(); // 自动打开弹窗让用户看到导入结果
|
||||||
|
if (added > 0) {
|
||||||
|
alert(t('glossaryImported').replace('{n}', added));
|
||||||
|
} else {
|
||||||
|
alert(t('glossaryImportNone'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('glossary import error', err);
|
||||||
|
alert(t('glossaryImportError') + ': ' + err.message);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
reader.onerror = () => alert(t('glossaryImportError') + ': read failed');
|
||||||
|
reader.readAsText(f, 'utf-8');
|
||||||
});
|
});
|
||||||
if (glossaryInput.value) glossaryInput.value.value = '';
|
if (glossaryInput.value) glossaryInput.value.value = '';
|
||||||
};
|
};
|
||||||
@@ -1484,7 +1569,12 @@
|
|||||||
// 清空当前编辑并从服务端重新拉取(避免误删共享数据)
|
// 清空当前编辑并从服务端重新拉取(避免误删共享数据)
|
||||||
await loadGlossaryFromServer();
|
await loadGlossaryFromServer();
|
||||||
};
|
};
|
||||||
const openGlossaryModal = () => new bootstrap.Modal(document.getElementById('glossaryModal')).show();
|
// 复用同一个 Modal 实例,避免重复创建
|
||||||
|
const glossaryModalInstance = () => {
|
||||||
|
const el = document.getElementById('glossaryModal');
|
||||||
|
return bootstrap.Modal.getOrCreateInstance(el);
|
||||||
|
};
|
||||||
|
const openGlossaryModal = () => glossaryModalInstance().show();
|
||||||
|
|
||||||
// Task Management
|
// Task Management
|
||||||
const createNewTask = (backendId = null) => {
|
const createNewTask = (backendId = null) => {
|
||||||
@@ -2154,7 +2244,7 @@
|
|||||||
errors, clearError,
|
errors, clearError,
|
||||||
t, createNewTask, removeTask, handleTaskFileSelect, handleTaskFileDrop, toggleTaskState,
|
t, createNewTask, removeTask, handleTaskFileSelect, handleTaskFileDrop, toggleTaskState,
|
||||||
handleGlossaryFiles, clearGlossary, openGlossaryModal,
|
handleGlossaryFiles, clearGlossary, openGlossaryModal,
|
||||||
addGlossaryRow, deleteGlossaryRow, saveGlossary, exportGlossaryCsv,
|
addGlossaryRow, deleteGlossaryRow, saveGlossary, exportGlossaryCsv, triggerGlossaryImport,
|
||||||
openPreview, setPreviewMode, toggleSyncScroll, printPdf, copyLog, getFileIcon,
|
openPreview, setPreviewMode, toggleSyncScroll, printPdf, copyLog, getFileIcon,
|
||||||
exportConfig, importConfig, setLang, setTheme, capitalize,
|
exportConfig, importConfig, setLang, setTheme, capitalize,
|
||||||
currentWorkflowConfig, ocrOptions, stepMap,
|
currentWorkflowConfig, ocrOptions, stepMap,
|
||||||
|
|||||||
Reference in New Issue
Block a user