internationalisation fr:en
This commit is contained in:
@@ -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é
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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 · <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 · <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
@@ -149,7 +149,7 @@
|
|||||||
<div class="sheet">
|
<div class="sheet">
|
||||||
|
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<div class="kicker">user manual · rev. 2.4 · <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 · <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>
|
||||||
|
|||||||
Reference in New Issue
Block a user