teach/public/js/core.js
Ramon 6f593e05b2
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m1s
fix: corrigeer woordplaatjes in taalbibliotheek (v0.4.44-beta)
2026-07-21 22:01:41 +02:00

1313 lines
101 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* teach - kern: versie, i18n, accounts, opslag, geluid */
"use strict";
/* version — shown until /api/version resolves (or if the fetch fails, e.g. offline).
Kept in sync by hand with the VERSION file at the repo root on every release. */
const VERSION = "0.4.44-beta";
(function(){
const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION;
/* the real deployed version always wins once known: CI reads the VERSION file
and passes it as APP_VERSION ("0.2.10-beta" on dev, the release tag on prod)
- never a raw git sha, that's only kept as a hover-title for support/debug */
fetch("/api/version")
.then(r=>r.ok ? r.json() : null)
.then(v=>{
if(!v || typeof v.version!=="string") return;
let text = v.version.startsWith("v") ? v.version : "v"+v.version;
const isBeta = text.includes("beta");
/* on dev the sha is shown inline (not just as a hover-title) so a deploy is always
visibly different, even if the VERSION file itself was forgotten in a commit */
if(isBeta && v.sha) text += " ("+v.sha+")";
tag.textContent = text;
tag.classList.toggle("beta", isBeta);
if(v.sha) tag.title = v.sha;
})
.catch(()=>{});
})();
document.getElementById("copyright").textContent = "© "+new Date().getFullYear()+" Ramon Besselink";
/* =========================================================
i18n
==========================================================*/
const I18N = {
nl:{
addWidget:"Widget", chooseWidget:"Kies een widget", saveBoard:"Bord opslaan",
emptyBoard:"Het bord is leeg. Klik op “Widget” om te beginnen.",
wg_letters:"Letterblokken", wg_letters_d:"Leer lezen en spellen met letterblokken 3 niveaus, zoals Ik leer lezen.",
wg_notes:"Notities", wg_notes_d:"Een vel om op te schrijven.",
wg_timer:"Timer", wg_timer_d:"Afteltimer met kijkklok voor de klas.",
wg_write:"Schrijfblad", wg_write_d:"Schrijven en rekenen: pen, gum en papier met schrijflijnen of rekenblokjes.",
bgBlank:"Leeg blad", bgLines:"Lijntjes", bgWrite:"Schrijflijnen",
bgGridS:"Kleine rekenblokjes", bgGridB:"Grote rekenblokjes",
penSize:"Dikte", penColor:"Kleur", penExtras:"Hulpmiddelen", recentColors:"Recent gebruikt", paletteColors:"Kleuren", customColor:"Eigen kleur", eraser:"Gum", clearAll:"Alles wissen",
toolPen:"Potlood (zachte, gelijkmatige lijn)", toolFountain:"Vulpen (kalligrafie: dik in de neerhaal, dun in de ophaal)",
toolMark:"Markeerstift", toolText:"Tekst typen (klik op het blad)",
wMin:"Minimaliseren", wMax:"Herstellen",
lvl1:"Niveau 1", lvl2:"Niveau 2", lvl3:"Niveau 3",
instr1:"Leg de letterblokken op de goede plek. Kijk goed naar de letters!",
instr2_begin:"Zoek de <b>beginletter</b> van het woord.",
instr2_mid:"Zoek de <b>middelste letter</b> van het woord.",
instr2_end:"Zoek de <b>eindletter</b> van het woord.",
instr3:"Spel het woord bij het plaatje. De nopjes helpen je!",
card:"Kaart", cardGeneral:"Algemeen", cardStart:"Startwoorden", wordThemes:"Woordthemas", word:"Woord", welldone:"Goed zo!", next:"Volgende woord ▸",
cardDone:"Kaart klaar! 🎉", pickCard:"Kies een nieuwe kaart of een ander niveau.",
myWords:"Mijn woorden", manage:"Thema's en woorden beheren", addWord:" Toevoegen",
pickPic:"Kies een plaatje",
newTheme:"Nieuw thema", themeNamePh:"naam van het thema…", delTheme:"Thema verwijderen",
themeFolder:"Thema's en mappen",
exFixedFolder:"Deze map wordt automatisch beheerd.",
anchorLibTitle:"Ankerdiagrammen", anchorLibSave:"Bewaar dit diagram in de bibliotheek",
anchorLibOpen:"Ankerbibliotheek openen", anchorLibSaved:"Bewaard ✓",
anchorLibEmptyGrid:"Zet eerst woorden in het diagram.",
numWords:"Woorden per spel", all:"Alle", noThemes:"— nog geen thema's —",
muteAll:"Alle geluiden aan/uit", sndWidget:"Geluid van deze widget aan/uit",
catTaal:"Taal", catRekenen:"Rekenen", catWorld:"Wereldoriëntatie", catClass:"Feest & klas", catTools:"Hulpmiddelen", catMedia:"Media",
wg_madd:"Plussommen", wg_madd_d:"Optellen tot 10, 20 of 100.",
wg_msub:"Minsommen", wg_msub_d:"Aftrekken tot 10, 20 of 100.",
wg_mmul:"Keersommen", wg_mmul_d:"De tafels van 1 t/m 12 oefenen.",
wg_mdiv:"Deelsommen", wg_mdiv_d:"Delen met de tafels van 1 t/m 12.",
wg_mpct:"Procenten", wg_mpct_d:"Procenten uitrekenen met mooie getallen.",
mGood:"Goed zo!", mAnsWas:"Het goede antwoord is", mAgain:"Nog een keer ▸",
mGoed:"goed", mOf:"van", mDoneT:"Klaar!",
amode:"Blokjes slepen of typen", penMode:"Tekenen op het bord",
fabWidgets:"Widget toevoegen", fabDraw:"Tekenen & schrijven", fabMind:"Mindmap op het bord",
fabImg:"Afbeelding op het bord",
nodeColor:"Kleur", nodeFont:"Lettergrootte", lineColor:"Lijnkleur", lineStyle:"Lijnstijl",
a11y:"Hoog contrast (voor slechtzienden)",
zoomUI:"Beeldgrootte: alles groter of kleiner tonen",
lensBig:"Loep groter", lensSmall:"Loep kleiner", lensShape:"Rond of vierkant",
toolHand:"Handje: verplaatsen en bedienen",
boardsNav:"Borden en mappen", newBoard:" Bord", newFolder:" Map",
folderPh:"naam nieuwe map…", boardName:"Bord", folderName:"Map",
delBoard:"Bord verwijderen (2× tikken)",
exFolder:"Map", exRename:"Hernoem", exDelete:"Verwijder", exConfirm:"zeker?",
exSelectFirst:"Klik eerst een map of bord aan.", exNoSlash:"Een naam mag geen / bevatten.",
exExists:"Die naam bestaat al.", exEmpty:"Deze map is leeg.",
exFolderNotEmpty:"Map is niet leeg - verplaats of verwijder eerst de inhoud.",
exIntoSelf:"Een map kan niet in zichzelf.", exNoRootItems:"Borden horen in een map.",
exLastBoard:"Het laatste bord kan niet weg.",
shGlobal:"Systeem", shSchool:"School",
shPublished:"Gedeeld ✓", shCopied:"Gekopieerd naar je eigen mappen ✓",
shNoCreateHere:"Nieuw maken kan alleen in je eigen mappen - sleep het daarna hierheen om te delen.",
shReadonly:"Alleen lezen - kopieer het naar je eigen mappen om te bewerken.",
exLock:"Alleen lezen", shLockedOk:"Vergrendeld ✓", shUnlockedOk:"Ontgrendeld ✓",
exWrongBranch:"Dat hoort niet in deze map (borden en ankerdiagrammen zijn gescheiden).",
anchorLibCreateHint:"Nieuw diagram maken: gebruik 💾 op een ankerwidget.",
hudMin:"Menu inklappen/uitklappen", hudOrient:"Horizontaal of verticaal",
wFull:"Maximaliseren", wUnfull:"Terugzetten",
wZoom:"Inhoud groter of kleiner (voor slechtzienden)",
wg_flits:"Flitswoorden", wg_flits_d:"Een woord flitst kort in beeld lezen voordat het verdwijnt!",
flFlash:"⚡ Flits", flShow:"👁 Toon", flNext:"Volgende ▸", flSec:"sec", flMission:"Lees het woord voordat het verdwijnt.", flQuestion:"Kon je het woord zelf lezen?", flGotIt:"🌟 Gelukt!", flAgain:"Nog oefenen", flScore:"{score} sterren",
wg_dice:"Dobbelstenen", wg_dice_d:"Gooi 1, 2 of 3 grote dobbelstenen.",
diceRoll:"🎲 Gooi!", diceSum:"Samen", diceMission:"Gooi, tel de ogen en controleer de uitkomst.", diceHistory:"Vorige worpen",
wg_light:"Klassenstoplicht", wg_light_d:"Maak in één oogopslag duidelijk of kinderen luisteren, fluisteren of rustig mogen werken.",
lightRed:"Rood", lightRedHint:"Stop en luister", lightOrange:"Oranje", lightOrangeHint:"Fluister en rond af", lightGreen:"Groen", lightGreenHint:"Je mag rustig werken", lightStatus:"{color}: {hint}",
wg_names:"Namenkiezer", wg_names_d:"Kies willekeurig een naam uit de klas.",
namesPh:"Eén naam per regel…", namesPick:"🎯 Kies!", namesNoRep:"Zonder herhaling",
namesDone:"Iedereen is geweest! 🎉", namesReset:"Opnieuw", namesProgress:"{used} van {total} geweest",
wg_hak:"Hakken & plakken", wg_hak_d:"Woorden in klanken hakken en weer plakken, met voorlezen.",
hakChop:"✂️ Hak", hakGlue:"🔗 Plak", hakSay:"🔊 Lees voor", hakHint:"Bekijk en hoor de klankgroepen in het woord.",
wg_ball:"Ballonnenwoord", wg_ball_d:"Raad het woord letter voor letter voordat de ballonnen knappen.",
wg_anchor:"Ankerwoorden", wg_anchor_d:"Woordweb voor woordenschatonderwijs: leer woorden in samenhang rond een themawoord (het anker), met plaatjes. Elk woord verankert aan de woorden eromheen.",
fabAnchor:"Ankerdiagram (woordweb)",
wg_birthday:"Verjaardagstaart", wg_birthday_d:"Maak een persoonlijke taart met naam, leeftijd, kleuren, lagen, versiering en kaarsjes.",
birthdayName:"naam van de jarige", birthdayAge:"Leeftijd", birthdayLayers:"Lagen", birthdayCakeColor:"Taart", birthdayIcing:"Glazuur", birthdayPlate:"Bord",
birthdayCandles:"Kaarsjes", birthdayCelebrate:"🎉 Vieren!", birthdayTitle:"Er is er één jarig!", birthdayFor:"Hoera voor {name}!",
birthdaySprinkles:"Spikkels", birthdayFruit:"Fruit", birthdayStars:"Sterren", birthdayNone:"Geen versiering",
wg_numberline:"Getallenlijn", wg_numberline_d:"Oefen getalbegrip met een instelbare, interactieve getallenlijn.",
numberlineFrom:"Van", numberlineTo:"Tot", numberlineStep:"Stap", numberlineNew:"Nieuwe opdracht", numberlineTask:"Zet de stip op {number}",
numberlineCheck:"Controleren", numberlineGood:"Goed gevonden! 🌟", numberlineTry:"Kijk nog eens naar de getallenlijn.", numberlineScore:"{score} sterren · reeks {streak}",
wg_fractions:"Breukenmuur", wg_fractions_d:"Bouw breuken, vergelijk de gekleurde delen en ontdek gelijkwaardige breuken.",
wg_clock:"Oefenklok", wg_clock_d:"Leer kloktijden op vier niveaus met meerdere leerwijzen en spellen.",
wg_placevalue:"Plaatswaardetabel", wg_placevalue_d:"Ontdek de waarde van cijfers tot honderdduizend.",
wg_money:"Geldrekenen", wg_money_d:"Oefen bedragen maken, geld tellen en wisselgeld op drie niveaus.",
wg_trees:"Bomen & bladeren", wg_trees_d:"Leer achttien Nederlandse boomsoorten met vijf foto-, kenmerk- en koppeloefeningen.",
wg_birds:"Vogels herkennen", wg_birds_d:"Leer achttien Nederlandse vogels met vier foto-, kenmerk- en naamoefeningen.",
wg_plants:"Wilde planten", wg_plants_d:"Herken twaalf algemene wilde planten aan echte foto's en duidelijke veldkenmerken.",
wg_topography:"Topografie", wg_topography_d:"Oefen Nederland én de wereld: provincies, plaatsen, landsdelen, rivieren en werelddelen op nauwkeurige kaarten.",
worldActivity:"Werkvorm", worldLearn:"Leren", worldQuiz:"Quiz", worldFlash:"Flitskaarten", worldSpell:"Letterblokken", worldExercise:"Oefening", worldLevel:"Niveau", worldTreeView:"Wat zie je?", worldTreeWhole:"Hele boom", worldTreeLeaf:"Blad", worldTreeMixed:"Boom en blad",
worldSpellHint:"Sleep de letters naar de vakken en spel de naam.", worldSpellComplete:"Goed gespeld! 🌟",
worldLevel1Hint:"Niveau 1: de bekendste soorten en plekken", worldLevel2Hint:"Niveau 2: ook wat minder bekende soorten en plekken", worldLevel3Hint:"Niveau 3: alle soorten en plekken, ook de zeldzame",
worldExerciseRecognize:"Herken de soort", worldExercisePair:"Koppel boom en blad", worldExerciseClue:"Raad met kenmerken", worldExercisePhoto:"Kies de juiste foto", worldExerciseLatin:"Wetenschappelijke naam",
worldScore:"Score: {score} · reeks: {streak}", worldExploreHint:"Bekijk de kenmerken en vergelijk de soorten.", worldQuestionTree:"Welke boom zie je?", worldQuestionLeaf:"Bij welke boom hoort dit blad?", worldQuestionBird:"Welke vogel zie je?", worldQuestionPlant:"Welke wilde plant zie je?",
worldQuestionPair:"Bij welke boom horen deze boom en dit blad?", worldQuestionClue:"Welke soort hoort bij deze kenmerken?", worldQuestionLatin:"Welke soort hoort bij deze wetenschappelijke naam?", worldFindTreePhoto:"Tik op de foto van de {name}.", worldFindBirdPhoto:"Tik op de foto van de {name}.", worldFindPlantPhoto:"Tik op de foto van de {name}.",
worldCorrect:"Goed herkend! 🌟", worldTryAgain:"Kijk nog eens goed naar de kenmerken.", worldNext:"Volgende opdracht",
worldPhotoTree:"Boom", worldPhotoLeaf:"Blad of naalden", worldPhotoBird:"Vogel", worldPhotoPlant:"Wilde plant", worldPhotoCredit:"Foto: {author} · {license}", worldPhotoOption:"Afbeelding {number}",
topoMode:"Oefenen met", topoProvinces:"Provincies", topoCapitals:"Provinciehoofdsteden", topoCities:"Belangrijke steden", topoRegions:"Landsdelen", topoRivers:"Rivieren", topoContinents:"Werelddelen", topoMapLabel:"Kaart van Nederland", topoWorldMapLabel:"Kaart van de wereld",
topoExploreHint:"Bekijk de echte provinciegrenzen en vergelijk de ligging van de plaatsen.", topoExploreRegions:"Bekijk welke provincies samen Noord-, Oost-, West- en Zuid-Nederland vormen.", topoExploreRivers:"Volg de loop van de grote Nederlandse rivieren van grens tot delta.", topoExploreContinents:"Vergelijk de vorm en ligging van de zeven werelddelen.",
topoFindProvince:"Tik op de provincie {name}.", topoFindCapital:"Waar ligt provinciehoofdstad {name}?", topoFindCity:"Waar ligt {name}?", topoFindRegion:"Tik op {name}.", topoFindRiver:"Tik op de rivier {name}.", topoFindContinent:"Tik op het werelddeel {name}.", topoMapSource:"Kaart: Gust Justice · CC BY-SA 4.0", topoWorldMapSource:"Wereldkaart: Natural Earth · publiek domein",
moneyLearningStyle:"Opdracht tonen", moneyStyleAmount:"Als bedrag", moneyStyleWords:"In woorden", moneyStyleBoth:"Bedrag en woorden",
moneyGame:"Spel", moneyGameMake:"Bedrag maken", moneyGameCount:"Geld tellen", moneyGameChange:"Wisselgeld",
moneyCategory:"Koopcategorie", moneyCategoryAll:"Alle producten", moneyCategoryFood:"Eten en drinken", moneyCategorySchool:"Schoolspullen", moneyCategoryPlay:"Spelen en sport",
moneyLevel:"Niveau", moneyLevel1Hint:"Niveau 1: hele euros tot € 9", moneyLevel2Hint:"Niveau 2: tot € 10 in stappen van 5 cent", moneyLevel3Hint:"Niveau 3: tot € 50 met alle centen, munten en biljetten",
moneyScore:"Score: {score} · reeks: {streak}", moneyMakeTitle:"Maak het gevraagde bedrag", moneyCountTitle:"Hoeveel geld zie je?", moneyChangeTitle:"Bereken het wisselgeld",
moneyYourTask:"Jouw opdracht", moneyPrice:"Prijs", moneyPaid:"Betaald", moneyWallet:"Jouw geld", moneyWalletHint:"Sleep geld hierheen of tik op een munt of biljet. Sleep geplaatst geld naar buiten om het weg te halen.", moneyRemove:"Sleep naar buiten om weg te halen", moneyTray:"Munten en biljetten",
moneyBuildHint:"Sleep de cijfers naar de vakken en maak het bedrag.", moneyTypeHint:"Voer het bedrag in met een komma.", moneyIncomplete:"Maak eerst het volledige bedrag af.",
moneyCorrect:"Goed gedaan! Kies de volgende opdracht. 🌟", moneyTryAgain:"Bijna! Reken het bedrag nog eens na.", moneyNeed:"Nog {amount} nodig.", moneyTooMuch:"{amount} te veel.",
moneyClear:"Wissen", moneyNextTask:"Volgende opdracht",
wg_wordtypes:"Woordsoorten", wg_wordtypes_d:"Geef woorden kleur per woordsoort.",
wg_schedule:"Dagplanning", wg_schedule_d:"Maak een duidelijke visuele planning voor de klas.",
wg_mood:"Gevoelsmeter", wg_mood_d:"Doe een snelle, anonieme check-in met de klas.",
wg_poll:"Klaspoll", wg_poll_d:"Stem samen op maximaal zes antwoorden.",
wg_noise:"Geluidsmeter", wg_noise_d:"Meet het geluidsniveau in de klas met de microfoon - alles blijft lokaal, er wordt niets opgenomen of verstuurd.",
noiseOff:"Zet de microfoon aan om te starten.", noiseStatus_green:"Lekker rustig!", noiseStatus_yellow:"Een beetje luidruchtig…", noiseStatus_red:"Te luid! Even rustig aan.",
noiseDenied:"Kan niet bij de microfoon. Geef toestemming in de browser en probeer opnieuw.",
noiseStart:"Microfoon aan", noiseStop:"Stop", noiseZoneGreen:"Rustig tot", noiseZoneYellow:"Te luid vanaf",
noisePrivacy:"Er wordt niets opgenomen of verstuurd: het geluidsniveau blijft alleen in deze browser.",
wg_feelings:"Gevoelszones", wg_feelings_d:"Individuele check-in: kies je zone en krijg een passende tip om jezelf te helpen.",
fzZone_blue:"Blauw", fzZone_green:"Groen", fzZone_yellow:"Geel", fzZone_red:"Rood",
fzDesc_blue:"Ik voel me moe of verdrietig.", fzDesc_green:"Ik voel me kalm en klaar om te leren.", fzDesc_yellow:"Ik voel me druk, gestrest of opgewonden.", fzDesc_red:"Ik voel me boos of overprikkeld.",
fzTip_blue:"Even rustig zitten, wat water drinken of een boek pakken kan helpen.", fzTip_green:"Fijn zo! Dit is een goed moment om aan de slag te gaan.", fzTip_yellow:"Diep ademhalen of een korte pauze kan helpen om weer rustig te worden.", fzTip_red:"Ga naar de rustige plek, haal diep adem of vraag de juf of meester om hulp.",
wg_breathe:"Ademhalingsoefening", wg_breathe_d:"Adem rustig mee met de groeiende en krimpende cirkel, in drie tempo's.",
bre_in:"Adem in", bre_hold:"Houd vast", bre_out:"Adem uit", breStart:"Start", breStop:"Stop", breRounds:"{n} rondjes gedaan",
"brePattern_4-4":"4 tellen in, 4 uit", "brePattern_4-7-8":"4 in, 7 vast, 8 uit", "brePattern_4-4-4-4":"4 in, 4 vast, 4 uit, 4 vast",
wg_compliments:"Complimentenmachine", wg_compliments_d:"Trek een willekeurig compliment om iemand blij te maken.",
complimentsDraw:"Trek een compliment",
wg_avatar:"Mijn personage", wg_avatar_d:"Stel je eigen personage samen: soort, kleuren, haar en accessoires.",
stAvatar:"Mijn personage", stAvatarSub:"Stel je eigen personage samen.",
avatarAlt:"Mijn personage", avatarSpecies_dino:"Dino", avatarSpecies_monster:"Monster", avatarSpecies_animal:"Dier",
avatarBodyColor:"Lichaamskleur", avatarHairColor:"Haarkleur", avatarHairStyle:"Haarstijl",
avatarHair_none:"Geen", avatarHair_spiky:"Piekerig", avatarHair_curly:"Krullen", avatarHair_ponytail:"Staartje", avatarHair_mohawk:"Hanenkam",
avatarAccHead:"Hoofdaccessoire", avatarAccBody:"Lichaamsaccessoire",
avatarAcc_none:"Geen", avatarAcc_cap:"Petje", avatarAcc_crown:"Kroon", avatarAcc_glasses:"Bril", avatarAcc_bow:"Strik",
avatarAcc_sunglasses:"Zonnebril", avatarAcc_headphones:"Koptelefoon", avatarAcc_partyhat:"Feesthoedje",
avatarAcc_scarf:"Sjaal", avatarAcc_cape:"Cape", avatarAcc_badge:"Insigne",
avatarAcc_necklace:"Ketting", avatarAcc_backpack:"Rugtas",
avatarSave:"Opslaan", avatarSaved:"Personage opgeslagen ✓",
clock24Hour:"24-uursklok",
widgetSettings:"Instellingen", clockWelcome:"Oefenen met de klok", clockWelcomeHint:"Kies bovenaan niveau 1, 2 of 3. Kies bij Instellingen hoe de opdracht wordt getoond en start daarna.", clockSetTitle:"Zet de wijzers op de goede tijd", clockReadTitle:"Bekijk de opdracht en maak de tijd", clockChooseTitle:"Kies het juiste tijdblok", clockYourTask:"Jouw opdracht", clockLinkHands:"Grote wijzer draait mee", clockTypeHint:"Voer de tijd in als UU:MM.", clockLevel1Hint:"Niveau 1: hele en halve uren", clockLevel2Hint:"Niveau 2: stappen van vijf minuten", clockLevel3Hint:"Niveau 3: alle minuten, met optioneel 24 uur en dagdelen", clockBuildHint:"Sleep vier cijfers naar de vakken en maak UU:MM.", clockDigitGhost:"?", clockDigit:"Cijfer {digit}", clockIncomplete:"Maak eerst de volledige tijd af.", clockCorrect:"Goed gedaan! Kies de volgende opdracht. 🌟", clockTryAgain:"Bijna! Probeer het nog een keer.", clockStartTask:"Start opdracht", clockNextTask:"Volgende opdracht", clockLevel:"Niveau", clockLevelHour:"Hele uren", clockLevelQuarter:"Kwartieren", clockLevelFive:"Vijf minuten", clockLevelMinute:"Iedere minuut", clockLearningStyle:"Opdracht tonen", clockStyleAnalogue:"Analoge klok", clockStyleDigital:"Grote digitale tijd", clockStyleWords:"In woorden", clockStyleBoth:"Klok, cijfers en woorden", clockDragHint:"Sleep de wijzers om de tijd te veranderen.", clockGame:"Spel", clockGameSet:"Klok zetten", clockGameRead:"Tijd aflezen", clockGameChoose:"Kies de tijd", clockStart:"Kies Nieuwe opdracht om te beginnen", clockRead:"Welke tijd zie je? Stel je antwoord hieronder in.", clockChoose:"Welke digitale tijd hoort bij deze klok?", clockTry:"Nog niet. Het goede antwoord is {time}.", clockScore:"Score: {score} · reeks: {streak}", clockWordsHour:"{hour} uur", clockWordsQuarterPast:"kwart over {hour}", clockWordsHalf:"half {hour}", clockWordsQuarterTo:"kwart voor {hour}", clockWordsPast:"{minutes} over {hour}", clockWordsTo:"{minutes} voor {hour}", clockMorning:"in de ochtend", clockAfternoon:"in de middag", clockEvening:"in de avond", clockNight:"in de nacht", fractionRows:"Rijen", fractionLabels:"Breuken tonen", fractionHint:"Tik in een rij tot waar je de breuk wilt kleuren.", fractionSelected:"Gekozen breuk: {fraction}", fractionClear:"Leegmaken", clockTask:"Oefenopdracht", clockSet:"Zet de klok op {time}", clockHour:"Uur", clockMinute:"Minuten", placeNumber:"Getal", placeGame:"Spel", placeGameExplore:"Verkennen", placeGameBuild:"Bouwen", placeLevel:"Niveau", placeLevel1Hint:"Niveau 1: getallen tot 100 (T en E)", placeLevel2Hint:"Niveau 2: getallen tot 1000 (H, T en E)", placeLevel3Hint:"Niveau 3: getallen tot 1.000.000 (HD, TD, D, H, T en E)", placeBuildTitle:"Bouw het getal", placeYourTask:"Jouw opdracht", placeBuildHint:"Sleep de cijfers naar de vakken en bouw het getal.", placeDigitGhost:"?", placeIncomplete:"Maak eerst het hele getal af.", placeCorrect:"Goed gedaan! Kies de volgende opdracht. 🌟", placeTryAgain:"Bijna! Probeer het nog een keer.", placeNextTask:"Volgende opdracht", placeScore:"Score: {score} · reeks: {streak}", moneyMake:"Maak € {amount}", wordtypeSentence:"Zin", wordtypeHint:"Tik: blauw zelfstandig naamwoord · groen werkwoord · geel bijvoeglijk naamwoord", scheduleActivity:"activiteit…", scheduleEmpty:"Voeg de eerste activiteit toe.", scheduleCount:"{count} momenten op de planning", pollReset:"Opnieuw", pollQuestion:"Vraag", pollOptions:"Antwoorden (één per regel)", pollTotal:"{count} stemmen", moodTotal:"{count} kinderen hebben gekozen", moodVote:"Kies {face}", galleryFavorites:"Favorieten", galleryFavorite:"Favoriet maken", galleryEmpty:"Geen widgets gevonden.", gallerySearch:"Zoek widgets…", templateMath:"Rekenles", templateLanguage:"Taalles", templateDay:"Dagstart", exportBoard:"Exporteren", importBoard:"Importeren", importError:"Dit is geen geldig Teach-bordbestand.", widgetDuplicate:"Dupliceren", widgetLock:"Vastzetten", widgetUnlock:"Losmaken", widgetPreview:"Voorbeeld zonder bediening", undo:"Ongedaan maken", redo:"Opnieuw",
wg_sentence:"Zinsbouwer", wg_sentence_d:"Bouw zinnen met kleurgecodeerde zinsdelen en voeg eigen woorden toe.",
sentence_who:"Wie?", sentence_does:"Doet wat?", sentence_what:"Wat of waar?", sentenceHint:"Kies woorden om een zin te maken…",
sentenceAdd:"eigen woord of zinsdeel…", sentenceClear:"Wissen", sentenceSpeak:"🔊 Lees voor", sentenceRemove:"Verwijder woord", sentenceSurprise:"✨ Verrassingszin",
anchorPh:"thema…", anchorHint:"Woordweb: zet het themawoord in het midden en bouw eromheen · tik op een vak en typ · sleep om te schuiven",
anchorHintReadonly:"Sleep de achtergrond om te schuiven",
anchorColor:"Kleur: klik om te wisselen (verbind woorden met dezelfde kleur)",
ballWin:"Geraden! 🎉", ballLose:"Helaas! Het woord was:",
bgDots:"Stippen", toolRuler:"Liniaal", toolGeo:"Geodriehoek", toolLens:"Vergrootglas",
wg_mind:"Mindmap", wg_mind_d:"Woordweb, spin of boomdiagram om samen ideeën te ordenen.",
mmSpider:"Spin", mmTree:"Boom", mmWeb:"Woordweb", mmRoot:"Onderwerp",
mmHint:"Klik op tekst om te typen · sleep aan de rand",
wg_media:"Foto & video", wg_media_d:"Toon foto's en video's van internet, zoals YouTube of Vimeo.",
mediaPh:"Plak een link (YouTube, Vimeo, foto of video)…", mediaShow:"Tonen",
mediaEmpty:"Plak hierboven een link en klik op Tonen.",
mediaErr:"Deze link kan niet worden getoond. Probeer een YouTube-, Vimeo-, foto- of videolink, of open hem met ↗ in een nieuw tabblad.",
mediaOpen:"Open in nieuw tabblad",
wordPh:"woord (25 letters)", close:"Sluiten",
badWord:"Gebruik 2 t/m 5 letters (a-z).", needPic:"Kies eerst een plaatje.",
noWords:"Nog geen eigen woorden. Voeg er hierboven een toe!",
tabEmoji:"😀 Emoji", tabPicto:"🖼️ Picto's", tabUpload:"📷 Upload", tabLibrary:"🗂 Bibliotheek",
pictoSearch:"Zoeken", pictoPh:"zoekwoord…", pictoNone:"Niets gevonden. Probeer een ander woord.",
pictoLoad:"Zoeken…", pictoErr:"Kon picto's niet laden (internet nodig).",
pictoCredit:"Picto's: <a href='https://arasaac.org' target='_blank'>ARASAAC</a> (CC BY-NC-SA, Gobierno de Aragón). Ook leuk: <a href='https://www.sclera.be' target='_blank'>sclera.be</a> (gratis download, dan via Upload toevoegen).",
upload:"📷 Kies een afbeelding…",
upHint:"Tip: vierkant plaatje, witte of doorzichtige achtergrond, één duidelijk voorwerp, géén tekst in het plaatje.",
save:"Opslaan", cancel:"Annuleren",
imgCatalog:"Afbeeldingscatalogus", imgSearch:"Zoek op naam, map of thema…",
imgOpenLibrary:"Open afbeeldingenbibliotheek", imgLanguageHint:"Hier vind je alle vaste woordplaatjes, eigen woordkaarten en uploads bij elkaar.",
imgWordThemes:"Taalthemas",
imgFolders:"Afbeeldingen en mappen", imgUpload:"Afbeeldingen uploaden",
imgChooseFiles:"Kies één of meer afbeeldingen…", imgFolder:"Map", imgThemes:"Themas",
imgNewTheme:"Nieuw thema…", imgAddTheme:"Thema toevoegen", imgEdit:"Afbeelding bewerken", imgName:"Naam",
imgAllThemes:"Alle themas", imgAllScopes:"Alle bronnen", imgAllFolders:"Alle mappen", imgFolderRoot:"Hoofdmap",
imgScopeGlobal:"Systeem", imgScopeSchool:"School", imgScopePersonal:"Mijn afbeeldingen",
imgQuotaUsed:"{used} gebruikt", imgQuotaOf:"limiet {limit}", imgSchoolQuota:"school: {used} van {limit}",
imgUnlimited:"Onbeperkt", imgNoResults:"Geen afbeeldingen gevonden.", imgNoManage:"Je mag dit onderdeel niet beheren.",
imgLoginRequired:"Log in om de afbeeldingscatalogus te gebruiken.", imgInvalidFile:"Kies een geldige afbeelding.",
imgFileTooLarge:"Een afbeelding mag maximaal 50 MB groot zijn.", imgUploadFailed:"Uploaden is mislukt.",
imgUploading:"Afbeeldingen uploaden…", imgUploaded:"Afbeeldingen toegevoegd ✓", imgUntitled:"Afbeelding",
imgLoading:"Afbeeldingen laden…", imgSaved:"Afbeelding opgeslagen ✓", imgDeleted:"Afbeelding verwijderd ✓",
imgThemeAdded:"Thema toegevoegd ✓", imgStorageAdmin:"Opslaglimieten afbeeldingen",
imgQuotaUsage:"{used} gebruikt · {limit}", imgQuotaDefault:"Standaard", imgQuotaCustom:"Aangepast",
imgPickSchool:"— kies een school —", imgSystemScope:"Systeem (zonder school)",
imgQuotaDefaultOf:"Standaard ({limit})",
imgQuotaMb:"limiet in MB", imgQuotaInvalid:"Vul een geldige limiet in.", imgSchools:"Scholen",
imgUsers:"Gebruikers", imgSearchUsers:"Zoek gebruiker…",
cats:["Dieren","Eten","Natuur","Vervoer","Spullen","Lichaam"],
notesPh:"Schrijf hier…", notesColor:"Kleur briefje", notesLarge:"Grote letters", notesCount:"{count} tekens",
start:"Start", pause:"Pauze", reset:"Reset", min:"min",
tmDigits:"Cijfers", tmVisual:"Kijkklok", tmBoth:"Allebei", tmSound:"Geluid aan/uit",
tmHint:"Sleep aan de rode schijf om de tijd in te stellen.",
login:"Inloggen", loginBtn:"Inloggen", registerBtn:"Account maken", logout:"Uitloggen",
settingsTitle:"Instellingen", tabLogin:"Inloggen", tabLinkCode:"Koppelcode", busy:"Bezig…",
stAccount:"Account", stPassword:"Wachtwoord", stSession:"Sessie",
stSearchPh:"Zoeken in instellingen…", stNoResults:"Niets gevonden.",
stOpenSection:"open dit onderdeel",
stGroupPersonal:"Persoonlijk", stGroupSchool:"School", stGroupSystem:"Systeem",
stAccountSub:"Je gegevens, wachtwoord en reservekopieën.",
stViewSub:"Weergave op dit apparaat: rolvoorbeeld, thema, taal en menubalk.",
wikiTitle:"Curriculumwiki",
wikiSub:"Nieuwe SLO-kerndoelen voor het primair onderwijs, per leergebied.",
wikiKicker:"Primair onderwijs · juli 2026",
wikiIntro:"Bekijk de 40 nieuwe kerndoelen, hun onderdelen en de huidige aansluiting van Teach.",
wikiAreas:"leergebieden", wikiGoals:"kerndoelen", wikiGoal:"Kerndoel",
wikiGoalsRange:"Kerndoelen {range}", wikiParts:"Onderdelen",
wikiStatusTitle:"Status van de doelen",
wikiStatusText:"Dit zijn definitieve conceptkerndoelen. De wettelijke invoering wordt gefaseerd vastgelegd; de officiële SLO-publicatie blijft leidend.",
wikiOfficialSource:"Open officiële SLO-publicatie",
wikiOfficialArea:"Bekijk {area} in de officiële publicatie",
wikiTuleTitle:"TULE en de nieuwe leerlijnen",
wikiYearProfiles:"Leerjaarprofielen taal en rekenen",
wikiYearProfilesText:"Praktische Teach-uitwerking voor widgetopdrachten per groep. Gebaseerd op de kerndoelen 2026 en TULE; geen officiële SLO-jaarnormen.",
wikiYear:"Groep {n}", wikiProfileGoals:"Leerdoelen",
wikiTuleText:"TULE werkt de kerndoelen uit 2006 voorbeeldmatig uit. Nieuwe SLO-leerlijnen bij de doelen van 2026 zijn nog in ontwikkeling. Een niveau-indeling in Teach is daarom geen officiële SLO-leerlijn.",
wikiTuleSource:"Open TULE bij SLO", wikiProgressionsSource:"Volg de ontwikkeling van nieuwe leerlijnen",
wikiSearchLabel:"Doorzoek de curriculumwiki",
wikiSearchPlaceholder:"Zoek op doel, domein, onderwerp of widget…",
wikiAreaFilter:"Filter op leergebied", wikiAllAreas:"Alle leergebieden",
wikiCoveragePartial:"Deels in Teach", wikiCoverageSupport:"Lesondersteuning", wikiCoverageOpen:"Nog open",
wikiWidgets:"Huidige widgets", wikiNoResults:"Geen kerndoelen gevonden.",
wikiCarefulTitle:"Zorgvuldig gebruiken.",
wikiCarefulText:"Een widget die één onderdeel oefent, dekt niet automatisch het volledige kerndoel. School- en ervaringsdoelen vragen andere vormen van bewijs dan een goed/fout-score.",
widgetCurriculumInfo:"Kerndoelinformatie", widgetCurriculumIntro:"Deze widget draagt bij aan de onderstaande kerndoelen.",
widgetCurriculumNone:"Voor deze ondersteunende widget is geen directe koppeling met een kerndoel vastgesteld.",
widgetCurriculumWiki:"Bekijk dit kerndoel in de curriculumwiki", widgetCurriculumAll:"Open de volledige curriculumwiki",
widgetCurriculumCareful:"Een widget ondersteunt onderdelen van een kerndoel en vormt op zichzelf geen bewijs dat het volledige doel is behaald.",
wikiLanguageNote:"The official SLO goals and these implementation summaries are shown in Dutch.",
stUsersSub:"Accounts beheren: leerlingen, staf, ouders en aanmeldingen.",
stKlassenSub:"Klassen aanmaken, koppelen en beheren.",
stToewijzSub:"Bepaal wat een klas of leerling te zien krijgt.",
stVoortgangSub:"Volg per leerling de resultaten in de tijd.",
stScholenSub:"Scholen en systeemmanagers, over alle scholen heen.",
amSchoolMenu:"School", stSchoolMenuSub:"Scholen, klassen en gebruikers beheren.",
stOpslag:"Opslag", stOpslagSub:"Afbeeldingencatalogus en opslagruimte per school.",
stInhoud:"Inhoud", stInhoudSub:"Welkomstwoord, meldingen en widget-uitleg - direct aanpasbaar, zonder release.",
cmsWelcomeH:"Welkomstwoord (inlogscherm)",
cmsAnnounceH:"Melding voor alle gebruikers",
cmsWidgetH:"Widget-uitleg (bibliotheek)",
cmsActive:"Melding tonen", cmsLevelInfo:"informatie", cmsLevelWarn:"belangrijk",
cmsSaved:"Opgeslagen ✓", cmsNl:"Nederlands", cmsEn:"English",
cmsWidgetPick:"— kies een widget… —",
cmsWidgetHint:"Leeg laten = de standaarduitleg.",
cmsShortcutsH:"Systeembrede inhoud",
cmsShortcutsHint:"Ankerdiagrammen en borden deel je via de gedeelde bibliotheek (map 🌐 Systeem in de verkenner); afbeeldingen via de catalogus. Iedereen kan ze daar kopiëren.",
cmsOpenShared:"🌐 Borden & ankerdiagrammen", cmsOpenImages:"🖼 Afbeeldingencatalogus",
stBackups:"Reservekopieën", backupsNote:"Automatisch bewaarde eerdere versies van je borden en woorden.",
backupsEmpty:"Nog geen reservekopieën.", backupRestore:"herstel", backupRestoreConfirm:"zeker?",
backupRestored:"Versie hersteld ✓",
stView:"Weergave", vwTheme:"Thema", vwLight:"Licht", vwDark:"Donker", vwAuto:"Auto",
vwContrast:"Hoog contrast", vwOn:"Aan", vwOff:"Uit", vwLang:"Taal",
vwTbar:"Menubalk", vwTop:"Boven", vwLeft:"Links", vwRight:"Rechts",
vwMotion:"Animaties", vwGlass:"Glas-effect",
vwRole:"Weergave als", vwRoleSystem:"Systeem", vwRoleAdmin:"Schoolbeheerder", vwRoleTeacher:"Leerkracht", vwRoleParent:"Ouder", vwRolePupil:"Leerling",
vwRoleNote:"Alleen de interface verandert. Je blijft ingelogd als systeemmanager; serverrechten en gegevens wijzigen niet.",
vwPreview:"Voorbeeldweergave", vwPreviewReset:"Terug naar systeem",
vwParentPreview:"Voorbeeld van het ouderportaal. Echte kindgegevens en acties zijn in deze weergave uitgeschakeld.",
vwPupilPreview:"Voorbeeld van de leerlingomgeving met de taal- en rekenwidgets van het huidige bord. Bediening en voortgang zijn uitgeschakeld.",
vwPupilAddWidget:"Widget toevoegen", vwPupilChooseWidget:"Kies een leerlingwidget",
vwPupilAddWidgetNote:"Alleen taal- en rekenwidgets die in de leerlingomgeving werken worden getoond.",
vwNote:"Deze voorkeuren gelden voor dit apparaat.",
pwChanged:"Wachtwoord gewijzigd ✓", pwCurrentPh:"huidig wachtwoord",
namePh:"naam", passPh:"wachtwoord",
guestNote:"Zonder inloggen wordt niets bewaard. Log in met je schoolaccount om alles op de server te bewaren.",
storedNote:"Je werk staat op de school-server en is op elk digibord beschikbaar.",
errLogin:"Onbekende naam of verkeerd wachtwoord.",
schoolSel:"— kies je school (leeg = systeem) —",
linkIntro:"Nieuw account? Activeer het met je koppelcode:",
linkBtn:"Account activeren", pwChange:"Wijzig", pwNewPh:"nieuw wachtwoord (min. 8, met cijfer)",
pwConfirmPh:"herhaal wachtwoord", pwShow:"👁 Toon", pwHide:"🙈 Verberg",
errPwMatch:"Wachtwoorden komen niet overeen.",
codePh:"koppelcode",
roleSuper:"Systeemmanager", roleAdmin:"Schoolbeheerder",
roleTeacher:"Groepsleiding", rolePupil:"Leerling", roleParent:"Ouder",
tabParentReg:"Ouders",
parentRegIntro:"Maak een ouderaccount en volg je kind - ook als de school (nog) niet meedoet:",
parentRegBtn:"Account maken",
parentTitle:"Mijn kinderen",
parentNoChildren:"Nog geen kinderen gekoppeld.",
parentAddChild:" Kind", parentChildNamePh:"naam van je kind…", parentAddBtn:"Toevoegen",
parentPwNote:"Bewaar dit wachtwoord: je kind logt ermee in (schoolveld leeg laten).",
parentLinkTitle:" Ouder-code", parentLinkPh:"ouder-code van school…", parentLinkBtn:"Koppel",
parentLinkNote:"Vraag de school om een ouder-code voor je kind.",
parentHome:"thuis",
parentApply:"🏫 Aanmelden", parentApplied:"aangemeld bij", parentApplyBtn:"Meld aan",
parentSchoolPh:"zoek je school…",
parentUnlink:"Ontkoppel",
parentUnlinkWarn:"Verwijdert alleen de koppeling; het account van je kind blijft bestaan.",
parentProgress:"📈 Voortgang", parentProgressEmpty:"Nog geen voortgang.",
parentLevel:"🎯 Leerniveau", parentLevelTaal:"Taal", parentLevelRekenen:"Rekenen",
parentLevelYear:"Groep", parentLevelPath:"Uitwerking", parentLevelSave:"Opslaan",
parentLevelSaved:"Leerniveau opgeslagen.",
dtLen:"Lengte", dtContrast:"Hoog contrast",
dtLock:"Vastzetten: kan niet meer verschuiven of draaien, alleen nog langs tekenen",
dtMode:"Basis: grote hoofdhoeken en hele centimeters. Volledig: graden per 2° met dubbele schaal, millimeters en evenwijdige hulplijnen.",
dtModeBasic:"basis", dtModeFull:"volledig",
pupilEmpty:"Je leerkracht heeft nog niets voor je klaargezet.",
pupilWatching:"meekijken",
amUsers:"Gebruikers", amClasses:"Klassen", amSchools:"Scholen", amOverarching:"Systeem",
amPickSchoolHint:"Kies hierboven eerst een school.", amPickSchool:"Kies een school",
amPupils:"Leerlingen", amPupilOne:"Leerling", amEdit:"Bewerken",
amAdmins:"Schoolbeheerders", amAdminOne:"Schoolbeheerder",
amClassAdd:"— voeg toe aan klas… —", amClassRemove:"Uit deze klas halen",
amSure:"weet je het zeker?", amDelete:"verwijder",
amFieldName:"Naam", amFieldRole:"Functie", amFieldClass:"Klas", amFieldClasses:"Klassen",
amFieldPw:"Wachtwoord", amFieldActions:"Acties", amPwTypePh:"typ een wachtwoord…",
amNewPwWarn:"Maakt een nieuw wachtwoord aan. Het oude wachtwoord werkt daarna niet meer en de gebruiker wordt overal uitgelogd.",
amNewCodeWarn:"Maakt een nieuwe koppelcode aan. Het huidige wachtwoord vervalt direct; de gebruiker moet opnieuw activeren.",
amDeleteWarn:"Verwijdert dit account definitief, inclusief borden en woorden.",
amResultPw:"Nieuw wachtwoord", amResultCode:"Nieuwe koppelcode",
amColStatus:"Status", amNeverLoggedIn:"nog niet ingelogd", amFieldSchool:"School", amStatusActive:"actief",
amNoSchool:"— geen school —", amNoSchoolGroup:"Zonder school", amShowMore:"meer tonen",
amMoveToSchool:"— verplaats naar school… —", amFieldMoveSchool:"Verplaats naar", amPickSchoolFirst:"Kies hierboven eerst een school om de klassen te zien of aan te maken.",
amNoClassesHere:"Deze school heeft nog geen klassen. Bestaande klassen kunnen bij een andere school horen — kies die school hierboven, of maak hieronder een nieuwe klas.",
amLoadError:"Laden mislukt",
amDetailEmpty:"Selecteer links een gebruiker om te bewerken, of maak een nieuwe aan.",
amNewUser:"Nieuwe gebruiker", amRequestOne:"Aanmelding", amReqNew:"nieuw",
amParents:"Ouders",
amParentCode:"Ouder-code", amResultParentCode:"Ouder-code",
amParentCodeWarn:"Maakt een eenmalige ouder-code aan; een eerdere code vervalt.",
amLinkRequests:"Aanmeldingen", amRequestVia:"via",
amAdopt:"Adopteer", amAdoptUserPh:"andere gebruikersnaam (bij conflict)…",
amMatchPick:"— koppel aan bestaande leerling… —", amMatchBtn:"Koppel",
amReject:"Afwijzen", amRejectWarn:"Wijst de aanmelding af; de ouder ziet het verzoek verdwijnen.",
amSchoolNamePh:"naam van de school…", amNewSchoolConfirm:"Aanmaken:",
amDeleteSchoolWarn:"Verwijdert deze school met alle klassen en gebruikers. Dit kan niet ongedaan worden gemaakt.",
amClassNamePh:"naam van de klas…",
amDeleteClassWarn:"Verwijdert deze klas; leerlingen blijven bestaan maar staan daarna zonder klas.",
amNewSchool:" School", amNewClass:" Klas", amNewTeacher:" Groepsleiding",
amNewAdmin:" Schoolbeheerder", amNewSuper:" Systeemmanager",
amNewPupil:" Leerling", amUserPh:"gebruikersnaam…", amNamePh:"naam…",
amSearchPh:"zoek op naam…",
amCode:"koppelcode", amNewCode:"nieuwe code", amPw:"wachtwoord",
amNoClass:"— geen klas —", amPending:"nog niet geactiveerd",
amCouple:"koppel leerkracht", amAllSchools:"— alle scholen —", amSuperGroup:"Systeem",
amBack:"Terug", amStaff:"Staf", amSupers:"Systeemmanagers",
amUnassigned:"Niet ingedeeld", amNoResults:"Geen resultaten",
amCreated:"Aangemaakt! Geef deze koppelcode door:",
amPupilMade:"Leerling aangemaakt. Wachtwoord:",
amPwHidden:"al gebruikt", amGenerate:"genereer wachtwoord",
amNewPw:"nieuw wachtwoord", amPwSet:"Wachtwoord ingesteld:",
amPwOptional:"wachtwoord (leeg = koppelcode)",
amOwnPassword:"Eigen wachtwoord ter bevestiging", amOwnPasswordPh:"jouw huidige wachtwoord…",
amAssignments:"Toewijzingen", amAssignClasses:"Per klas (standaard)", amAssignPupils:"Per leerling (uitzondering)",
amPickPupil:"— kies een leerling —", amNoAssignment:"— geen toewijzing —",
amAssignedAs:"Toegewezen:", amAssign:"Toewijzen", amAssignSaved:"Toewijzing opgeslagen.",
amUnlink:"Loskoppelen",
amWholeBoard:"— heel bord —", amModeWatch:"kijken", amModeWork:"werken",
cast:"Cast", castTitle:"Cast bord", castWhat:"Wat cast je?",
castClasses:"Klassen", castPupils:"Leerlingen",
castNoClasses:"Geen klassen in deze school.", castNoPupils:"Geen leerlingen in deze school.",
castActive:"✓ Actief",
amProgress:"Voortgang", amProgressEmpty:"Nog geen voortgang voor deze leerling.",
amProgressLast14Days:"Laatste 14 dagen", amProgressMistakes:"Recente fouten",
amProgressTimeline:"Volledige tijdlijn",
amProgressGiven:"Gegeven: {answer}", amProgressExpected:"Juist: {answer}",
amExportClass:"Exporteren per klas", amExportCsv:"Exporteer als CSV", amPickClassHint:"Kies eerst een klas.",
amExportColPupil:"Leerling", amExportColWidget:"Widget", amExportColAttempts:"Pogingen",
amExportColCorrect:"Goed", amExportColStars:"Sterren", amExportColDate:"Datum",
amExportColUsername:"Gebruikersnaam",
amExportDone:"Export gedownload ✓",
amLevels:"Klassenmanagement", amLevelsSub:"Stel per vakgebied een niveaugroep en niveau in.",
amLevelYearGroup:"Groep", amLevelLevel:"Niveau", amYearGroupN:"Groep {n}",
amLevelClassDefault:"Klas-standaard", amLevelPupils:"Leerlingen",
amLevelInherited:"erft van klas", amLevelNone:"nog niet ingesteld",
amLevelReset:"terug naar klas-standaard", amLevelSaved:"Opgeslagen ✓",
amSubject_taal:"Taal", amSubject_rekenen:"Rekenen", amSubject_world:"Wereldoriëntatie",
amConfirmDel:"Weet je zeker dat je deze gebruiker wilt verwijderen?",
amCopy:"kopieer", amCopyAll:"kopieer alles",
amResetClass:"reset wachtwoorden klas", amResetSchool:"reset alle leerlingwachtwoorden",
amConfirmReset:"Alle leerlingwachtwoorden van deze klas vernieuwen?",
amConfirmResetSchool:"Alle leerlingwachtwoorden van deze hele school vernieuwen? Dit kan niet ongedaan worden gemaakt.",
amResetNone:"Geen leerlingen om te resetten.",
errExists:"Deze naam bestaat al.",
errName:"Naam: 220 letters of cijfers.",
errPass:"Wachtwoord: minimaal 4 tekens.",
errCrypto:"Inloggen werkt niet in deze browser.",
hello:"Hoi", boardSaved:"Opgeslagen ✓", loginFirst:"Eerst inloggen",
saving:"Opslaan…", saveFailed:"⚠ Niet opgeslagen",
saveConflict:"⚠ Elders bewerkt", saveConflictHint:"In een ander tabblad bewerkt. Klik om de nieuwste versie te laden (jouw niet-opgeslagen wijzigingen hier vervallen)."
},
en:{
addWidget:"Widget", chooseWidget:"Choose a widget", saveBoard:"Save board",
emptyBoard:"The board is empty. Click “Widget” to get started.",
wg_letters:"Letter blocks", wg_letters_d:"Learn to read and spell with letter blocks 3 levels, like Ik leer lezen.",
wg_notes:"Notes", wg_notes_d:"A sheet to write on.",
wg_timer:"Timer", wg_timer_d:"Countdown timer with a visual clock for class.",
wg_write:"Writing pad", wg_write_d:"Writing and maths: pen, eraser and paper with handwriting lines or maths squares.",
bgBlank:"Blank sheet", bgLines:"Lines", bgWrite:"Handwriting lines",
bgGridS:"Small maths squares", bgGridB:"Big maths squares",
penSize:"Thickness", penColor:"Colour", penExtras:"Tools", recentColors:"Recently used", paletteColors:"Colours", customColor:"Custom colour", eraser:"Eraser", clearAll:"Clear all",
toolPen:"Pencil (soft, even line)", toolFountain:"Fountain pen (calligraphy: thick downstroke, thin upstroke)",
toolMark:"Highlighter", toolText:"Type text (click the sheet)",
wMin:"Minimize", wMax:"Restore",
lvl1:"Level 1", lvl2:"Level 2", lvl3:"Level 3",
instr1:"Place the letter blocks in the right spot. Look closely at the letters!",
instr2_begin:"Find the <b>beginning letter</b> of the word.",
instr2_mid:"Find the <b>middle letter</b> of the word.",
instr2_end:"Find the <b>ending letter</b> of the word.",
instr3:"Spell the word for the picture. The little studs will help you!",
card:"Card", cardGeneral:"General", cardStart:"Starter words", wordThemes:"Word themes", word:"Word", welldone:"Well done!", next:"Next word ▸",
cardDone:"Card complete! 🎉", pickCard:"Pick a new card or another level.",
myWords:"My words", manage:"Manage themes and words", addWord:" Add",
pickPic:"Choose a picture",
newTheme:"New theme", themeNamePh:"name of the theme…", delTheme:"Delete theme",
themeFolder:"Themes and folders",
exFixedFolder:"This folder is managed automatically.",
anchorLibTitle:"Anchor diagrams", anchorLibSave:"Save this diagram to the library",
anchorLibOpen:"Open anchor library", anchorLibSaved:"Saved ✓",
anchorLibEmptyGrid:"Put words in the diagram first.",
numWords:"Words per game", all:"All", noThemes:"— no themes yet —",
muteAll:"All sounds on/off", sndWidget:"This widget's sound on/off",
catTaal:"Language", catRekenen:"Maths", catWorld:"World studies", catClass:"Celebration & class", catTools:"Tools", catMedia:"Media",
wg_madd:"Addition", wg_madd_d:"Adding up to 10, 20 or 100.",
wg_msub:"Subtraction", wg_msub_d:"Subtracting up to 10, 20 or 100.",
wg_mmul:"Multiplication", wg_mmul_d:"Practise the times tables 112.",
wg_mdiv:"Division", wg_mdiv_d:"Dividing with the tables 112.",
wg_mpct:"Percentages", wg_mpct_d:"Working out percentages with friendly numbers.",
mGood:"Well done!", mAnsWas:"The right answer is", mAgain:"Again ▸",
mGoed:"correct", mOf:"of", mDoneT:"Done!",
amode:"Drag blocks or type", penMode:"Draw on the board",
fabWidgets:"Add widget", fabDraw:"Draw & write", fabMind:"Mindmap on the board",
fabImg:"Picture on the board",
nodeColor:"Colour", nodeFont:"Font size", lineColor:"Line colour", lineStyle:"Line style",
a11y:"High contrast (for low vision)",
zoomUI:"Display size: show everything bigger or smaller",
lensBig:"Larger magnifier", lensSmall:"Smaller magnifier", lensShape:"Round or square",
toolHand:"Hand: move and interact",
boardsNav:"Boards and folders", newBoard:" Board", newFolder:" Folder",
folderPh:"name of new folder…", boardName:"Board", folderName:"Folder",
delBoard:"Delete board (tap twice)",
exFolder:"Folder", exRename:"Rename", exDelete:"Delete", exConfirm:"sure?",
exSelectFirst:"Select a folder or board first.", exNoSlash:"A name cannot contain /.",
exExists:"That name already exists.", exEmpty:"This folder is empty.",
exFolderNotEmpty:"Folder is not empty - move or delete its contents first.",
exIntoSelf:"A folder cannot go inside itself.", exNoRootItems:"Boards belong in a folder.",
exLastBoard:"The last board cannot be deleted.",
shGlobal:"System", shSchool:"School",
shPublished:"Shared ✓", shCopied:"Copied to your own folders ✓",
shNoCreateHere:"Create it in your own folders first - then drag it here to share.",
shReadonly:"Read-only - copy it to your own folders to edit.",
exLock:"Read-only", shLockedOk:"Locked ✓", shUnlockedOk:"Unlocked ✓",
exWrongBranch:"That doesn't belong in this folder (boards and anchor diagrams are separate).",
anchorLibCreateHint:"To create a new diagram: use 💾 on an anchor widget.",
hudMin:"Collapse/expand menu", hudOrient:"Horizontal or vertical",
wFull:"Maximize", wUnfull:"Restore",
wZoom:"Content bigger or smaller (low vision)",
wg_flits:"Flash words", wg_flits_d:"A word flashes briefly read it before it disappears!",
flFlash:"⚡ Flash", flShow:"👁 Show", flNext:"Next ▸", flSec:"sec", flMission:"Read the word before it disappears.", flQuestion:"Could you read the word by yourself?", flGotIt:"🌟 Got it!", flAgain:"Practise again", flScore:"{score} stars",
wg_dice:"Dice", wg_dice_d:"Roll 1, 2 or 3 big dice.",
diceRoll:"🎲 Roll!", diceSum:"Total", diceMission:"Roll, count the dots and check the total.", diceHistory:"Previous rolls",
wg_light:"Class traffic light", wg_light_d:"Show at a glance whether children listen, whisper or work quietly.",
lightRed:"Red", lightRedHint:"Stop and listen", lightOrange:"Amber", lightOrangeHint:"Whisper and finish", lightGreen:"Green", lightGreenHint:"You may work quietly", lightStatus:"{color}: {hint}",
wg_names:"Name picker", wg_names_d:"Pick a random name from the class.",
namesPh:"One name per line…", namesPick:"🎯 Pick!", namesNoRep:"No repeats",
namesDone:"Everyone has had a turn! 🎉", namesReset:"Start over", namesProgress:"{used} of {total} picked",
wg_hak:"Chop & blend", wg_hak_d:"Chop words into sounds and blend them back, with read-aloud.",
hakChop:"✂️ Chop", hakGlue:"🔗 Blend", hakSay:"🔊 Read aloud", hakHint:"See and hear the sound groups in the word.",
wg_ball:"Balloon word", wg_ball_d:"Guess the word letter by letter before the balloons pop.",
wg_anchor:"Anchor words", wg_anchor_d:"Word web for vocabulary teaching: learn words in meaningful clusters around a theme word (the anchor), with pictures. Every word anchors to its neighbours.",
fabAnchor:"Anchor diagram (word web)",
wg_birthday:"Birthday cake", wg_birthday_d:"Create a personal cake with a name, age, colours, layers, decorations and candles.",
birthdayName:"birthday person's name", birthdayAge:"Age", birthdayLayers:"Layers", birthdayCakeColor:"Cake", birthdayIcing:"Icing", birthdayPlate:"Plate",
birthdayCandles:"Candles", birthdayCelebrate:"🎉 Celebrate!", birthdayTitle:"Someone has a birthday!", birthdayFor:"Hooray for {name}!",
birthdaySprinkles:"Sprinkles", birthdayFruit:"Fruit", birthdayStars:"Stars", birthdayNone:"No decoration",
wg_numberline:"Number line", wg_numberline_d:"Practise number sense with an adjustable, interactive number line.",
numberlineFrom:"From", numberlineTo:"To", numberlineStep:"Step", numberlineNew:"New task", numberlineTask:"Move the marker to {number}",
numberlineCheck:"Check", numberlineGood:"Well done! 🌟", numberlineTry:"Take another look at the number line.", numberlineScore:"{score} stars · streak {streak}",
wg_fractions:"Fraction wall", wg_fractions_d:"Build fractions, compare coloured parts and discover equivalent fractions.",
wg_clock:"Practice clock", wg_clock_d:"Learn time at four levels with multiple learning styles and games.",
wg_placevalue:"Place value", wg_placevalue_d:"Explore digit values up to hundred thousands.",
wg_money:"Money", wg_money_d:"Practise making amounts, counting money and change at three levels.",
wg_trees:"Trees & leaves", wg_trees_d:"Learn eighteen Dutch tree species with five photo, feature and matching exercises.",
wg_birds:"Bird identification", wg_birds_d:"Learn eighteen Dutch birds with four photo, feature and naming exercises.",
wg_plants:"Wild plants", wg_plants_d:"Recognise twelve common wild plants from real photographs and clear field marks.",
wg_topography:"Topography", wg_topography_d:"Practise the Netherlands and the world: provinces, places, regions, rivers and continents on accurate maps.",
worldActivity:"Activity", worldLearn:"Learn", worldQuiz:"Quiz", worldFlash:"Flashcards", worldSpell:"Letter blocks", worldExercise:"Exercise", worldLevel:"Level", worldTreeView:"What do you see?", worldTreeWhole:"Whole tree", worldTreeLeaf:"Leaf", worldTreeMixed:"Tree and leaf",
worldSpellHint:"Drag the letters into the slots to spell the name.", worldSpellComplete:"Well spelled! 🌟",
worldLevel1Hint:"Level 1: the most familiar species and places", worldLevel2Hint:"Level 2: also some less familiar species and places", worldLevel3Hint:"Level 3: all species and places, including the rare ones",
worldExerciseRecognize:"Recognise the species", worldExercisePair:"Match tree and leaf", worldExerciseClue:"Guess from features", worldExercisePhoto:"Choose the correct photo", worldExerciseLatin:"Scientific name",
worldScore:"Score: {score} · streak: {streak}", worldExploreHint:"Study the features and compare the species.", worldQuestionTree:"Which tree do you see?", worldQuestionLeaf:"Which tree does this leaf belong to?", worldQuestionBird:"Which bird do you see?", worldQuestionPlant:"Which wild plant do you see?",
worldQuestionPair:"Which species do this tree and leaf belong to?", worldQuestionClue:"Which species matches these features?", worldQuestionLatin:"Which species matches this scientific name?", worldFindTreePhoto:"Tap the photograph of the {name}.", worldFindBirdPhoto:"Tap the photograph of the {name}.", worldFindPlantPhoto:"Tap the photograph of the {name}.",
worldCorrect:"Well recognised! 🌟", worldTryAgain:"Take another close look at the features.", worldNext:"Next task",
worldPhotoTree:"Tree", worldPhotoLeaf:"Leaf or needles", worldPhotoBird:"Bird", worldPhotoPlant:"Wild plant", worldPhotoCredit:"Photo: {author} · {license}", worldPhotoOption:"Image {number}",
topoMode:"Practise", topoProvinces:"Provinces", topoCapitals:"Provincial capitals", topoCities:"Important cities", topoRegions:"Regions", topoRivers:"Rivers", topoContinents:"Continents", topoMapLabel:"Map of the Netherlands", topoWorldMapLabel:"Map of the world",
topoExploreHint:"Study the real provincial boundaries and compare the locations of the places.", topoExploreRegions:"Study which provinces make up the northern, eastern, western and southern Netherlands.", topoExploreRivers:"Follow the major Dutch rivers from the border to the delta.", topoExploreContinents:"Compare the shapes and locations of the seven continents.",
topoFindProvince:"Tap the province {name}.", topoFindCapital:"Where is the provincial capital {name}?", topoFindCity:"Where is {name}?", topoFindRegion:"Tap {name}.", topoFindRiver:"Tap the river {name}.", topoFindContinent:"Tap the continent {name}.", topoMapSource:"Map: Gust Justice · CC BY-SA 4.0", topoWorldMapSource:"World map: Natural Earth · public domain",
moneyLearningStyle:"Show task as", moneyStyleAmount:"As an amount", moneyStyleWords:"In words", moneyStyleBoth:"Amount and words",
moneyGame:"Game", moneyGameMake:"Make an amount", moneyGameCount:"Count money", moneyGameChange:"Change",
moneyCategory:"Shopping category", moneyCategoryAll:"All products", moneyCategoryFood:"Food and drinks", moneyCategorySchool:"School supplies", moneyCategoryPlay:"Play and sports",
moneyLevel:"Level", moneyLevel1Hint:"Level 1: whole euros up to €9", moneyLevel2Hint:"Level 2: up to €10 in five-cent steps", moneyLevel3Hint:"Level 3: up to €50 with all cents, coins and notes",
moneyScore:"Score: {score} · streak: {streak}", moneyMakeTitle:"Make the requested amount", moneyCountTitle:"How much money do you see?", moneyChangeTitle:"Work out the change",
moneyYourTask:"Your task", moneyPrice:"Price", moneyPaid:"Paid", moneyWallet:"Your money", moneyWalletHint:"Drag money here or tap a coin or note. Drag placed money outside to remove it.", moneyRemove:"Drag outside to remove", moneyTray:"Coins and notes",
moneyBuildHint:"Drag the digits into the slots to make the amount.", moneyTypeHint:"Enter the amount with a decimal point.", moneyIncomplete:"Complete the full amount first.",
moneyCorrect:"Well done! Choose the next task. 🌟", moneyTryAgain:"Almost! Check the amount again.", moneyNeed:"You still need {amount}.", moneyTooMuch:"{amount} too much.",
moneyClear:"Clear", moneyNextTask:"Next task",
wg_wordtypes:"Word classes", wg_wordtypes_d:"Colour words by their word class.",
wg_schedule:"Day schedule", wg_schedule_d:"Create a clear visual class schedule.",
wg_mood:"Mood check-in", wg_mood_d:"Run a quick anonymous class check-in.",
wg_poll:"Class poll", wg_poll_d:"Vote together on up to six answers.",
wg_noise:"Noise meter", wg_noise_d:"Measure the classroom noise level with the microphone - everything stays local, nothing is recorded or sent anywhere.",
noiseOff:"Turn on the microphone to start.", noiseStatus_green:"Nice and quiet!", noiseStatus_yellow:"A bit noisy…", noiseStatus_red:"Too loud! Let's settle down.",
noiseDenied:"Can't reach the microphone. Allow access in your browser and try again.",
noiseStart:"Microphone on", noiseStop:"Stop", noiseZoneGreen:"Quiet up to", noiseZoneYellow:"Too loud from",
noisePrivacy:"Nothing is recorded or sent anywhere: the noise level stays in this browser only.",
wg_feelings:"Feelings zones", wg_feelings_d:"Individual check-in: pick your zone and get a matching tip to help yourself.",
fzZone_blue:"Blue", fzZone_green:"Green", fzZone_yellow:"Yellow", fzZone_red:"Red",
fzDesc_blue:"I feel tired or sad.", fzDesc_green:"I feel calm and ready to learn.", fzDesc_yellow:"I feel restless, stressed or excited.", fzDesc_red:"I feel angry or overwhelmed.",
fzTip_blue:"Sitting quietly, drinking some water, or grabbing a book can help.", fzTip_green:"Great! This is a good moment to get started.", fzTip_yellow:"Taking a deep breath or a short break can help you feel calm again.", fzTip_red:"Go to the quiet spot, take a deep breath, or ask your teacher for help.",
wg_breathe:"Breathing exercise", wg_breathe_d:"Breathe along with the growing and shrinking circle, in three paces.",
bre_in:"Breathe in", bre_hold:"Hold", bre_out:"Breathe out", breStart:"Start", breStop:"Stop", breRounds:"{n} rounds done",
"brePattern_4-4":"4 counts in, 4 out", "brePattern_4-7-8":"4 in, 7 hold, 8 out", "brePattern_4-4-4-4":"4 in, 4 hold, 4 out, 4 hold",
wg_compliments:"Compliment machine", wg_compliments_d:"Draw a random compliment to make someone's day.",
complimentsDraw:"Draw a compliment",
wg_avatar:"My character", wg_avatar_d:"Build your own character: species, colours, hair and accessories.",
stAvatar:"My character", stAvatarSub:"Build your own character.",
avatarAlt:"My character", avatarSpecies_dino:"Dino", avatarSpecies_monster:"Monster", avatarSpecies_animal:"Animal",
avatarBodyColor:"Body colour", avatarHairColor:"Hair colour", avatarHairStyle:"Hair style",
avatarHair_none:"None", avatarHair_spiky:"Spiky", avatarHair_curly:"Curly", avatarHair_ponytail:"Ponytail", avatarHair_mohawk:"Mohawk",
avatarAccHead:"Head accessory", avatarAccBody:"Body accessory",
avatarAcc_none:"None", avatarAcc_cap:"Cap", avatarAcc_crown:"Crown", avatarAcc_glasses:"Glasses", avatarAcc_bow:"Bow",
avatarAcc_sunglasses:"Sunglasses", avatarAcc_headphones:"Headphones", avatarAcc_partyhat:"Party hat",
avatarAcc_scarf:"Scarf", avatarAcc_cape:"Cape", avatarAcc_badge:"Badge",
avatarAcc_necklace:"Necklace", avatarAcc_backpack:"Backpack",
avatarSave:"Save", avatarSaved:"Character saved ✓",
clock24Hour:"24-hour clock",
widgetSettings:"Settings", clockWelcome:"Practise telling time", clockWelcomeHint:"Choose level 1, 2 or 3 at the top. In Settings, choose how the task is shown, then start.", clockSetTitle:"Move the hands to the correct time", clockReadTitle:"View the task and make the time", clockChooseTitle:"Choose the correct time block", clockYourTask:"Your task", clockLinkHands:"Move the minute hand along", clockTypeHint:"Enter the time as HH:MM.", clockLevel1Hint:"Level 1: whole and half hours", clockLevel2Hint:"Level 2: five-minute steps", clockLevel3Hint:"Level 3: every minute, with optional 24-hour time and day parts", clockBuildHint:"Drag four digits into the slots to make HH:MM.", clockDigitGhost:"?", clockDigit:"Digit {digit}", clockIncomplete:"Complete the full time first.", clockCorrect:"Well done! Choose the next task. 🌟", clockTryAgain:"Almost! Try again.", clockStartTask:"Start task", clockNextTask:"Next task", clockLevel:"Level", clockLevelHour:"Whole hours", clockLevelQuarter:"Quarter hours", clockLevelFive:"Five minutes", clockLevelMinute:"Every minute", clockLearningStyle:"Show task as", clockStyleAnalogue:"Analogue clock", clockStyleDigital:"Large digital time", clockStyleWords:"In words", clockStyleBoth:"Clock, digits and words", clockDragHint:"Drag the hands to change the time.", clockGame:"Game", clockGameSet:"Set the clock", clockGameRead:"Read the time", clockGameChoose:"Choose the time", clockStart:"Choose New task to begin", clockRead:"What time is it? Set your answer below.", clockChoose:"Which digital time matches this clock?", clockTry:"Not yet. The correct answer is {time}.", clockScore:"Score: {score} · streak: {streak}", clockWordsHour:"{hour} o'clock", clockWordsQuarterPast:"quarter past {hour}", clockWordsHalf:"half past {hour}", clockWordsQuarterTo:"quarter to {hour}", clockWordsPast:"{minutes} past {hour}", clockWordsTo:"{minutes} to {hour}", clockMorning:"in the morning", clockAfternoon:"in the afternoon", clockEvening:"in the evening", clockNight:"at night", fractionRows:"Rows", fractionLabels:"Show fractions", fractionHint:"Tap a row up to the part you want to colour.", fractionSelected:"Selected fraction: {fraction}", fractionClear:"Clear", clockTask:"Practice task", clockSet:"Set the clock to {time}", clockHour:"Hour", clockMinute:"Minutes", placeNumber:"Number", placeGame:"Game", placeGameExplore:"Explore", placeGameBuild:"Build", placeLevel:"Level", placeLevel1Hint:"Level 1: numbers up to 100 (T and E)", placeLevel2Hint:"Level 2: numbers up to 1000 (H, T and E)", placeLevel3Hint:"Level 3: numbers up to 1,000,000 (HD, TD, D, H, T and E)", placeBuildTitle:"Build the number", placeYourTask:"Your task", placeBuildHint:"Drag the digits into the slots to build the number.", placeDigitGhost:"?", placeIncomplete:"Complete the whole number first.", placeCorrect:"Well done! Choose the next task. 🌟", placeTryAgain:"Almost! Try again.", placeNextTask:"Next task", placeScore:"Score: {score} · streak: {streak}", moneyMake:"Make € {amount}", wordtypeSentence:"Sentence", wordtypeHint:"Tap: blue noun · green verb · yellow adjective", scheduleActivity:"activity…", scheduleEmpty:"Add the first activity.", scheduleCount:"{count} moments in the schedule", pollReset:"Reset", pollQuestion:"Question", pollOptions:"Answers (one per line)", pollTotal:"{count} votes", moodTotal:"{count} children have chosen", moodVote:"Choose {face}", galleryFavorites:"Favourites", galleryFavorite:"Add favourite", galleryEmpty:"No widgets found.", gallerySearch:"Search widgets…", templateMath:"Maths lesson", templateLanguage:"Language lesson", templateDay:"Day start", exportBoard:"Export", importBoard:"Import", importError:"This is not a valid Teach board file.", widgetDuplicate:"Duplicate", widgetLock:"Lock", widgetUnlock:"Unlock", widgetPreview:"Preview without controls", undo:"Undo", redo:"Redo",
wg_sentence:"Sentence builder", wg_sentence_d:"Build sentences with colour-coded parts and add your own words.",
sentence_who:"Who?", sentence_does:"Does what?", sentence_what:"What or where?", sentenceHint:"Choose words to build a sentence…",
sentenceAdd:"your own word or phrase…", sentenceClear:"Clear", sentenceSpeak:"🔊 Read aloud", sentenceRemove:"Remove word", sentenceSurprise:"✨ Surprise sentence",
anchorPh:"theme…", anchorHint:"Word web: put the theme word in the middle and build around it · tap a cell and type · drag to pan",
anchorHintReadonly:"Drag the background to pan",
anchorColor:"Colour: click to cycle (connect words with the same colour)",
ballWin:"You got it! 🎉", ballLose:"Oh no! The word was:",
bgDots:"Dots", toolRuler:"Ruler", toolGeo:"Set square", toolLens:"Magnifier",
wg_mind:"Mind map", wg_mind_d:"Word web, spider or tree diagram to organise ideas together.",
mmSpider:"Spider", mmTree:"Tree", mmWeb:"Word web", mmRoot:"Topic",
mmHint:"Click text to type · drag by the edge",
wg_media:"Photo & video", wg_media_d:"Show photos and videos from the internet, such as YouTube or Vimeo.",
mediaPh:"Paste a link (YouTube, Vimeo, photo or video)…", mediaShow:"Show",
mediaEmpty:"Paste a link above and click Show.",
mediaErr:"This link cannot be shown. Try a YouTube, Vimeo, photo or video link, or open it with ↗ in a new tab.",
mediaOpen:"Open in new tab",
wordPh:"word (25 letters)", close:"Close",
badWord:"Use 2 to 5 letters (a-z).", needPic:"Pick a picture first.",
noWords:"No custom words yet. Add one above!",
tabEmoji:"😀 Emoji", tabPicto:"🖼️ Pictos", tabUpload:"📷 Upload", tabLibrary:"🗂 Library",
pictoSearch:"Search", pictoPh:"search word…", pictoNone:"Nothing found. Try another word.",
pictoLoad:"Searching…", pictoErr:"Could not load pictos (internet required).",
pictoCredit:"Pictos: <a href='https://arasaac.org' target='_blank'>ARASAAC</a> (CC BY-NC-SA, Gobierno de Aragón). Also nice: <a href='https://www.sclera.be' target='_blank'>sclera.be</a> (free download, then add via Upload).",
upload:"📷 Choose an image…",
upHint:"Tip: square picture, white or transparent background, one clear object, no text in the picture.",
save:"Save", cancel:"Cancel",
imgCatalog:"Image catalogue", imgSearch:"Search by name, folder or theme…",
imgOpenLibrary:"Open image library", imgLanguageHint:"All starter pictures, custom word cards and uploads are collected here.",
imgWordThemes:"Language themes",
imgFolders:"Images and folders", imgUpload:"Upload images",
imgChooseFiles:"Choose one or more images…", imgFolder:"Folder", imgThemes:"Themes",
imgNewTheme:"New theme…", imgAddTheme:"Add theme", imgEdit:"Edit image", imgName:"Name",
imgAllThemes:"All themes", imgAllScopes:"All sources", imgAllFolders:"All folders", imgFolderRoot:"Root folder",
imgScopeGlobal:"System", imgScopeSchool:"School", imgScopePersonal:"My images",
imgQuotaUsed:"{used} used", imgQuotaOf:"limit {limit}", imgSchoolQuota:"school: {used} of {limit}",
imgUnlimited:"Unlimited", imgNoResults:"No images found.", imgNoManage:"You cannot manage this item.",
imgLoginRequired:"Log in to use the image catalogue.", imgInvalidFile:"Choose a valid image.",
imgFileTooLarge:"An image may be at most 50 MB.", imgUploadFailed:"Upload failed.",
imgUploading:"Uploading images…", imgUploaded:"Images added ✓", imgUntitled:"Image",
imgLoading:"Loading images…", imgSaved:"Image saved ✓", imgDeleted:"Image deleted ✓",
imgThemeAdded:"Theme added ✓", imgStorageAdmin:"Image storage limits",
imgQuotaUsage:"{used} used · {limit}", imgQuotaDefault:"Default", imgQuotaCustom:"Custom",
imgPickSchool:"— pick a school —", imgSystemScope:"System (no school)",
imgQuotaDefaultOf:"Default ({limit})",
imgQuotaMb:"limit in MB", imgQuotaInvalid:"Enter a valid limit.", imgSchools:"Schools",
imgUsers:"Users", imgSearchUsers:"Search user…",
cats:["Animals","Food","Nature","Transport","Things","Body"],
notesPh:"Write here…", notesColor:"Note colour", notesLarge:"Large letters", notesCount:"{count} characters",
start:"Start", pause:"Pause", reset:"Reset", min:"min",
tmDigits:"Digits", tmVisual:"Visual clock", tmBoth:"Both", tmSound:"Sound on/off",
tmHint:"Drag the red disc to set the time.",
login:"Log in", loginBtn:"Log in", registerBtn:"Create account", logout:"Log out",
settingsTitle:"Settings", tabLogin:"Log in", tabLinkCode:"Link code", busy:"Please wait…",
stAccount:"Account", stPassword:"Password", stSession:"Session",
stSearchPh:"Search settings…", stNoResults:"Nothing found.",
stOpenSection:"open this section",
stGroupPersonal:"Personal", stGroupSchool:"School", stGroupSystem:"System",
stAccountSub:"Your details, password and backups.",
stViewSub:"Display on this device: role preview, theme, language and toolbar.",
wikiTitle:"Curriculum wiki",
wikiSub:"New SLO attainment targets for Dutch primary education, grouped by learning area.",
wikiKicker:"Primary education · July 2026",
wikiIntro:"Browse all 40 new attainment targets, their components and Teach's current coverage.",
wikiAreas:"learning areas", wikiGoals:"attainment targets", wikiGoal:"Target",
wikiGoalsRange:"Targets {range}", wikiParts:"Components",
wikiStatusTitle:"Status of these targets",
wikiStatusText:"These are final draft targets. Their legal introduction is being phased; the official SLO publication remains authoritative.",
wikiOfficialSource:"Open the official SLO publication",
wikiOfficialArea:"View {area} in the official publication",
wikiTuleTitle:"TULE and the new learning progressions",
wikiYearProfiles:"Language and maths year profiles",
wikiYearProfilesText:"Practical Teach interpretation for widget tasks per year. Based on the 2026 targets and TULE; not official SLO year standards.",
wikiYear:"Year {n}", wikiProfileGoals:"Learning goals",
wikiTuleText:"TULE is an illustrative elaboration of the 2006 targets. New SLO learning progressions for the 2026 targets are still being developed. Teach levels are therefore not official SLO progressions.",
wikiTuleSource:"Open TULE at SLO", wikiProgressionsSource:"Follow the development of new progressions",
wikiSearchLabel:"Search the curriculum wiki",
wikiSearchPlaceholder:"Search by target, domain, topic or widget…",
wikiAreaFilter:"Filter by learning area", wikiAllAreas:"All learning areas",
wikiCoveragePartial:"Partly in Teach", wikiCoverageSupport:"Lesson support", wikiCoverageOpen:"Not covered yet",
wikiWidgets:"Current widgets", wikiNoResults:"No attainment targets found.",
wikiCarefulTitle:"Use with care.",
wikiCarefulText:"A widget practising one component does not automatically cover the full target. School-level and experience targets require different evidence from a right/wrong score.",
widgetCurriculumInfo:"Attainment target information", widgetCurriculumIntro:"This widget contributes to the attainment targets below.",
widgetCurriculumNone:"No direct attainment-target link has been established for this supporting widget.",
widgetCurriculumWiki:"View this target in the curriculum wiki", widgetCurriculumAll:"Open the complete curriculum wiki",
widgetCurriculumCareful:"A widget supports parts of a target and is not, by itself, evidence that the full target has been achieved.",
wikiLanguageNote:"The official SLO goals and these implementation summaries are shown in Dutch.",
stUsersSub:"Manage accounts: pupils, staff, parents and enrolments.",
stKlassenSub:"Create, link and manage classes.",
stToewijzSub:"Choose what a class or pupil gets to see.",
stVoortgangSub:"Follow each pupil's results over time.",
stScholenSub:"Schools and system managers, across all schools.",
amSchoolMenu:"School", stSchoolMenuSub:"Manage schools, classes and users.",
stOpslag:"Storage", stOpslagSub:"Image catalogue and storage per school.",
stInhoud:"Content", stInhoudSub:"Welcome text, announcements and widget descriptions - editable without a release.",
cmsWelcomeH:"Welcome text (login screen)",
cmsAnnounceH:"Announcement for all users",
cmsWidgetH:"Widget descriptions (library)",
cmsActive:"Show announcement", cmsLevelInfo:"information", cmsLevelWarn:"important",
cmsSaved:"Saved ✓", cmsNl:"Nederlands", cmsEn:"English",
cmsWidgetPick:"— pick a widget… —",
cmsWidgetHint:"Leave empty for the default description.",
cmsShortcutsH:"System-wide content",
cmsShortcutsHint:"Share anchor diagrams and boards through the shared library (🌐 System folder in the explorer); images through the catalogue. Everyone can copy them from there.",
cmsOpenShared:"🌐 Boards & anchor diagrams", cmsOpenImages:"🖼 Image catalogue",
stBackups:"Backups", backupsNote:"Automatically kept earlier versions of your boards and words.",
backupsEmpty:"No backups yet.", backupRestore:"restore", backupRestoreConfirm:"sure?",
backupRestored:"Version restored ✓",
stView:"Display", vwTheme:"Theme", vwLight:"Light", vwDark:"Dark", vwAuto:"Auto",
vwContrast:"High contrast", vwOn:"On", vwOff:"Off", vwLang:"Language",
vwTbar:"Menu bar", vwTop:"Top", vwLeft:"Left", vwRight:"Right",
vwMotion:"Animations", vwGlass:"Glass effect",
vwRole:"View as", vwRoleSystem:"System", vwRoleAdmin:"School administrator", vwRoleTeacher:"Teacher", vwRoleParent:"Parent", vwRolePupil:"Pupil",
vwRoleNote:"Only the interface changes. You remain signed in as system manager; server permissions and data do not change.",
vwPreview:"Preview mode", vwPreviewReset:"Return to system",
vwParentPreview:"Preview of the parent portal. Real child data and actions are disabled in this view.",
vwPupilPreview:"Preview of the pupil environment using language and maths widgets from the current board. Controls and progress are disabled.",
vwPupilAddWidget:"Add widget", vwPupilChooseWidget:"Choose a pupil widget",
vwPupilAddWidgetNote:"Only language and maths widgets supported in the pupil environment are shown.",
vwNote:"These preferences apply to this device.",
pwChanged:"Password changed ✓", pwCurrentPh:"current password",
namePh:"name", passPh:"password",
guestNote:"Without logging in nothing is saved. Log in with your school account to keep everything on the server.",
storedNote:"Your work is stored on the school server and available on any board.",
errLogin:"Unknown name or wrong password.",
schoolSel:"— choose your school (empty = overarching) —",
linkIntro:"New account? Activate it with your link code:",
linkBtn:"Activate account", pwChange:"Change", pwNewPh:"new password (min. 8, incl. number)",
pwConfirmPh:"repeat password", pwShow:"👁 Show", pwHide:"🙈 Hide",
errPwMatch:"Passwords don't match.",
codePh:"link code",
roleSuper:"System manager", roleAdmin:"School administrator",
roleTeacher:"Group leader", rolePupil:"Pupil", roleParent:"Parent",
tabParentReg:"Parents",
parentRegIntro:"Create a parent account and follow your child - even if the school doesn't take part (yet):",
parentRegBtn:"Create account",
parentTitle:"My children",
parentNoChildren:"No children linked yet.",
parentAddChild:" Child", parentChildNamePh:"your child's name…", parentAddBtn:"Add",
parentPwNote:"Keep this password: your child logs in with it (leave the school field empty).",
parentLinkTitle:" Parent code", parentLinkPh:"parent code from school…", parentLinkBtn:"Link",
parentLinkNote:"Ask the school for a parent code for your child.",
parentHome:"home",
parentApply:"🏫 Apply", parentApplied:"applied to", parentApplyBtn:"Apply",
parentSchoolPh:"search your school…",
parentUnlink:"Unlink",
parentUnlinkWarn:"Removes only the link; your child's account remains.",
parentProgress:"📈 Progress", parentProgressEmpty:"No progress yet.",
parentLevel:"🎯 Learning level", parentLevelTaal:"Language", parentLevelRekenen:"Maths",
parentLevelYear:"Year", parentLevelPath:"Path", parentLevelSave:"Save",
parentLevelSaved:"Learning level saved.",
dtLen:"Length", dtContrast:"High contrast",
dtLock:"Lock in place: can no longer move or rotate, only draw along it",
dtMode:"Basic: big main angles and whole centimetres. Full: degrees every 2° with a double scale, millimetres and parallel guide lines.",
dtModeBasic:"basic", dtModeFull:"full",
pupilEmpty:"Your teacher hasn't set up anything for you yet.",
pupilWatching:"watching",
amUsers:"Users", amClasses:"Classes", amSchools:"Schools", amOverarching:"System",
amPickSchoolHint:"First pick a school above.", amPickSchool:"Pick a school",
amPupils:"Pupils", amPupilOne:"Pupil", amEdit:"Edit",
amAdmins:"School admins", amAdminOne:"School admin",
amClassAdd:"— add to class… —", amClassRemove:"Remove from this class",
amSure:"are you sure?", amDelete:"delete",
amFieldName:"Name", amFieldRole:"Role", amFieldClass:"Class", amFieldClasses:"Classes",
amFieldPw:"Password", amFieldActions:"Actions", amPwTypePh:"type a password…",
amNewPwWarn:"Generates a new password. The old one stops working and the user is signed out everywhere.",
amNewCodeWarn:"Generates a new link code. The current password stops working immediately; the user must re-activate.",
amDeleteWarn:"Deletes this account permanently, including boards and words.",
amResultPw:"New password", amResultCode:"New link code",
amColStatus:"Status", amNeverLoggedIn:"not signed in yet", amFieldSchool:"School", amStatusActive:"active",
amNoSchool:"— no school —", amNoSchoolGroup:"No school", amShowMore:"show more",
amMoveToSchool:"— move to school… —", amFieldMoveSchool:"Move to", amPickSchoolFirst:"First pick a school above to see or create its classes.",
amNoClassesHere:"This school has no classes yet. Existing classes may belong to another school — pick that school above, or create a new class below.",
amLoadError:"Failed to load",
amDetailEmpty:"Select a user on the left to edit, or create a new one.",
amNewUser:"New user", amRequestOne:"Enrolment", amReqNew:"new",
amParents:"Parents",
amParentCode:"Parent code", amResultParentCode:"Parent code",
amParentCodeWarn:"Generates a one-time parent code; a previous code stops working.",
amLinkRequests:"Enrolments", amRequestVia:"via",
amAdopt:"Adopt", amAdoptUserPh:"different username (on conflict)…",
amMatchPick:"— link to existing pupil… —", amMatchBtn:"Link",
amReject:"Reject", amRejectWarn:"Rejects the enrolment; the request disappears for the parent.",
amSchoolNamePh:"name of the school…", amNewSchoolConfirm:"Create:",
amDeleteSchoolWarn:"Deletes this school with all its classes and users. This cannot be undone.",
amClassNamePh:"name of the class…",
amDeleteClassWarn:"Deletes this class; pupils remain but end up without a class.",
amNewSchool:" School", amNewClass:" Class", amNewTeacher:" Group leader",
amNewAdmin:" School admin", amNewSuper:" System manager",
amNewPupil:" Pupil", amUserPh:"username…", amNamePh:"name…",
amSearchPh:"search by name…",
amCode:"link code", amNewCode:"new code", amPw:"password",
amNoClass:"— no class —", amPending:"not yet activated",
amCouple:"link teacher", amAllSchools:"— all schools —", amSuperGroup:"System",
amBack:"Back", amStaff:"Staff", amSupers:"System managers",
amUnassigned:"Unassigned", amNoResults:"No results",
amCreated:"Created! Pass on this link code:",
amPupilMade:"Pupil created. Password:",
amPwHidden:"already used", amGenerate:"generate password",
amNewPw:"new password", amPwSet:"Password set:",
amPwOptional:"password (empty = link code)",
amOwnPassword:"Your own password, to confirm", amOwnPasswordPh:"your current password…",
amAssignments:"Assignments", amAssignClasses:"Per class (default)", amAssignPupils:"Per pupil (override)",
amPickPupil:"— pick a pupil —", amNoAssignment:"— no assignment —",
amAssignedAs:"Assigned:", amAssign:"Assign", amAssignSaved:"Assignment saved.",
amUnlink:"Unlink",
amWholeBoard:"— whole board —", amModeWatch:"watch", amModeWork:"work",
cast:"Cast", castTitle:"Cast board", castWhat:"What are you casting?",
castClasses:"Classes", castPupils:"Pupils",
castNoClasses:"No classes in this school.", castNoPupils:"No pupils in this school.",
castActive:"✓ Active",
amProgress:"Progress", amProgressEmpty:"No progress for this pupil yet.",
amProgressLast14Days:"Last 14 days", amProgressMistakes:"Recent mistakes",
amProgressTimeline:"Full timeline",
amProgressGiven:"Given: {answer}", amProgressExpected:"Correct: {answer}",
amExportClass:"Export by class", amExportCsv:"Export as CSV", amPickClassHint:"Pick a class first.",
amExportColPupil:"Pupil", amExportColWidget:"Widget", amExportColAttempts:"Attempts",
amExportColCorrect:"Correct", amExportColStars:"Stars", amExportColDate:"Date",
amExportColUsername:"Username",
amExportDone:"Export downloaded ✓",
amLevels:"Class management", amLevelsSub:"Set a year group and level per subject.",
amLevelYearGroup:"Year group", amLevelLevel:"Level", amYearGroupN:"Year {n}",
amLevelClassDefault:"Class default", amLevelPupils:"Pupils",
amLevelInherited:"inherited from class", amLevelNone:"not set yet",
amLevelReset:"reset to class default", amLevelSaved:"Saved ✓",
amSubject_taal:"Language", amSubject_rekenen:"Maths", amSubject_world:"World orientation",
amConfirmDel:"Are you sure you want to delete this user?",
amCopy:"copy", amCopyAll:"copy all",
amResetClass:"reset class passwords", amResetSchool:"reset all pupil passwords",
amConfirmReset:"Renew all pupil passwords for this class?",
amConfirmResetSchool:"Renew all pupil passwords for this whole school? This cannot be undone.",
amResetNone:"No pupils to reset.",
errExists:"This name already exists.",
errName:"Name: 220 letters or digits.",
errPass:"Password: at least 4 characters.",
errCrypto:"Login does not work in this browser.",
hello:"Hi", boardSaved:"Saved ✓", loginFirst:"Log in first",
saving:"Saving…", saveFailed:"⚠ Not saved",
saveConflict:"⚠ Edited elsewhere", saveConflictHint:"Edited in another tab. Click to load the latest version (your unsaved changes here will be lost)."
}
};
let LANG = "nl";
/* vangnet: ontbreekt een sleutel in de actieve taal, val terug op NL en
daarna op de sleutelnaam zelf - een knop mag nooit zonder tekst staan */
const T = k => I18N[LANG][k] ?? I18N.nl[k] ?? k;
/* effective CSS zoom of the app (display-size setting); pointer coordinates
are visual pixels, absolute positions are layout pixels → divide by VZ() */
const VZ = ()=>{ const z = parseFloat(document.body.style.zoom); return z>0 ? z : 1; };
function applyI18n(){
document.querySelectorAll("[data-i18n]").forEach(el=>{ el.textContent = T(el.dataset.i18n); });
document.querySelectorAll("#langToggle button").forEach(b=>b.classList.toggle("active", b.dataset.lang===LANG));
document.getElementById("liName").placeholder = T("namePh");
document.getElementById("liPass").placeholder = T("passPh");
document.getElementById("liCode").placeholder = T("codePh");
document.getElementById("liPassNew").placeholder = T("pwNewPh");
document.getElementById("liPassNew2").placeholder = T("pwConfirmPh");
document.getElementById("prName").placeholder = T("namePh");
document.getElementById("prPass").placeholder = T("pwNewPh");
document.getElementById("prPass2").placeholder = T("pwConfirmPh");
document.getElementById("ownPwCurrent").placeholder = T("pwCurrentPh");
document.getElementById("ownPw").placeholder = T("pwNewPh");
document.getElementById("ownPw2").placeholder = T("pwConfirmPh");
document.getElementById("btnLiPassToggle").textContent = T("pwShow");
document.getElementById("btnOwnPwToggle").textContent = T("pwShow");
document.getElementById("btnMute").title = T("muteAll");
updateUserUI();
document.dispatchEvent(new CustomEvent("langchange"));
}
document.querySelectorAll("#langToggle button").forEach(b=>{
b.addEventListener("click", ()=>{
LANG=b.dataset.lang;
try{ localStorage.setItem("teach.lang", LANG); }catch(e){}
applyI18n();
});
});
document.getElementById("btnFs").addEventListener("click", ()=>{
if(document.fullscreenElement) document.exitFullscreen(); else document.documentElement.requestFullscreen();
});
/* =========================================================
Accounts & sessies — via de server-API (src/api.js)
Rollen: super (systeemmanager), admin (school), teacher (groepsleiding),
pupil (leerling). Zonder inloggen wordt niets bewaard.
==========================================================*/
// Oude bearer-tokens worden bewust verwijderd; sessies lopen via een HttpOnly-cookie.
try{ localStorage.removeItem("teach.token"); }catch(e){}
let currentUser = null; /* {id, username, displayName, role, schoolId, classId} */
async function api(path, opts={}){
/* alleen Content-Type meesturen als er ook echt een JSON-body is (bv. niet
bij DELETE) - anders weigert Fastify's parser het lege lichaam met een
ondoorzichtige "Body cannot be empty when content-type is set to
'application/json'", vóórdat een route-handler zelfs maar draait */
const headers = { ...(opts.body ? { "Content-Type": "application/json" } : {}), ...(opts.headers||{}) };
const r = await fetch("/api" + path, {
credentials: "same-origin",
method: opts.method || (opts.body ? "POST" : "GET"),
headers,
body: opts.body ? JSON.stringify(opts.body) : undefined
});
const j = await r.json().catch(()=>({}));
if(!r.ok){
const err = new Error(j.error || ("fout " + r.status));
err.status = r.status;
throw err;
}
return j;
}
/* =========================================================
Opslaan - alles van de gebruiker staat op de server.
Autosave: elke wijziging plant (debounced) een save; de status
is altijd zichtbaar op de opslaan-knop en fouten worden NIET
meer ingeslikt (die stilte verborg de 1MB-limiet-bug).
==========================================================*/
let saveTimer = null;
let lastSaveError = null;
/* rev van de data zoals geladen van de server; gaat als x-data-rev mee met
elke save zodat de server een verouderde schrijver kan weigeren (409) */
let DATA_REV = 0;
let saveConflict = false;
let saveInFlight = false;
function canPersist(){
/* ouders slaan geen eigen borden-data op (het portaal werkt met gekoppelde
kind-accounts); schoolleerlingen ook niet (zij zien de toewijzing van de
leerkracht). Thuiskinderen (leerling zónder school, ouderportaal) hebben
wél een eigen whiteboard met eigen opslag. */
if(!currentUser) return false;
const role=typeof activeRole==="function" ? activeRole() : currentUser.role;
if(role==="parent") return false;
if(role==="pupil") return currentUser.role==="pupil" && !currentUser.schoolId;
return true;
}
/* welke secties er sinds de laatste geslaagde save zijn gewijzigd; een save
verstuurt alleen die secties i.p.v. altijd de complete blob (die MB's kan
zijn door borden met afbeeldingen) */
const DIRTY = new Set();
const DATA_SECTIONS = { themes: ()=>THEMES, generalExtra: ()=>GENERAL_EXTRA, boards: ()=>BS, anchors: ()=>ANCHOR_LIB };
function persistUser(){
if(!canPersist()) return Promise.resolve(true);
const todo = [...DIRTY].filter(s=>s in DATA_SECTIONS);
const run = async ()=>{
if(!todo.length || todo.length >= Object.keys(DATA_SECTIONS).length){
/* alles (of niets specifieks) gewijzigd: volledige save als terugval */
const res = await api("/me/data", { method:"PUT", headers:{ "x-data-rev": String(DATA_REV) }, body:{
themes: THEMES, generalExtra: GENERAL_EXTRA, boards: BS, anchors: ANCHOR_LIB
}});
if(typeof res.rev === "number") DATA_REV = res.rev;
}else{
/* sequentieel, niet parallel: elke sectie-PUT bumpt de rev en de
volgende moet de nieuwe rev meesturen om geen 409 te krijgen */
for(const s of todo){
const res = await api("/me/data/"+s, { method:"PUT", headers:{ "x-data-rev": String(DATA_REV) }, body: DATA_SECTIONS[s]() });
if(typeof res.rev === "number") DATA_REV = res.rev;
}
}
DIRTY.clear();
lastSaveError = null;
saveConflict = false;
return true;
};
return run().catch(e=>{ lastSaveError = e.message; saveConflict = e.status === 409; return false; });
}
/* status op de opslaan-knop: idle / saving / saved / error / conflict */
function setSaveStatus(state){
const btn = document.getElementById("btnSave");
const lbl = btn.querySelector("span");
btn.classList.remove("ok","err");
btn.title = "";
if(state==="saving"){ lbl.textContent = T("saving"); }
else if(state==="saved"){ lbl.textContent = T("boardSaved"); btn.classList.add("ok"); }
else if(state==="conflict"){
lbl.textContent = T("saveConflict");
btn.classList.add("err");
btn.title = T("saveConflictHint");
}
else if(state==="error"){
lbl.textContent = T("saveFailed");
btn.classList.add("err");
btn.title = lastSaveError || "";
}
else lbl.textContent = T("saveBoard");
}
let saveStatusTimer = null;
async function doSave(){
if(!canPersist()) return false;
clearTimeout(saveTimer);
saveTimer = null;
stashCurrent();
setSaveStatus("saving");
saveInFlight = true;
const ok = await persistUser();
saveInFlight = false;
clearTimeout(saveStatusTimer);
setSaveStatus(ok ? "saved" : (saveConflict ? "conflict" : "error"));
/* een fout blijft staan tot een geslaagde save; succes ebt weg */
if(ok) saveStatusTimer = setTimeout(()=>setSaveStatus("idle"), 2000);
return ok;
}
/* niet weg-navigeren met onopgeslagen werk: geplande/lopende save of een
mislukte laatste save betekent dat de server nog niet alles heeft */
window.addEventListener("beforeunload", (e)=>{
if(!canPersist()) return;
if(saveTimer || saveInFlight || lastSaveError){ e.preventDefault(); e.returnValue = ""; }
});
/* autosave: door alles aan te roepen wat het bord of de woorden wijzigt;
de optionele sectie markeert wat er precies vuil is (zie DIRTY) */
function scheduleSave(section){
if(section) DIRTY.add(section);
if(!canPersist()) return;
clearTimeout(saveTimer);
saveTimer = setTimeout(doSave, 2500);
}
function persistWords(){
DIRTY.add("themes");
DIRTY.add("generalExtra");
scheduleSave();
}
function generalExtraFromData(data){
return (data && data.generalExtra) ? data.generalExtra : {nl:[], en:[]};
}
/* prefetchedData: alleen gebruikt door de opstart-boot in app.js, die /me/data
al parallel met /auth/me heeft opgehaald (zie daar) i.p.v. serieel te
wachten - een leerling/ouder gebruikt het resultaat toch niet (zie de twee
vroege returns hieronder), dus dat verzoek is nooit verspild werk, alleen
soms ongebruikt. Elke andere aanroep (na inloggen, na een save-conflict)
geeft niets mee en haalt gewoon vers op, zoals voorheen. */
async function hydrateFromServer(prefetchedData){
/* schoolleerlingen hebben geen eigen borden - zij zien enkel wat de
leerkracht heeft toegewezen (zie pupil.js), nooit het whiteboard.
Thuiskinderen (leerling zonder school, ouderportaal) vallen hier expres
buiten: zij krijgen hieronder het volledige whiteboard met eigen opslag. */
if(currentUser && currentUser.role === "pupil" && currentUser.schoolId){
THEMES = {nl:[],en:[]};
GENERAL_EXTRA = {nl:[],en:[]};
stopParentView();
startPupilView();
document.dispatchEvent(new CustomEvent("userchange"));
return;
}
/* ouders krijgen het portaal (parent.js) i.p.v. het whiteboard */
if(currentUser && currentUser.role === "parent"){
THEMES = {nl:[],en:[]};
GENERAL_EXTRA = {nl:[],en:[]};
stopPupilView();
startParentView();
document.dispatchEvent(new CustomEvent("userchange"));
return;
}
stopPupilView();
stopParentView();
/* Een thuiskind beheert het eigen leerjaar. Haal dit op voordat de
bordwidgets mounten, zodat de eerste opdracht meteen bij de keuze past. */
if(currentUser && currentUser.role === "pupil" && !currentUser.schoolId &&
typeof refreshSelfLearningLevels === "function") await refreshSelfLearningLevels();
const res = prefetchedData || await api("/me/data");
const d = res.data || {};
DATA_REV = typeof res.rev === "number" ? res.rev : 0;
THEMES = themesFromData(d);
GENERAL_EXTRA = generalExtraFromData(d);
ANCHOR_LIB = Array.isArray(d.anchors) ? d.anchors : [];
BS = boardsFromData(d);
document.dispatchEvent(new CustomEvent("wordschange"));
loadCurrentSlot();
document.dispatchEvent(new CustomEvent("userchange"));
}
function resetToGuest(){
currentUser = null;
if(typeof refreshSelfLearningLevels === "function") refreshSelfLearningLevels();
stopPupilView();
stopParentView();
THEMES = {nl:[],en:[]};
GENERAL_EXTRA = {nl:[],en:[]};
ANCHOR_LIB = [];
BS = freshBS();
loadCurrentSlot();
updateUserUI();
document.dispatchEvent(new CustomEvent("wordschange"));
document.dispatchEvent(new CustomEvent("userchange"));
}
/* login UI */
const loginWrap = document.getElementById("loginWrap");
const loginMsg = document.getElementById("loginMsg");
const settingsWrap = document.getElementById("settingsWrap");
const settingsMsg = document.getElementById("settingsMsg");
const roleKey = r => r==="super" ? "roleSuper" : r==="admin" ? "roleAdmin" : r==="teacher" ? "roleTeacher" : r==="parent" ? "roleParent" : "rolePupil";
function updateUserUI(){
document.getElementById("userLbl").textContent = currentUser ? currentUser.displayName : T("login");
document.getElementById("btnUser").classList.toggle("on", !!currentUser);
/* welke instellingen-secties zichtbaar zijn bepaalt settings.js per rol
(sectieregister); hier alleen nog de accountgegevens zelf */
if(currentUser){
document.getElementById("helloName").textContent = currentUser.displayName;
const roles = [currentUser.role, ...(currentUser.extraRoles||[])];
document.getElementById("helloRole").textContent = roles.map(r=>T(roleKey(r))).join(" + ");
document.getElementById("pwSection").style.display = can("password.own.change") ? "" : "none";
}
}
/* login-modal tabs: "inloggen" / "koppelcode activeren" / "ouderaccount maken" */
function switchLoginTab(tab){
document.querySelectorAll("#loginModal .ed-tabs button").forEach(b=>b.classList.toggle("active", b.dataset.tab===tab));
document.getElementById("loginBox").style.display = tab==="login" ? "block" : "none";
document.getElementById("linkBox").style.display = tab==="link" ? "block" : "none";
document.getElementById("parentRegBox").style.display = tab==="parentreg" ? "block" : "none";
}
document.querySelectorAll("#loginModal .ed-tabs button").forEach(b=>{
b.addEventListener("click", ()=>switchLoginTab(b.dataset.tab));
});
async function loadSchools(){
const sel = document.getElementById("liSchool");
try{
const list = await api("/schools");
sel.innerHTML = `<option value="">${T("schoolSel")}</option>` +
list.map(x=>`<option value="${x.id}">${x.name.replace(/</g,"&lt;")}</option>`).join("");
}catch(e){ sel.innerHTML = `<option value="">${T("schoolSel")}</option>`; }
}
/* wachtwoordvelden pas op verzoek: de kop "Wachtwoord" klapt het blok uit */
{
const pwHead = document.getElementById("pwToggleHead");
const pwBlock = document.getElementById("ownPwBlock");
pwHead.addEventListener("click", ()=>{
pwBlock.classList.toggle("open");
pwHead.classList.toggle("open");
});
}
/* openSettings() zelf woont in settings.js (sectieregister + zoeken);
de knoppen hieronder roepen hem via window aan */
/* reservekopieën (Instellingen → Account): automatisch bewaarde eerdere
versies van de borddata, met een arm-then-confirm herstelknop zoals de
verwijderknop in de boardStrip */
async function loadBackups(){
const head = document.getElementById("backupsHead");
const block = document.getElementById("backupsBlock");
const list = document.getElementById("backupsList");
const show = canPersist();
head.style.display = block.style.display = show ? "" : "none";
if(!show) return;
list.textContent = "";
let versions = [];
try{ versions = (await api("/me/data/versions")).versions || []; }
catch(e){ list.textContent = e.message; return; }
if(!versions.length){
const d = document.createElement("div");
d.className = "guestnote"; d.textContent = T("backupsEmpty");
list.appendChild(d);
return;
}
versions.forEach(v=>{
const row = document.createElement("div");
row.className = "lrow";
const when = document.createElement("span");
when.className = "am-code";
when.textContent = new Date(v.createdAt).toLocaleString(LANG==="nl" ? "nl-NL" : "en-GB");
const btn = document.createElement("button");
btn.className = "tbtn ghost";
btn.type = "button";
btn.textContent = T("backupRestore");
let armed = false;
btn.addEventListener("click", async ()=>{
if(!armed){ armed = true; btn.textContent = T("backupRestoreConfirm"); btn.classList.add("danger"); return; }
try{
const res = await api("/me/data/restore", { body:{ versionId: v.id } });
DATA_REV = typeof res.rev === "number" ? res.rev : DATA_REV;
const d = res.data || {};
THEMES = themesFromData(d);
GENERAL_EXTRA = generalExtraFromData(d);
ANCHOR_LIB = Array.isArray(d.anchors) ? d.anchors : [];
BS = boardsFromData(d);
document.dispatchEvent(new CustomEvent("wordschange"));
loadCurrentSlot();
settingsMsg.textContent = T("backupRestored");
settingsMsg.classList.add("ok");
loadBackups();
}catch(e){ settingsMsg.textContent = e.message; }
});
row.appendChild(when);
row.appendChild(btn);
list.appendChild(row);
});
}
document.getElementById("btnUser").addEventListener("click", ()=>{
if(currentUser){ openSettings(); return; }
loginMsg.textContent = ""; loginMsg.classList.remove("ok");
switchLoginTab("login");
loadSchools();
loginWrap.classList.add("open");
});
/* gast-ingang naar de weergave-instellingen vanuit het inlogscherm */
document.getElementById("btnGuestView").addEventListener("click", ()=>{
loginWrap.classList.remove("open");
openSettings();
});
document.getElementById("btnSettingsClose").addEventListener("click", ()=>settingsWrap.classList.remove("open"));
loginWrap.addEventListener("click", e=>{ if(e.target===loginWrap) loginWrap.classList.remove("open"); });
settingsWrap.addEventListener("click", e=>{ if(e.target===settingsWrap) settingsWrap.classList.remove("open"); });
async function afterLogin(res){
currentUser = res.user;
updateUserUI();
loginWrap.classList.remove("open");
await hydrateFromServer();
}
/* laadstatus op een knop tijdens een async actie, altijd hersteld na afloop */
async function withBusy(btn, fn){
const orig = btn.textContent;
btn.disabled = true; btn.textContent = T("busy");
try{ await fn(); }
finally{ btn.disabled = false; btn.textContent = orig; }
}
document.getElementById("btnLogin").addEventListener("click", async ()=>{
const btn = document.getElementById("btnLogin");
const schoolId = document.getElementById("liSchool").value || null;
const username = document.getElementById("liName").value.trim();
const password = document.getElementById("liPass").value;
loginMsg.classList.remove("ok");
await withBusy(btn, async ()=>{
try{
await afterLogin(await api("/auth/login", { body:{ schoolId: schoolId ? +schoolId : null, username, password } }));
document.getElementById("liPass").value = "";
}catch(e){ loginMsg.textContent = e.message || T("errLogin"); }
});
});
document.getElementById("liPass").addEventListener("keydown", e=>{
if(e.key==="Enter") document.getElementById("btnLogin").click();
});
/* wachtwoord tonen/verbergen bij activeren en eigen wachtwoord wijzigen */
function pwToggle(btnId, ...fieldIds){
const btn = document.getElementById(btnId);
let shown = false;
btn.addEventListener("click", ()=>{
shown = !shown;
fieldIds.forEach(id=>{ document.getElementById(id).type = shown ? "text" : "password"; });
btn.textContent = T(shown ? "pwHide" : "pwShow");
});
}
pwToggle("btnLiPassToggle", "liPassNew", "liPassNew2");
pwToggle("btnOwnPwToggle", "ownPwCurrent", "ownPw", "ownPw2");
pwToggle("btnPrPassToggle", "prPass", "prPass2");
/* ouderaccount registreren: direct ingelogd, parent.js toont daarna het portaal */
document.getElementById("btnParentReg").addEventListener("click", async ()=>{
const btn = document.getElementById("btnParentReg");
const username = document.getElementById("prName").value.trim();
const password = document.getElementById("prPass").value;
const password2 = document.getElementById("prPass2").value;
loginMsg.classList.remove("ok");
if(password !== password2){ loginMsg.textContent = T("errPwMatch"); return; }
await withBusy(btn, async ()=>{
try{
await afterLogin(await api("/auth/register-parent", { body:{ username, password } }));
document.getElementById("prPass").value = "";
document.getElementById("prPass2").value = "";
}catch(e){ loginMsg.textContent = e.message; }
});
});
/* account activeren met een koppelcode */
document.getElementById("btnLink").addEventListener("click", async ()=>{
const btn = document.getElementById("btnLink");
const code = document.getElementById("liCode").value.trim().toUpperCase();
const password = document.getElementById("liPassNew").value;
const password2 = document.getElementById("liPassNew2").value;
loginMsg.classList.remove("ok");
if(password !== password2){ loginMsg.textContent = T("errPwMatch"); return; }
await withBusy(btn, async ()=>{
try{
await afterLogin(await api("/auth/link", { body:{ code, password } }));
document.getElementById("liCode").value = "";
document.getElementById("liPassNew").value = "";
document.getElementById("liPassNew2").value = "";
}catch(e){ loginMsg.textContent = e.message; }
});
});
document.getElementById("btnLogout").addEventListener("click", async ()=>{
try{ await api("/auth/logout", { method:"POST", body:{} }); }catch(e){}
resetToGuest();
loginWrap.classList.remove("open");
settingsWrap.classList.remove("open");
});
/* eigen wachtwoord wijzigen (niet voor leerlingen) */
document.getElementById("btnOwnPw").addEventListener("click", async ()=>{
const btn = document.getElementById("btnOwnPw");
const currentPassword = document.getElementById("ownPwCurrent").value;
const pw = document.getElementById("ownPw").value;
const pw2 = document.getElementById("ownPw2").value;
settingsMsg.classList.remove("ok");
if(pw !== pw2){ settingsMsg.textContent = T("errPwMatch"); return; }
await withBusy(btn, async ()=>{
try{
await api("/auth/password", { method:"PATCH", body:{ currentPassword, password: pw } });
document.getElementById("ownPwCurrent").value = "";
document.getElementById("ownPw").value = "";
document.getElementById("ownPw2").value = "";
settingsMsg.textContent = T("pwChanged"); settingsMsg.classList.add("ok");
}catch(e){ settingsMsg.textContent = e.message; }
});
});
/* save board */
document.getElementById("btnSave").addEventListener("click", async ()=>{
const lbl = document.getElementById("btnSave").querySelector("span");
if(!currentUser){
lbl.textContent = T("loginFirst");
setTimeout(()=>{ lbl.textContent = T("saveBoard"); }, 2200);
loadSchools();
loginWrap.classList.add("open");
return;
}
/* na een conflict (409) niet blind overschrijven: de klik haalt eerst de
nieuwste serverversie op, daarna kan er weer gewoon opgeslagen worden */
if(saveConflict){
saveConflict = false;
lastSaveError = null;
clearTimeout(saveTimer);
saveTimer = null;
await hydrateFromServer();
setSaveStatus("idle");
return;
}
clearTimeout(saveTimer); /* expliciet opslaan gaat vóór de geplande autosave */
await doSave();
});
/* =========================================================
Sounds
==========================================================*/
let AC=null, MUTED=false;
const btnMute = document.getElementById("btnMute");
btnMute.addEventListener("click", ()=>{
MUTED = !MUTED;
btnMute.textContent = MUTED ? "🔇" : "🔊";
btnMute.title = T("muteAll");
});
function tone(freq, dur=0.12, delay=0, type="sine", vol=0.18){
if(MUTED) return;
try{
AC = AC || new (window.AudioContext||window.webkitAudioContext)();
const o=AC.createOscillator(), g=AC.createGain();
o.type=type; o.frequency.value=freq;
g.gain.setValueAtTime(vol, AC.currentTime+delay);
g.gain.exponentialRampToValueAtTime(0.001, AC.currentTime+delay+dur);
o.connect(g); g.connect(AC.destination);
o.start(AC.currentTime+delay); o.stop(AC.currentTime+delay+dur+0.02);
}catch(e){}
}
const sndGood = ()=> tone(880,0.1);
const sndBad = ()=> tone(180,0.18,0,"square",0.10);
const sndWin = ()=>{ [523,659,784,1047].forEach((f,i)=>tone(f,0.16,i*0.12,"triangle",0.2)); };