uvako-cli 0.3.0 → 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 CHANGED
@@ -44,7 +44,7 @@ paneline giriş yapmış olmanız gerekir. Tema, panelden yayınlanana kadar vit
44
44
  | `uvako login --store <mağaza>` | Mağazaya tema erişim anahtarıyla bağlanır. |
45
45
  | `uvako logout` · `uvako whoami` | Kayıtlı girişi siler · bağlı mağazayı gösterir. |
46
46
  | `uvako theme init <anahtar>` | Yeni tema iskeleti (`--base`, `--name`, `--author`). |
47
- | `uvako theme check [klasör]` | Temayı denetler: yapı, ayarlar, bölümler, görseller, güvenlik. |
47
+ | `uvako theme check [klasör]` | Temayı denetler: yapı, ayarlar, bölümler, görseller, çeviriler, güvenlik. |
48
48
  | `uvako theme dev [klasör]` | Canlı geliştirme: kaydettikçe yükler, önizleme yenilenir. Durdurunca mağaza eski hâline döner (`--keep` ile kütüphanede kalır). |
49
49
  | `uvako theme push [klasör]` | Temayı mağazanın tema kütüphanesine yükler. |
50
50
  | `uvako theme pull <anahtar>` | Yüklediğiniz temayı indirir (`--dir`, `--force`). |
@@ -66,6 +66,7 @@ theme.json kimlik: key, name, version, base; mağaza vitrini
66
66
  config/settings_data.json temel temadan farklı olan ayar değerleri
67
67
  config/settings_schema.json (isteğe bağlı) panelde görünen ayarlar
68
68
  templates/index.json (isteğe bağlı) ana sayfadaki bölüm sırası
69
+ locales/en.json, de.json, tr.json (isteğe bağlı) temanın kendi metinlerinin çevirisi
69
70
  styles/_tokens.scss renk, yazı tipi, köşe değişkenleri (temanın tamamına yayılır)
70
71
  styles/theme.scss temaya özgü SCSS
71
72
  assets/ logo, slayt ve banner görselleri, yazı tipleri
@@ -75,6 +76,37 @@ reference/ temel temanın ayarları ve bölüm türleri (pak
75
76
  Bölümlerin kendisi (slayt, banner, ürün vitrini…) temel temadan gelir; paket hangilerinin hangi sırayla
76
77
  ve hangi görünümle kullanılacağını söyler. Katalog temaları: [themes.uvako.com](https://themes.uvako.com).
77
78
 
79
+ ### Diller
80
+
81
+ Mağazalar vitrinini Türkçe, İngilizce ve Almanca yayınlayabilir. Temanın kendi metinlerini
82
+ (`config/settings_data.json` içindeki başlık ve buton yazıları, `theme.json` içindeki açıklama, sektör ve
83
+ özellikler) çevirmek için pakete sözlük dosyası koyun:
84
+
85
+ ```json
86
+ {
87
+ "Yeni sezon geldi": "The new season is here",
88
+ "Alışverişe başla": "Start shopping"
89
+ }
90
+ ```
91
+
92
+ - Dosyalar: `locales/en.json`, `locales/de.json`, `locales/tr.json`. Anahtar temadaki metnin kendisi,
93
+ değer o dildeki karşılığıdır. Düz bir sözlüktür; iç içe nesne olamaz.
94
+ - Temayı Türkçe yazdıysanız `en.json` ve `de.json` yeterlidir. Başka bir dilde yazdıysanız Türkçe
95
+ karşılıkları `tr.json` dosyasına koyun.
96
+ - Çevirisi olmayan metin yazıldığı gibi gösterilir. `uvako theme check` her dosyada eksik kalan
97
+ metinleri uyarı olarak listeler.
98
+ - Mağaza sahibinin panelden değiştirdiği metin sözlükte yoksa olduğu gibi kalır.
99
+ - Sınırlar: dosya başına 256 KB ve 2.000 metin; metin 500, çevirisi 2.000 karakter.
100
+
101
+ Temel temadan gelen metinler (sepet, ödeme, üyelik, menü) platform tarafından çevrilir; onları
102
+ sözlüğe yazmanız gerekmez.
103
+
104
+ ### CLI dili
105
+
106
+ Çıktı Türkçe ya da İngilizcedir. Dil sırasıyla `--lang` seçeneğinden, `UVAKO_LANG` ortam değişkeninden
107
+ ve sistemin dilinden okunur: `uvako theme check --lang en`. İngilizce çalışırken `theme init` iskeleti
108
+ (README, SCSS açıklamaları) İngilizce yazar ve Türkçe karşılığı `locales/tr.json` dosyasına koyar.
109
+
78
110
  ### Ürün sayfası sekmeleri
79
111
 
80
112
  Ürün detay sayfasındaki sekmeler `config/settings_data.json` içindeki `product_detail_page_tabs` ile açılır
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uvako-cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Uvako tema geliştirme aracı. Tema başlatır, denetler, mağazaya yükler ve canlı önizletir. https://uvako.com",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/src/api.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { t } from "./i18n.js";
1
2
  // Mağazanın tema uç noktası (/api/theme-cli). Erişim anahtarı yalnızca girişte seçilen mağazaya
2
3
  // gönderilir: yönlendirmeler izlenmez, böylece anahtar başka bir adrese taşınmaz.
3
4
 
@@ -21,7 +22,7 @@ export async function call({ origin, token }, method, route, { body, raw = false
21
22
  signal: AbortSignal.timeout(timeout),
22
23
  });
23
24
  } catch (error) {
24
- throw new ApiError(`Mağazaya bağlanılamadı (${origin}): ${error.cause?.code ?? error.message}`);
25
+ throw new ApiError(t("Mağazaya bağlanılamadı ({0}): {1}", origin, error.cause?.code ?? error.message));
25
26
  }
26
27
  if (response.ok) return raw ? Buffer.from(await response.arrayBuffer()) : response.json();
27
28
  let payload = null;
@@ -31,7 +32,7 @@ export async function call({ origin, token }, method, route, { body, raw = false
31
32
  // JSON olmayan yanıt (ör. ağ geçidi hatası).
32
33
  }
33
34
  const problem = payload?.error ?? {};
34
- const fallback = response.status === 404 ? "Bu adreste Uvako tema uç noktası yok. Mağaza adresini kontrol edin." : `Sunucu ${response.status} yanıtı verdi.`;
35
+ const fallback = response.status === 404 ? t("Bu adreste Uvako tema uç noktası yok. Mağaza adresini kontrol edin.") : t("Sunucu {0} yanıtı verdi.", response.status);
35
36
  throw new ApiError(problem.message || fallback, { status: response.status, code: problem.code || "", problems: Array.isArray(problem.problems) ? problem.problems : [] });
36
37
  }
37
38
 
@@ -41,9 +42,9 @@ export async function starter(site, base) {
41
42
  try {
42
43
  response = await fetch(`${site}/api/site/themes/${encodeURIComponent(base)}/starter`, { headers: { Accept: "application/json", "User-Agent": "uvako-cli" }, signal: AbortSignal.timeout(30000) });
43
44
  } catch (error) {
44
- throw new ApiError(`uvako.com'a bağlanılamadı: ${error.cause?.code ?? error.message}`);
45
+ throw new ApiError(t("uvako.com'a bağlanılamadı: {0}", error.cause?.code ?? error.message));
45
46
  }
46
- if (response.status === 404) throw new ApiError(`"${base}" adında bir temel tema yok. Temalar: https://themes.uvako.com`, { status: 404 });
47
- if (!response.ok) throw new ApiError(`Temel tema okunamadı (${response.status}).`, { status: response.status });
47
+ if (response.status === 404) throw new ApiError(t("\"{0}\" adında bir temel tema yok. Temalar: https://themes.uvako.com", base), { status: 404 });
48
+ if (!response.ok) throw new ApiError(t("Temel tema okunamadı ({0}).", response.status), { status: response.status });
48
49
  return response.json();
49
50
  }
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(`${label} geçerli JSON değil: ${error.message}`);
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(`Ayar şeması: "${key}" ayarının türü yok (${where}).`);
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(`${rel}: ${what} içeriyor. Tema içinde betik çalıştırılamaz.`);
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(`${rel}: gizli anahtar ya da parola gibi görünen bir değer içeriyor.`);
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(`${rel}: tarayıcıda kod çalıştıran CSS (expression, behavior, -moz-binding) içeriyor.`);
117
- if (/javascript:/i.test(all)) report.errors.push(`${rel}: javascript: adresi içeriyor.`);
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(`${rel}: @${rule[1].toLowerCase()} kullanılamaz. Tema stilleri döngü, fonksiyon ve mixin tanımı içeremez; temel temanın mixin'leri @include ile kullanılabilir.`);
121
- if (text.includes("\\")) report.errors.push(`${rel}: tırnak dışında ters eğik çizgi kullanılamaz.`);
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(`${rel}: yalnızca görsel ve yazı tipi data: adresleri kullanılabilir.`);
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(`${rel}: izin verilmeyen dış adres (${hostOf(target)}). Görsel ve yazı tiplerini assets/ altına koyun; yalnızca şu adresler çağrılabilir: ${[...allowedStyleHosts].join(", ")}.`);
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(`${rel}: izin verilmeyen dış adres (${hostOf(target)}).`);
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(`${rel}: @import "${target}" pakette yok. Yalnızca paketin kendi styles/ dosyaları içe aktarılabilir.`);
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(`${rel}: yalnızca değişken ataması ($ad: değer;) ve yazı tipi @import'u içerebilir.`);
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(`theme.json: "key" küçük harf, rakam ve tireden oluşmalı (ör. "linea").`);
158
- else if (key !== path.basename(dir)) report.errors.push(`theme.json: "key" (${key}) klasör adıyla (${path.basename(dir)}) aynı olmalı.`);
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(`theme.json: "version" 1.0.0 biçiminde olmalı.`);
161
- if (!String(meta.description ?? "").trim()) report.warnings.push(`theme.json: "description" boş; tema listesinde açıklama görünmez.`);
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(`templates/index.json: "order" boş; ana sayfada bölüm görünmez.`);
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(`templates/index.json: "order" içindeki "${id}" bölümü "sections" altında tanımlı değil.`);
171
- else if (types && !types.has(section.type)) report.errors.push(`templates/index.json: "${id}" bölümünün türü (${section.type}) ${source} yok.`);
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(`${where} 1 (hepsi açık) ya da {"display_product_comments": 1, …} nesnesi olmalı.`);
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(`${where}: "${key}" bilinen bir sekme değil (${Object.keys(productTabKeys).join(", ")}).`);
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(`${where}: "${key}" değeri 1 ya da 0 olmalı.`);
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(`config/settings_data.json: "${label}" sekme başlığı metin olmalı.`);
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(`config/settings_data.json: "${name}" ayarındaki görsel bulunamadı (assets/${value}).`);
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(`theme.json: "base" gerekli: paketin temel aldığı katalog teması (ör. "linea"). Temalar: https://themes.uvako.com`);
218
- if (meta && (Number(meta.price ?? 0) < 0 || Number(meta.price ?? 0) > 100000 || Number.isNaN(Number(meta.price ?? 0)))) report.errors.push(`theme.json: "price" 0 ile 100.000 arasında olmalı.`);
219
- if (meta?.demo_url && !/^https:\/\/[^\s]+$/.test(String(meta.demo_url))) report.errors.push(`theme.json: "demo_url" https:// ile başlayan bir adres olmalı.`);
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(`${link}: sembolik bağ pakete alınamaz.`);
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(`${rel}: dosya adında yalnızca harf, rakam, nokta, tire ve alt çizgi olabilir; gizli dosya olamaz.`);
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(`${rel}: 1 MB sınırını aşıyor.`);
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(`${rel}: bu dosya türü (${ext || "uzantısız"}) temada bulunamaz.`);
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(`${rel}: ${(file.size / 1024 / 1024).toFixed(1)} MB; tek dosya sınırı ${limits.fileBytes / 1024 / 1024} MB.`);
257
- else if (file.size > 1024 * 1024 && /\.(png|jpe?g|gif)$/i.test(ext)) report.warnings.push(`${rel}: ${(file.size / 1024 / 1024).toFixed(1)} MB. WebP ya da sıkıştırılmış JPEG sayfayı hızlandırır.`);
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(`${rel}: bu dosya pakette bulunamaz. İzin verilenler: theme.json, config/settings_schema.json, config/settings_data.json, templates/index.json, styles/*.scss, assets/**.`);
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(`Stil dosyaları toplam ${limits.styleBytes / 1024} KB sınırını aşıyor.`);
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(`config/settings_schema.json: "group" sekmeli olmalı ({"type":"tab","values":[…]}).`);
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, `${base} temasında`);
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(`theme.json: ekran görüntüsü pakette yok (assets/${rel}).`);
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(`Tema ${(totals.bytes / 1024 / 1024).toFixed(0)} MB; sınır ${limits.totalBytes / 1024 / 1024} MB.`);
286
- if (totals.files > limits.files) report.errors.push(`Tema ${totals.files} dosya içeriyor; sınır ${limits.files}.`);
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(`${label}/${link}: sembolik bağ pakete alınamaz.`);
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(`${rel}: gizli dosya ya da üst klasöre çıkan yol.`);
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(`${rel}: bu dosya türü (${ext || "uzantısız"}) temada bulunamaz.`);
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(`${rel}: ${(file.size / 1024 / 1024).toFixed(1)} MB; tek dosya sınırı ${limits.fileBytes / 1024 / 1024} MB.`);
306
- else if (file.size > 1024 * 1024 && /\.(png|jpe?g|gif)$/i.test(ext)) report.warnings.push(`${rel}: ${(file.size / 1024 / 1024).toFixed(1)} MB. WebP ya da sıkıştırılmış JPEG sayfayı hızlandırır.`);
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(`config/settings_schema.json: "group" sekmeli olmalı ({"type":"tab","values":[…]}).`);
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(`config/sections.json: "sections" altında en az bir bölüm türü olmalı.`);
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(`config/sections.json: "${type}" bölüm türü tema anahtarıyla ("${key}-") başlamıyor.`);
330
- if (!String(section?.name ?? "").trim()) report.errors.push(`config/sections.json: "${type}" bölümünün adı yok.`);
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(`config/sections.json: "${type}" bölümünün "keys" alanı liste olmalı.`);
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(`Stil dosyası yok: ${path.join(where.styles, "theme.scss")}`);
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(`Tema ${(totals.bytes / 1024 / 1024).toFixed(0)} MB; sınır ${limits.totalBytes / 1024 / 1024} MB.`);
344
- if (totals.files > limits.files) report.errors.push(`Tema ${totals.files} dosya içeriyor; sınır ${limits.files}.`);
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(`theme.json: ekran görüntüsü bulunamadı (assets/${shot}).`);
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(`${dir} bir tema klasörü değil: theme.json bulunamadı.`);
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 ? ` · ${report.files} dosya, ${(report.bytes / 1024 / 1024).toFixed(1)} MB` : "";
372
- if (report.errors.length) console.error(`${title}: ${report.errors.length} hata, ${report.warnings.length} uyarı${size}`);
373
- else console.log(`${title}: denetim geçti${report.warnings.length ? `, ${report.warnings.length} uyarı` : ""}${size}`);
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,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(`Mağaza adresi okunamadı: ${input}`);
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 ? `Bu mağaza için giriş yapılmamış: ${origin}\nÖnce: uvako login --store ${storeOption}` : "Giriş yapılmamış. Önce: uvako login --store <mağaza>");
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
  }