diff --git a/CHANGELOG.md b/CHANGELOG.md index c9f514b..49c6618 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,24 @@ Conventions : --- +## [2.5] — interface bilingue + +### Ajouté +- **Sélecteur de langue FR / EN** dans l'en-tête de l'application. La totalité de l'interface bascule : titres de modules, libellés, listes déroulantes, boutons, infobulles, messages d'état, ligne d'aide du clavier et texte des slots vides. +- **Détection au démarrage** d'après la langue du navigateur (`fr` → français, sinon anglais), modifiable d'un clic. +- La langue retenue est **enregistrée dans les sessions `.json`** et restaurée au chargement. +- L'attribut `lang` du document suit la langue choisie, pour les lecteurs d'écran et la synthèse vocale. + +### Modifié +- La ligne d'aide du clavier combine désormais langue et disposition : quatre variantes possibles (FR/EN × QWERTY/AZERTY). +- Le vocabulaire musical universel reste inchangé dans les deux langues : noms de notes et d'accords, types (DIM, MIN, MAJ, AUG, SUS2, SUS4, 5, m♭6), tensions et altérations, modes de performance (block, strum, harp, arp), drop voicings et valeurs rythmiques. + +### Notes d'implémentation +- Le français reste la version de référence dans le balisage ; les textes d'origine sont capturés au chargement et restaurés au retour en FR, ce qui évite toute perte après plusieurs changements de langue. +- Un seul fichier applicatif : pas de version anglaise séparée, donc aucun risque de divergence entre les deux langues. + +--- + ## [2.4] — dispositions clavier QWERTY et AZERTY ### Ajouté diff --git a/README.md b/README.md index c67c40d..762976c 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,9 @@ Générateur d'accords et de progressions harmoniques dans le navigateur. Un seu - Modes de performance : block · strum ↑↓ · harp · arp ↑/↓/↕/random (1/8 à 1/32) - Humanisation vélocité/timing +**Interface** +- **Bilingue FR / EN** : sélecteur dans l'en-tête, langue détectée au démarrage et conservée dans les sessions + **Entrées / sorties** - **Web MIDI out live** : accords et basse sur deux canaux séparés, vers n'importe quelle interface USB-MIDI - **Export `.mid`** : format 1, PPQ 480, tempo inclus, performance rendue en option diff --git a/TUTORIEL_UTILISATION.md b/TUTORIEL_UTILISATION.md index 626bb99..546ccd5 100644 --- a/TUTORIEL_UTILISATION.md +++ b/TUTORIEL_UTILISATION.md @@ -22,6 +22,8 @@ L'écran est organisé comme une face avant de machine hardware : └──────────────────┴──────────────────┴───────────────┘ ``` +L'en-tête porte un sélecteur **FR / EN** qui bascule toute l'interface. La langue est détectée au démarrage d'après le navigateur et conservée dans les sessions `.json`. Le vocabulaire musical (noms d'accords, types, tensions, modes de performance) est identique dans les deux langues. + Le **LCD** est votre référence permanente : il affiche le nom de l'accord courant, ses notes réelles (avec octaves), et l'état des modes (voicing, drop, key, perf, MIDI, voice-leading). --- diff --git a/index.html b/index.html index 52e5c7c..1dcfe96 100644 --- a/index.html +++ b/index.html @@ -50,6 +50,8 @@ header h1{font-size:1.15rem;font-weight:800;letter-spacing:.06em} header h1 span{color:var(--accent)} header .sub{font-size:.62rem;letter-spacing:.18em;text-transform:uppercase;opacity:.7} + header .hdr-right{display:flex;align-items:center;gap:.7rem;flex-wrap:wrap;justify-content:flex-end} + header .seg .segbtn{padding:.2rem .45rem} /* ---------- LCD ---------- */ .lcd{ @@ -254,8 +256,13 @@
-

_chordlab·v2.4

-
chord ideas machine · underscorelab / leradiologue
+

_chordlab·v2

+
+ chord ideas machine · underscorelab / leradiologue + + + +
@@ -461,11 +468,11 @@
@@ -498,6 +505,7 @@ const state = { perf:"block", rate:0.25, humanize:false, layout:(navigator.language||"").toLowerCase().startsWith("fr") ? "azerty" : "qwerty", + lang:(navigator.language||"").toLowerCase().startsWith("fr") ? "fr" : "en", }; /* ---------- Moteur d'accords ---------- */ @@ -780,15 +788,15 @@ let midiAccess=null, midiOut=null; const $ = id => document.getElementById(id); function midiInit(){ if (!navigator.requestMIDIAccess){ - $("midiStatus").textContent = "Web MIDI non supporté (Chrome/Edge/Firefox)."; + $("midiStatus").textContent = t("Web MIDI non supporté (Chrome/Edge/Firefox)."); $("lcdMidi").textContent = "N/A"; return; } navigator.requestMIDIAccess({sysex:false}).then(acc=>{ midiAccess = acc; refreshMidiDevices(); acc.onstatechange = refreshMidiDevices; - $("midiStatus").textContent = "Web MIDI prêt."; - }).catch(()=>{ $("midiStatus").textContent = "Accès MIDI refusé."; }); + $("midiStatus").textContent = t("Web MIDI prêt."); + }).catch(()=>{ $("midiStatus").textContent = t("Accès MIDI refusé."); }); } function refreshMidiDevices(){ const sel = $("midiDev"); const cur = sel.value; @@ -846,7 +854,7 @@ function trackChunk(events){ } function exportMidi(){ if (!state.progression.length){ - $("exportStatus").textContent = "Progression vide."; + $("exportStatus").textContent = t("Progression vide."); return; } const PPQ=480, bpm=getBpm(); @@ -893,7 +901,7 @@ function exportMidi(){ const labels=state.progression.map(c=>c.label.replace(/[^\w#]/g,"")).slice(0,4).join("-"); a.download=`chordlab_${labels}_${bpm}bpm.mid`; a.click(); URL.revokeObjectURL(a.href); - $("exportStatus").textContent=`Exporté : ${a.download}`; + $("exportStatus").textContent=t("Exporté : ")+a.download; } /* ========================================================= @@ -907,7 +915,7 @@ function saveSession(){ drop:state.drop, slash:state.slash, bass:state.bass, bassOct:state.bassOct, keyMode:state.keyMode, keyRoot:state.keyRoot, keyScale:state.keyScale, perf:state.perf, rate:state.rate, humanize:state.humanize, - layout:state.layout, + layout:state.layout, lang:state.lang, bpm:getBpm(), vlAuto:state.vlAuto, }, progression:state.progression, @@ -917,7 +925,7 @@ function saveSession(){ a.href=URL.createObjectURL(blob); a.download="chordlab_session.json"; a.click(); URL.revokeObjectURL(a.href); - $("sessionStatus").textContent="Session sauvée."; + $("sessionStatus").textContent=t("Session sauvée."); } function loadSession(file){ const r=new FileReader(); @@ -939,14 +947,15 @@ function loadSession(file){ state.rate=s.rate||0.25; state.humanize=!!s.humanize; state.layout=(s.layout==="qwerty")?"qwerty":(s.layout==="azerty"?"azerty":state.layout); + state.lang=(s.lang==="en")?"en":(s.lang==="fr"?"fr":state.lang); state.vlAuto=!!s.vlAuto; state.progression=Array.isArray(d.progression)?d.progression:[]; state.progression.forEach(c=>{ c.beats=Math.max(1,Math.min(16,c.beats||4)); }); if (s.bpm) $("bpm").value=s.bpm; syncUiFromState(); renderProgression(); - $("sessionStatus").textContent="Session chargée."; - }catch(e){ $("sessionStatus").textContent="Fichier invalide."; } + $("sessionStatus").textContent=t("Session chargée."); + }catch(e){ $("sessionStatus").textContent=t("Fichier invalide."); } }; r.readAsText(file); } @@ -966,7 +975,7 @@ function syncUiFromState(){ $("lcdDrop").textContent=state.drop.toUpperCase(); $("lcdPerf").textContent=$("perfSel").selectedOptions[0].textContent.toUpperCase(); $("lcdVl").textContent=state.vlAuto?"ON":"OFF"; - updateKeyLcd(); refreshKeyDimming(); setVoicing(state.voicing, true); + updateKeyLcd(); refreshKeyDimming(); setVoicing(state.voicing, true); applyLang(); } /* ========================================================= @@ -1001,7 +1010,7 @@ function triggerChord(rootPc){ const chord=currentChord(rootPc); if (!chord){ $("lcdName").textContent="×"; - $("lcdNotes").textContent="hors tonalité"; + $("lcdNotes").textContent=t("hors tonalité"); return null; } state.lastRoot=rootPc; @@ -1059,7 +1068,7 @@ function setLayout(l, silent){ $("layAzerty").classList.toggle("on", !isQ); $("layQwerty").setAttribute("aria-pressed", isQ); $("layAzerty").setAttribute("aria-pressed", !isQ); - $("kbdHint").textContent = KEYHINT[state.layout]; + $("kbdHint").textContent = (state.lang==="en"?KEYHINT_EN:KEYHINT)[state.layout]; } window.addEventListener("keydown",ev=>{ @@ -1184,7 +1193,7 @@ $("optimizeBtn").addEventListener("click",()=>{ state.progression[i].notes, state.progression[i-1].notes); } renderProgression(); - $("sessionStatus").textContent="Voice-leading appliqué."; + $("sessionStatus").textContent=t("Voice-leading appliqué."); }); function renderProgression(){ @@ -1193,22 +1202,22 @@ function renderProgression(){ if (!state.progression.length){ const e=document.createElement("div"); e.className="slot-empty"; - e.textContent="vide — joue un accord puis ADD, ou ✦ suggérer"; + e.textContent=t("vide — joue un accord puis ADD, ou ✦ suggérer"); el.appendChild(e); return; } state.progression.forEach((c,i)=>{ const s=document.createElement("div"); s.className="slot"+(i===state.playIdx?" playing":""); - s.innerHTML=` + s.innerHTML=`
${c.label}
${c.notes.map(n=>NOTE_NAMES[n%12]).join(" ")}${c.bass!==null?" +B":""}
- - - ${c.beats}♩ - - + + + ${c.beats}♩ + +
`; s.querySelector(".del").addEventListener("click",ev=>{ ev.stopPropagation(); @@ -1327,12 +1336,137 @@ $("fileInput").addEventListener("change",function(){ this.value=""; }); +/* ========================================================= + I18N — le français est la version de référence dans le HTML. + Le dictionnaire ci-dessous ne décrit que l'anglais ; les textes + français d'origine sont capturés au chargement et restaurés + quand on repasse en FR. + ========================================================= */ +const EN = { + txt:{ // textContent + "#tagline":"chord ideas machine · underscorelab / leradiologue", + "#lcdNotes":"play a note", + "#midiStatus":"Web MIDI: waiting…", + "#presetLoad":"load", "#progClear":"clear", + "#suggestBtn":"✦ suggest", "#optimizeBtn":"⇄ voice-leading", + "#exportBtn":"↓ export .mid", + "#saveJson":"↓ save session (.json)", "#loadJson":"↑ load a session", + "#doclinkFr":"Manual (FR)", "#doclinkEn":"Manual (EN)", + }, + h2:{ "Chord type":"Chord type", "Root":"Root", "Voicing":"Voicing", + "Progression":"Progression", "MIDI out (live)":"MIDI out (live)", + "Export MIDI":"MIDI export", "Session":"Session" }, + rowlabel:{ "Tensions":"Tensions", "Altérations":"Alterations", + "combos : dim+maj=aug · min+sus=sus2":"" }, + label:{ "drop":"drop", "basse /":"bass /", "gamme":"scale", + "périphérique":"device", "canal accords":"chord channel", + "canal basse":"bass channel", "pistes":"tracks", "perf rendue":"render perf" }, + option:{ "fondamentale":"root", "tierce":"third", "quinte":"fifth", "septième":"seventh", + "−2 oct":"−2 oct", "−1 oct":"−1 oct", + "majeur":"major", "mineur nat.":"nat. minor", "mineur harm.":"harm. minor", + "dorien":"dorian", "phrygien":"phrygian", "lydien":"lydian", + "mixolydien":"mixolydian", "locrien":"locrian", + "— preset (tonalité KEY) —":"— preset (KEY tonality) —", + "— désactivé —":"— off —", + "accords + basse (2 pistes)":"chords + bass (2 tracks)", + "accords seuls":"chords only", + "oui (strum/arp dans le .mid)":"yes (strum/arp in the .mid)", + "non (accords plaqués)":"no (block chords)" }, + title:{ "suggère l'accord suivant (harmonie fonctionnelle)":"suggest the next chord (functional harmony)", + "re-voice toute la progression pour minimiser les mouvements":"re-voice the whole progression to minimise movement", + "applique le voice-leading automatiquement à chaque ADD":"apply voice leading automatically on every ADD", + "humanise vélocité et timing":"humanise velocity and timing", + "Octave basse":"Bass octave" }, + small:{ "BPM":"BPM", "perf":"perf", "rate":"rate", "durée/accord":"length/chord" }, + /* chaînes générées par le code */ + run:{ + "play a note":"play a note", + "hors tonalité":"out of key", + "Web MIDI non supporté (Chrome/Edge/Firefox).":"Web MIDI not supported (use Chrome/Edge/Firefox).", + "Web MIDI prêt.":"Web MIDI ready.", + "Accès MIDI refusé.":"MIDI access denied.", + "Progression vide.":"Progression is empty.", + "Session sauvée.":"Session saved.", + "Session chargée.":"Session loaded.", + "Fichier invalide.":"Invalid file.", + "Voice-leading appliqué.":"Voice leading applied.", + "vide — joue un accord puis ADD, ou ✦ suggérer":"empty — play a chord then ADD, or ✦ suggest", + "supprimer":"delete", + "déplacer à gauche":"move left", + "déplacer à droite":"move right", + "−1 temps":"−1 beat", + "+1 temps":"+1 beat", + "durée en temps (1 à 16)":"length in beats (1 to 16)", + "Exporté : ":"Exported: ", + }, +}; +const KEYHINT_EN = { + qwerty:"computer keys: A W S E D F T G Y H U J · ␣ = ADD · ← → = voicing", + azerty:"computer keys: Q Z S E D F T G Y H U J · ␣ = ADD · ← → = voicing", +}; + +/* traduit une chaîne runtime selon la langue courante */ +function t(s){ return state.lang==="en" ? (EN.run[s] ?? s) : s; } + +const FR_BASE = new Map(); // élément → texte français d'origine +function rememberFr(el, kind, val){ + const k = el.dataset.i18nKey || (el.dataset.i18nKey = "k"+FR_BASE.size); + if (!FR_BASE.has(k)) FR_BASE.set(k, {}); + FR_BASE.get(k)[kind] = val; +} +function frOf(el, kind){ return FR_BASE.get(el.dataset.i18nKey)?.[kind]; } + +function applyLang(){ + const en = state.lang==="en"; + document.documentElement.lang = en ? "en" : "fr"; + // textes simples ciblés par sélecteur + Object.entries(EN.txt).forEach(([sel, val])=>{ + if (val===null) return; + const el = document.querySelector(sel); + if (!el) return; + rememberFr(el,"txt",frOf(el,"txt") ?? el.textContent); + el.textContent = en ? val : frOf(el,"txt"); + }); + // titres de modules, row-labels, labels, options, titles + const swap = (nodes, dict, kind, get, set)=>{ + nodes.forEach(el=>{ + const cur = get(el); + const fr = frOf(el,kind) ?? cur; + rememberFr(el,kind,fr); + const v = en ? (dict[fr] !== undefined ? dict[fr] : fr) : fr; + set(el, v); + }); + }; + const T = el=>el.textContent, S=(el,v)=>{el.textContent=v;}; + swap([...document.querySelectorAll(".mod h2")], EN.h2, "h2", T, S); + swap([...document.querySelectorAll(".row-label")], EN.rowlabel, "rl", T, S); + swap([...document.querySelectorAll(".field label, .io-line label")], EN.label, "lb", T, S); + swap([...document.querySelectorAll("option")], EN.option, "op", T, S); + swap([...document.querySelectorAll(".bpm-wrap")], EN.small, "sm", + el=>el.childNodes[0].textContent.trim(), + (el,v)=>{ el.childNodes[0].textContent = v+" "; }); + swap([...document.querySelectorAll("[title]")].filter(el=>!el.closest(".slot")), EN.title, "ti", + el=>el.getAttribute("title"), (el,v)=>el.setAttribute("title",v)); + // aide clavier + LCD au repos + $("kbdHint").textContent = en ? KEYHINT_EN[state.layout] : KEYHINT[state.layout]; + if (state.lastRoot===null) $("lcdNotes").textContent = en ? "play a note" : "joue une note"; + // boutons de langue + $("langFr").classList.toggle("on", !en); + $("langEn").classList.toggle("on", en); + $("langFr").setAttribute("aria-pressed", !en); + $("langEn").setAttribute("aria-pressed", en); + renderProgression(); +} +$("langFr").addEventListener("click",()=>{ state.lang="fr"; applyLang(); }); +$("langEn").addEventListener("click",()=>{ state.lang="en"; applyLang(); }); + /* init */ buildKeyboard(); setLayout(state.layout); renderProgression(); refreshKeyDimming(); setVoicing(0,true); +applyLang(); midiInit(); diff --git a/tuto.html b/tuto.html index 5036e77..9ae26d4 100644 --- a/tuto.html +++ b/tuto.html @@ -149,7 +149,7 @@
-
manuel d'utilisation · rev. 2.4  ·  English version
+
manuel d'utilisation · rev. 2.5  ·  English version

_chordlab

Machine à accords et à progressions. Le principe tient en une phrase : vous ne jouez pas les notes d'un accord, vous le décrivez — une fondamentale, un type, des tensions — et la machine fait le reste, jusqu'à vos synthés ou votre DAW.

@@ -212,6 +212,7 @@
fig. 1 — cartographie de la face avant
+

L'en-tête de l'application porte un sélecteur FR / EN qui bascule toute l'interface : libellés, listes, boutons, infobulles et messages. La langue est détectée au démarrage d'après votre navigateur et conservée dans les sessions .json. Le vocabulaire musical — noms d'accords, types, tensions, modes de performance — reste identique dans les deux langues.

@@ -729,7 +730,7 @@ - +
diff --git a/tuto_en.html b/tuto_en.html index a419f64..0790c9f 100644 --- a/tuto_en.html +++ b/tuto_en.html @@ -149,7 +149,7 @@
-
user manual · rev. 2.4  ·  Version française
+
user manual · rev. 2.5  ·  Version française

_chordlab

A chord and progression machine. The whole idea fits in one sentence: you don't play the notes of a chord, you describe it — a root, a type, some tensions — and the machine takes it from there, all the way to your synths or your DAW.

@@ -212,6 +212,7 @@
fig. 1 — front panel map
+

The app header carries an FR / EN switch that flips the whole interface: labels, dropdowns, buttons, tooltips and messages. The language is detected at startup from your browser and stored in .json sessions. Musical vocabulary — chord names, types, tensions, performance modes — stays the same in both languages.

@@ -729,7 +730,7 @@ - +