---
name: gh-translate-button
description: >-
  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.
---

# gh-translate-button

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.

## Requirements (non-negotiable)

- **Languages (9):** `en`, `it`, `ar`, `nl`, `zh-CN`, `es`, `fr`, `de`, `pt-BR`.
- **Arabic is RTL.** When `ar` is active, set `dir="rtl"` on `<html>`; all others are `ltr`.
- **State lives in `?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.
- **No reload on switch.** Update the URL with `history.replaceState` and re-render in place.
- **Graceful fallback.** Unknown/missing `?lang=` → default `en`. Missing key in a dictionary →
  fall back to the `en` string.
- **Accessible.** The control is a labelled `<select>` (or button group) with an `aria-label`.

## How content is marked up

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.

## Drop-in snippet (vanilla, no build step)

```html
<!-- 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>
```

## Adapting it

- **Add a string:** add the key to every `I18N[code]`; mark the element with `data-i18n="key"`.
- **Add a language:** append to `LANGS` and add its `I18N` entry. Only `ar` is RTL today.
- **Framework port:** the same three pieces (read `?lang=`, write via `history.replaceState`,
  re-render) port directly to React/Vue — keep `?lang=` as the source of truth; do not introduce
  `localStorage`.
- **SEO (optional):** emit `<link rel="alternate" hreflang="<code>" href="...?lang=<code>">` per
  language so each variant is discoverable.

## Acceptance check

- [ ] All 9 languages selectable; switching updates text without reload.
- [ ] `?lang=ar` sets `dir="rtl"`; others `ltr`.
- [ ] State is in the URL only — no `localStorage`/cookies anywhere.
- [ ] Unknown `?lang=` and missing keys fall back to `en`.
- [ ] Shared link with `?lang=` reproduces the exact language.
