livelocale 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +230 -5
- package/dist/client/index.cjs +150 -86
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +35 -21
- package/dist/client/index.d.ts +35 -21
- package/dist/client/index.js +150 -86
- package/dist/client/index.js.map +1 -1
- package/dist/codemod/index.cjs +3057 -152
- package/dist/codemod/index.cjs.map +1 -1
- package/dist/codemod/index.js +3058 -153
- package/dist/codemod/index.js.map +1 -1
- package/dist/react/index.cjs +157 -92
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.js +157 -92
- package/dist/react/index.js.map +1 -1
- package/dist/server/index.cjs +100 -64
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +12 -2
- package/dist/server/index.d.ts +12 -2
- package/dist/server/index.js +100 -64
- package/dist/server/index.js.map +1 -1
- package/dist/studio/assets/index-Bvbtkry-.css +1 -0
- package/dist/studio/assets/index-DC_jRsox.js +13 -0
- package/dist/studio/assets/inter-latin-300-normal-BVlfKGgI.woff2 +0 -0
- package/dist/studio/assets/inter-latin-300-normal-i8F0SvXL.woff +0 -0
- package/dist/studio/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/dist/studio/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
- package/dist/studio/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
- package/dist/studio/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/dist/studio/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
- package/dist/studio/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/dist/studio/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
- package/dist/studio/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/dist/studio/assets/outfit-latin-400-normal-BGsTXAXT.woff2 +0 -0
- package/dist/studio/assets/outfit-latin-400-normal-DMwTpYkH.woff +0 -0
- package/dist/studio/assets/outfit-latin-500-normal-ClnHRwRh.woff +0 -0
- package/dist/studio/assets/outfit-latin-500-normal-DKnIMDSk.woff2 +0 -0
- package/dist/studio/assets/outfit-latin-600-normal-B7SfZ07L.woff2 +0 -0
- package/dist/studio/assets/outfit-latin-600-normal-BEfTtDA7.woff +0 -0
- package/dist/studio/assets/outfit-latin-700-normal-Cu9v6i1X.woff2 +0 -0
- package/dist/studio/assets/outfit-latin-700-normal-D4itBLBr.woff +0 -0
- package/dist/studio/index.html +3 -6
- package/dist/vite/index.cjs +739 -77
- package/dist/vite/index.cjs.map +1 -1
- package/dist/vite/index.d.cts +22 -2
- package/dist/vite/index.d.ts +22 -2
- package/dist/vite/index.js +738 -77
- package/dist/vite/index.js.map +1 -1
- package/package.json +4 -1
- package/dist/studio/assets/index-D6D5nPQI.js +0 -10
- package/dist/studio/assets/index-z0sbyt6N.css +0 -2
package/dist/server/index.cjs
CHANGED
|
@@ -593,14 +593,6 @@ var KeyScanner = class {
|
|
|
593
593
|
};
|
|
594
594
|
|
|
595
595
|
// src/server/file-mutator.ts
|
|
596
|
-
function assertEnabled(component) {
|
|
597
|
-
const reason = getDisabledReason();
|
|
598
|
-
if (reason !== null) {
|
|
599
|
-
throw new Error(
|
|
600
|
-
`[livelocale] ${component} can only be executed in development mode (${reason}).`
|
|
601
|
-
);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
596
|
var FileMutex = class {
|
|
605
597
|
queue = Promise.resolve();
|
|
606
598
|
async acquire(fn) {
|
|
@@ -616,7 +608,23 @@ var FileMutex = class {
|
|
|
616
608
|
};
|
|
617
609
|
var FileMutator = class {
|
|
618
610
|
fileLocks = /* @__PURE__ */ new Map();
|
|
619
|
-
|
|
611
|
+
requireEnabled;
|
|
612
|
+
quiet;
|
|
613
|
+
scanner;
|
|
614
|
+
constructor(options = {}) {
|
|
615
|
+
this.requireEnabled = options.requireEnabled ?? true;
|
|
616
|
+
this.quiet = options.quiet ?? false;
|
|
617
|
+
this.scanner = new KeyScanner({ requireEnabled: this.requireEnabled });
|
|
618
|
+
}
|
|
619
|
+
assertEnabled() {
|
|
620
|
+
if (!this.requireEnabled) return;
|
|
621
|
+
const reason = getDisabledReason();
|
|
622
|
+
if (reason !== null) {
|
|
623
|
+
throw new Error(
|
|
624
|
+
`[livelocale] FileMutator can only be executed in development mode (${reason}).`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
620
628
|
/**
|
|
621
629
|
* Helper to serialize execution per file path
|
|
622
630
|
*/
|
|
@@ -634,7 +642,7 @@ var FileMutator = class {
|
|
|
634
642
|
* structural JSON validation, and concurrency serialization.
|
|
635
643
|
*/
|
|
636
644
|
async updateLocaleKey(basePath, locale, keyPath, newValue) {
|
|
637
|
-
assertEnabled(
|
|
645
|
+
this.assertEnabled();
|
|
638
646
|
if (!isSafeLocaleSegment(locale)) {
|
|
639
647
|
throw new Error(
|
|
640
648
|
`[livelocale] Path traversal attempt detected in locale identifier: "${locale}"`
|
|
@@ -737,7 +745,9 @@ var FileMutator = class {
|
|
|
737
745
|
`[livelocale] Failed to rename temp file to target at ${filePath}. Operation: rename. Original error: ${err.message}`
|
|
738
746
|
);
|
|
739
747
|
}
|
|
740
|
-
|
|
748
|
+
if (!this.quiet) {
|
|
749
|
+
console.log(`[livelocale] Updated key '${keyPath}' in locale '${locale}'`);
|
|
750
|
+
}
|
|
741
751
|
});
|
|
742
752
|
}
|
|
743
753
|
/**
|
|
@@ -745,7 +755,7 @@ var FileMutator = class {
|
|
|
745
755
|
* `*.json` inside the `{locale}/` folder merged under its namespace.
|
|
746
756
|
*/
|
|
747
757
|
async readLocale(basePath, locale) {
|
|
748
|
-
assertEnabled(
|
|
758
|
+
this.assertEnabled();
|
|
749
759
|
const localePath = path3.resolve(basePath, locale);
|
|
750
760
|
let isDir = false;
|
|
751
761
|
try {
|
|
@@ -772,7 +782,7 @@ var FileMutator = class {
|
|
|
772
782
|
* Lists all locale codes dynamically scanned from the locales directory.
|
|
773
783
|
*/
|
|
774
784
|
async listLocales(basePath) {
|
|
775
|
-
assertEnabled(
|
|
785
|
+
this.assertEnabled();
|
|
776
786
|
const resolvedBasePath = path3.resolve(basePath);
|
|
777
787
|
const items = await import_fs2.promises.readdir(resolvedBasePath, { withFileTypes: true });
|
|
778
788
|
const localesSet = /* @__PURE__ */ new Set();
|
|
@@ -794,7 +804,7 @@ var FileMutator = class {
|
|
|
794
804
|
* Adds a new locale directory/file based on current repository structure.
|
|
795
805
|
*/
|
|
796
806
|
async addLocale(basePath, locale) {
|
|
797
|
-
assertEnabled(
|
|
807
|
+
this.assertEnabled();
|
|
798
808
|
if (!isSafeLocaleSegment(locale)) {
|
|
799
809
|
throw new Error(
|
|
800
810
|
`[livelocale] Path traversal attempt detected in locale identifier: "${locale}"`
|
|
@@ -850,7 +860,7 @@ var FileMutator = class {
|
|
|
850
860
|
* Renames an existing locale (directory or file) to a new name.
|
|
851
861
|
*/
|
|
852
862
|
async renameLocale(basePath, oldLocale, newLocale) {
|
|
853
|
-
assertEnabled(
|
|
863
|
+
this.assertEnabled();
|
|
854
864
|
if (!isSafeLocaleSegment(oldLocale) || !isSafeLocaleSegment(newLocale)) {
|
|
855
865
|
throw new Error("[livelocale] Path traversal attempt detected");
|
|
856
866
|
}
|
|
@@ -908,7 +918,7 @@ var FileMutator = class {
|
|
|
908
918
|
* Deletes an existing locale (directory or file).
|
|
909
919
|
*/
|
|
910
920
|
async deleteLocale(basePath, locale) {
|
|
911
|
-
assertEnabled(
|
|
921
|
+
this.assertEnabled();
|
|
912
922
|
if (!isSafeLocaleSegment(locale)) {
|
|
913
923
|
throw new Error("[livelocale] Path traversal attempt detected");
|
|
914
924
|
}
|
|
@@ -1699,6 +1709,67 @@ function validateMessageSyntax(message) {
|
|
|
1699
1709
|
return parseIcu(prepared).error;
|
|
1700
1710
|
}
|
|
1701
1711
|
|
|
1712
|
+
// src/core/save-rules.ts
|
|
1713
|
+
var reject = (status, code, error) => ({
|
|
1714
|
+
ok: false,
|
|
1715
|
+
status,
|
|
1716
|
+
code,
|
|
1717
|
+
error
|
|
1718
|
+
});
|
|
1719
|
+
function validateWrite(input, options = {}) {
|
|
1720
|
+
const { locale, key, value } = input;
|
|
1721
|
+
if (!isValidLocaleFormat(locale)) {
|
|
1722
|
+
return reject(400, "INVALID_LOCALE_FORMAT", "Invalid locale string parameter.");
|
|
1723
|
+
}
|
|
1724
|
+
if (!isValidKeyFormat(key)) {
|
|
1725
|
+
return reject(
|
|
1726
|
+
400,
|
|
1727
|
+
"INVALID_KEY_FORMAT",
|
|
1728
|
+
"Invalid key path. Must be 1-200 characters containing alphanumeric, dots, underscores, or hyphens."
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
if (!isSafeKeyPath(key)) {
|
|
1732
|
+
return reject(
|
|
1733
|
+
400,
|
|
1734
|
+
"INVALID_KEY_FORMAT",
|
|
1735
|
+
"Forbidden key path: segments like __proto__, constructor, or prototype are not allowed."
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
if (!isValidValueFormat(value)) {
|
|
1739
|
+
return reject(
|
|
1740
|
+
400,
|
|
1741
|
+
"INVALID_VALUE_FORMAT",
|
|
1742
|
+
"Invalid value. Must be a string less than 10000 characters."
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
const accepted = {
|
|
1746
|
+
ok: true,
|
|
1747
|
+
locale,
|
|
1748
|
+
key,
|
|
1749
|
+
value
|
|
1750
|
+
};
|
|
1751
|
+
if (options.force === true) return accepted;
|
|
1752
|
+
const syntaxError = validateMessageSyntax(accepted.value);
|
|
1753
|
+
if (syntaxError) {
|
|
1754
|
+
return reject(422, "MESSAGE_SYNTAX", `Message syntax: ${syntaxError}.`);
|
|
1755
|
+
}
|
|
1756
|
+
const comparison = comparePlaceholders(options.reference, accepted.value);
|
|
1757
|
+
if (!comparison.ok) {
|
|
1758
|
+
const parts = [];
|
|
1759
|
+
if (comparison.missing.length) parts.push(`missing ${comparison.missing.join(", ")}`);
|
|
1760
|
+
if (comparison.extra.length) parts.push(`unexpected ${comparison.extra.join(", ")}`);
|
|
1761
|
+
return {
|
|
1762
|
+
...reject(
|
|
1763
|
+
422,
|
|
1764
|
+
"PLACEHOLDER_MISMATCH",
|
|
1765
|
+
`Placeholders differ from the reference: ${parts.join("; ")}.`
|
|
1766
|
+
),
|
|
1767
|
+
details: { missing: comparison.missing, extra: comparison.extra }
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
return accepted;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1702
1773
|
// src/server/router.ts
|
|
1703
1774
|
var DEFAULT_ALLOWED_ORIGINS = ["http://localhost:3010", "http://127.0.0.1:3010"];
|
|
1704
1775
|
var MAX_BODY_BYTES = 1e6;
|
|
@@ -1886,53 +1957,18 @@ function createMutationRouter(mutator, options) {
|
|
|
1886
1957
|
await mutator.deleteLocale(basePath, locale);
|
|
1887
1958
|
return json(200, headers, { success: true, locale });
|
|
1888
1959
|
}
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
code: "INVALID_KEY_FORMAT"
|
|
1899
|
-
});
|
|
1900
|
-
}
|
|
1901
|
-
if (!isSafeKeyPath(key)) {
|
|
1902
|
-
return json(400, headers, {
|
|
1903
|
-
error: "Forbidden key path: segments like __proto__, constructor, or prototype are not allowed.",
|
|
1904
|
-
code: "INVALID_KEY_FORMAT"
|
|
1905
|
-
});
|
|
1906
|
-
}
|
|
1907
|
-
if (!isValidValueFormat(value)) {
|
|
1908
|
-
return json(400, headers, {
|
|
1909
|
-
error: "Invalid value. Must be a string less than 10000 characters.",
|
|
1910
|
-
code: "INVALID_VALUE_FORMAT"
|
|
1960
|
+
const verdict = validateWrite(
|
|
1961
|
+
{ locale, key, value },
|
|
1962
|
+
{ reference: typeof reference === "string" ? reference : null, force: force === true }
|
|
1963
|
+
);
|
|
1964
|
+
if (!verdict.ok) {
|
|
1965
|
+
return json(verdict.status, headers, {
|
|
1966
|
+
error: verdict.error,
|
|
1967
|
+
code: verdict.code,
|
|
1968
|
+
details: verdict.details
|
|
1911
1969
|
});
|
|
1912
1970
|
}
|
|
1913
|
-
|
|
1914
|
-
const syntaxError = validateMessageSyntax(value);
|
|
1915
|
-
if (syntaxError) {
|
|
1916
|
-
return json(422, headers, {
|
|
1917
|
-
error: `Message syntax: ${syntaxError}.`,
|
|
1918
|
-
code: "MESSAGE_SYNTAX"
|
|
1919
|
-
});
|
|
1920
|
-
}
|
|
1921
|
-
if (typeof reference === "string") {
|
|
1922
|
-
const comparison = comparePlaceholders(reference, value);
|
|
1923
|
-
if (!comparison.ok) {
|
|
1924
|
-
const parts = [];
|
|
1925
|
-
if (comparison.missing.length) parts.push(`missing ${comparison.missing.join(", ")}`);
|
|
1926
|
-
if (comparison.extra.length) parts.push(`unexpected ${comparison.extra.join(", ")}`);
|
|
1927
|
-
return json(422, headers, {
|
|
1928
|
-
error: `Placeholders differ from the reference: ${parts.join("; ")}.`,
|
|
1929
|
-
code: "PLACEHOLDER_MISMATCH",
|
|
1930
|
-
details: { missing: comparison.missing, extra: comparison.extra }
|
|
1931
|
-
});
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
1934
|
-
}
|
|
1935
|
-
await mutator.updateLocaleKey(basePath, locale, key, value);
|
|
1971
|
+
await mutator.updateLocaleKey(basePath, verdict.locale, verdict.key, verdict.value);
|
|
1936
1972
|
return json(200, headers, { success: true, key, locale });
|
|
1937
1973
|
}
|
|
1938
1974
|
return {
|
|
@@ -2006,7 +2042,7 @@ var BodyTooLargeError = class extends Error {
|
|
|
2006
2042
|
}
|
|
2007
2043
|
};
|
|
2008
2044
|
function readNodeBody(req, maxBytes) {
|
|
2009
|
-
return new Promise((resolve5,
|
|
2045
|
+
return new Promise((resolve5, reject2) => {
|
|
2010
2046
|
let bodyText = "";
|
|
2011
2047
|
let settled = false;
|
|
2012
2048
|
req.on("data", (chunk) => {
|
|
@@ -2014,7 +2050,7 @@ function readNodeBody(req, maxBytes) {
|
|
|
2014
2050
|
bodyText += chunk.toString();
|
|
2015
2051
|
if (Buffer.byteLength(bodyText, "utf-8") > maxBytes) {
|
|
2016
2052
|
settled = true;
|
|
2017
|
-
|
|
2053
|
+
reject2(new BodyTooLargeError());
|
|
2018
2054
|
req.destroy?.();
|
|
2019
2055
|
}
|
|
2020
2056
|
});
|
|
@@ -2026,7 +2062,7 @@ function readNodeBody(req, maxBytes) {
|
|
|
2026
2062
|
req.on("error", (err) => {
|
|
2027
2063
|
if (settled) return;
|
|
2028
2064
|
settled = true;
|
|
2029
|
-
|
|
2065
|
+
reject2(err);
|
|
2030
2066
|
});
|
|
2031
2067
|
});
|
|
2032
2068
|
}
|