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 [![Downloads](https://img.shields.io/github/downloads/CaYatur/SoundVisualizer/total?label=downloads)](https://github.com/CaYatur/SoundVisualizer/releases) [![License: MIT](https://img.shields.io/badge/License-MIT-e11d2a.svg)](LICENSE) [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.svg)](#download) -[![Tests](https://img.shields.io/badge/tests-3183%20passing-2ea043.svg)](#tests) +[![Tests](https://img.shields.io/badge/tests-3189%20passing-2ea043.svg)](#tests) [![Electron](https://img.shields.io/badge/Electron-43-47848F.svg)](https://www.electronjs.org/) Download for Windows @@ -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ü**. [![İndirme](https://img.shields.io/github/downloads/CaYatur/SoundVisualizer/total?label=indirme)](https://github.com/CaYatur/SoundVisualizer/releases) [![Lisans: MIT](https://img.shields.io/badge/Lisans-MIT-e11d2a.svg)](LICENSE) [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.svg)](#hızlı-başlangıç) -[![Test](https://img.shields.io/badge/test-3183%20geçiyor-2ea043.svg)](#testler) +[![Test](https://img.shields.io/badge/test-3189%20geçiyor-2ea043.svg)](#testler) [![Electron](https://img.shields.io/badge/Electron-43-47848F.svg)](https://www.electronjs.org/) Windows için indir @@ -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' : ''\)/); +});