@@ -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