# Internationalization

Strings that the components speak come from a message catalog. They are not hard-coded at every call site. The built-in locales are `en` and `zh-CN`. The type `AstryxLocale` is that pair.

Provide the locale once at the root. Descendants read it with `useAstryxLocale` and `useAstryxMessages`. If nobody provides one, the locale is `en`.

```ts
import { provideAstryxLocale } from '@astryx-vue/core/locale'

provideAstryxLocale('zh-CN')
```

`provideAstryxLocale`, `useAstryxLocale`, `useAstryxMessages`, `useLocale`, `useTranslator`, `normalizeLocale`, `loadAstryxCatalog`, and `useCollator` come from `@astryx-vue/core/locale` and are re-exported from `@astryx-vue/core`. `formatCalendarMonth` and `weekdayNames` stay on `@astryx-vue/core/locale`.

`normalizeLocale` maps `zh`, `zh-CN`, and `zh-Hans` to `zh-CN`, the built-in Simplified Chinese catalog. Every other tag keeps its region and only has its case fixed, so `en-gb` becomes `en-GB` and dates, numbers, and sorting follow that region. Component copy has two built-in catalogs, Simplified Chinese and English; `en`, `en-GB`, or `fr-FR` without `messages` all read the English one. An empty or unparseable tag becomes `en`.

## Reading the locale outside a component

Components format dates, numbers, relative times, and sort order with the locale of the nearest `InternationalizationProvider`, never `navigator.language`. Do the same in your own helpers:

```ts
import { useLocale, useTranslator } from '@astryx-vue/core/locale'

const { locale } = useLocale()
const { t } = useTranslator()

// Both follow the provider when it switches language.
const label = computed(() => new Intl.DateTimeFormat(locale.value).format(date))
const announce = () => t('@astryx.pagination.pageAnnounce', { current: 3 })
```

`t` takes an upstream `@astryx.*` key and returns the sentence for the active locale. An unknown key comes back unchanged, so a typo is easy to spot. It is safe to call from event handlers.

## Locales besides Chinese and English

Upstream Astryx ships about 30 React Intl JSON files in `packages/core/locales`, including `ja-JP`, `zh-TW`, and `ar-SA`. Those files are vendored, unchanged, under this package's `locales/` directory. They are not copied into TypeScript by hand. `loadAstryxCatalog` maps their keys onto `AstryxMessages`. Pass the result as the second argument of `provideAstryxLocale`.

Keys that do not fit a field are returned in `unmapped`, with the original sentence still attached. Nothing in the file is dropped quietly. Fields the file never fills stay in English; their paths are `fallbackFields`. Mapped keys missing from that file are `missingKeys`.

The built-in English matches upstream `en.json` word for word, and `pnpm check:i18n-catalog` keeps it that way. The built-in `zh-CN` is written here and reads more naturally than upstream `zh-CN.json`; do not replace it with that file. Use the loader for the other locales.

```ts
import { loadAstryxCatalog, provideAstryxLocale } from '@astryx-vue/core/locale'
import catalog from '@astryx-vue/core/locales/ja-JP.json'

const loaded = loadAstryxCatalog('ja-JP', catalog)
provideAstryxLocale(loaded.locale, loaded.messages)
```

`provideAstryxLocale('ja-JP')` without `messages` still formats dates and `useCollator` in Japanese, but component sentences stay in English. `zh-TW` does not silently become Simplified Chinese.

To refresh the JSON, run `node scripts/vendor-astryx-locales.mjs`. It follows the latest upstream `main`; pass a commit to reproduce an earlier fetch. The commit used is written to `locales/SOURCE.json`, and the source and license are recorded in `packages/core/THIRD_PARTY_NOTICES.md`.

Then run `pnpm check:i18n-catalog` (it is part of `pnpm check`). It checks that:

- every built-in field exists in both `en` and `zh-CN`, with the same shape;
- the built-in copy uses every upstream placeholder, and the built-in English matches upstream word for word;
- every translation in the 29 locale files parses, uses the same placeholder names and kinds (`{name}`, `number`, `plural`, `select`) as the English source, and has no stale keys;
- every upstream key is either mapped in `packages/core/src/locale/upstream/` or listed with a reason in `scripts/i18n-catalog/coverage.mjs`.

## Locale-aware comparison

`useCollator` returns an `Intl.Collator` for the active locale, wrapped in a computed. With no provider, the locale is `en`.

```ts
import { useCollator } from '@astryx-vue/core/locale'

const collator = useCollator({ numeric: true })
items.sort((a, b) => collator.value.compare(a.name, b.name))
```

`AstryxMessages` is the copy components actually read. Text that is not in that type is the `label` you passed, and it does not change with the locale.

The Chinese and English docs are two VitePress trees, `/` and `/en/`. That is the documentation language, not `provideAstryxLocale`. Component examples call `provideAstryxLocale` for the language of the page you are reading.

## Right-to-left and pseudo-locale testing

Arabic, Hebrew, and Persian read right to left. The components do most of that work for you, but two steps are yours.

### Step 1: tell the components the direction

`InternationalizationProvider` takes `dir`, either `'ltr'` or `'rtl'`. Leave it out and the direction is derived from `locale`: `ar-SA` and `he` resolve to `rtl`, everything else to `ltr`.

The derivation is `getLocaleDirection()`. It asks the browser for the writing direction through `Intl.Locale`; where that is unavailable it falls back to a built-in list of right-to-left languages (`ar`, `he`, `fa`, `ur`, `ps`, `sd`, and a few more). A tag it cannot parse resolves to `ltr` rather than throwing.

```vue
<script setup lang="ts">
import { InternationalizationProvider } from '@astryx-vue/core/InternationalizationProvider'
</script>

<template>
  <!-- Direction derived from locale -->
  <InternationalizationProvider locale="ar-SA">
    <App />
  </InternationalizationProvider>

  <!-- English strings, forced right to left: for testing the layout -->
  <InternationalizationProvider locale="en" dir="rtl">
    <App />
  </InternationalizationProvider>
</template>
```

Inside a composable, read the direction with `useLocaleDirection()`. It returns a ref that follows the locale:

```ts
import { useLocaleDirection } from '@astryx-vue/core/InternationalizationProvider'

const direction = useLocaleDirection()
// 'ltr' or 'rtl'
```

### Step 2: set `dir` on the DOM yourself

`dir` only decides what Astryx components read. It does not touch the DOM. That is your job, usually on `<html>`:

```ts
import { getLocaleDirection } from '@astryx-vue/core/InternationalizationProvider'

// Keep it in sync when the locale changes in the browser
document.documentElement.dir = getLocaleDirection(locale.value)
document.documentElement.lang = locale.value
```

Skip this and text still runs left to right, punctuation and mixed-direction text flow the wrong way, and the scrollbar sits on the wrong side. The components mirror correctly inside a page that still looks backwards.

### What the components already do

- **Spacing and offsets.** Component styles lean on logical properties: `padding-inline-start`, `margin-inline-end`, `inset-inline-start`. Reverse the direction and those values swap sides with no second stylesheet.
- **Text alignment.** They use `text-align: start` and `end`, not `left` and `right`.
- **A few places need help.** Tooltip and hover-card `start` and `end` placements flip under `[dir='rtl']`; the breadcrumb slash is mirrored; the slider thumb is corrected with `:dir(rtl)`.
- **Arrow keys.** The calendar reads `document.documentElement.dir`, so with right-to-left text the left and right arrow keys swap. Scroll calculations also map to the logical axis.

That last point is why step 2 cannot be skipped: the calendar and the scroll logic read `dir` from the DOM, not from the provider.

### Pseudo-locale testing

The cheapest check needs no language pack, just two moves.

1. Set `dir` to `rtl` and put `dir="rtl"` on `<html>`. See whether the page still holds together.
2. Replace the strings with longer fake text and see whether the layout breaks.

Step 2 uses the `overrides` prop. It replaces the keys you name, per locale:

```vue
<script setup lang="ts">
import { InternationalizationProvider } from '@astryx-vue/core/InternationalizationProvider'

// Keys match the upstream locale files. This one is deliberately long.
const pseudo = {
  '@astryx.table.noData': 'No data found, and this sentence is written far longer than anyone would, on purpose, to see whether the cell bursts',
}
</script>

<template>
  <InternationalizationProvider locale="en" :overrides="pseudo">
    <App />
  </InternationalizationProvider>
</template>
```

### Checklist

With the direction reversed and the text stretched, walk through these.

- Numbers, prices, and dates still read left to right instead of flipping with the paragraph.
- Icons and text are in the right order: the back arrow points right, and the dropdown chevron sits to the left of the label.
- The caret starts on the right in a text input, and the placeholder aligns right.
- The sidebar and side panels moved to the right, and the resize handle reversed with them. Whatever did not move has a hardcoded `left` or `right`.
- Buttons do not wrap, table columns do not collapse, and headings do not overflow their block.
- Numeric table columns stay right-aligned; numbers themselves run left to right.

Run that list once in dark mode and once at a narrow width and you have most of the coverage you need.
