> Part of the PWA DevGuide (split from pwa_dev_style.md on 2026-07-02 — see that file for the index; ADR-099).

## 21. Bilingual (EN / HI) Pattern

Used in: Recce PWA. Apply to any PWA that needs an EN/HI language toggle.

### 21.1 CSS Toggle Mechanism

```css
.hi-txt { display: none }
body.lang-hi .en-txt { display: none }
body.lang-hi .hi-txt { display: inline }
```

Applied at the `<body>` level — one class switch flips every bilingual pair on the whole page simultaneously.

### 21.2 Static HTML — Wrap Both Languages in Spans

```html
<span class="en-txt">Submit Recce ✓</span><span class="hi-txt">रेकी सबमिट करें ✓</span>
```

Apply to: button labels, card titles, field labels, nav items, dialog text, badge words, status messages, empty-state text, header titles, placeholder-adjacent labels (not the placeholder attribute itself).

### 21.3 JS State, Toggle Function, and Translation Helper

```js
let LANG = localStorage.getItem('recce-lang') || 'en';

function setLang(l) {
  LANG = l;
  localStorage.setItem('recce-lang', l);
  document.body.classList.toggle('lang-hi', l === 'hi');
}

// Used in dynamic HTML generation and textContent assignments
function t(en, hi) { return LANG === 'hi' ? hi : en; }
```

Language toggle button (header):
```html
<button class="lang-btn" onclick="setLang(LANG==='en'?'hi':'en')">EN | हि</button>
```

On init, call `setLang(LANG)` before rendering anything — ensures `body.lang-hi` is applied before first paint.

### 21.4 Dynamic JS — Use `t()` in Template Literals

Whenever HTML is built in JS (via `innerHTML`, `insertAdjacentHTML`), use `t()` instead of injecting `.en-txt`/`.hi-txt` spans. The spans approach requires the browser to parse and hide elements; `t()` inserts only the correct language string from the start.

```js
// ✅ Correct — template literal with t()
cont.innerHTML = brandings.map((b, i) => `
  <div class="bitem-title">${t('Branding', 'ब्रांडिंग')} ${i + 1}</div>
  <label>${t('Type', 'प्रकार')} <span class="req">*</span></label>
  <div class="pslot-lbl">${t('Tap to capture', 'दबाकर कैप्चर करें')}</div>
`).join('');

// ✅ Correct — textContent assignment
el.textContent = t('Enter W × H', 'W × H दर्ज करें');

// ✅ Correct — toast messages
toast(t('Max 10 items', 'अधिकतम 10 आइटम'), 'error');

// ✅ Correct — dialog titles set via JS
showBDialog(
  t('Add reference photos?', 'अतिरिक्त फोटो जोड़ें?'),
  t('Optional — for future reference.', 'वैकल्पिक — भविष्य के लिए।'),
  callback
);
```

### 21.5 Map Variable Name Conflict

If your code has both a `t()` translation function and an array `.map(t => ...)`, rename the map parameter to avoid shadowing:

```js
// ❌ Shadows t() — chip onclick will call the map parameter, not translate
BTYPE.map(t => `<div onclick="setType('${t}')">${t}</div>`)

// ✅ Rename map parameter
BTYPE.map(tp => `<div onclick="setType('${tp}')">${tp}</div>`)
```

Same issue applies to any other `.map()`, `.filter()`, `.forEach()` that uses `t` as the parameter name.

### 21.6 Bilingual Coverage Checklist (per screen)

Before marking a screen bilingual-complete, verify each of these:

- [ ] Header title + step subtitle (static HTML)
- [ ] All card section titles
- [ ] All field labels + required markers text
- [ ] All button labels — primary, secondary, back, outline, destructive
- [ ] All chip and radio button labels
- [ ] Empty state messages (`empty-txt`)
- [ ] Toast messages → use `t()` in all `toast(...)` calls
- [ ] Dialog title + subtitle → use `t()` in `showDialog(...)` call, not static HTML (since `textContent` assignment replaces spans)
- [ ] Dynamically rendered list/item labels → use `t()` in template literals
- [ ] Badge / counter text that contains words (not bare numbers)
- [ ] Status row text (network status, sync status, last sync)
- [ ] Settings section headings + descriptions (lower priority — admin-facing)

### 21.7 Language Button Styling

```css
.lang-btn {
  background: none;
  border: 1px solid rgba(255,255,255,.3);
  color: rgba(255,255,255,.8);
  font-size: 12px; font-weight: 700;
  padding: 4px 10px;
  border-radius: 20px;
  cursor: pointer;
  display: flex; align-items: center; gap: 3px;
  white-space: nowrap; flex-shrink: 0;
}
.lang-btn:active { background: rgba(255,255,255,.15) }
```

Place in header hero row alongside Hub button and settings gear.

---

