internationalisation fr:en

This commit is contained in:
lrdlg
2026-07-21 07:03:39 +02:00
parent e72a32d238
commit a680173138
6 changed files with 188 additions and 29 deletions
+18
View File
@@ -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 ## [2.4] — dispositions clavier QWERTY et AZERTY
### Ajouté ### Ajouté
+3
View File
@@ -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) - Modes de performance : block · strum ↑↓ · harp · arp ↑/↓/↕/random (1/8 à 1/32)
- Humanisation vélocité/timing - 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** **Entrées / sorties**
- **Web MIDI out live** : accords et basse sur deux canaux séparés, vers n'importe quelle interface USB-MIDI - **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 - **Export `.mid`** : format 1, PPQ 480, tempo inclus, performance rendue en option
+2
View File
@@ -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). 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).
--- ---
+159 -25
View File
@@ -50,6 +50,8 @@
header h1{font-size:1.15rem;font-weight:800;letter-spacing:.06em} header h1{font-size:1.15rem;font-weight:800;letter-spacing:.06em}
header h1 span{color:var(--accent)} header h1 span{color:var(--accent)}
header .sub{font-size:.62rem;letter-spacing:.18em;text-transform:uppercase;opacity:.7} 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 ---------- */
.lcd{ .lcd{
@@ -254,8 +256,13 @@
<body> <body>
<div class="unit"> <div class="unit">
<header> <header>
<h1><span>_chordlab·v2.4</span></h1> <h1><span>_chordlab·v2</span></h1>
<div class="sub">chord ideas machine · underscorelab / leradiologue</div> <div class="hdr-right">
<span class="sub" id="tagline">chord ideas machine · underscorelab / leradiologue</span>
<span class="seg" role="group" aria-label="Language">
<button class="segbtn on" id="langFr" aria-pressed="true">FR</button><button class="segbtn" id="langEn" aria-pressed="false">EN</button>
</span>
</div>
</header> </header>
<!-- LCD --> <!-- LCD -->
@@ -461,11 +468,11 @@
</div> </div>
<footer> <footer>
<a href="tuto.html" class="doclink">Manuel (FR)</a> <a href="tuto.html" class="doclink" id="doclinkFr">Manuel (FR)</a>
<span class="docsep">·</span> <span class="docsep">·</span>
<a href="tuto_en.html" class="doclink">Manual (EN)</a> <a href="tuto_en.html" class="doclink" id="doclinkEn">Manual (EN)</a>
<span class="docsep">·</span> <span class="docsep">·</span>
_chordlab v2.4 · underscorelab / leradiologue _chordlab v2.5 · underscorelab / leradiologue
</footer> </footer>
</div> </div>
@@ -498,6 +505,7 @@ const state = {
perf:"block", rate:0.25, perf:"block", rate:0.25,
humanize:false, humanize:false,
layout:(navigator.language||"").toLowerCase().startsWith("fr") ? "azerty" : "qwerty", layout:(navigator.language||"").toLowerCase().startsWith("fr") ? "azerty" : "qwerty",
lang:(navigator.language||"").toLowerCase().startsWith("fr") ? "fr" : "en",
}; };
/* ---------- Moteur d'accords ---------- */ /* ---------- Moteur d'accords ---------- */
@@ -780,15 +788,15 @@ let midiAccess=null, midiOut=null;
const $ = id => document.getElementById(id); const $ = id => document.getElementById(id);
function midiInit(){ function midiInit(){
if (!navigator.requestMIDIAccess){ 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"; $("lcdMidi").textContent = "N/A";
return; return;
} }
navigator.requestMIDIAccess({sysex:false}).then(acc=>{ navigator.requestMIDIAccess({sysex:false}).then(acc=>{
midiAccess = acc; refreshMidiDevices(); midiAccess = acc; refreshMidiDevices();
acc.onstatechange = refreshMidiDevices; acc.onstatechange = refreshMidiDevices;
$("midiStatus").textContent = "Web MIDI prêt."; $("midiStatus").textContent = t("Web MIDI prêt.");
}).catch(()=>{ $("midiStatus").textContent = "Accès MIDI refusé."; }); }).catch(()=>{ $("midiStatus").textContent = t("Accès MIDI refusé."); });
} }
function refreshMidiDevices(){ function refreshMidiDevices(){
const sel = $("midiDev"); const cur = sel.value; const sel = $("midiDev"); const cur = sel.value;
@@ -846,7 +854,7 @@ function trackChunk(events){
} }
function exportMidi(){ function exportMidi(){
if (!state.progression.length){ if (!state.progression.length){
$("exportStatus").textContent = "Progression vide."; $("exportStatus").textContent = t("Progression vide.");
return; return;
} }
const PPQ=480, bpm=getBpm(); 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("-"); const labels=state.progression.map(c=>c.label.replace(/[^\w#]/g,"")).slice(0,4).join("-");
a.download=`chordlab_${labels}_${bpm}bpm.mid`; a.download=`chordlab_${labels}_${bpm}bpm.mid`;
a.click(); URL.revokeObjectURL(a.href); 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, drop:state.drop, slash:state.slash, bass:state.bass, bassOct:state.bassOct,
keyMode:state.keyMode, keyRoot:state.keyRoot, keyScale:state.keyScale, keyMode:state.keyMode, keyRoot:state.keyRoot, keyScale:state.keyScale,
perf:state.perf, rate:state.rate, humanize:state.humanize, perf:state.perf, rate:state.rate, humanize:state.humanize,
layout:state.layout, layout:state.layout, lang:state.lang,
bpm:getBpm(), vlAuto:state.vlAuto, bpm:getBpm(), vlAuto:state.vlAuto,
}, },
progression:state.progression, progression:state.progression,
@@ -917,7 +925,7 @@ function saveSession(){
a.href=URL.createObjectURL(blob); a.href=URL.createObjectURL(blob);
a.download="chordlab_session.json"; a.download="chordlab_session.json";
a.click(); URL.revokeObjectURL(a.href); a.click(); URL.revokeObjectURL(a.href);
$("sessionStatus").textContent="Session sauvée."; $("sessionStatus").textContent=t("Session sauvée.");
} }
function loadSession(file){ function loadSession(file){
const r=new FileReader(); const r=new FileReader();
@@ -939,14 +947,15 @@ function loadSession(file){
state.rate=s.rate||0.25; state.rate=s.rate||0.25;
state.humanize=!!s.humanize; state.humanize=!!s.humanize;
state.layout=(s.layout==="qwerty")?"qwerty":(s.layout==="azerty"?"azerty":state.layout); 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.vlAuto=!!s.vlAuto;
state.progression=Array.isArray(d.progression)?d.progression:[]; state.progression=Array.isArray(d.progression)?d.progression:[];
state.progression.forEach(c=>{ c.beats=Math.max(1,Math.min(16,c.beats||4)); }); state.progression.forEach(c=>{ c.beats=Math.max(1,Math.min(16,c.beats||4)); });
if (s.bpm) $("bpm").value=s.bpm; if (s.bpm) $("bpm").value=s.bpm;
syncUiFromState(); syncUiFromState();
renderProgression(); renderProgression();
$("sessionStatus").textContent="Session chargée."; $("sessionStatus").textContent=t("Session chargée.");
}catch(e){ $("sessionStatus").textContent="Fichier invalide."; } }catch(e){ $("sessionStatus").textContent=t("Fichier invalide."); }
}; };
r.readAsText(file); r.readAsText(file);
} }
@@ -966,7 +975,7 @@ function syncUiFromState(){
$("lcdDrop").textContent=state.drop.toUpperCase(); $("lcdDrop").textContent=state.drop.toUpperCase();
$("lcdPerf").textContent=$("perfSel").selectedOptions[0].textContent.toUpperCase(); $("lcdPerf").textContent=$("perfSel").selectedOptions[0].textContent.toUpperCase();
$("lcdVl").textContent=state.vlAuto?"ON":"OFF"; $("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); const chord=currentChord(rootPc);
if (!chord){ if (!chord){
$("lcdName").textContent="×"; $("lcdName").textContent="×";
$("lcdNotes").textContent="hors tonalité"; $("lcdNotes").textContent=t("hors tonalité");
return null; return null;
} }
state.lastRoot=rootPc; state.lastRoot=rootPc;
@@ -1059,7 +1068,7 @@ function setLayout(l, silent){
$("layAzerty").classList.toggle("on", !isQ); $("layAzerty").classList.toggle("on", !isQ);
$("layQwerty").setAttribute("aria-pressed", isQ); $("layQwerty").setAttribute("aria-pressed", isQ);
$("layAzerty").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=>{ window.addEventListener("keydown",ev=>{
@@ -1184,7 +1193,7 @@ $("optimizeBtn").addEventListener("click",()=>{
state.progression[i].notes, state.progression[i-1].notes); state.progression[i].notes, state.progression[i-1].notes);
} }
renderProgression(); renderProgression();
$("sessionStatus").textContent="Voice-leading appliqué."; $("sessionStatus").textContent=t("Voice-leading appliqué.");
}); });
function renderProgression(){ function renderProgression(){
@@ -1193,22 +1202,22 @@ function renderProgression(){
if (!state.progression.length){ if (!state.progression.length){
const e=document.createElement("div"); const e=document.createElement("div");
e.className="slot-empty"; 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); el.appendChild(e);
return; return;
} }
state.progression.forEach((c,i)=>{ state.progression.forEach((c,i)=>{
const s=document.createElement("div"); const s=document.createElement("div");
s.className="slot"+(i===state.playIdx?" playing":""); s.className="slot"+(i===state.playIdx?" playing":"");
s.innerHTML=`<button class="del" title="supprimer">×</button> s.innerHTML=`<button class="del" title="${t('supprimer')}">×</button>
<div class="sname">${c.label}</div> <div class="sname">${c.label}</div>
<div class="snotes">${c.notes.map(n=>NOTE_NAMES[n%12]).join(" ")}${c.bass!==null?" +B":""}</div> <div class="snotes">${c.notes.map(n=>NOTE_NAMES[n%12]).join(" ")}${c.bass!==null?" +B":""}</div>
<div class="srow"> <div class="srow">
<button class="mv sl" title="déplacer à gauche"></button> <button class="mv sl" title="${t('déplacer à gauche')}"></button>
<button class="mv bminus" title="1 temps"></button> <button class="mv bminus" title="${t('1 temps')}"></button>
<span class="badge" title="durée en temps (1 à 16)">${c.beats}♩</span> <span class="badge" title="${t('durée en temps (1 à 16)')}">${c.beats}♩</span>
<button class="mv bplus" title="+1 temps">+</button> <button class="mv bplus" title="${t('+1 temps')}">+</button>
<button class="mv sr" title="déplacer à droite"></button> <button class="mv sr" title="${t('déplacer à droite')}"></button>
</div>`; </div>`;
s.querySelector(".del").addEventListener("click",ev=>{ s.querySelector(".del").addEventListener("click",ev=>{
ev.stopPropagation(); ev.stopPropagation();
@@ -1327,12 +1336,137 @@ $("fileInput").addEventListener("change",function(){
this.value=""; 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 */ /* init */
buildKeyboard(); buildKeyboard();
setLayout(state.layout); setLayout(state.layout);
renderProgression(); renderProgression();
refreshKeyDimming(); refreshKeyDimming();
setVoicing(0,true); setVoicing(0,true);
applyLang();
midiInit(); midiInit();
</script> </script>
</body> </body>
+3 -2
View File
@@ -149,7 +149,7 @@
<div class="sheet"> <div class="sheet">
<div class="cover"> <div class="cover">
<div class="kicker">manuel d'utilisation · rev. 2.4 &nbsp;·&nbsp; <a href="tuto_en.html" style="color:var(--accent);text-decoration:none;border-bottom:1px solid var(--accent)">English version</a></div> <div class="kicker">manuel d'utilisation · rev. 2.5 &nbsp;·&nbsp; <a href="tuto_en.html" style="color:var(--accent);text-decoration:none;border-bottom:1px solid var(--accent)">English version</a></div>
<h1><em>_chordlab</em></h1> <h1><em>_chordlab</em></h1>
<p>Machine à accords et à progressions. Le principe tient en une phrase : <strong>vous ne jouez pas les notes d'un accord, vous le décrivez</strong> — une fondamentale, un type, des tensions — et la machine fait le reste, jusqu'à vos synthés ou votre DAW.</p> <p>Machine à accords et à progressions. Le principe tient en une phrase : <strong>vous ne jouez pas les notes d'un accord, vous le décrivez</strong> — une fondamentale, un type, des tensions — et la machine fait le reste, jusqu'à vos synthés ou votre DAW.</p>
</div> </div>
@@ -212,6 +212,7 @@
</ul> </ul>
<figcaption>fig. 1 — cartographie de la face avant</figcaption> <figcaption>fig. 1 — cartographie de la face avant</figcaption>
</figure> </figure>
<p>L'en-tête de l'application porte un sélecteur <strong>FR / EN</strong> 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 <code>.json</code>. Le vocabulaire musical — noms d'accords, types, tensions, modes de performance — reste identique dans les deux langues.</p>
</section> </section>
<!-- ============ 02 PREMIER ACCORD ============ --> <!-- ============ 02 PREMIER ACCORD ============ -->
@@ -729,7 +730,7 @@
</table> </table>
</section> </section>
<footer>_chordlab · manuel rev. 2.4 · underscorelab / leradiologue</footer> <footer>_chordlab · manuel rev. 2.5 · underscorelab / leradiologue</footer>
</div> </div>
</body> </body>
</html> </html>
+3 -2
View File
@@ -149,7 +149,7 @@
<div class="sheet"> <div class="sheet">
<div class="cover"> <div class="cover">
<div class="kicker">user manual · rev. 2.4 &nbsp;·&nbsp; <a href="tuto.html" style="color:var(--accent);text-decoration:none;border-bottom:1px solid var(--accent)">Version française</a></div> <div class="kicker">user manual · rev. 2.5 &nbsp;·&nbsp; <a href="tuto.html" style="color:var(--accent);text-decoration:none;border-bottom:1px solid var(--accent)">Version française</a></div>
<h1><em>_chordlab</em></h1> <h1><em>_chordlab</em></h1>
<p>A chord and progression machine. The whole idea fits in one sentence: <strong>you don't play the notes of a chord, you describe it</strong> — a root, a type, some tensions — and the machine takes it from there, all the way to your synths or your DAW.</p> <p>A chord and progression machine. The whole idea fits in one sentence: <strong>you don't play the notes of a chord, you describe it</strong> — a root, a type, some tensions — and the machine takes it from there, all the way to your synths or your DAW.</p>
</div> </div>
@@ -212,6 +212,7 @@
</ul> </ul>
<figcaption>fig. 1 — front panel map</figcaption> <figcaption>fig. 1 — front panel map</figcaption>
</figure> </figure>
<p>The app header carries an <strong>FR / EN</strong> switch that flips the whole interface: labels, dropdowns, buttons, tooltips and messages. The language is detected at startup from your browser and stored in <code>.json</code> sessions. Musical vocabulary — chord names, types, tensions, performance modes — stays the same in both languages.</p>
</section> </section>
<!-- ============ 02 FIRST CHORD ============ --> <!-- ============ 02 FIRST CHORD ============ -->
@@ -729,7 +730,7 @@
</table> </table>
</section> </section>
<footer>_chordlab · manual rev. 2.4 · underscorelab / leradiologue</footer> <footer>_chordlab · manual rev. 2.5 · underscorelab / leradiologue</footer>
</div> </div>
</body> </body>
</html> </html>