Form view: structured key/value editor (keys read-only, values editable; side-by-side desktop, stacked mobile) + testable strings-format.js

This commit is contained in:
FuelBoard Contributor
2026-08-13 14:09:19 +01:00
parent 4b3f7365cf
commit f06cd833c0
4 changed files with 343 additions and 36 deletions
+94
View File
@@ -0,0 +1,94 @@
/*
* FuelBoard Copy Editor — .strings format helpers (pure, no DOM).
*
* The editor can show Localizable.strings either as raw text or as a
* structured key/value form. These functions parse the file into blocks
* (entries + comments + blanks) and serialize them back, preserving every
* comment and blank line byte-for-byte.
*
* .strings shape (verified against the real file):
* "Key" = "Value"; -> entry (single line)
* /* Section banner *\/ -> comment
* (blank) -> blank
* Multi-line comment blocks (like the header) are kept verbatim. Any line
* that is not a valid entry, comment, or blank is kept verbatim as "raw"
* and the form view refuses to activate (falls back to Raw editing) so we
* can never clobber a file we don't understand.
*/
(function (root, factory) {
if (typeof module !== "undefined" && module.exports) {
module.exports = factory();
} else {
root.StringsFormat = factory();
}
})(this, function () {
"use strict";
// Display value -> file-escaped value. Backslash FIRST, then quotes.
function escapeStr(s) {
return s
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.replace(/\n/g, "\\n")
.replace(/\t/g, "\\t");
}
// File-escaped value -> display value. Unknown escapes (\uXXXX, \') are
// passed through untouched so round-trips never lose data.
function unescapeStr(s) {
return s.replace(/\\(.)/g, function (m, c) {
return c === "n" ? "\n" : c === "t" ? "\t" : c === '"' ? '"' : c === "\\" ? "\\" : m;
});
}
// Text -> [{type:"blank"|"comment"|"entry"|"raw", ...}]
// entry: {type, key, value (unescaped), raw}
function parseStrings(text) {
var lines = text.split("\n");
var blocks = [];
var i = 0;
while (i < lines.length) {
var raw = lines[i];
var s = raw.trim();
if (s === "") {
blocks.push({ type: "blank", raw: "" });
i++;
continue;
}
if (s.indexOf("/*") === 0) {
// Comment block — may span lines (the file header does).
var block = [raw];
i++;
while (i < lines.length && block[block.length - 1].indexOf("*/") === -1) {
block.push(lines[i]);
i++;
}
blocks.push({ type: "comment", raw: block.join("\n") });
continue;
}
var m = s.match(/^"((?:[^"\\]|\\.)*)"\s*=\s*"((?:[^"\\]|\\.)*)"\s*;$/);
if (m) {
blocks.push({ type: "entry", key: m[1], value: unescapeStr(m[2]), raw: raw });
i++;
continue;
}
// Not something we understand — keep verbatim, flag for the caller.
blocks.push({ type: "raw", raw: raw });
i++;
}
return blocks;
}
function serializeStrings(blocks) {
return blocks
.map(function (b) {
if (b.type === "entry") {
return '"' + b.key + '" = "' + escapeStr(b.value) + '";';
}
return b.raw;
})
.join("\n");
}
return { parseStrings: parseStrings, serializeStrings: serializeStrings, escapeStr: escapeStr, unescapeStr: unescapeStr };
});