Translations
Show text in the user's Discord language.
Write each string once per language in i18n/. Unbound reads it in the user's Discord language each time you show it.
Quick Start
- manifest.json
- i18nOne file per language
- en-US.jsonEvery key. Required, and the fallback
- de.jsonGerman
- src
- index.tsx
- unbound-i18n.d.tsGenerated from en-US.json. Commit it
{
"GREETING_TITLE": "Welcome back",
"GREETING_BODY": "Hello, {name}!",
"UNREAD_COUNT": "{count, plural, one {# unread message} other {# unread messages}}"
}{
"GREETING_TITLE": "Willkommen zurück",
"GREETING_BODY": "Hallo, {name}!",
"UNREAD_COUNT": "{count, plural, one {# ungelesene Nachricht} other {# ungelesene Nachrichten}}"
}Read plain strings from messages. Use format for strings with placeholders.
import { toasts } from '@unbound-app/api';
import { format, messages } from '@unbound-app/i18n';
export function showGreeting(name: string) {
toasts.show({
title: messages.GREETING_TITLE,
content: format('GREETING_BODY', { name }),
});
}The build makes @unbound-app/i18n from your i18n/ folder. Your keys are kept apart from Unbound's and other plugins', so they need no prefix.
Fallback Order
- The user's languagede, when Discord is in German
- en-USYour source of truth
- The key itselfGREETING_TITLE
A missing translation shows English, so only en-US.json must have every key.
File Names
Name each file after Discord's language code. Most are two letters, such as de, fr and ja. These name a region: en-US, en-GB, es-ES, es-419, pt-BR, sv-SE, zh-CN, zh-TW.
Exports
| Export | Signature | What it is |
|---|---|---|
messages | Messages | Every en-US string without placeholders, read in the user's language on each access |
format | format(key, vars?) => string | A string with placeholders filled. vars is required when the key has placeholders, rejected otherwise |
useMessages | () => string | Re-renders on a language change. Returns the locale code |
MessageKey | type | The union of your en-US keys |
Messages, FormatVars | type | The en-US strings, and each key's placeholders |
@unbound-app/api takes keys for setting labels, command descriptions, and toast and dialog text. @unbound-app/api/testing adds setLocale(locale) for tests. The SDK's i18n namespace is a different API: Unbound's own strings and the AddonLocales type.
Placeholders
Strings use ICU message syntax. # prints a plural's number. Each language writes its own plural forms. format types each key's values from en-US:
| Placeholder | Type |
|---|---|
{name} | string | number |
{count, number}, {count, plural, ...} | number |
{when, date}, {when, time} | Date | number |
{kind, select, a {...} b {...}} | 'a' | 'b', plus any string with an other branch |
Components
The build adds useMessages() to every component and hook that reads messages or format, so it re-renders on a language change. That covers callbacks such as onPress, helpers in the same file, and React.memo children whose props did not change.
import { Text } from 'react-native';
import { format, messages } from '@unbound-app/i18n';
export function UnreadBadge({ count }: { count: number }) {
return (
<Text>
{messages.GREETING_TITLE} · {format('UNREAD_COUNT', { count })}
</Text>
);
}A component is a PascalCase function, a function passed to memo or forwardRef, or a use* hook. Set UNBOUND_VERBOSE=1 to list each one the build changes.
Call useMessages() yourself when:
- The strings come from a helper in another file.
- A memoised value builds on strings. Pass the returned locale as a dependency.
import { format, useMessages } from '@unbound-app/i18n';
import { useMemo } from 'react';
import { Text } from 'react-native';
export function Greeting({ name }: { name: string }) {
const locale = useMessages();
const greeting = useMemo(() => format('GREETING_BODY', { name }), [name, locale]);
return <Text>{greeting}</Text>;
}A class component cannot use hooks. The build warns, with file and line, when one reads messages or format.
Discord's Own Text
To match a label Discord shows, such as "Delete Message", read it from Discord's i18n module by its hashed id from the decompiled bundle. Keep an English fallback.
import { metro } from '@unbound-app/api';
/** Discord's hashed message ID for "Delete Message", the label of the delete row in the message sheet. */
const DELETE_MESSAGE_MESSAGE = 'xwMqD7';
const ENGLISH_LABEL = 'Delete Message';
/**
* @description Reads Discord's "Delete Message" label in the user's language.
* @returns The label, or the English one when Discord's i18n module or the message is missing.
*/
export function getDeleteLabel(): string {
const { i18n } = metro.common;
const message = i18n?.t?.[DELETE_MESSAGE_MESSAGE];
if (!message) return ENGLISH_LABEL;
try {
return i18n.intl.string(message) || ENGLISH_LABEL;
} catch (error) {
logger.error('Failed to read the Delete Message label:', error);
return ENGLISH_LABEL;
}
}Look it up when needed, and guard every step: a Discord update can rename the id or module. Use this only for text that must equal Discord's.
Shipping
en-US.json is in your bundle. Each other language ships as dist/i18n/<code>.json, listed under locales in dist/manifest.json. Unbound downloads all of them on install or update, and loads only the active one into memory. Until it loads, your plugin shows English.
Build Checks
| Problem | Result |
|---|---|
| A file is not a JSON object, a value is not a string, or a string is invalid ICU | Build fails |
@unbound-app/i18n imported without i18n/en-US.json | Build fails |
| A translation has a key en-US lacks | Warning, key left out |
| A translation's placeholders differ from en-US | Warning |
The build writes src/unbound-i18n.d.ts from en-US.json. Commit it and never edit it. ubd dev rewrites it within a second of saving en-US.json, and pushes every language file on save.
Adding a Language
- Copy
i18n/en-US.jsonto the language's code, such asi18n/ja.json. - Translate each string. Keep keys and placeholders unchanged.
A partial file is fine. Missing keys fall back to en-US.
ubd i18n check reports each language's coverage and every placeholder or syntax problem. ubd i18n fix adds missing keys, ubd i18n unused lists unused keys, ubd i18n add adds a key to en-US. See CLI.