Starting with v2.9.0, BeeEngine supports plugins. The engine stays "blind": it doesn't import or know anything about any plugin. Each plugin hooks itself in with attach(engine) and removes itself with detach().
The first official plugin is BeeLocale: a tiny helper to make your canvas games multilingual.
npm install beeengine@2.9.0
1. The problem: hardcoded text
// ❌ Before: to translate the game you have to hunt every string in your code
ctx.fillText('Score: ' + score, 20, 30);
ctx.fillText('Game Over', 300, 200);
ctx.fillText('Press SPACE to restart', 260, 240);
2. The solution: BeeLocale
import { BeeEngine } from 'beeengine';
import { BeeLocale } from 'beeengine/plugins';
const canvas = document.getElementById('game');
const engine = new BeeEngine(canvas, 800, 600);
// All translations in one place
const locale = new BeeLocale({
language: 'it', // active language
fallback: 'en', // backup language
strings: {
en: {
score: 'Score',
gameover: 'Game Over',
restart: 'Press SPACE to restart'
},
it: {
score: 'Punteggio',
gameover: 'Hai perso!',
restart: 'Premi SPAZIO per ricominciare'
}
}
});
// The plugin registers itself in the engine as 'locale'
locale.attach(engine);
3. Use it wherever you draw text
// ✅ After: only keys in your code
ctx.fillText(locale.t('score') + ': ' + score, 20, 30);
ctx.fillText(locale.t('gameover'), 300, 200);
ctx.fillText(locale.t('restart'), 260, 240);
get(key) does exactly the same as t(key), if you prefer that name:
locale.get('gameover'); // "Hai perso!"
You can also grab it from the engine, anywhere in your game:
const loc = engine.plugin('locale');
loc.t('score'); // "Punteggio"
4. Switch language on the fly
document.getElementById('btn-en').onclick = () => locale.setLanguage('en');
document.getElementById('btn-it').onclick = () => locale.setLanguage('it');
// Or automatically, from the browser language
locale.setLanguage(navigator.language.slice(0, 2)); // "it", "en", "fr"...
The next frame already draws the text in the new language.
5. Fallback: never an empty string
BeeLocale looks up a key in this order:
-
active language (
it) -
fallback language (
en) - the key itself, left visible
const locale = new BeeLocale({
language: 'fr',
fallback: 'en',
strings: {
en: { score: 'Score', lives: 'Lives' },
fr: { score: 'Score' } // "lives" is missing in French
}
});
locale.t('score'); // "Score" → found in French
locale.t('lives'); // "Lives" → missing in French, falls back to English
locale.t('bonus'); // "bonus" → missing everywhere, shows the key
A missing translation shows up on screen right away instead of silently disappearing. Empty strings ('') count as missing too.
6. Detaching the plugin
locale.detach();
engine.plugin('locale'); // null
Under the hood: the plugin contract
Every plugin needs just two methods: attach(engine) and detach().
class MyPlugin {
attach(engine) {
this.engine = engine;
engine.registerPlugin('my-plugin', this);
return this;
}
detach() {
if (this.engine) this.engine.unregisterPlugin('my-plugin');
this.engine = null;
return this;
}
}
A plugin can't call engine.loop(), override update/render, or patch the main loop.
If it needs to run something every frame, it uses onTick, which returns a function to turn the hook off:
class FrameCounter {
attach(engine) {
this.engine = engine;
this.frames = 0;
engine.registerPlugin('frames', this);
this.off = engine.onTick(() => { this.frames++; });
return this;
}
detach() {
if (this.off) this.off(); // always turn the hook off
if (this.engine) this.engine.unregisterPlugin('frames');
this.engine = null;
return this;
}
}
BeeLocale doesn't use onTick at all: it only works when you call t(), so it costs nothing per frame.
What's next
More plugins are coming, one at a time. Which one would you like to see next? Let me know in the comments! 🐝
Top comments (0)