From gh-toolkit
Use when the user wants to add a language switcher, a translate button, an i18n toggle, or multi-language support to a page or component. Triggers on "translation button", "language switcher", "language selector", "i18n toggle", "add languages to this page", "multi-language", "localize this", "RTL support", "Arabic support", "lang switcher", "translate widget". Specs a reusable component supporting EN, IT, AR, NL, ZH-CN, ES, FR, DE, PT-BR — with Arabic as RTL and the active language driven by a ?lang= URL param (never localStorage) — and ships a drop-in vanilla HTML/JS snippet.
How this skill is triggered — by the user, by Claude, or both
Slash command
/gh-toolkit:gh-translate-buttonThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
A reusable, dependency-free language switcher. Drop it into any page; it switches UI strings,
A reusable, dependency-free language switcher. Drop it into any page; it switches UI strings,
flips direction for RTL, and persists the choice only through the ?lang= URL parameter.
en, it, ar, nl, zh-CN, es, fr, de, pt-BR.ar is active, set dir="rtl" on <html>; all others are ltr.?lang= only. The active language is read from and written to the URL query
param. Never use localStorage/cookies — the URL is the single source of truth, so a shared
link reproduces the exact language, and there's no hidden persisted state.history.replaceState and re-render in place.?lang= → default en. Missing key in a dictionary →
fall back to the en string.<select> (or button group) with an aria-label.Tag any translatable element with data-i18n="<key>"; the script swaps its textContent per the
active language's dictionary. Set <html lang> and dir automatically.
<!-- 1) Place the switcher anywhere -->
<div id="lang-switcher" aria-label="Language"></div>
<!-- 2) Mark translatable text -->
<h1 data-i18n="hero_title">We handle it</h1>
<a href="#" data-i18n="cta">Get started</a>
<!-- 3) Drop-in logic -->
<script>
(function () {
// Supported languages. `rtl: true` only for Arabic.
const LANGS = [
{ code: 'en', label: 'English', rtl: false },
{ code: 'it', label: 'Italiano', rtl: false },
{ code: 'ar', label: 'العربية', rtl: true },
{ code: 'nl', label: 'Nederlands', rtl: false },
{ code: 'zh-CN', label: '简体中文', rtl: false },
{ code: 'es', label: 'Español', rtl: false },
{ code: 'fr', label: 'Français', rtl: false },
{ code: 'de', label: 'Deutsch', rtl: false },
{ code: 'pt-BR', label: 'Português', rtl: false },
];
const DEFAULT = 'en';
// String table: I18N[code][key]. Fill in your real copy; en is the fallback.
const I18N = {
'en': { hero_title: 'We handle it', cta: 'Get started' },
'it': { hero_title: 'Lo gestiamo noi', cta: 'Inizia' },
'ar': { hero_title: 'نحن نتولى الأمر', cta: 'ابدأ الآن' },
'nl': { hero_title: 'Wij regelen het', cta: 'Begin nu' },
'zh-CN': { hero_title: '交给我们', cta: '开始使用' },
'es': { hero_title: 'Nos encargamos', cta: 'Empezar' },
'fr': { hero_title: 'On s’en occupe', cta: 'Commencer' },
'de': { hero_title: 'Wir kümmern uns', cta: 'Loslegen' },
'pt-BR': { hero_title: 'A gente cuida', cta: 'Começar' },
};
const known = c => LANGS.some(l => l.code === c);
// Active language = ?lang= param, else default. No storage, ever.
function currentLang() {
const p = new URLSearchParams(location.search).get('lang');
return known(p) ? p : DEFAULT;
}
function apply(code) {
const lang = LANGS.find(l => l.code === code) || LANGS.find(l => l.code === DEFAULT);
const dict = I18N[lang.code] || {};
const fallback = I18N[DEFAULT] || {};
document.documentElement.lang = lang.code;
document.documentElement.dir = lang.rtl ? 'rtl' : 'ltr';
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
const val = dict[key] != null ? dict[key] : fallback[key];
if (val != null) el.textContent = val;
});
}
function setLang(code) {
const url = new URL(location.href);
url.searchParams.set('lang', code); // the ONLY place state is kept
history.replaceState(null, '', url); // no reload, no localStorage
apply(code);
render(code);
}
function render(active) {
const host = document.getElementById('lang-switcher');
if (!host) return;
host.innerHTML = '';
const sel = document.createElement('select');
sel.setAttribute('aria-label', 'Select language');
LANGS.forEach(l => {
const opt = document.createElement('option');
opt.value = l.code;
opt.textContent = l.label;
if (l.code === active) opt.selected = true;
sel.appendChild(opt);
});
sel.addEventListener('change', e => setLang(e.target.value));
host.appendChild(sel);
}
function init() { const c = currentLang(); apply(c); render(c); }
window.addEventListener('popstate', init); // back/forward keeps URL ↔ UI in sync
if (document.readyState !== 'loading') init();
else document.addEventListener('DOMContentLoaded', init);
})();
</script>
I18N[code]; mark the element with data-i18n="key".LANGS and add its I18N entry. Only ar is RTL today.?lang=, write via history.replaceState,
re-render) port directly to React/Vue — keep ?lang= as the source of truth; do not introduce
localStorage.<link rel="alternate" hreflang="<code>" href="...?lang=<code>"> per
language so each variant is discoverable.?lang=ar sets dir="rtl"; others ltr.localStorage/cookies anywhere.?lang= and missing keys fall back to en.?lang= reproduces the exact language.Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub aymandakir-gh/gh-claude-toolkit --plugin gh-toolkit