internationalisation fr:en
This commit is contained in:
+159
-25
@@ -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 @@
|
||||
<body>
|
||||
<div class="unit">
|
||||
<header>
|
||||
<h1><span>_chordlab·v2.4</span></h1>
|
||||
<div class="sub">chord ideas machine · underscorelab / leradiologue</div>
|
||||
<h1><span>_chordlab·v2</span></h1>
|
||||
<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>
|
||||
|
||||
<!-- LCD -->
|
||||
@@ -461,11 +468,11 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
_chordlab v2.4 · underscorelab / leradiologue
|
||||
_chordlab v2.5 · underscorelab / leradiologue
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -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=`<button class="del" title="supprimer">×</button>
|
||||
s.innerHTML=`<button class="del" title="${t('supprimer')}">×</button>
|
||||
<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="srow">
|
||||
<button class="mv sl" title="déplacer à gauche">‹</button>
|
||||
<button class="mv bminus" title="−1 temps">−</button>
|
||||
<span class="badge" title="durée en temps (1 à 16)">${c.beats}♩</span>
|
||||
<button class="mv bplus" title="+1 temps">+</button>
|
||||
<button class="mv sr" title="déplacer à droite">›</button>
|
||||
<button class="mv sl" title="${t('déplacer à gauche')}">‹</button>
|
||||
<button class="mv bminus" title="${t('−1 temps')}">−</button>
|
||||
<span class="badge" title="${t('durée en temps (1 à 16)')}">${c.beats}♩</span>
|
||||
<button class="mv bplus" title="${t('+1 temps')}">+</button>
|
||||
<button class="mv sr" title="${t('déplacer à droite')}">›</button>
|
||||
</div>`;
|
||||
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();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user