Skip to main content

useTranslation

Bind an addon's typed translation verbs to the player viewing the UI.

Import​

import { useTranslation } from '@bedrock-core/ui';

Signature​

function useTranslation<R>(instance: I18n<R>): BoundI18n<R>

Parameters​

ParameterTypeDescription
instanceI18n<R>The addon's own createI18n(bundle) instance. Passing it is what keeps the verbs fully typed — selectors, interpolation arguments and plural leaves all come from that bundle's type

Returns​

BoundI18n<R> — The verb set bound to the viewing player's resolved locale — locale, t, key, raw, resolve, display.

This is exactly instance.forPlayer(usePlayer()).

Usage​

import { Panel, Text, useTranslation } from '@bedrock-core/ui';
import { i18n } from '../i18n';

function Shop() {
const { t, key, raw } = useTranslation(i18n);

return (
<Panel padding={10} gap={4}>
<Text>{key($ => $.shop.title)}</Text>
<Text>{raw($ => $.shop.bought, { item: 'Apple', price: 64 })}</Text>
<Text>{t($ => $.shop.total, { amount: 128 })}</Text>
</Panel>
);
}

Where i18n is the addon's single instance:

// BP/scripts/i18n.ts
import bundle from '@bedrock-core/generated/i18n';
import { createI18n } from '@bedrock-core/i18n';

export const i18n = createI18n(bundle);

See the i18n API reference for what each verb does and when to prefer which.

The locale it binds to​

The bound locale comes from the locale resolution chain: a persisted per-player override, then the client language, a sibling region, then the bundle's default.

locale on the returned set is the one that won, which is handy to show back to the player:

const { t, locale } = useTranslation(i18n);

<Text>{t($ => $.settings.currentLanguage, { locale })}</Text>

Examples​

Plurals​

A plural leaf needs a bound verb set, because which suffix wins depends on the target language's rules. useTranslation is that binding.

function Stock({ count }: { count: number }) {
const { t } = useTranslation(i18n);

return <Text>{t($ => $.shop.stock, { count })}</Text>;
}

Switching language from the UI​

setLocale writes a dynamic property rather than component state, but you do not have to force a refresh: the new locale is already in effect when the screen comes back.

function LanguagePicker() {
const player = usePlayer();
const { t } = useTranslation(i18n);

return (
<Panel flexDirection={'row'} gap={4}>
<Button onPress={() => i18n.setLocale(player, 'es_ES')}>
<Text>{t($ => $.settings.spanish)}</Text>
</Button>
<Button onPress={() => i18n.clearLocale(player)}>
<Text>{t($ => $.settings.systemLanguage)}</Text>
</Button>
</Panel>
);
}

Turning a key back into a string​

display() resolves any DisplayText — a literal, a key, or a RawMessage — to a plain string, for the places a key must become text: chat prefixes, string concatenation, values passed to APIs that take no RawMessage.

function BuyButton({ item }: { item: Item }) {
const player = usePlayer();
const { display, key, raw } = useTranslation(i18n);

return (
<Button onPress={() => {
const prefix = display(key($ => $.shop.chatPrefix));
player.sendMessage(`${prefix} ${display(raw($ => $.shop.bought, { item: item.name, price: item.price }))}`);
}}>
<Text>{key($ => $.shop.buy)}</Text>
</Button>
);
}

For chat specifically, prefer sending the RawMessage itself — player.sendMessage(raw(…)) lets the client resolve it. Reach for display() only when you need a string.

Outside a component​

There is no fiber and no player to read, so bind explicitly instead — it is the same call:

world.afterEvents.playerSpawn.subscribe(({ player }) => {
const { raw } = i18n.forPlayer(player);
player.sendMessage(raw($ => $.shop.welcome, { name: player.name }));
});

Notes​

Create the instance once​

// ✅ Good — one module-scope instance, imported everywhere
export const i18n = createI18n(bundle);

// ❌ Bad — a new instance per render re-registers the default source every pass
function Screen() {
const i18n = createI18n(bundle);
const { t } = useTranslation(i18n);
}

Prefer key() / raw() over t() for painted text​

key() and raw() let the client resolve the string in its own language, with no length cap. Reach for t() when your code needs the actual string — layout maths, chat composition, a value you concatenate.

Don't hand-roll the hook​

// ❌ Redundant
const { t } = i18n.forPlayer(usePlayer());

// ✅ Same thing, one call
const { t } = useTranslation(i18n);
  • i18n — the verbs, plurals, interpolation and locale chain
  • useTranslationResolver — the active resolver, for components that build display strings themselves
  • TranslationContext — overriding which resolver a subtree sees