Skip to content

Commit 9ce0c7c

Browse files
committed
feat: UI i18n (EN/ZH), settings persistence; conditional outputs; layout reorg; table actions and JSON modal; docs cross-link and client-side note
1 parent 53cbc31 commit 9ce0c7c

4 files changed

Lines changed: 208 additions & 25 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# SQL INSERT ➜ JSON + HTML Table
22

3+
English | [中文说明](README.zh.md)
4+
35
A lightweight web tool to convert SQL INSERT statements into JSON and a rendered HTML table. Supports multiple statements, schema inference from CREATE TABLE, inline validation, JSON previews, downloads, and more.
46

7+
Note: This is a 100% client-side web app (pure HTML/JS). It requires no backend and works perfectly on GitHub Pages.
8+
59
## Features
610

711
- Robust parsing of `INSERT INTO ... VALUES (...), (...);` across multiple tables

README.zh.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# SQL INSERT ➜ JSON 与 HTML 表格
22

3+
[English README](README.md) | 中文说明
4+
35
一个轻量级 Web 工具,用于将 SQL INSERT 语句转换为 JSON 与可视化 HTML 表格。支持多语句、从 CREATE TABLE 推断列名、内联校验、JSON 预览、下载等功能。
46

7+
说明:本项目为 100% 纯前端(纯 HTML/JS),无需后端,适合 GitHub Pages 静态托管。
8+
59
## 功能特点
610

711
- 可靠解析 `INSERT INTO ... VALUES (...), (...);`,支持多表多语句

index.html

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,27 @@
3535
</head>
3636
<body>
3737
<div class="container">
38-
<h1 class="mb-4">SQL to JSON/Table Converter</h1>
38+
<div class="d-flex align-items-center justify-content-between mb-4">
39+
<h1 id="title" class="m-0">SQL to JSON/Table Converter</h1>
40+
<div class="d-flex align-items-center gap-2">
41+
<label id="langLabel" for="langSelect" class="form-label m-0">Language</label>
42+
<select id="langSelect" class="form-select form-select-sm" style="width: 140px;">
43+
<option value="en" selected>English</option>
44+
<option value="zh">中文</option>
45+
</select>
46+
</div>
47+
</div>
3948

4049
<!-- Section 1: Input & Actions -->
4150
<section class="mb-4">
42-
<h2 class="h4 mb-3">Input & Actions</h2>
4351
<div class="mb-3">
44-
<label for="sqlInput" class="form-label">Paste your SQL here (you can include CREATE TABLE and multiple INSERT statements):</label>
52+
<label id="sqlLabel" for="sqlInput" class="form-label">Paste your SQL here (you can include CREATE TABLE and multiple INSERT statements):</label>
4553
<textarea class="form-control" id="sqlInput" rows="8" placeholder="CREATE TABLE ...;&#10;INSERT INTO table_name (col1, col2, ...) VALUES (...), (...);"></textarea>
4654
</div>
4755
<div class="d-flex align-items-center gap-2 mb-2">
4856
<button id="convertBtn" class="btn btn-primary">Convert</button>
4957
<div class="ms-auto d-flex align-items-center gap-2">
50-
<label for="outputMode" class="form-label m-0">Output:</label>
58+
<label id="outputModeLabel" for="outputMode" class="form-label m-0">Output:</label>
5159
<select id="outputMode" class="form-select form-select-sm" style="width: 180px;">
5260
<option value="grouped" selected>Grouped by Table</option>
5361
<option value="flat">Flat Array</option>
@@ -56,19 +64,27 @@ <h2 class="h4 mb-3">Input & Actions</h2>
5664
<button id="downloadCsvBtn" class="btn btn-outline-secondary btn-sm" type="button">Download CSV</button>
5765
</div>
5866
</div>
67+
<div class="d-flex align-items-center gap-2 mb-0">
68+
<div class="form-check">
69+
<input class="form-check-input" type="checkbox" id="saveInputToggle">
70+
<label id="saveInputLabel" class="form-check-label" for="saveInputToggle">Save input in browser</label>
71+
</div>
72+
</div>
5973
<div id="messages" class="mb-0"></div>
74+
6075
</section>
6176

62-
<hr/>
77+
6378

6479
<!-- Section 2: Output - JSON Actions -->
65-
<section class="mb-4">
66-
<h2 class="h4 mb-3">Output: JSON Actions</h2>
80+
<section id="jsonActionsSection" class="mb-4 d-none">
81+
<hr/>
82+
<h2 id="jsonSectionTitle" class="h4 mb-3">Output: JSON Actions</h2>
6783
<div class="d-flex align-items-center justify-content-between mb-2">
6884
<div class="d-flex align-items-center gap-2">
6985
<div class="form-check form-switch">
7086
<input class="form-check-input" type="checkbox" id="expandJsonToggle">
71-
<label class="form-check-label" for="expandJsonToggle">Expand JSON values</label>
87+
<label id="expandJsonLabel" class="form-check-label" for="expandJsonToggle">Expand JSON values</label>
7288
</div>
7389
</div>
7490
<div class="d-flex align-items-center gap-2">
@@ -81,14 +97,15 @@ <h2 class="h4 mb-3">Output: JSON Actions</h2>
8197
<pre id="jsonOutput"></pre>
8298
</div>
8399
</div>
100+
<hr/>
84101
</section>
85102

86-
<hr/>
103+
87104

88105
<!-- Section 3: Output - HTML Table -->
89-
<section class="mb-4">
106+
<section id="tableSection" class="mb-4 d-none">
90107
<div class="d-flex align-items-center justify-content-between mb-2">
91-
<h2 class="h4 m-0">Output: HTML Table</h2>
108+
<h2 id="tableSectionTitle" class="h4 m-0">Output: HTML Table</h2>
92109
<div class="d-flex align-items-center gap-2">
93110
<button id="compareBtn" class="btn btn-sm btn-outline-secondary" type="button">Compare Selected</button>
94111
<button id="generateUpdateBtn" class="btn btn-sm btn-outline-primary" type="button">Generate UPDATEs</button>

script.js

Lines changed: 172 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ document.addEventListener('DOMContentLoaded', function() {
66
const outputDiv = document.getElementById('output');
77
const messagesDiv = document.getElementById('messages');
88
const outputModeSel = document.getElementById('outputMode');
9+
const langSelect = document.getElementById('langSelect');
10+
const saveInputToggle = document.getElementById('saveInputToggle');
911
const downloadJsonBtn = document.getElementById('downloadJsonBtn');
1012
const downloadCsvBtn = document.getElementById('downloadCsvBtn');
1113
const expandJsonToggle = document.getElementById('expandJsonToggle');
@@ -18,27 +20,136 @@ document.addEventListener('DOMContentLoaded', function() {
1820
const actionCopyBtn = document.getElementById('actionCopyBtn');
1921
const compareBtn = document.getElementById('compareBtn');
2022
const generateUpdateBtn = document.getElementById('generateUpdateBtn');
21-
22-
// state for downloads
23+
const jsonActionsSection = document.getElementById('jsonActionsSection');
24+
const tableSection = document.getElementById('tableSection');
25+
26+
// i18n
27+
const I18N = {
28+
en: {
29+
langLabel: 'Language',
30+
title: 'SQL to JSON/Table Converter',
31+
inputSectionTitle: 'Input & Actions',
32+
sqlLabel: 'Paste your SQL here (you can include CREATE TABLE and multiple INSERT statements):',
33+
convert: 'Convert',
34+
outputModeLabel: 'Output:',
35+
outputGrouped: 'Grouped by Table',
36+
outputFlat: 'Flat Array',
37+
downloadJson: 'Download JSON',
38+
downloadCsv: 'Download CSV',
39+
jsonSectionTitle: 'Output: JSON Actions',
40+
expandJsonLabel: 'Expand JSON values',
41+
viewJson: 'View JSON',
42+
copyJson: 'Copy JSON',
43+
tableSectionTitle: 'Output: HTML Table',
44+
compareSelected: 'Compare Selected',
45+
generateUpdates: 'Generate UPDATEs',
46+
mismatch: 'mismatch',
47+
saveInputLabel: 'Save input in browser',
48+
msgEnterSql: 'Please enter SQL input.',
49+
msgParseError: 'Error parsing SQL: ',
50+
msgNothingToDownload: 'Nothing to download yet. Convert some SQL first.',
51+
msgJsonCopied: 'JSON copied to clipboard.',
52+
msgCopyFailed: 'Failed to copy JSON: ',
53+
msgNothingToView: 'Nothing to view yet. Convert some SQL first.',
54+
msgContentCopied: 'Content copied to clipboard.',
55+
msgCopyFailedGeneric: 'Failed to copy: ',
56+
msgSelectTwo: 'Select exactly two rows to compare.',
57+
msgSameTable: 'Please select rows from the same table to compare.',
58+
msgSelectRowsForUpdate: 'Select one or more rows to generate UPDATE statements.',
59+
},
60+
zh: {
61+
langLabel: '语言',
62+
title: 'SQL 插入语句 ➜ JSON 与 HTML 表格',
63+
inputSectionTitle: '输入与操作',
64+
sqlLabel: '在此粘贴 SQL(可包含 CREATE TABLE 与多条 INSERT 语句):',
65+
convert: '转换',
66+
outputModeLabel: '输出:',
67+
outputGrouped: '按表分组',
68+
outputFlat: '扁平数组',
69+
downloadJson: '下载 JSON',
70+
downloadCsv: '下载 CSV',
71+
jsonSectionTitle: '输出:JSON 操作',
72+
expandJsonLabel: '默认展开 JSON 值',
73+
viewJson: '查看 JSON',
74+
copyJson: '复制 JSON',
75+
tableSectionTitle: '输出:HTML 表格',
76+
compareSelected: '比较所选',
77+
generateUpdates: '生成 UPDATE',
78+
mismatch: '不匹配',
79+
saveInputLabel: '在浏览器中保存输入',
80+
msgEnterSql: '请输入 SQL。',
81+
msgParseError: '解析错误:',
82+
msgNothingToDownload: '暂无可下载内容,请先转换 SQL。',
83+
msgJsonCopied: 'JSON 已复制到剪贴板。',
84+
msgCopyFailed: '复制 JSON 失败:',
85+
msgNothingToView: '暂无可查看内容,请先转换 SQL。',
86+
msgContentCopied: '内容已复制到剪贴板。',
87+
msgCopyFailedGeneric: '复制失败:',
88+
msgSelectTwo: '请精确选择两行进行比较。',
89+
msgSameTable: '请从同一张表中选择行进行比较。',
90+
msgSelectRowsForUpdate: '请选择至少一行以生成 UPDATE 语句。',
91+
}
92+
};
93+
let currentLang = (langSelect && langSelect.value) || 'en';
94+
95+
function t(key) { return (I18N[currentLang] && I18N[currentLang][key]) || I18N.en[key] || key; }
96+
97+
function applyI18n() {
98+
const setText = (id, text) => { const el = document.getElementById(id); if (el) el.textContent = text; };
99+
setText('langLabel', t('langLabel'));
100+
setText('title', t('title'));
101+
setText('inputSectionTitle', t('inputSectionTitle'));
102+
setText('sqlLabel', t('sqlLabel'));
103+
const convertBtnEl = document.getElementById('convertBtn'); if (convertBtnEl) convertBtnEl.textContent = t('convert');
104+
setText('outputModeLabel', t('outputModeLabel'));
105+
// Output mode options
106+
if (outputModeSel && outputModeSel.options && outputModeSel.options.length >= 2) {
107+
outputModeSel.options[0].textContent = t('outputGrouped');
108+
outputModeSel.options[1].textContent = t('outputFlat');
109+
}
110+
const dlJsonBtn = document.getElementById('downloadJsonBtn'); if (dlJsonBtn) dlJsonBtn.textContent = t('downloadJson');
111+
const dlCsvBtn = document.getElementById('downloadCsvBtn'); if (dlCsvBtn) dlCsvBtn.textContent = t('downloadCsv');
112+
setText('jsonSectionTitle', t('jsonSectionTitle'));
113+
setText('expandJsonLabel', t('expandJsonLabel'));
114+
const viewBtn = document.getElementById('viewJsonBtn'); if (viewBtn) viewBtn.textContent = t('viewJson');
115+
const copyBtn = document.getElementById('copyJsonBtn'); if (copyBtn) copyBtn.textContent = t('copyJson');
116+
setText('tableSectionTitle', t('tableSectionTitle'));
117+
const cmpBtn = document.getElementById('compareBtn'); if (cmpBtn) cmpBtn.textContent = t('compareSelected');
118+
const genBtn = document.getElementById('generateUpdateBtn'); if (genBtn) genBtn.textContent = t('generateUpdates');
119+
const saveLabel = document.getElementById('saveInputLabel'); if (saveLabel) saveLabel.textContent = t('saveInputLabel');
120+
}
23121
let lastFlatRows = [];
24122
let lastGrouped = {};
25123
let lastItems = [];
26124

125+
function setSectionVisible(visible) {
126+
const method = visible ? 'remove' : 'add';
127+
jsonActionsSection?.classList[method]('d-none');
128+
tableSection?.classList[method]('d-none');
129+
outputDiv?.classList[method]('d-none');
130+
}
131+
132+
// Hide sections initially
133+
setSectionVisible(false);
134+
27135
convertBtn.addEventListener('click', function() {
28136
clearMessages();
29137
const sql = sqlInput.value.trim();
30138
if (!sql) {
31-
addMessage('warning', 'Please enter SQL input.');
139+
addMessage('warning', t('msgEnterSql'));
140+
setSectionVisible(false);
32141
return;
33142
}
34143

35144
try {
36145
const { items, warnings } = parseSQLInsert(sql);
37146
lastItems = items;
38147
displayResults(items, warnings);
148+
setSectionVisible(true);
39149
} catch (error) {
40-
addMessage('danger', 'Error parsing SQL: ' + error.message);
150+
addMessage('danger', t('msgParseError') + error.message);
41151
console.error(error);
152+
setSectionVisible(false);
42153
}
43154
});
44155

@@ -51,7 +162,7 @@ document.addEventListener('DOMContentLoaded', function() {
51162

52163
downloadJsonBtn.addEventListener('click', () => {
53164
if (!lastFlatRows.length && !Object.keys(lastGrouped).length) {
54-
addMessage('warning', 'Nothing to download yet. Convert some SQL first.');
165+
addMessage('warning', t('msgNothingToDownload'));
55166
return;
56167
}
57168
const isGrouped = outputModeSel.value === 'grouped';
@@ -78,16 +189,16 @@ document.addEventListener('DOMContentLoaded', function() {
78189
copyJsonBtn?.addEventListener('click', async () => {
79190
try {
80191
await navigator.clipboard.writeText(jsonOutput.textContent || '');
81-
addMessage('success', 'JSON copied to clipboard.');
192+
addMessage('success', t('msgJsonCopied'));
82193
} catch (e) {
83-
addMessage('danger', 'Failed to copy JSON: ' + (e?.message || e));
194+
addMessage('danger', t('msgCopyFailed') + (e?.message || e));
84195
}
85196
});
86197

87198
viewJsonBtn?.addEventListener('click', () => {
88199
const content = jsonOutput.textContent || '';
89200
if (!content) {
90-
addMessage('warning', 'Nothing to view yet. Convert some SQL first.');
201+
addMessage('warning', t('msgNothingToView'));
91202
return;
92203
}
93204
jsonModalBody.textContent = content;
@@ -104,9 +215,9 @@ document.addEventListener('DOMContentLoaded', function() {
104215
actionCopyBtn?.addEventListener('click', async () => {
105216
try {
106217
await navigator.clipboard.writeText(actionModalBody.textContent || '');
107-
addMessage('success', 'Content copied to clipboard.');
218+
addMessage('success', t('msgContentCopied'));
108219
} catch (e) {
109-
addMessage('danger', 'Failed to copy: ' + (e?.message || e));
220+
addMessage('danger', t('msgCopyFailedGeneric') + (e?.message || e));
110221
}
111222
});
112223

@@ -528,7 +639,7 @@ document.addEventListener('DOMContentLoaded', function() {
528639
rows.forEach((r, idx) => {
529640
const tr = document.createElement('tr');
530641
const item = groupedItems[tableName][idx];
531-
const mismatchBadge = item && item.mismatch ? ' <span class="badge bg-warning text-dark">mismatch</span>' : '';
642+
const mismatchBadge = item && item.mismatch ? ` <span class="badge bg-warning text-dark">${t('mismatch')}</span>` : '';
532643
let html = `<td><input type="checkbox" class="form-check-input row-select" data-table="${tableName}" data-index="${idx}"></td>`;
533644
html += `<td>${idx + 1}${mismatchBadge}</td>`;
534645
allCols.forEach(c => {
@@ -576,12 +687,12 @@ document.addEventListener('DOMContentLoaded', function() {
576687
compareBtn?.addEventListener('click', () => {
577688
const selected = getSelected(groupedItems);
578689
if (selected.length !== 2) {
579-
addMessage('warning', 'Select exactly two rows to compare.');
690+
addMessage('warning', t('msgSelectTwo'));
580691
return;
581692
}
582693
const [a, b] = selected;
583694
if (a.table !== b.table) {
584-
addMessage('warning', 'Please select rows from the same table to compare.');
695+
addMessage('warning', t('msgSameTable'));
585696
return;
586697
}
587698
const diffText = diffRecords(a.data, b.data, a.table);
@@ -592,7 +703,7 @@ document.addEventListener('DOMContentLoaded', function() {
592703
generateUpdateBtn?.addEventListener('click', () => {
593704
const selected = getSelected(groupedItems);
594705
if (selected.length === 0) {
595-
addMessage('warning', 'Select one or more rows to generate UPDATE statements.');
706+
addMessage('warning', t('msgSelectRowsForUpdate'));
596707
return;
597708
}
598709
const sqls = selected.map(it => buildUpdateSQL(it.table, it.data));
@@ -601,6 +712,53 @@ document.addEventListener('DOMContentLoaded', function() {
601712
});
602713
}
603714

715+
// Persisted settings keys
716+
const LS_LANG = 'sql2table.lang';
717+
const LS_SAVE_INPUT = 'sql2table.saveInput';
718+
const LS_INPUT = 'sql2table.input';
719+
720+
// Load persisted settings
721+
try {
722+
const savedLang = localStorage.getItem(LS_LANG);
723+
if (savedLang && langSelect) langSelect.value = savedLang;
724+
currentLang = (langSelect && langSelect.value) || currentLang;
725+
const saveInput = localStorage.getItem(LS_SAVE_INPUT);
726+
if (saveInputToggle) saveInputToggle.checked = saveInput === '1';
727+
if (saveInputToggle && saveInputToggle.checked) {
728+
const savedInput = localStorage.getItem(LS_INPUT);
729+
if (savedInput && sqlInput) sqlInput.value = savedInput;
730+
}
731+
} catch (_) {}
732+
733+
// language switching
734+
langSelect?.addEventListener('change', () => {
735+
currentLang = langSelect.value || 'en';
736+
try { localStorage.setItem(LS_LANG, currentLang); } catch (_) {}
737+
applyI18n();
738+
if (lastItems.length) displayResults(lastItems, []);
739+
});
740+
741+
// input persistence toggle
742+
saveInputToggle?.addEventListener('change', () => {
743+
const on = !!saveInputToggle.checked;
744+
try { localStorage.setItem(LS_SAVE_INPUT, on ? '1' : '0'); } catch (_) {}
745+
if (!on) {
746+
try { localStorage.removeItem(LS_INPUT); } catch (_) {}
747+
} else if (on && sqlInput) {
748+
try { localStorage.setItem(LS_INPUT, sqlInput.value || ''); } catch (_) {}
749+
}
750+
});
751+
752+
// save input on typing when enabled
753+
sqlInput?.addEventListener('input', () => {
754+
if (saveInputToggle && saveInputToggle.checked) {
755+
try { localStorage.setItem(LS_INPUT, sqlInput.value || ''); } catch (_) {}
756+
}
757+
});
758+
759+
// initial i18n on load
760+
applyI18n();
761+
604762
function getSelected(groupedItems) {
605763
const sels = [];
606764
document.querySelectorAll('.row-select:checked').forEach(cb => {

0 commit comments

Comments
 (0)