# GraphQL glossary i18n overlays

Per-language translation overlays for the GraphQL glossary. The English primary glossary lives at `../graphql-glossary.json`; each file here adds translations for one BCP-47 locale.

## File layout

```
graphql-glossary-i18n/
├── README.md (this file)
├── ar.json
├── de.json
├── es.json
├── fr.json
├── hi.json
├── id.json
├── it.json
├── ja.json
├── ko.json
├── mr.json
├── pt-BR.json
├── pt.json
├── ro.json
├── ru.json
├── sq.json
├── uk.json
└── zh.json
```

Filenames must match the language code listed in `../graphql-glossary.manifest.json` (which the loader walks to discover overlays).

## File schema

Each overlay is a flat JSON object: keys are **English primary terms** (must match exactly one of `entry.terms` from the primary glossary), values are arrays of translated terms in the target language.

```json
{
  "royalties": ["direitos autorais"],
  "contract": ["contrato"],
  "advance": ["adiantamento", "antecipação"]
}
```

## Conventions

1. **Don't include the English loanword in the translation array.** The loader filters loanwords against `entry.terms` (case-insensitive) at load time anyway, but it's clearer to leave them out. The primary entry already covers the English form at full English-stemmer weight.

2. **Use NFC-normalized native script.** All non-Latin overlays (`ja`, `ko`, `zh`, `hi`, `mr`, `ru`, `uk`, `ar`) are normalized to NFC at write time. `matchGlossary` applies NFC at query time too, so consistency at index time prevents subtle Unicode-equivalence misses.

3. **One file per locale.** Region-tagged locales like `pt-BR` are first-class; the loader treats `pt-BR` and `pt` as distinct because their translations diverge (Brazilian vs European Portuguese vocabulary).

4. **Orphan keys are errors.** A key in an overlay that doesn't match any `entry.terms` from the primary fails `lintGlossary` and refuses to load. If you rename a primary term, update every overlay.

5. **Missing coverage is OK.** Not every term needs a translation in every language. The loader logs a summary count; production search just falls back to the primary English term.

## Adding a new locale

1. Create `<bcp-47-code>.json` in this directory (e.g. `nl.json` for Dutch).
2. Register it in `../graphql-glossary.manifest.json`:
   ```json
   "languages": {
     ...
     "nl": "graphql-glossary-i18n/nl.json"
   }
   ```
3. Add the language code to `SUPPORTED_LANGUAGES` in `packages/search/src/tokenize.ts` so the tokenizer/stemmer/stop-words pick up the right per-language config.
4. Verify the language has a Snowball stemmer in `packages/search/src/stem.ts` (Dutch isn't in the current list — would use identity fallback).
5. Run `pnpm --filter @coda/extensions test:unit` to catch malformed JSON, orphan keys, etc.

## Adding a new primary term

If you add a term to the primary glossary that affects rankings, also add overlays for the same term to each existing language file so non-English users aren't left without coverage. `lintGlossary` produces a `missing-coverage` warning (summarized at startup) when a primary term has no translation in a language.

## How translations flow at runtime

At index time, `localizedKeywordFields` (D10) injects each `localizedTerms.get(lang)` into the BM25 index keyed under that language's tokenizer/stemmer. At query time, the user's `Accept-Language` flows through to `tokenize(query, lang)` so the query tokens match the language-stemmed localized fields.

See `apps/search/src/engine/localized-keywords.ts` for the injection logic and `packages/search/src/stages/glossary-index.ts` for the per-language expansion (post-PR-245 review fix C3).
