Modules extend LingoCon — add studio tools, interactive widgets on public pages, sound-change packs, themes, and more. They're hosted by the platform, so anyone can add yours with one click; nobody downloads anything.
Every module runs in one of three tiers, chosen by how much power it needs:
Pick a type when you create a module. It determines where the module shows up:
Studio sidebar · Studio module panel
Studio sidebar · Studio module panel · Public language page (Tools)
Studio module panel
Studio sidebar · Studio module panel · Dictionary transform in studio
Studio sidebar · Studio module panel
Studio sidebar · Studio module panel
Studio sidebar · Studio module panel
Studio sidebar · Studio module panel · Public language page (Tools)
Studio sidebar · Studio module panel
Studio sidebar · Studio module panel · Public language page (theme)
A global host object is injected into every client-sandbox module. Render your UI into the #app element and use these methods:
host.ready() // call once, when your script has loaded
host.onInit(function (ctx) { ... }) // ctx = { languageSlug, languageId, permissions, theme }
host.request(method, params) // returns a Promise of data (see below)
host.reportHeight() // re-measure after you change the DOM
host.context() // the init context, or null before initThe iframe auto-resizes to your content, but call host.reportHeight() after async renders to be safe.
Read a language's data with host.request(). Each method requires a permission that the installer must grant; declare them when you publish.
getLanguage—{ name, slug, description }getDictionaryread:dictionary{ entries: [{ lemma, gloss, ipa, partOfSpeech }] }getPhonologyread:phonology{ symbols: [{ symbol, ipa, latin, name }] }getParadigmsread:paradigms{ paradigms: [{ id, name, slots, words }] }Requesting a method you didn't declare (or that wasn't granted) returns an error.
Client-sandbox code runs in a sandbox="allow-scripts" iframe with a null origin: it cannot read cookies, localStorage, or the host DOM.
A strict Content-Security-Policy (default-src 'none') blocks all network egress — no fetch, XHR, WebSocket, or beacons. The only channel out is postMessage to the host.
At publish time bundles are statically scanned (size limit + denylist for document.cookie, storage, eval, dynamic import, etc.). All language data is brokered by the host against granted permissions.
A complete reader widget that lists the 20 most recent words:
host.onInit(async function (ctx) {
const root = document.getElementById("app");
root.textContent = "Loading…";
try {
const { entries } = await host.request("getDictionary");
root.innerHTML = "";
const h = document.createElement("h3");
h.textContent = ctx.languageSlug + " · " + entries.length + " words";
root.appendChild(h);
const ul = document.createElement("ul");
entries.slice(0, 20).forEach(function (e) {
const li = document.createElement("li");
li.textContent = e.lemma + (e.ipa ? " /" + e.ipa + "/" : "") + " — " + (e.gloss || "");
ul.appendChild(li);
});
root.appendChild(ul);
} catch (err) {
root.textContent = "Could not load data.";
}
host.reportHeight();
});
host.ready();Paste this into the playground, grant read:dictionary, and press Run.