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
+159 -25
View File
@@ -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>