> ## Documentation Index
> Fetch the complete documentation index at: https://narrator.ami.rip/llms.txt
> Use this file to discover all available pages before exploring further.

# Locale

> The type an output language implements.

```ts theme={null}
import { extendLocale, mergePhrases, type Locale, type Grammar, type CorePhrases } from "@usenarrator/core";
import { en } from "@usenarrator/locale-en";
```

```ts theme={null}
interface Locale {
  id: string;
  grammar: Grammar;
  phrases: CorePhrases;
  plugins?: { [K in keyof PluginPhrases]?: DeepPartial<PluginPhrases[K]> } & Record<string, unknown>;
}

interface PluginPhrases {} // plugins add themselves by declaration merging

// Every nested group optional; phrases and strings stay whole.
type DeepPartial<T> = T extends (...args: any[]) => unknown ? T : T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
```

<ParamField path="id" type="string" required>
  A BCP 47 tag such as `"en"`, `"es"` or `"pt-BR"`. Plugins look up their own phrasebooks by this id.
</ParamField>

<ParamField path="grammar" type="Grammar" required>
  Word mechanics for this language.
</ParamField>

<ParamField path="phrases" type="CorePhrases" required>
  Every phrase the TypeScript engine can produce.
</ParamField>

<ParamField path="plugins" type="Record<string, unknown>">
  Phrasebooks for plugins that don't ship this locale, keyed by plugin name. They can be partial: each phrase here takes priority over the plugin's own translation, and anything missing falls back to it and then to the plugin's English. Entries for plugins registered in `PluginPhrases` are type-checked against that plugin's phrasebook type. See [Typed overrides](/guides/translating#typed-overrides).
</ParamField>

## extendLocale

```ts theme={null}
function extendLocale(
  base: Locale,
  ext: { id: string; grammar?: Partial<Grammar>; phrases?: DeepPartial<CorePhrases>; plugins?: Locale["plugins"] },
): Locale;
```

Builds a locale on top of another. `phrases` and each plugin's phrasebook are merged into the base key by key, so anything you leave out keeps the base's wording. `grammar` helpers replace the base's one by one. This is the recommended way to start a translation.

## mergePhrases

```ts theme={null}
function mergePhrases<T>(base: T, ...overrides: (DeepPartial<T> | undefined)[]): T;
```

The merge `extendLocale` and the engine use: later phrasebooks win, nested groups are merged rather than replaced, and anything they leave out comes from the earlier ones.

## Grammar

```ts theme={null}
interface Grammar {
  list(items: string[], conj?: "and" | "or"): string;
  formatNumber(n: number): string;
  article(noun: string): string;
  possessive(noun: string): string;
  plural(noun: string): string;
  singular(noun: string): string;
  isPlural(noun: string): boolean;
  negatePredicateHead(head: string): string;
  bareNoun(phrase: string): string;
  isPluralSubject(subject: string): boolean;
  isTimeLike(phrase: string): boolean;
}
```

| Method | English behaviour |
| - | - |
| `list` | `["a", "b", "c"]` becomes "a, b, and c" |
| `article` | "a" or "an" |
| `possessive` | "customer" becomes "customer's", "users" becomes "users'" |
| `negatePredicateHead` | "is" becomes "is not", "has" becomes "has no", "can" becomes "cannot" |
| `bareNoun` | "a number" becomes "number", for use after "a list of" |
| `isPluralSubject` | `true` for "customer's items" and `false` for "the number of items", so verbs agree ("are" or "is") |
| `isTimeLike` | `true` for phrases like "expires at" or "now", so comparisons read "before" and "after" |

All of these receive strings that may contain [markup](/concepts/markup).

There is no hook for grammatical gender, because the engine doesn't know a noun's gender. `article` receives the noun it goes with, so a locale can look the gender up itself. See [Grammar, gender and number](/guides/translating#grammar-gender-and-number).

## CorePhrases

`CorePhrases` groups phrases by area. Each phrase is a function from already-rendered pieces to a string, or a plain string for fixed words.

| Area | Covers | Example phrase |
| - | - | - |
| `expressions` | Values, operators, member access, arrays, objects, ternaries, assignment, lambdas, JSX, errors | `ops["+"]`, `member.countOf`, `ternary.inline` |
| `calls` | Calls no plugin claimed | `action`, `predicate`, `convert`, `newInstance` |
| `conditions` | Yes or no conditions | `isEmpty`, `equals`, `compare["<"]`, `instanceOf` |
| `statements` | Control flow | `ifThen`, `forOf`, `return`, `throw`, `try`, `switch` |
| `declarations` | Functions, classes, variables, type headings | `let`, `signature`, `class`, `title.converter` |
| `types` | Type annotations and the type explainer | `listOf`, `oneOf`, `objectWith`, `pick`, `omit` |

The complete, commented type ships with `@usenarrator/core`. The English implementation is on GitHub in [`packages/locale-en/src/phrases`](https://github.com/SirTenzin/narrator/tree/main/packages/locale-en/src/phrases), one file per area.

## Example

```ts theme={null}
import { extendLocale } from "@usenarrator/core";
import { en } from "@usenarrator/locale-en";

const es = extendLocale(en, {
  id: "es",
  phrases: { statements: { ifThen: ({ condition, action }) => `Si ${condition}, ${action}` } },
});
```

See [Translating Narrator](/guides/translating) for a full walkthrough.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.