uvako-cli 0.2.1 → 0.4.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 +175 -125
- package/package.json +1 -1
- package/src/api.js +6 -5
- package/src/check.js +155 -63
- package/src/config.js +6 -5
- package/src/i18n.js +67 -0
- package/src/init.js +74 -17
- package/src/locales/en.js +226 -0
- package/src/main.js +15 -10
- package/src/package.js +3 -2
- package/src/remote.js +51 -50
- package/src/zip.js +5 -4
package/src/check.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { walk } from "./files.js";
|
|
4
|
+
import { t } from "./i18n.js";
|
|
4
5
|
|
|
5
6
|
// Tema denetimi. Mağazaya yüklenen paketi platform aynı kurallarla yeniden denetler; buradaki
|
|
6
7
|
// denetim geliştiriciye hatayı yüklemeden önce göstermek içindir, güvenlik kararı sunucudadır.
|
|
@@ -10,7 +11,11 @@ import { walk } from "./files.js";
|
|
|
10
11
|
// üstüne ayar, bölüm sırası, SCSS ve görsel getirir.
|
|
11
12
|
// depo — platform deposundaki katalog teması (themes/<anahtar>).
|
|
12
13
|
|
|
13
|
-
export const limits = { fileBytes: 5 * 1024 * 1024, totalBytes: 60 * 1024 * 1024, files: 600, styleBytes: 512 * 1024, archiveBytes: 40 * 1024 * 1024 };
|
|
14
|
+
export const limits = { fileBytes: 5 * 1024 * 1024, totalBytes: 60 * 1024 * 1024, files: 600, styleBytes: 512 * 1024, archiveBytes: 40 * 1024 * 1024, localeBytes: 256 * 1024, localeEntries: 2000 };
|
|
15
|
+
|
|
16
|
+
// Temanın kendi metinlerinin çevirisi: locales/<dil>.json, {"temadaki metin": "çevirisi"}.
|
|
17
|
+
// Vitrin ve panel bu sözlükle temanın varsayılan metinlerini ziyaretçinin diline çevirir.
|
|
18
|
+
export const textLocales = ["tr", "en", "de"];
|
|
14
19
|
|
|
15
20
|
const keyPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
16
21
|
const versionPattern = /^\d+\.\d+\.\d+$/;
|
|
@@ -30,8 +35,8 @@ const allowedStyleHosts = new Set(["fonts.googleapis.com", "fonts.gstatic.com",
|
|
|
30
35
|
const scriptPatterns = [
|
|
31
36
|
{ pattern: /<script\b/i, text: "<script> etiketi" },
|
|
32
37
|
{ pattern: /javascript:/i, text: "javascript: adresi" },
|
|
33
|
-
{ pattern: /\son[a-z]+\s*=/i, text: "olay özniteliği (onclick, onload…)" },
|
|
34
|
-
{ pattern: /<iframe\b|<object\b|<embed\b|<foreignObject\b/i, text: "gömülü içerik etiketi" },
|
|
38
|
+
{ pattern: /\son[a-z]+\s*=/i, text: t("olay özniteliği (onclick, onload…)") },
|
|
39
|
+
{ pattern: /<iframe\b|<object\b|<embed\b|<foreignObject\b/i, text: t("gömülü içerik etiketi") },
|
|
35
40
|
];
|
|
36
41
|
|
|
37
42
|
const secretPatterns = [
|
|
@@ -51,9 +56,54 @@ function readJson(file, report, label, required = true) {
|
|
|
51
56
|
try {
|
|
52
57
|
return JSON.parse(readFileSync(file, "utf8").replace(/^\uFEFF/, ""));
|
|
53
58
|
} catch (error) {
|
|
54
|
-
report.errors.push(
|
|
59
|
+
report.errors.push(t("{0} geçerli JSON değil: {1}", label, error.message));
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// locales/<dil>.json: düz bir metin sözlüğü olmalı. Geçerliyse sözlüğü, değilse null döndürür.
|
|
65
|
+
function checkLocale(rel, file, report) {
|
|
66
|
+
let texts;
|
|
67
|
+
try {
|
|
68
|
+
texts = JSON.parse(readFileSync(file, "utf8").replace(/^\uFEFF/, ""));
|
|
69
|
+
} catch (error) {
|
|
70
|
+
report.errors.push(t("{0} geçerli JSON değil: {1}", rel, error.message));
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
if (!texts || typeof texts !== "object" || Array.isArray(texts) || Object.values(texts).some((value) => typeof value !== "string")) {
|
|
74
|
+
report.errors.push(t("{0}: düz bir metin sözlüğü olmalı ({\"temadaki metin\": \"çevirisi\"}).", rel));
|
|
55
75
|
return null;
|
|
56
76
|
}
|
|
77
|
+
const entries = Object.entries(texts);
|
|
78
|
+
if (entries.length > limits.localeEntries) report.errors.push(t("{0}: en çok {1} metin içerebilir.", rel, limits.localeEntries));
|
|
79
|
+
if (entries.some(([source, text]) => !source.trim() || source.length > 500 || text.length > 2000)) report.errors.push(t("{0}: metin en çok 500, çevirisi en çok 2000 karakter olabilir; boş metin olamaz.", rel));
|
|
80
|
+
return texts;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Çevrilebilecek metinler: theme.json'daki tanıtım alanları ve settings_data.json'daki yazılar.
|
|
84
|
+
// Renk, adres, dosya yolu ve sayılar metin sayılmaz.
|
|
85
|
+
function translatable(meta, data) {
|
|
86
|
+
const out = new Set();
|
|
87
|
+
const add = (value) => {
|
|
88
|
+
const text = typeof value === "string" ? value.trim() : "";
|
|
89
|
+
if (!text || !/\p{L}{2}/u.test(text)) return;
|
|
90
|
+
if (/^(#|https?:|\/|uploads\/|assets\/|rgba?\(|[\w.-]+\.(png|jpe?g|webp|avif|gif|svg|ico|woff2?|ttf|otf)$)/i.test(text)) return;
|
|
91
|
+
if (/^[a-z0-9_.,:-]+$/.test(text)) return;
|
|
92
|
+
out.add(text);
|
|
93
|
+
};
|
|
94
|
+
add(meta?.description);
|
|
95
|
+
for (const item of [...(Array.isArray(meta?.industries) ? meta.industries : []), ...(Array.isArray(meta?.features) ? meta.features : [])]) add(item);
|
|
96
|
+
if (data && typeof data === "object" && !Array.isArray(data)) for (const value of Object.values(data)) add(value);
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function checkTranslations(locales, meta, data, report) {
|
|
101
|
+
const texts = translatable(meta, data);
|
|
102
|
+
for (const [locale, dict] of Object.entries(locales)) {
|
|
103
|
+
if (!dict) continue;
|
|
104
|
+
const missing = [...texts].filter((text) => !(text in dict));
|
|
105
|
+
if (missing.length) report.warnings.push(t("locales/{0}.json: {1} metnin çevirisi yok (ör. \"{2}\"). Çevirisi olmayan metin yazıldığı gibi gösterilir.", locale, missing.length, missing[0].slice(0, 60)));
|
|
106
|
+
}
|
|
57
107
|
}
|
|
58
108
|
|
|
59
109
|
// Şemadaki ayar adlarını toplar ({"type":"tab","values":[{name, settings, group}]}).
|
|
@@ -64,7 +114,7 @@ function schemaKeys(node, into, report, trail = "") {
|
|
|
64
114
|
const where = trail ? `${trail} › ${tab.name ?? "?"}` : String(tab.name ?? "?");
|
|
65
115
|
for (const [key, setting] of Object.entries(tab.settings ?? {})) {
|
|
66
116
|
into.add(key);
|
|
67
|
-
if (!setting || typeof setting !== "object" || !setting.type) report.errors.push(
|
|
117
|
+
if (!setting || typeof setting !== "object" || !setting.type) report.errors.push(t("Ayar şeması: \"{0}\" ayarının türü yok ({1}).", key, where));
|
|
68
118
|
}
|
|
69
119
|
schemaKeys(tab.group, into, report, where);
|
|
70
120
|
}
|
|
@@ -83,10 +133,10 @@ function locate(dir) {
|
|
|
83
133
|
|
|
84
134
|
function scanText(rel, text, report) {
|
|
85
135
|
for (const { pattern, text: what } of scriptPatterns) {
|
|
86
|
-
if (pattern.test(text)) report.errors.push(
|
|
136
|
+
if (pattern.test(text)) report.errors.push(t("{0}: {1} içeriyor. Tema içinde betik çalıştırılamaz.", rel, what));
|
|
87
137
|
}
|
|
88
138
|
for (const pattern of secretPatterns) {
|
|
89
|
-
if (pattern.test(text)) report.errors.push(
|
|
139
|
+
if (pattern.test(text)) report.errors.push(t("{0}: gizli anahtar ya da parola gibi görünen bir değer içeriyor.", rel));
|
|
90
140
|
}
|
|
91
141
|
}
|
|
92
142
|
|
|
@@ -113,19 +163,19 @@ function scanStyle(rel, source, report, { strict = false, styles = new Set(), to
|
|
|
113
163
|
return (match ? held[Number(match[1])] ?? "" : value).trim();
|
|
114
164
|
};
|
|
115
165
|
const all = `${text} ${held.join(" ")}`;
|
|
116
|
-
if (/expression\s*\(|behavior\s*:|-moz-binding|<\/style/i.test(all)) report.errors.push(
|
|
117
|
-
if (/javascript:/i.test(all)) report.errors.push(
|
|
166
|
+
if (/expression\s*\(|behavior\s*:|-moz-binding|<\/style/i.test(all)) report.errors.push(t("{0}: tarayıcıda kod çalıştıran CSS (expression, behavior, -moz-binding) içeriyor.", rel));
|
|
167
|
+
if (/javascript:/i.test(all)) report.errors.push(t("{0}: javascript: adresi içeriyor.", rel));
|
|
118
168
|
if (strict) {
|
|
119
169
|
const rule = /@(while|for|each|function|mixin|use|forward|debug|warn|error|charset)\b/i.exec(text);
|
|
120
|
-
if (rule) report.errors.push(
|
|
121
|
-
if (text.includes("\\")) report.errors.push(
|
|
170
|
+
if (rule) report.errors.push(t("{0}: @{1} kullanılamaz. Tema stilleri döngü, fonksiyon ve mixin tanımı içeremez; temel temanın mixin'leri @include ile kullanılabilir.", rel, rule[1].toLowerCase()));
|
|
171
|
+
if (text.includes("\\")) report.errors.push(t("{0}: tırnak dışında ters eğik çizgi kullanılamaz.", rel));
|
|
122
172
|
}
|
|
123
173
|
text = text.replace(/url\(([^)]*)\)/gi, (_, inner) => {
|
|
124
174
|
const target = resolve(inner);
|
|
125
175
|
if (/^data:/i.test(target)) {
|
|
126
|
-
if (strict && !/^data:(image\/(png|jpe?g|gif|webp)|font\/woff2?|application\/font-woff2?);base64,/i.test(target)) report.errors.push(
|
|
176
|
+
if (strict && !/^data:(image\/(png|jpe?g|gif|webp)|font\/woff2?|application\/font-woff2?);base64,/i.test(target)) report.errors.push(t("{0}: yalnızca görsel ve yazı tipi data: adresleri kullanılabilir.", rel));
|
|
127
177
|
} else if ((target.includes("//") || (strict && target.includes(":"))) && !allowedUrl(target)) {
|
|
128
|
-
report.errors.push(
|
|
178
|
+
report.errors.push(t("{0}: izin verilmeyen dış adres ({1}). Görsel ve yazı tiplerini assets/ altına koyun; yalnızca şu adresler çağrılabilir: {2}.", rel, hostOf(target), [...allowedStyleHosts].join(", ")));
|
|
129
179
|
}
|
|
130
180
|
return "url()";
|
|
131
181
|
});
|
|
@@ -134,9 +184,9 @@ function scanStyle(rel, source, report, { strict = false, styles = new Set(), to
|
|
|
134
184
|
if (/^\s*url\(\)/i.test(part)) continue;
|
|
135
185
|
const target = resolve(part);
|
|
136
186
|
if (target.includes("//") || target.includes(":")) {
|
|
137
|
-
if (!allowedUrl(target)) report.errors.push(
|
|
187
|
+
if (!allowedUrl(target)) report.errors.push(t("{0}: izin verilmeyen dış adres ({1}).", rel, hostOf(target)));
|
|
138
188
|
} else if (strict && !(/^[a-z0-9]+(?:[-_][a-z0-9]+)*$/.test(target) && (styles.has(`${target}.scss`) || styles.has(`_${target}.scss`)))) {
|
|
139
|
-
report.errors.push(
|
|
189
|
+
report.errors.push(t("{0}: @import \"{1}\" pakette yok. Yalnızca paketin kendi styles/ dosyaları içe aktarılabilir.", rel, target));
|
|
140
190
|
}
|
|
141
191
|
}
|
|
142
192
|
}
|
|
@@ -144,7 +194,7 @@ function scanStyle(rel, source, report, { strict = false, styles = new Set(), to
|
|
|
144
194
|
for (const raw of text.split(";")) {
|
|
145
195
|
const statement = raw.trim();
|
|
146
196
|
if (statement === "" || /^\$[A-Za-z0-9_-]+\s*:\s*[^{}@]+$/.test(statement) || /^@import/i.test(statement)) continue;
|
|
147
|
-
report.errors.push(
|
|
197
|
+
report.errors.push(t("{0}: yalnızca değişken ataması ($ad: değer;) ve yazı tipi @import'u içerebilir.", rel));
|
|
148
198
|
break;
|
|
149
199
|
}
|
|
150
200
|
}
|
|
@@ -154,21 +204,51 @@ function checkIdentity(dir, meta, report) {
|
|
|
154
204
|
const key = typeof meta?.key === "string" ? meta.key.trim() : "";
|
|
155
205
|
report.key = key;
|
|
156
206
|
if (!meta) return key;
|
|
157
|
-
if (!keyPattern.test(key) || key.length > 40) report.errors.push(
|
|
158
|
-
else if (key !== path.basename(dir)) report.errors.push(
|
|
207
|
+
if (!keyPattern.test(key) || key.length > 40) report.errors.push(t("theme.json: \"key\" küçük harf, rakam ve tireden oluşmalı (ör. \"linea\")."));
|
|
208
|
+
else if (key !== path.basename(dir)) report.errors.push(t("theme.json: \"key\" ({0}) klasör adıyla ({1}) aynı olmalı.", key, path.basename(dir)));
|
|
159
209
|
if (!String(meta.name ?? "").trim()) report.errors.push(`theme.json: "name" gerekli.`);
|
|
160
|
-
if (!versionPattern.test(String(meta.version ?? ""))) report.errors.push(
|
|
161
|
-
if (!String(meta.description ?? "").trim()) report.warnings.push(
|
|
210
|
+
if (!versionPattern.test(String(meta.version ?? ""))) report.errors.push(t("theme.json: \"version\" 1.0.0 biçiminde olmalı."));
|
|
211
|
+
if (!String(meta.description ?? "").trim()) report.warnings.push(t("theme.json: \"description\" boş; tema listesinde açıklama görünmez."));
|
|
162
212
|
return key;
|
|
163
213
|
}
|
|
164
214
|
|
|
165
215
|
function checkTemplate(index, types, report, source) {
|
|
166
216
|
if (!index) return;
|
|
167
|
-
if (!Array.isArray(index.order) || index.order.length === 0) report.errors.push(
|
|
217
|
+
if (!Array.isArray(index.order) || index.order.length === 0) report.errors.push(t("templates/index.json: \"order\" boş; ana sayfada bölüm görünmez."));
|
|
168
218
|
for (const id of index.order ?? []) {
|
|
169
219
|
const section = index.sections?.[id];
|
|
170
|
-
if (!section) report.errors.push(
|
|
171
|
-
else if (types && !types.has(section.type)) report.errors.push(
|
|
220
|
+
if (!section) report.errors.push(t("templates/index.json: \"order\" içindeki \"{0}\" bölümü \"sections\" altında tanımlı değil.", id));
|
|
221
|
+
else if (types && !types.has(section.type)) report.errors.push(t("templates/index.json: \"{0}\" bölümünün türü ({1}) {2} yok.", id, section.type, source));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Ürün sayfası sekmeleri: product_detail_page_tabs anahtarları ve her sekmenin başlık ayarı.
|
|
226
|
+
export const productTabKeys = {
|
|
227
|
+
display_product_comments: "tab_product_comments",
|
|
228
|
+
display_product_questions: "tab_product_questions",
|
|
229
|
+
display_payment_options: "tab_product_payment_options",
|
|
230
|
+
display_product_feeds: "tab_product_feeds",
|
|
231
|
+
display_shopping_experiences: "tab_shopping_experiences",
|
|
232
|
+
display_special_information_area: null,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
function checkProductTabs(data, report) {
|
|
236
|
+
if (!data || typeof data !== "object" || !("product_detail_page_tabs" in data)) return;
|
|
237
|
+
const value = data.product_detail_page_tabs;
|
|
238
|
+
if ([0, 1, "0", "1", true, false].includes(value)) return;
|
|
239
|
+
const where = 'config/settings_data.json: "product_detail_page_tabs"';
|
|
240
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
241
|
+
report.errors.push(t("{0} 1 (hepsi açık) ya da {\"display_product_comments\": 1, …} nesnesi olmalı.", where));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
for (const [key, flag] of Object.entries(value)) {
|
|
245
|
+
if (!(key in productTabKeys)) {
|
|
246
|
+
report.warnings.push(t("{0}: \"{1}\" bilinen bir sekme değil ({2}).", where, key, Object.keys(productTabKeys).join(", ")));
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (![0, 1, "0", "1", true, false].includes(flag)) report.errors.push(t("{0}: \"{1}\" değeri 1 ya da 0 olmalı.", where, key));
|
|
250
|
+
const label = productTabKeys[key];
|
|
251
|
+
if (label && label in data && typeof data[label] !== "string") report.errors.push(t("config/settings_data.json: \"{0}\" sekme başlığı metin olmalı.", label));
|
|
172
252
|
}
|
|
173
253
|
}
|
|
174
254
|
|
|
@@ -176,7 +256,7 @@ function checkImages(data, assets, report) {
|
|
|
176
256
|
if (!data || typeof data !== "object") return;
|
|
177
257
|
for (const [name, value] of Object.entries(data)) {
|
|
178
258
|
if (typeof value !== "string" || !/^uploads\/[^\s]+\.(png|jpe?g|webp|avif|gif|svg|ico)$/i.test(value)) continue;
|
|
179
|
-
if (!existsSync(path.join(assets, value))) report.errors.push(
|
|
259
|
+
if (!existsSync(path.join(assets, value))) report.errors.push(t("config/settings_data.json: \"{0}\" ayarındaki görsel bulunamadı (assets/{1}).", name, value));
|
|
180
260
|
}
|
|
181
261
|
}
|
|
182
262
|
|
|
@@ -184,17 +264,18 @@ function checkImages(data, assets, report) {
|
|
|
184
264
|
|
|
185
265
|
function checkPackage(dir, meta, report) {
|
|
186
266
|
const base = String(meta?.base ?? "").trim();
|
|
187
|
-
if (meta && !keyPattern.test(base)) report.errors.push(
|
|
188
|
-
if (meta && (Number(meta.price ?? 0) < 0 || Number(meta.price ?? 0) > 100000 || Number.isNaN(Number(meta.price ?? 0)))) report.errors.push(
|
|
189
|
-
if (meta?.demo_url && !/^https:\/\/[^\s]+$/.test(String(meta.demo_url))) report.errors.push(
|
|
267
|
+
if (meta && !keyPattern.test(base)) report.errors.push(t("theme.json: \"base\" gerekli: paketin temel aldığı katalog teması (ör. \"linea\"). Temalar: https://themes.uvako.com"));
|
|
268
|
+
if (meta && (Number(meta.price ?? 0) < 0 || Number(meta.price ?? 0) > 100000 || Number.isNaN(Number(meta.price ?? 0)))) report.errors.push(t("theme.json: \"price\" 0 ile 100.000 arasında olmalı."));
|
|
269
|
+
if (meta?.demo_url && !/^https:\/\/[^\s]+$/.test(String(meta.demo_url))) report.errors.push(t("theme.json: \"demo_url\" https:// ile başlayan bir adres olmalı."));
|
|
190
270
|
|
|
191
271
|
const { files, links } = walk(dir);
|
|
192
|
-
for (const link of links) report.errors.push(
|
|
272
|
+
for (const link of links) report.errors.push(t("{0}: sembolik bağ pakete alınamaz.", link));
|
|
193
273
|
const styles = new Set(files.filter((file) => file.rel.startsWith("styles/")).map((file) => file.rel.slice("styles/".length)));
|
|
194
274
|
const totals = { files: 0, bytes: 0, style: 0 };
|
|
195
275
|
let schema = null;
|
|
196
276
|
let data = null;
|
|
197
277
|
let index = null;
|
|
278
|
+
const locales = {};
|
|
198
279
|
for (const file of files) {
|
|
199
280
|
const rel = file.rel;
|
|
200
281
|
if (localOnly(rel)) continue;
|
|
@@ -202,15 +283,23 @@ function checkPackage(dir, meta, report) {
|
|
|
202
283
|
totals.bytes += file.size;
|
|
203
284
|
const parts = rel.split("/");
|
|
204
285
|
if (parts.some((part) => !segmentPattern.test(part))) {
|
|
205
|
-
report.errors.push(
|
|
286
|
+
report.errors.push(t("{0}: dosya adında yalnızca harf, rakam, nokta, tire ve alt çizgi olabilir; gizli dosya olamaz.", rel));
|
|
206
287
|
continue;
|
|
207
288
|
}
|
|
208
289
|
const ext = path.extname(rel).toLowerCase();
|
|
209
290
|
const text = () => readFileSync(file.full, "utf8");
|
|
210
291
|
if (rel === "theme.json") scanText(rel, text(), report);
|
|
211
292
|
else if (rel === "config/settings_schema.json" || rel === "config/settings_data.json" || rel === "templates/index.json") {
|
|
212
|
-
if (file.size > 1024 * 1024) report.errors.push(
|
|
293
|
+
if (file.size > 1024 * 1024) report.errors.push(t("{0}: 1 MB sınırını aşıyor.", rel));
|
|
213
294
|
else scanText(rel, text(), report);
|
|
295
|
+
} else if (parts[0] === "locales" && parts.length === 2 && rel.endsWith(".json")) {
|
|
296
|
+
const locale = parts[1].slice(0, -5);
|
|
297
|
+
if (!textLocales.includes(locale)) report.errors.push(t("{0}: desteklenmeyen dil. Kullanılabilir dosyalar: {1}.", rel, textLocales.map((item) => `locales/${item}.json`).join(", ")));
|
|
298
|
+
else if (file.size > limits.localeBytes) report.errors.push(t("{0}: {1} KB sınırını aşıyor.", rel, limits.localeBytes / 1024));
|
|
299
|
+
else {
|
|
300
|
+
scanText(rel, text(), report);
|
|
301
|
+
locales[locale] = checkLocale(rel, file.full, report);
|
|
302
|
+
}
|
|
214
303
|
} else if (parts[0] === "styles" && parts.length === 2 && stylePattern.test(parts[1])) {
|
|
215
304
|
totals.style += file.size;
|
|
216
305
|
if (file.size <= limits.styleBytes) {
|
|
@@ -220,39 +309,41 @@ function checkPackage(dir, meta, report) {
|
|
|
220
309
|
}
|
|
221
310
|
} else if (parts[0] === "assets" && parts.length >= 2) {
|
|
222
311
|
if (!assetExtensions.has(ext)) {
|
|
223
|
-
report.errors.push(
|
|
312
|
+
report.errors.push(t("{0}: bu dosya türü ({1}) temada bulunamaz.", rel, ext || "uzantısız"));
|
|
224
313
|
continue;
|
|
225
314
|
}
|
|
226
|
-
if (file.size > limits.fileBytes) report.errors.push(
|
|
227
|
-
else if (file.size > 1024 * 1024 && /\.(png|jpe?g|gif)$/i.test(ext)) report.warnings.push(
|
|
315
|
+
if (file.size > limits.fileBytes) report.errors.push(t("{0}: {1} MB; tek dosya sınırı {2} MB.", rel, (file.size / 1024 / 1024).toFixed(1), limits.fileBytes / 1024 / 1024));
|
|
316
|
+
else if (file.size > 1024 * 1024 && /\.(png|jpe?g|gif)$/i.test(ext)) report.warnings.push(t("{0}: {1} MB. WebP ya da sıkıştırılmış JPEG sayfayı hızlandırır.", rel, (file.size / 1024 / 1024).toFixed(1)));
|
|
228
317
|
if (ext === ".svg" && file.size <= limits.fileBytes) scanText(rel, text(), report);
|
|
229
318
|
} else {
|
|
230
|
-
report.errors.push(
|
|
319
|
+
report.errors.push(t("{0}: bu dosya pakette bulunamaz. İzin verilenler: theme.json, config/settings_schema.json, config/settings_data.json, templates/index.json, locales/tr.json, locales/en.json, locales/de.json, styles/*.scss, assets/**.", rel));
|
|
231
320
|
}
|
|
232
321
|
}
|
|
233
|
-
if (totals.style > limits.styleBytes) report.errors.push(
|
|
234
|
-
if (styles.size > 0 && !styles.has("theme.scss") && !(styles.size === 1 && styles.has("_tokens.scss"))) report.errors.push("styles/theme.scss yok: stil dosyaları oradan içe aktarılır.");
|
|
322
|
+
if (totals.style > limits.styleBytes) report.errors.push(t("Stil dosyaları toplam {0} KB sınırını aşıyor.", limits.styleBytes / 1024));
|
|
323
|
+
if (styles.size > 0 && !styles.has("theme.scss") && !(styles.size === 1 && styles.has("_tokens.scss"))) report.errors.push(t("styles/theme.scss yok: stil dosyaları oradan içe aktarılır."));
|
|
235
324
|
|
|
236
325
|
schema = readJson(path.join(dir, "config", "settings_schema.json"), report, "config/settings_schema.json", false);
|
|
237
326
|
if (schema) {
|
|
238
|
-
if (schema.group?.type !== "tab" || !Array.isArray(schema.group?.values) || schema.group.values.length === 0) report.errors.push(
|
|
327
|
+
if (schema.group?.type !== "tab" || !Array.isArray(schema.group?.values) || schema.group.values.length === 0) report.errors.push(t("config/settings_schema.json: \"group\" sekmeli olmalı ({\"type\":\"tab\",\"values\":[…]})."));
|
|
239
328
|
schemaKeys(schema.group, new Set(), report);
|
|
240
329
|
}
|
|
241
330
|
data = readJson(path.join(dir, "config", "settings_data.json"), report, "config/settings_data.json", false);
|
|
242
|
-
if (data && (typeof data !== "object" || Array.isArray(data))) report.errors.push("config/settings_data.json: anahtar-değer nesnesi olmalı.");
|
|
331
|
+
if (data && (typeof data !== "object" || Array.isArray(data))) report.errors.push(t("config/settings_data.json: anahtar-değer nesnesi olmalı."));
|
|
243
332
|
index = readJson(path.join(dir, "templates", "index.json"), report, "templates/index.json", false);
|
|
244
333
|
// Bölüm türleri temel temadan gelir; `theme init` bunları reference/sections.json dosyasına yazar.
|
|
245
334
|
const reference = existsSync(path.join(dir, "reference", "sections.json")) ? readJson(path.join(dir, "reference", "sections.json"), { errors: [] }, "") : null;
|
|
246
|
-
checkTemplate(index, reference?.sections ? new Set(Object.keys(reference.sections)) : null, report,
|
|
335
|
+
checkTemplate(index, reference?.sections ? new Set(Object.keys(reference.sections)) : null, report, t("{0} temasında", base));
|
|
247
336
|
checkImages(data, path.join(dir, "assets"), report);
|
|
337
|
+
checkProductTabs(data, report);
|
|
338
|
+
checkTranslations(locales, meta, data, report);
|
|
248
339
|
for (const shot of [meta?.screenshot, ...(Array.isArray(meta?.screenshots) ? meta.screenshots : [])]) {
|
|
249
340
|
const rel = String(shot ?? "").trim().replace(/^\//, "").replace(/^assets\//, "");
|
|
250
|
-
if (rel && !existsSync(path.join(dir, "assets", rel))) report.errors.push(
|
|
341
|
+
if (rel && !existsSync(path.join(dir, "assets", rel))) report.errors.push(t("theme.json: ekran görüntüsü pakette yok (assets/{0}).", rel));
|
|
251
342
|
}
|
|
252
343
|
report.files = totals.files;
|
|
253
344
|
report.bytes = totals.bytes;
|
|
254
|
-
if (totals.bytes > limits.totalBytes) report.errors.push(
|
|
255
|
-
if (totals.files > limits.files) report.errors.push(
|
|
345
|
+
if (totals.bytes > limits.totalBytes) report.errors.push(t("Tema {0} MB; sınır {1} MB.", (totals.bytes / 1024 / 1024).toFixed(0), limits.totalBytes / 1024 / 1024));
|
|
346
|
+
if (totals.files > limits.files) report.errors.push(t("Tema {0} dosya içeriyor; sınır {1}.", totals.files, limits.files));
|
|
256
347
|
}
|
|
257
348
|
|
|
258
349
|
// ── platform deposundaki katalog teması ────────────────────────────────────────────────────────
|
|
@@ -260,19 +351,19 @@ function checkPackage(dir, meta, report) {
|
|
|
260
351
|
function scanRepoFiles(root, label, report, totals) {
|
|
261
352
|
if (!existsSync(root)) return;
|
|
262
353
|
const { files, links } = walk(root);
|
|
263
|
-
for (const link of links) report.errors.push(
|
|
354
|
+
for (const link of links) report.errors.push(t("{0}: sembolik bağ pakete alınamaz.", `${label}/${link}`));
|
|
264
355
|
for (const file of files) {
|
|
265
356
|
const rel = `${label}/${file.rel}`;
|
|
266
357
|
const ext = path.extname(file.rel).toLowerCase();
|
|
267
358
|
totals.files += 1;
|
|
268
359
|
totals.bytes += file.size;
|
|
269
|
-
if (file.rel.split("/").some((part) => part === ".." || part.startsWith("."))) report.errors.push(
|
|
360
|
+
if (file.rel.split("/").some((part) => part === ".." || part.startsWith("."))) report.errors.push(t("{0}: gizli dosya ya da üst klasöre çıkan yol.", rel));
|
|
270
361
|
if (!repoExtensions.has(ext)) {
|
|
271
|
-
report.errors.push(
|
|
362
|
+
report.errors.push(t("{0}: bu dosya türü ({1}) temada bulunamaz.", rel, ext || "uzantısız"));
|
|
272
363
|
continue;
|
|
273
364
|
}
|
|
274
|
-
if (file.size > limits.fileBytes) report.errors.push(
|
|
275
|
-
else if (file.size > 1024 * 1024 && /\.(png|jpe?g|gif)$/i.test(ext)) report.warnings.push(
|
|
365
|
+
if (file.size > limits.fileBytes) report.errors.push(t("{0}: {1} MB; tek dosya sınırı {2} MB.", rel, (file.size / 1024 / 1024).toFixed(1), limits.fileBytes / 1024 / 1024));
|
|
366
|
+
else if (file.size > 1024 * 1024 && /\.(png|jpe?g|gif)$/i.test(ext)) report.warnings.push(t("{0}: {1} MB. WebP ya da sıkıştırılmış JPEG sayfayı hızlandırır.", rel, (file.size / 1024 / 1024).toFixed(1)));
|
|
276
367
|
if ([".json", ".scss", ".css", ".svg", ".md", ".txt"].includes(ext) && file.size <= limits.fileBytes) {
|
|
277
368
|
const text = readFileSync(file.full, "utf8");
|
|
278
369
|
scanText(rel, text, report);
|
|
@@ -284,43 +375,44 @@ function scanRepoFiles(root, label, report, totals) {
|
|
|
284
375
|
function checkRepo(dir, meta, key, where, report) {
|
|
285
376
|
const schema = readJson(path.join(dir, "config", "settings_schema.json"), report, "config/settings_schema.json");
|
|
286
377
|
if (schema) {
|
|
287
|
-
if (schema.group?.type !== "tab" || !Array.isArray(schema.group?.values)) report.errors.push(
|
|
378
|
+
if (schema.group?.type !== "tab" || !Array.isArray(schema.group?.values)) report.errors.push(t("config/settings_schema.json: \"group\" sekmeli olmalı ({\"type\":\"tab\",\"values\":[…]})."));
|
|
288
379
|
schemaKeys(schema.group, new Set(), report);
|
|
289
380
|
}
|
|
290
381
|
const data = readJson(path.join(dir, "config", "settings_data.json"), report, "config/settings_data.json");
|
|
291
|
-
if (data && (typeof data !== "object" || Array.isArray(data))) report.errors.push("config/settings_data.json: anahtar-değer nesnesi olmalı.");
|
|
382
|
+
if (data && (typeof data !== "object" || Array.isArray(data))) report.errors.push(t("config/settings_data.json: anahtar-değer nesnesi olmalı."));
|
|
383
|
+
else checkProductTabs(data, report);
|
|
292
384
|
|
|
293
385
|
const sections = readJson(path.join(dir, "config", "sections.json"), report, "config/sections.json");
|
|
294
386
|
const types = new Set(Object.keys(sections?.sections ?? {}));
|
|
295
|
-
if (sections && types.size === 0) report.errors.push(
|
|
387
|
+
if (sections && types.size === 0) report.errors.push(t("config/sections.json: \"sections\" altında en az bir bölüm türü olmalı."));
|
|
296
388
|
for (const [type, section] of Object.entries(sections?.sections ?? {})) {
|
|
297
|
-
if (key && !type.startsWith(`${key}-`)) report.warnings.push(
|
|
298
|
-
if (!String(section?.name ?? "").trim()) report.errors.push(
|
|
389
|
+
if (key && !type.startsWith(`${key}-`)) report.warnings.push(t("config/sections.json: \"{0}\" bölüm türü tema anahtarıyla (\"{1}-\") başlamıyor.", type, key));
|
|
390
|
+
if (!String(section?.name ?? "").trim()) report.errors.push(t("config/sections.json: \"{0}\" bölümünün adı yok.", type));
|
|
299
391
|
// "keys" isteğe bağlıdır; verilmişse liste olmalı.
|
|
300
|
-
if (section?.keys !== undefined && !Array.isArray(section.keys)) report.errors.push(
|
|
392
|
+
if (section?.keys !== undefined && !Array.isArray(section.keys)) report.errors.push(t("config/sections.json: \"{0}\" bölümünün \"keys\" alanı liste olmalı.", type));
|
|
301
393
|
}
|
|
302
|
-
checkTemplate(readJson(path.join(dir, "templates", "index.json"), report, "templates/index.json (ana sayfa şablonu)"), sections ? types : null, report, "config/sections.json içinde");
|
|
394
|
+
checkTemplate(readJson(path.join(dir, "templates", "index.json"), report, t("templates/index.json (ana sayfa şablonu)")), sections ? types : null, report, t("config/sections.json içinde"));
|
|
303
395
|
|
|
304
396
|
const totals = { files: 0, bytes: 0 };
|
|
305
397
|
for (const sub of ["config", "templates"]) scanRepoFiles(path.join(dir, sub), sub, report, totals);
|
|
306
398
|
scanRepoFiles(where.styles, "styles", report, totals);
|
|
307
399
|
scanRepoFiles(where.assets, "assets", report, totals);
|
|
308
|
-
if (!existsSync(path.join(where.styles, "theme.scss"))) report.errors.push(
|
|
400
|
+
if (!existsSync(path.join(where.styles, "theme.scss"))) report.errors.push(t("Stil dosyası yok: {0}", path.join(where.styles, "theme.scss")));
|
|
309
401
|
report.files = totals.files;
|
|
310
402
|
report.bytes = totals.bytes;
|
|
311
|
-
if (totals.bytes > limits.totalBytes) report.errors.push(
|
|
312
|
-
if (totals.files > limits.files) report.errors.push(
|
|
403
|
+
if (totals.bytes > limits.totalBytes) report.errors.push(t("Tema {0} MB; sınır {1} MB.", (totals.bytes / 1024 / 1024).toFixed(0), limits.totalBytes / 1024 / 1024));
|
|
404
|
+
if (totals.files > limits.files) report.errors.push(t("Tema {0} dosya içeriyor; sınır {1}.", totals.files, limits.files));
|
|
313
405
|
checkImages(data, where.assets, report);
|
|
314
406
|
if (meta?.screenshot) {
|
|
315
407
|
const shot = String(meta.screenshot).replace(new RegExp(`^/themes/${key}/assets/`), "");
|
|
316
|
-
if (!existsSync(path.join(where.assets, shot))) report.warnings.push(
|
|
408
|
+
if (!existsSync(path.join(where.assets, shot))) report.warnings.push(t("theme.json: ekran görüntüsü bulunamadı (assets/{0}).", shot));
|
|
317
409
|
}
|
|
318
410
|
}
|
|
319
411
|
|
|
320
412
|
export function checkTheme(dir) {
|
|
321
413
|
const report = { dir, key: "", layout: "", errors: [], warnings: [], files: 0, bytes: 0 };
|
|
322
414
|
if (!existsSync(path.join(dir, "theme.json"))) {
|
|
323
|
-
report.errors.push(
|
|
415
|
+
report.errors.push(t("{0} bir tema klasörü değil: theme.json bulunamadı.", dir));
|
|
324
416
|
return report;
|
|
325
417
|
}
|
|
326
418
|
const where = locate(dir);
|
|
@@ -333,10 +425,10 @@ export function checkTheme(dir) {
|
|
|
333
425
|
}
|
|
334
426
|
|
|
335
427
|
export function printReport(report) {
|
|
336
|
-
const title = report.key ? `${report.key} (${report.layout === "repo" ? "platform deposu" : "paket"})` : report.dir;
|
|
428
|
+
const title = report.key ? `${report.key} (${report.layout === "repo" ? t("platform deposu") : t("paket")})` : report.dir;
|
|
337
429
|
for (const error of report.errors) console.error(` ✖ ${error}`);
|
|
338
430
|
for (const warning of report.warnings) console.warn(` ! ${warning}`);
|
|
339
|
-
const size = report.files ?
|
|
340
|
-
if (report.errors.length) console.error(
|
|
341
|
-
else console.log(
|
|
431
|
+
const size = report.files ? t(" · {0} dosya, {1} MB", report.files, (report.bytes / 1024 / 1024).toFixed(1)) : "";
|
|
432
|
+
if (report.errors.length) console.error(t("{0}: {1} hata, {2} uyarı{3}", title, report.errors.length, report.warnings.length, size));
|
|
433
|
+
else console.log(t("{0}: denetim geçti{1}{2}", title, report.warnings.length ? t(", {0} uyarı", report.warnings.length) : "", size));
|
|
342
434
|
}
|
package/src/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { t } from "./i18n.js";
|
|
4
5
|
|
|
5
6
|
// Giriş bilgileri kullanıcının ev klasöründe, yalnızca kullanıcının okuyabildiği bir dosyada durur.
|
|
6
7
|
// Tema klasörüne hiçbir zaman yazılmaz; böylece pakete ya da sürüm kontrolüne karışmaz.
|
|
@@ -45,7 +46,7 @@ const local = (hostname) => hostname === "localhost" || hostname === "127.0.0.1"
|
|
|
45
46
|
*/
|
|
46
47
|
export function storeOrigin(input) {
|
|
47
48
|
let value = String(input ?? "").trim().replace(/\/+$/, "");
|
|
48
|
-
if (!value) throw new Error("Mağaza adresi gerekli (ör. magazam ya da magazam.uvoo.store).");
|
|
49
|
+
if (!value) throw new Error(t("Mağaza adresi gerekli (ör. magazam ya da magazam.uvoo.store)."));
|
|
49
50
|
if (!/^https?:\/\//i.test(value)) {
|
|
50
51
|
if (!value.includes(".") && !value.includes(":")) value = `${value}.${defaultStoreApex}`;
|
|
51
52
|
value = `${local(value.split(":")[0]) ? "http" : "https"}://${value}`;
|
|
@@ -54,10 +55,10 @@ export function storeOrigin(input) {
|
|
|
54
55
|
try {
|
|
55
56
|
url = new URL(value);
|
|
56
57
|
} catch {
|
|
57
|
-
throw new Error(
|
|
58
|
+
throw new Error(t("Mağaza adresi okunamadı: {0}", input));
|
|
58
59
|
}
|
|
59
|
-
if (url.protocol !== "https:" && !local(url.hostname)) throw new Error("Mağazaya yalnızca https ile bağlanılır.");
|
|
60
|
-
if (url.username || url.password) throw new Error("Mağaza adresinde kullanıcı adı ya da parola olamaz.");
|
|
60
|
+
if (url.protocol !== "https:" && !local(url.hostname)) throw new Error(t("Mağazaya yalnızca https ile bağlanılır."));
|
|
61
|
+
if (url.username || url.password) throw new Error(t("Mağaza adresinde kullanıcı adı ya da parola olamaz."));
|
|
61
62
|
return url.origin;
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -70,7 +71,7 @@ export function session(storeOption) {
|
|
|
70
71
|
const origin = storeOption ? storeOrigin(storeOption) : data.current;
|
|
71
72
|
const entry = origin ? data.stores[origin] : null;
|
|
72
73
|
if (!origin || !entry?.token) {
|
|
73
|
-
throw new Error(storeOption ?
|
|
74
|
+
throw new Error(storeOption ? t("Bu mağaza için giriş yapılmamış: {0}\nÖnce: uvako login --store {1}", origin, storeOption) : "Giriş yapılmamış. Önce: uvako login --store <mağaza>");
|
|
74
75
|
}
|
|
75
76
|
return { origin, token: entry.token, name: entry.name };
|
|
76
77
|
}
|
package/src/i18n.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { en } from "./locales/en.js";
|
|
2
|
+
|
|
3
|
+
// CLI dili. Kaynak dil Türkçedir: metinler kodda Türkçe yazılır ve t("…") ile sarılır; sözlük
|
|
4
|
+
// (locales/en.js) Türkçe metni anahtar olarak kullanır. Sözlükte karşılığı olmayan metin Türkçe yazılır.
|
|
5
|
+
//
|
|
6
|
+
// Dil sırasıyla şuradan okunur: --lang seçeneği, UVAKO_LANG ortam değişkeni, sistemin dili
|
|
7
|
+
// (LC_ALL, LC_MESSAGES, LANG). Sistem dili Türkçe değilse İngilizce, belirtilmemişse Türkçe kullanılır.
|
|
8
|
+
export const locales = ["tr", "en"];
|
|
9
|
+
|
|
10
|
+
const dictionaries = { en };
|
|
11
|
+
|
|
12
|
+
function fromArgs(argv) {
|
|
13
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14
|
+
if (argv[i] === "--lang" && argv[i + 1]) return argv[i + 1];
|
|
15
|
+
if (argv[i].startsWith("--lang=")) return argv[i].slice(7);
|
|
16
|
+
}
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function detect(argv, env) {
|
|
21
|
+
const asked = (fromArgs(argv) || env.UVAKO_LANG || "").toLowerCase().slice(0, 2);
|
|
22
|
+
if (locales.includes(asked)) return asked;
|
|
23
|
+
const system = (env.LC_ALL || env.LC_MESSAGES || env.LANG || "").toLowerCase();
|
|
24
|
+
if (!system || system === "c" || system.startsWith("c.") || system.startsWith("posix")) return "tr";
|
|
25
|
+
return system.startsWith("tr") ? "tr" : "en";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let locale = detect(process.argv.slice(2), process.env);
|
|
29
|
+
let patterns = null;
|
|
30
|
+
|
|
31
|
+
export const getLocale = () => locale;
|
|
32
|
+
|
|
33
|
+
/** Dili değiştirir (testler ve --lang için). Tanınmayan dil Türkçe sayılır. */
|
|
34
|
+
export function setLocale(next) {
|
|
35
|
+
locale = locales.includes(next) ? next : "tr";
|
|
36
|
+
patterns = null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const fill = (text, args) => (args.length ? text.replace(/\{(\d+)\}/g, (whole, index) => (args[Number(index)] !== undefined ? String(args[Number(index)]) : whole)) : text);
|
|
40
|
+
|
|
41
|
+
/** Türkçe kaynak metni CLI diline çevirir; {0}, {1}… yer tutucuları args ile doldurulur. */
|
|
42
|
+
export function t(text, ...args) {
|
|
43
|
+
return fill(dictionaries[locale]?.[text] ?? text, args);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Sunucudan gelen metni (denetim hatası, işlem mesajı) çevirir. Tam karşılığı yoksa yer tutuculu
|
|
50
|
+
* anahtarlarla eşleştirir: "{0}: geçerli JSON değil." ↔ "theme.json: geçerli JSON değil."
|
|
51
|
+
*/
|
|
52
|
+
export function tm(text) {
|
|
53
|
+
const dict = dictionaries[locale];
|
|
54
|
+
if (!text || !dict) return text ?? "";
|
|
55
|
+
if (dict[text] !== undefined) return dict[text];
|
|
56
|
+
patterns ??= Object.entries(dict)
|
|
57
|
+
.filter(([key]) => /\{\d+\}/.test(key))
|
|
58
|
+
.map(([key, value]) => {
|
|
59
|
+
const order = [...key.matchAll(/\{(\d+)\}/g)].map((match) => Number(match[1]));
|
|
60
|
+
return { match: new RegExp("^" + escape(key).replace(/\\\{\d+\\\}/g, "(.+?)") + "$", "s"), text: value.replace(/\{(\d+)\}/g, (_, index) => `{${order.indexOf(Number(index))}}`) };
|
|
61
|
+
});
|
|
62
|
+
for (const pattern of patterns) {
|
|
63
|
+
const found = pattern.match.exec(text);
|
|
64
|
+
if (found) return fill(pattern.text, found.slice(1).map((part) => (part === text ? part : tm(part))));
|
|
65
|
+
}
|
|
66
|
+
return text;
|
|
67
|
+
}
|