Skip to content

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.