diff --git a/CHANGELOG.md b/CHANGELOG.md
index c4ebf88..a6555da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -78,6 +78,14 @@ GitHub. The full reasoning and the measurements behind each change are in [ROADM
### Studio
+- **New presets start in the app language [772].** In English, Studio → "+ Shader" opened a visualizer
+ named "Yeni Görselleştirici", and the discard-changes prompt showed that Turkish name. The name,
+ the default "Amount" slider label, the variation names and the starter shader's comment header
+ become the user's own editable text, so they are now written in the UI language at creation,
+ like the import fix in [759].
+ A new draft that was not edited no longer asks before it is discarded (it did in both languages
+ before), and the self-test's untranslated-text scan skips user content marked with
+ `data-user-text` / `data-user-attr`, so saved preset names are no longer flagged.
- **Imported presets follow the app language [759].** A `.milk`, Shadertoy or ISF file imported
through Studio got a Turkish description even in English, and the import notes stayed Turkish.
The description is the user's own editable text once saved, so it is now written in the UI
diff --git a/README.md b/README.md
index 52f9023..43e53ff 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ layers and **40 GPU effects**, an **OBS overlay**, **Spout / Syphon**, **project
[](https://github.com/CaYatur/SoundVisualizer/releases)
[](LICENSE)
[](#download)
-[](#tests)
+[](#tests)
[](https://www.electronjs.org/)
@@ -1741,7 +1741,7 @@ npm test
npm start -- --smoke
```
-**3183 unit tests, all passing.** They are written to check answers, not to exercise lines:
+**3189 unit tests, all passing.** They are written to check answers, not to exercise lines:
- **Formulas** are checked against values derived by hand from their definitions — Viviani's curve
staying on its sphere, the torus tube radius, Chladni's m↔n antisymmetry, every attractor staying
diff --git a/README.tr.md b/README.tr.md
index 8384031..945fb0b 100644
--- a/README.tr.md
+++ b/README.tr.md
@@ -18,7 +18,7 @@ destesi**, ve **MCP üzerinden yapay zekâ kontrolü**.
[](https://github.com/CaYatur/SoundVisualizer/releases)
[](LICENSE)
[](#hızlı-başlangıç)
-[](#testler)
+[](#testler)
[](https://www.electronjs.org/)
@@ -1744,7 +1744,7 @@ npm test
npm start -- --smoke
```
-**3183 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar:
+**3189 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar:
- **Formüller**, tanımlarından elle türetilmiş değerlerle sınanıyor — Viviani eğrisinin küre üzerinde
kalması, simidin boru yarıçapı, Chladni'nin m↔n antisimetrisi, her çekicinin sınırlı kalması ve görüş
diff --git a/ROADMAP.md b/ROADMAP.md
index b8c2b92..cf019cc 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -131,7 +131,7 @@ npm test
npm start -- --smoke
```
-- **3183 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0;
+- **3189 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0;
105 came with v3.1.1; 163 came with v3.1.2; 157 came with v3.1.3 — 1128 at
that tag — 469 more with v3.1.4, most of them from the MilkDrop work — 1597 at
that tag — 1402 more with v3.1.5 — 2999 at that tag — and 27
diff --git a/src/admin/studio.js b/src/admin/studio.js
index dacfeb7..4c56120 100644
--- a/src/admin/studio.js
+++ b/src/admin/studio.js
@@ -32,16 +32,20 @@
let compileState = { ok: true, message: '', line: 0 };
let compileTimer = null;
- const STARTER_SHADER = `// Kendi görselleştiricin.
-// Kullanabileceklerin:
-// sv_resolution, sv_time, sv_level, sv_bass, sv_mid, sv_treble, sv_beat
-// sv_spec(x) -> 0..1 konumundaki spektrum değeri
-// sv_waveAt(x) -> dalga formu (-1..1)
-// sv_col(x) -> senin 5 renkli paletinden renk
-// sv_prev -> bir önceki kare (geri besleme), sv_media -> kamera/video
-// Aşağıya kendi kaydırıcılarını da ekleyebilirsin (Parametreler bölümü).
-
-void mainImage(out vec4 fragColor, in vec2 fragCoord) {
+ /* Şablonun başlık yorumu kullanıcının düzenleyeceği koda giriyor; içe aktarılan
+ açıklamalar gibi (#759) oluşturulurken arayüz dilinde yazılıyor. Satırlar
+ ayrı çevriliyor: birleşik metin sözlükte eşleşmez. */
+ const STARTER_HEADER = [
+ '// Kendi görselleştiricin.',
+ '// Kullanabileceklerin:',
+ '// sv_resolution, sv_time, sv_level, sv_bass, sv_mid, sv_treble, sv_beat',
+ '// sv_spec(x) -> 0..1 konumundaki spektrum değeri',
+ '// sv_waveAt(x) -> dalga formu (-1..1)',
+ '// sv_col(x) -> senin 5 renkli paletinden renk',
+ '// sv_prev -> bir önceki kare (geri besleme), sv_media -> kamera/video',
+ '// Aşağıya kendi kaydırıcılarını da ekleyebilirsin (Parametreler bölümü).',
+ ];
+ const STARTER_BODY = `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (fragCoord - 0.5 * sv_resolution) / min(sv_resolution.x, sv_resolution.y);
float r = length(uv);
float a = atan(uv.y, uv.x);
@@ -54,6 +58,7 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
fragColor = vec4(col, clamp(ring + sv_bass * 0.2, 0.0, 1.0));
}`;
+ const starterShader = () => STARTER_HEADER.map(trStudio).join('\n') + '\n\n' + STARTER_BODY;
// ==========================================================================
// GLSL renklendirme
@@ -224,12 +229,14 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
// ==========================================================================
function newShader(kind) {
return IS().normalize({
- name: kind === 'background' ? 'Yeni Arkaplan' : 'Yeni Görselleştirici',
+ /* Ad ve etiket kullanıcının kendi düzenlenebilir metni oluyor; editör onu
+ çevirmiyor. Bu yüzden oluşturulurken arayüz dilinde yazılıyor (#759 gibi). */
+ name: trStudio(kind === 'background' ? 'Yeni Arkaplan' : 'Yeni Görselleştirici'),
kind,
engine: 'shader',
- shader: STARTER_SHADER,
+ shader: starterShader(),
controls: [
- { name: 'uAmount', label: 'Miktar', type: 'slider', min: 0, max: 2, step: 0.01, default: 1 },
+ { name: 'uAmount', label: trStudio('Miktar'), type: 'slider', min: 0, max: 2, step: 0.01, default: 1 },
],
});
}
@@ -250,7 +257,7 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
function newVariation(kind) {
const look = currentLook(P().cfg(), kind);
return IS().normalize({
- name: kind === 'background' ? 'Arkaplan Varyasyonum' : 'Görselleştiricim',
+ name: trStudio(kind === 'background' ? 'Arkaplan Varyasyonum' : 'Görselleştiricim'),
kind,
engine: 'variation',
base: kind === 'background' ? look.background.type : look.visualizer.type,
@@ -342,7 +349,10 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
if (!(await confirmDiscard())) return;
draft = make();
selectedId = null;
- dirty = true;
+ /* Dokunulmamış yeni taslak "değişmiş" sayılmaz: atılırken sormaya gerek
+ yok. Eskiden burada true'ydu ve TR/EN fark etmeden onay çıkıyordu.
+ Kaydedilmemiş olduğu yine belli: Kaydet düğmesi !selectedId ile vurgulu. */
+ dirty = false;
compileNow();
P().rerender();
}
@@ -552,10 +562,12 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
class: 'studio-item' + (p.id === selectedId ? ' active' : ''),
type: 'button',
title: trStudio(p.description || p.name),
+ // Kendi presetinin adı/açıklaması kullanıcı içeriği: öz test çeviri aramaz
+ ...(p.builtin ? {} : { 'data-user-attr': '' }),
onclick: () => guardedSelect(p.id),
},
[
- el('span', { class: 'si-name', text: p.builtin ? trStudio(p.name) : p.name }),
+ el('span', { class: 'si-name', text: p.builtin ? trStudio(p.name) : p.name, ...(p.builtin ? {} : { 'data-user-text': '' }) }),
el('span', { class: 'si-tag', text: trStudio(p.builtin ? 'yerleşik' : p.engine === 'shader' ? 'shader' : 'varyasyon') }),
]
)
@@ -760,7 +772,7 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
}
const actions = el('div', { class: 'studio-actions' }, [
- el('button', { id: 'studioSave', class: 'btn ' + (dirty ? 'primary' : ''), type: 'button', icon: 'save', text: 'Kaydet', onclick: saveDraft }),
+ el('button', { id: 'studioSave', class: 'btn ' + (dirty || !selectedId ? 'primary' : ''), type: 'button', icon: 'save', text: 'Kaydet', onclick: saveDraft }),
el('button', { class: 'btn', type: 'button', icon: 'play', text: 'Sahnede Kullan', onclick: applyToScene }),
el('button', {
class: 'btn ghost small', type: 'button', icon: 'copy', text: 'Çoğalt',
diff --git a/src/main/main.js b/src/main/main.js
index dc499fd..76e00e0 100644
--- a/src/main/main.js
+++ b/src/main/main.js
@@ -7053,7 +7053,11 @@ async function runSmoke() {
// Yer tutucu ve başlık metinleri de taranır: gözle görünür oldukları
// halde metin düğümü olmadıkları için kolayca gözden kaçarlar.
var attrs = document.querySelectorAll('#sections [placeholder], #sections [title]');
+ /* Kullanıcı içeriği (preset, sahne, katman, klip adları) çevrilmez ve
+ taranmaz: onu çizen öğe data-user-text (metni ve altı) ya da
+ data-user-attr (yalnız title/placeholder) ile işaretlenir. */
for (var ai = 0; ai < attrs.length; ai++) {
+ if (attrs[ai].closest('[data-user-attr],[data-user-text]')) continue;
var av = (attrs[ai].getAttribute('placeholder') || '') + ' ' + (attrs[ai].getAttribute('title') || '');
if (/[çğıöşüÇĞİÖŞÜ]/.test(av)) out.push(av.trim().slice(0, 90));
}
@@ -7067,6 +7071,7 @@ async function runSmoke() {
while (el && el.id !== 'sections') {
for (var k = 0; k < skip.length; k++) if (el.classList && el.classList.contains(skip[k])) skipIt = true;
if (el.tagName === 'OPTION' || el.tagName === 'INPUT') skipIt = true;
+ if (el.hasAttribute && el.hasAttribute('data-user-text')) skipIt = true;
el = el.parentElement;
}
if (!skipIt) out.push(text.slice(0, 90));
diff --git a/src/shared/i18n.js b/src/shared/i18n.js
index 1fa5167..acc5f9e 100644
--- a/src/shared/i18n.js
+++ b/src/shared/i18n.js
@@ -2870,6 +2870,14 @@
'Yeni Görselleştirici': 'New Visualizer',
'Arkaplan Varyasyonum': 'My Background Variation',
'Görselleştiricim': 'My Visualizer',
+ // Yeni shader şablonunun başlık yorumu (satır satır)
+ '// Kendi görselleştiricin.': '// Your own visualizer.',
+ '// Kullanabileceklerin:': '// What you can use:',
+ '// sv_spec(x) -> 0..1 konumundaki spektrum değeri': '// sv_spec(x) -> spectrum value at position 0..1',
+ '// sv_waveAt(x) -> dalga formu (-1..1)': '// sv_waveAt(x) -> waveform (-1..1)',
+ '// sv_col(x) -> senin 5 renkli paletinden renk': '// sv_col(x) -> a color from your 5-color palette',
+ '// sv_prev -> bir önceki kare (geri besleme), sv_media -> kamera/video': '// sv_prev -> the previous frame (feedback), sv_media -> camera/video',
+ '// Aşağıya kendi kaydırıcılarını da ekleyebilirsin (Parametreler bölümü).': '// You can add your own sliders below too (Parameters section).',
'Dışa aktarılacak preset yok.': 'No presets to export.',
'CAYADEV Preset Paketi': 'CAYADEV Preset Pack',
'Dosyaya yazıldı.': 'Written to file.',
diff --git a/tests/studio-new-preset-i18n.test.js b/tests/studio-new-preset-i18n.test.js
new file mode 100644
index 0000000..c1b68d8
--- /dev/null
+++ b/tests/studio-new-preset-i18n.test.js
@@ -0,0 +1,100 @@
+'use strict';
+/* STUDIO'DA YENİ PRESET VARSAYILANLARI ARAYÜZ DİLİNDE.
+ *
+ * İngilizce arayüzde "+ Shader" yeni görselleştiriciyi "Yeni Görselleştirici"
+ * adıyla açıyordu; değişiklikleri at onayı da bu adı gösteriyordu. Ad, etiket
+ * ve şablonun başlık yorumu kullanıcının kendi düzenlenebilir metni oluyor
+ * ve editör onu çevirmiyor; bu yüzden oluşturulurken çevriliyorlar (#759 gibi).
+ */
+const test = require('node:test');
+const assert = require('node:assert');
+const fs = require('fs');
+const path = require('path');
+
+const read = (p) => fs.readFileSync(path.join(__dirname, '..', p), 'utf8').replace(/\r\n/g, '\n');
+const norm = (v) => String(v).replace(/\s+/g, ' ').trim();
+
+function dictionary() {
+ const src = read('src/shared/i18n.js');
+ const body = src.slice(src.indexOf('const EN = {'), src.indexOf('const EN_NORMALIZED'));
+ const STR = "'((?:[^'\\\\]|\\\\.)*)'";
+ const re = new RegExp(STR + '\\s*:\\s*' + STR, 'g');
+ const unesc = (s) => s.replace(/\\(.)/g, '$1');
+ const map = new Map();
+ let m;
+ while ((m = re.exec(body))) map.set(norm(unesc(m[1])), unesc(m[2]));
+ return map;
+}
+
+const studio = read('src/admin/studio.js');
+const fnBody = (name) => {
+ const i = studio.indexOf('function ' + name + '(');
+ assert.ok(i >= 0, name + ' bulunamadı');
+ return studio.slice(i, studio.indexOf('\n }\n', i));
+};
+
+test('yeni shader ve varyasyon adları ile etiketi çevrilerek yazılıyor', () => {
+ const d = dictionary();
+ const shader = fnBody('newShader');
+ const variation = fnBody('newVariation');
+ assert.match(shader, /name: trStudio\(kind === 'background' \? 'Yeni Arkaplan' : 'Yeni Görselleştirici'\)/);
+ assert.match(shader, /label: trStudio\('Miktar'\)/);
+ assert.match(shader, /shader: starterShader\(\)/);
+ assert.match(variation, /name: trStudio\(kind === 'background' \? 'Arkaplan Varyasyonum' : 'Görselleştiricim'\)/);
+ for (const k of ['Yeni Arkaplan', 'Yeni Görselleştirici', 'Miktar', 'Arkaplan Varyasyonum', 'Görselleştiricim']) {
+ assert.ok(d.has(k), 'çevirisi yok: ' + k);
+ }
+});
+
+test('şablon başlık yorumunun Türkçe her satırının İngilizcesi var', () => {
+ const d = dictionary();
+ const m = /const STARTER_HEADER = \[([\s\S]*?)\];/.exec(studio);
+ assert.ok(m, 'STARTER_HEADER bulunamadı');
+ const lines = [...m[1].matchAll(/'((?:[^'\\]|\\.)*)'/g)].map((x) => x[1]);
+ assert.ok(lines.length >= 6);
+ for (const l of lines) {
+ assert.ok(l.startsWith('//'), 'yorum değil: ' + l);
+ if (!/[^\x00-\x7f]|[a-z]+ [a-z]+ [a-z]/i.test(l.replace(/sv_\w+(\(x\))?,?/g, ''))) continue;
+ const en = d.get(norm(l));
+ assert.ok(en, 'çevirisi yok: ' + l);
+ assert.ok(en.startsWith('//'), 'çeviri yorum olarak kalmalı: ' + en);
+ // Hizalı kolonlar korunsun: "->" aynı sütunda
+ if (l.includes('->')) assert.strictEqual(en.indexOf('->'), l.indexOf('->'), en);
+ }
+ assert.ok(!/STARTER_SHADER/.test(studio), 'eski sabit şablon kalmamalı');
+});
+
+test('i18n sözlüğünde yinelenen anahtar yok (yeni satırlar)', () => {
+ const src = read('src/shared/i18n.js');
+ for (const k of ['// Kendi görselleştiricin.', '// Kullanabileceklerin:', 'Yeni Görselleştirici']) {
+ const n = src.split("'" + k + "':").length - 1;
+ assert.strictEqual(n, 1, k + ' ' + n + ' kez');
+ }
+});
+
+/* Öz testin "çevrilmemiş metin" taraması kullanıcının kendi adlarını
+ (ör. kaydedilmiş "A Yeni Görselleştirici") işaretliyordu. Kullanıcı içeriği
+ genel bir öznitelikle hariç tutulur; tek tek metin istisnası yok. */
+test('öz test çeviri taraması kullanıcı içeriği özniteliklerini atlıyor', () => {
+ const main = read('src/main/main.js');
+ const i = main.indexOf("untranslated string(s) in English UI");
+ const scan = main.slice(main.lastIndexOf('createTreeWalker(document.getElementById', i) - 1500, i);
+ assert.match(scan, /closest\('\[data-user-attr\],\[data-user-text\]'\)\) continue;/);
+ assert.match(scan, /hasAttribute\('data-user-text'\)\) skipIt = true;/);
+ assert.doesNotMatch(scan, /Yeni Görselleştirici/, 'metne özel istisna olmamalı');
+});
+
+test('Studio listesi kendi preset adlarını kullanıcı içeriği olarak işaretliyor', () => {
+ assert.match(studio, /class: 'si-name', text: p\.builtin \? trStudio\(p\.name\) : p\.name, \.\.\.\(p\.builtin \? \{\} : \{ 'data-user-text': '' \}\)/);
+ assert.match(studio, /\.\.\.\(p\.builtin \? \{\} : \{ 'data-user-attr': '' \}\)/);
+});
+
+test('dokunulmamış yeni taslak atılırken onay sorulmuyor', () => {
+ const body = fnBody('startDraft');
+ assert.match(body, /dirty = false;/);
+ assert.doesNotMatch(body, /dirty = true/);
+ const confirm = fnBody('confirmDiscard');
+ assert.match(confirm, /if \(!dirty \|\| !draft\) return true;/);
+ // Kaydedilmemiş yeni taslakta Kaydet yine vurgulu
+ assert.match(studio, /class: 'btn ' \+ \(dirty \|\| !selectedId \? 'primary' : ''\)/);
+});