How CSV to XLSX is built
CSV to XLSX combines multiple CSV files into a single Excel workbook, one worksheet per file. This post is about the encoding detection that makes that work with real-world CSVs, and the worksheet-naming rules a filename never had to satisfy until it became a sheet name.
Tech used
Papa Parse for reading, ExcelJS for writing
Each CSV is parsed with Papa Parse 5.4 (MIT) — the same library CSV Viewer uses — and the combined workbook is built with ExcelJS 4.4 (MIT), as in images-to-xlsx and xlsx-merge. Both are loaded via dynamic import(), so neither reaches the page’s initial bundle until a file is actually dropped.
Detecting Shift_JIS without being told the encoding
A CSV file carries no built-in signal about which text encoding it was saved in — unlike .xls, whose BIFF header declares a code page explicitly (see XLS to XLSX Converter). Given a file that isn’t valid UTF-8, the practical assumption for this fleet’s audience is Shift_JIS, since that’s overwhelmingly the encoding CSVs exported from older Japanese systems actually use. The detection strategy — try strict UTF-8 first via TextDecoder('utf-8', { fatal: true }), fall back to Shift_JIS on failure — is the same one CSV Viewer already established for exactly this reason: it’s a real recurring need across the catalog, not a one-off guess.
Implementation & operational notes
A worksheet name has rules a CSV filename never had to follow, and getting them wrong corrupts the output file. Excel worksheet names cap out at 31 characters, forbid the characters \ / ? * [ ] :, and reject exactly one specific string outright: History, reserved internally by the format regardless of what a user names their own sheet. sanitizeSheetNameBase() enforces all three — replacing forbidden characters, truncating to the limit, and appending an underscore to a literal History filename — before a name is ever handed to ExcelJS’s addWorksheet(). Skipping any one of these produces a workbook Excel itself may refuse to open.
Sheet-name collisions are resolved with the same case-insensitive matching Excel itself uses. Two CSV files named Report.csv and report.csv produce sheet names that are supposed to be treated as the same name by the spec, not two similar-looking names — usedNames.has(candidate.toLowerCase()) in createUniqueSheetName() checks case-insensitively for exactly that reason, appending (2), (3), and so on until it finds a name Excel would actually consider unique.
Encoding and row count are surfaced per file, not swallowed into a single combined success message. Each source file’s detected encoding, row count, and any CSV parse warnings are shown individually in the result — useful specifically because a workbook combining several CSVs from different sources is exactly the situation where one of them silently having the wrong encoding, or more parse errors than expected, is easy to miss if the only feedback is “done.”
A malformed or non-CSV file is rejected per-file, not for the whole batch. If a dropped file can’t be read as text, or fails CSV parsing outright, that specific file is reported as an error while the rest of the selected files still go through — so one bad file in a batch of ten doesn’t block combining the other nine.
Try it / source
- Tool: CSV to XLSX
- Source: github.com/GeppettoAndRomero/csv-to-xlsx