uvako-cli 0.3.0 → 0.5.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 +65 -2
- package/bin/uvako-cli.js +12 -7
- package/package.json +4 -3
- package/src/api.js +6 -5
- package/src/app.js +412 -0
- package/src/check.js +127 -67
- package/src/config.js +50 -7
- package/src/i18n.js +67 -0
- package/src/init.js +74 -17
- package/src/locales/en.js +311 -0
- package/src/main.js +56 -16
- package/src/package.js +3 -2
- package/src/partner.js +135 -0
- package/src/remote.js +51 -50
- package/src/zip.js +5 -4
- package/templates/app/README.md +39 -0
- package/templates/app/blocks/yildiz-puani/block.css +5 -0
- package/templates/app/blocks/yildiz-puani/block.html +8 -0
- package/templates/app/blocks/yildiz-puani/block.json +11 -0
- package/templates/app/server.js +357 -0
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,21 @@ 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));
|
|
172
222
|
}
|
|
173
223
|
}
|
|
174
224
|
|
|
@@ -188,17 +238,17 @@ function checkProductTabs(data, report) {
|
|
|
188
238
|
if ([0, 1, "0", "1", true, false].includes(value)) return;
|
|
189
239
|
const where = 'config/settings_data.json: "product_detail_page_tabs"';
|
|
190
240
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
191
|
-
report.errors.push(
|
|
241
|
+
report.errors.push(t("{0} 1 (hepsi açık) ya da {\"display_product_comments\": 1, …} nesnesi olmalı.", where));
|
|
192
242
|
return;
|
|
193
243
|
}
|
|
194
244
|
for (const [key, flag] of Object.entries(value)) {
|
|
195
245
|
if (!(key in productTabKeys)) {
|
|
196
|
-
report.warnings.push(
|
|
246
|
+
report.warnings.push(t("{0}: \"{1}\" bilinen bir sekme değil ({2}).", where, key, Object.keys(productTabKeys).join(", ")));
|
|
197
247
|
continue;
|
|
198
248
|
}
|
|
199
|
-
if (![0, 1, "0", "1", true, false].includes(flag)) report.errors.push(
|
|
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));
|
|
200
250
|
const label = productTabKeys[key];
|
|
201
|
-
if (label && label in data && typeof data[label] !== "string") report.errors.push(
|
|
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));
|
|
202
252
|
}
|
|
203
253
|
}
|
|
204
254
|
|
|
@@ -206,7 +256,7 @@ function checkImages(data, assets, report) {
|
|
|
206
256
|
if (!data || typeof data !== "object") return;
|
|
207
257
|
for (const [name, value] of Object.entries(data)) {
|
|
208
258
|
if (typeof value !== "string" || !/^uploads\/[^\s]+\.(png|jpe?g|webp|avif|gif|svg|ico)$/i.test(value)) continue;
|
|
209
|
-
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));
|
|
210
260
|
}
|
|
211
261
|
}
|
|
212
262
|
|
|
@@ -214,17 +264,18 @@ function checkImages(data, assets, report) {
|
|
|
214
264
|
|
|
215
265
|
function checkPackage(dir, meta, report) {
|
|
216
266
|
const base = String(meta?.base ?? "").trim();
|
|
217
|
-
if (meta && !keyPattern.test(base)) report.errors.push(
|
|
218
|
-
if (meta && (Number(meta.price ?? 0) < 0 || Number(meta.price ?? 0) > 100000 || Number.isNaN(Number(meta.price ?? 0)))) report.errors.push(
|
|
219
|
-
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ı."));
|
|
220
270
|
|
|
221
271
|
const { files, links } = walk(dir);
|
|
222
|
-
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));
|
|
223
273
|
const styles = new Set(files.filter((file) => file.rel.startsWith("styles/")).map((file) => file.rel.slice("styles/".length)));
|
|
224
274
|
const totals = { files: 0, bytes: 0, style: 0 };
|
|
225
275
|
let schema = null;
|
|
226
276
|
let data = null;
|
|
227
277
|
let index = null;
|
|
278
|
+
const locales = {};
|
|
228
279
|
for (const file of files) {
|
|
229
280
|
const rel = file.rel;
|
|
230
281
|
if (localOnly(rel)) continue;
|
|
@@ -232,15 +283,23 @@ function checkPackage(dir, meta, report) {
|
|
|
232
283
|
totals.bytes += file.size;
|
|
233
284
|
const parts = rel.split("/");
|
|
234
285
|
if (parts.some((part) => !segmentPattern.test(part))) {
|
|
235
|
-
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));
|
|
236
287
|
continue;
|
|
237
288
|
}
|
|
238
289
|
const ext = path.extname(rel).toLowerCase();
|
|
239
290
|
const text = () => readFileSync(file.full, "utf8");
|
|
240
291
|
if (rel === "theme.json") scanText(rel, text(), report);
|
|
241
292
|
else if (rel === "config/settings_schema.json" || rel === "config/settings_data.json" || rel === "templates/index.json") {
|
|
242
|
-
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));
|
|
243
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
|
+
}
|
|
244
303
|
} else if (parts[0] === "styles" && parts.length === 2 && stylePattern.test(parts[1])) {
|
|
245
304
|
totals.style += file.size;
|
|
246
305
|
if (file.size <= limits.styleBytes) {
|
|
@@ -250,40 +309,41 @@ function checkPackage(dir, meta, report) {
|
|
|
250
309
|
}
|
|
251
310
|
} else if (parts[0] === "assets" && parts.length >= 2) {
|
|
252
311
|
if (!assetExtensions.has(ext)) {
|
|
253
|
-
report.errors.push(
|
|
312
|
+
report.errors.push(t("{0}: bu dosya türü ({1}) temada bulunamaz.", rel, ext || "uzantısız"));
|
|
254
313
|
continue;
|
|
255
314
|
}
|
|
256
|
-
if (file.size > limits.fileBytes) report.errors.push(
|
|
257
|
-
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)));
|
|
258
317
|
if (ext === ".svg" && file.size <= limits.fileBytes) scanText(rel, text(), report);
|
|
259
318
|
} else {
|
|
260
|
-
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));
|
|
261
320
|
}
|
|
262
321
|
}
|
|
263
|
-
if (totals.style > limits.styleBytes) report.errors.push(
|
|
264
|
-
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."));
|
|
265
324
|
|
|
266
325
|
schema = readJson(path.join(dir, "config", "settings_schema.json"), report, "config/settings_schema.json", false);
|
|
267
326
|
if (schema) {
|
|
268
|
-
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\":[…]})."));
|
|
269
328
|
schemaKeys(schema.group, new Set(), report);
|
|
270
329
|
}
|
|
271
330
|
data = readJson(path.join(dir, "config", "settings_data.json"), report, "config/settings_data.json", false);
|
|
272
|
-
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ı."));
|
|
273
332
|
index = readJson(path.join(dir, "templates", "index.json"), report, "templates/index.json", false);
|
|
274
333
|
// Bölüm türleri temel temadan gelir; `theme init` bunları reference/sections.json dosyasına yazar.
|
|
275
334
|
const reference = existsSync(path.join(dir, "reference", "sections.json")) ? readJson(path.join(dir, "reference", "sections.json"), { errors: [] }, "") : null;
|
|
276
|
-
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));
|
|
277
336
|
checkImages(data, path.join(dir, "assets"), report);
|
|
278
337
|
checkProductTabs(data, report);
|
|
338
|
+
checkTranslations(locales, meta, data, report);
|
|
279
339
|
for (const shot of [meta?.screenshot, ...(Array.isArray(meta?.screenshots) ? meta.screenshots : [])]) {
|
|
280
340
|
const rel = String(shot ?? "").trim().replace(/^\//, "").replace(/^assets\//, "");
|
|
281
|
-
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));
|
|
282
342
|
}
|
|
283
343
|
report.files = totals.files;
|
|
284
344
|
report.bytes = totals.bytes;
|
|
285
|
-
if (totals.bytes > limits.totalBytes) report.errors.push(
|
|
286
|
-
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));
|
|
287
347
|
}
|
|
288
348
|
|
|
289
349
|
// ── platform deposundaki katalog teması ────────────────────────────────────────────────────────
|
|
@@ -291,19 +351,19 @@ function checkPackage(dir, meta, report) {
|
|
|
291
351
|
function scanRepoFiles(root, label, report, totals) {
|
|
292
352
|
if (!existsSync(root)) return;
|
|
293
353
|
const { files, links } = walk(root);
|
|
294
|
-
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}`));
|
|
295
355
|
for (const file of files) {
|
|
296
356
|
const rel = `${label}/${file.rel}`;
|
|
297
357
|
const ext = path.extname(file.rel).toLowerCase();
|
|
298
358
|
totals.files += 1;
|
|
299
359
|
totals.bytes += file.size;
|
|
300
|
-
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));
|
|
301
361
|
if (!repoExtensions.has(ext)) {
|
|
302
|
-
report.errors.push(
|
|
362
|
+
report.errors.push(t("{0}: bu dosya türü ({1}) temada bulunamaz.", rel, ext || "uzantısız"));
|
|
303
363
|
continue;
|
|
304
364
|
}
|
|
305
|
-
if (file.size > limits.fileBytes) report.errors.push(
|
|
306
|
-
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)));
|
|
307
367
|
if ([".json", ".scss", ".css", ".svg", ".md", ".txt"].includes(ext) && file.size <= limits.fileBytes) {
|
|
308
368
|
const text = readFileSync(file.full, "utf8");
|
|
309
369
|
scanText(rel, text, report);
|
|
@@ -315,44 +375,44 @@ function scanRepoFiles(root, label, report, totals) {
|
|
|
315
375
|
function checkRepo(dir, meta, key, where, report) {
|
|
316
376
|
const schema = readJson(path.join(dir, "config", "settings_schema.json"), report, "config/settings_schema.json");
|
|
317
377
|
if (schema) {
|
|
318
|
-
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\":[…]})."));
|
|
319
379
|
schemaKeys(schema.group, new Set(), report);
|
|
320
380
|
}
|
|
321
381
|
const data = readJson(path.join(dir, "config", "settings_data.json"), report, "config/settings_data.json");
|
|
322
|
-
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ı."));
|
|
323
383
|
else checkProductTabs(data, report);
|
|
324
384
|
|
|
325
385
|
const sections = readJson(path.join(dir, "config", "sections.json"), report, "config/sections.json");
|
|
326
386
|
const types = new Set(Object.keys(sections?.sections ?? {}));
|
|
327
|
-
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ı."));
|
|
328
388
|
for (const [type, section] of Object.entries(sections?.sections ?? {})) {
|
|
329
|
-
if (key && !type.startsWith(`${key}-`)) report.warnings.push(
|
|
330
|
-
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));
|
|
331
391
|
// "keys" isteğe bağlıdır; verilmişse liste olmalı.
|
|
332
|
-
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));
|
|
333
393
|
}
|
|
334
|
-
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"));
|
|
335
395
|
|
|
336
396
|
const totals = { files: 0, bytes: 0 };
|
|
337
397
|
for (const sub of ["config", "templates"]) scanRepoFiles(path.join(dir, sub), sub, report, totals);
|
|
338
398
|
scanRepoFiles(where.styles, "styles", report, totals);
|
|
339
399
|
scanRepoFiles(where.assets, "assets", report, totals);
|
|
340
|
-
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")));
|
|
341
401
|
report.files = totals.files;
|
|
342
402
|
report.bytes = totals.bytes;
|
|
343
|
-
if (totals.bytes > limits.totalBytes) report.errors.push(
|
|
344
|
-
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));
|
|
345
405
|
checkImages(data, where.assets, report);
|
|
346
406
|
if (meta?.screenshot) {
|
|
347
407
|
const shot = String(meta.screenshot).replace(new RegExp(`^/themes/${key}/assets/`), "");
|
|
348
|
-
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));
|
|
349
409
|
}
|
|
350
410
|
}
|
|
351
411
|
|
|
352
412
|
export function checkTheme(dir) {
|
|
353
413
|
const report = { dir, key: "", layout: "", errors: [], warnings: [], files: 0, bytes: 0 };
|
|
354
414
|
if (!existsSync(path.join(dir, "theme.json"))) {
|
|
355
|
-
report.errors.push(
|
|
415
|
+
report.errors.push(t("{0} bir tema klasörü değil: theme.json bulunamadı.", dir));
|
|
356
416
|
return report;
|
|
357
417
|
}
|
|
358
418
|
const where = locate(dir);
|
|
@@ -365,10 +425,10 @@ export function checkTheme(dir) {
|
|
|
365
425
|
}
|
|
366
426
|
|
|
367
427
|
export function printReport(report) {
|
|
368
|
-
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;
|
|
369
429
|
for (const error of report.errors) console.error(` ✖ ${error}`);
|
|
370
430
|
for (const warning of report.warnings) console.warn(` ! ${warning}`);
|
|
371
|
-
const size = report.files ?
|
|
372
|
-
if (report.errors.length) console.error(
|
|
373
|
-
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));
|
|
374
434
|
}
|
package/src/config.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
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.
|
|
7
8
|
|
|
8
9
|
export const defaultStoreApex = "uvoo.store";
|
|
9
10
|
export const siteOrigin = () => process.env.UVAKO_SITE_URL || "https://uvako.com";
|
|
11
|
+
/** Partner paneli: `uvako login` (cihaz girişi) ve `uvako app` komutları buraya bağlanır. */
|
|
12
|
+
export const partnerOrigin = () => {
|
|
13
|
+
const raw = (process.env.UVAKO_PARTNER_URL || "https://partner.uvako.com").replace(/\/+$/, "");
|
|
14
|
+
const url = new URL(raw);
|
|
15
|
+
if (url.protocol !== "https:" && !local(url.hostname)) throw new Error(t("Partner paneline yalnızca https ile bağlanılır."));
|
|
16
|
+
return url.origin;
|
|
17
|
+
};
|
|
10
18
|
|
|
11
19
|
export function configDir() {
|
|
12
20
|
if (process.env.UVAKO_CONFIG_DIR) return process.env.UVAKO_CONFIG_DIR;
|
|
@@ -19,9 +27,13 @@ const file = () => path.join(configDir(), "credentials.json");
|
|
|
19
27
|
function read() {
|
|
20
28
|
try {
|
|
21
29
|
const data = JSON.parse(readFileSync(file(), "utf8"));
|
|
22
|
-
return {
|
|
30
|
+
return {
|
|
31
|
+
current: typeof data.current === "string" ? data.current : "",
|
|
32
|
+
stores: data.stores && typeof data.stores === "object" ? data.stores : {},
|
|
33
|
+
partner: data.partner && typeof data.partner === "object" ? data.partner : null,
|
|
34
|
+
};
|
|
23
35
|
} catch {
|
|
24
|
-
return { current: "", stores: {} };
|
|
36
|
+
return { current: "", stores: {}, partner: null };
|
|
25
37
|
}
|
|
26
38
|
}
|
|
27
39
|
|
|
@@ -45,7 +57,7 @@ const local = (hostname) => hostname === "localhost" || hostname === "127.0.0.1"
|
|
|
45
57
|
*/
|
|
46
58
|
export function storeOrigin(input) {
|
|
47
59
|
let value = String(input ?? "").trim().replace(/\/+$/, "");
|
|
48
|
-
if (!value) throw new Error("Mağaza adresi gerekli (ör. magazam ya da magazam.uvoo.store).");
|
|
60
|
+
if (!value) throw new Error(t("Mağaza adresi gerekli (ör. magazam ya da magazam.uvoo.store)."));
|
|
49
61
|
if (!/^https?:\/\//i.test(value)) {
|
|
50
62
|
if (!value.includes(".") && !value.includes(":")) value = `${value}.${defaultStoreApex}`;
|
|
51
63
|
value = `${local(value.split(":")[0]) ? "http" : "https"}://${value}`;
|
|
@@ -54,10 +66,10 @@ export function storeOrigin(input) {
|
|
|
54
66
|
try {
|
|
55
67
|
url = new URL(value);
|
|
56
68
|
} catch {
|
|
57
|
-
throw new Error(
|
|
69
|
+
throw new Error(t("Mağaza adresi okunamadı: {0}", input));
|
|
58
70
|
}
|
|
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.");
|
|
71
|
+
if (url.protocol !== "https:" && !local(url.hostname)) throw new Error(t("Mağazaya yalnızca https ile bağlanılır."));
|
|
72
|
+
if (url.username || url.password) throw new Error(t("Mağaza adresinde kullanıcı adı ya da parola olamaz."));
|
|
61
73
|
return url.origin;
|
|
62
74
|
}
|
|
63
75
|
|
|
@@ -70,7 +82,7 @@ export function session(storeOption) {
|
|
|
70
82
|
const origin = storeOption ? storeOrigin(storeOption) : data.current;
|
|
71
83
|
const entry = origin ? data.stores[origin] : null;
|
|
72
84
|
if (!origin || !entry?.token) {
|
|
73
|
-
throw new Error(storeOption ?
|
|
85
|
+
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
86
|
}
|
|
75
87
|
return { origin, token: entry.token, name: entry.name };
|
|
76
88
|
}
|
|
@@ -98,3 +110,34 @@ export function sessions() {
|
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
export const credentialsPath = () => (existsSync(file()) ? file() : "");
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Partner oturumu (uvako app komutları): önce UVAKO_PARTNER_TOKEN (CI), sonra `uvako login` ile
|
|
116
|
+
* kaydedilen giriş. Anahtar yalnızca girişin yapıldığı partner adresine gönderilir.
|
|
117
|
+
*/
|
|
118
|
+
export function partnerSession() {
|
|
119
|
+
const origin = partnerOrigin();
|
|
120
|
+
if (process.env.UVAKO_PARTNER_TOKEN) return { origin, token: process.env.UVAKO_PARTNER_TOKEN.trim(), name: "" };
|
|
121
|
+
const entry = read().partner;
|
|
122
|
+
if (!entry?.token || entry.origin !== origin) throw new Error(t("Partner girişi yapılmamış. Önce: uvako login"));
|
|
123
|
+
return { origin, token: entry.token, name: entry.name ?? "", email: entry.email ?? "" };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function savePartner(origin, token, name, email) {
|
|
127
|
+
const data = read();
|
|
128
|
+
data.partner = { origin, token, name, email, savedAt: new Date().toISOString() };
|
|
129
|
+
write(data);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function dropPartner() {
|
|
133
|
+
const data = read();
|
|
134
|
+
const had = Boolean(data.partner);
|
|
135
|
+
data.partner = null;
|
|
136
|
+
write(data);
|
|
137
|
+
return had;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function partnerSaved() {
|
|
141
|
+
const entry = read().partner;
|
|
142
|
+
return entry?.token ? { origin: entry.origin, name: entry.name ?? "", email: entry.email ?? "" } : null;
|
|
143
|
+
}
|
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
|
+
}
|