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
i18n/en-US.json
{
	"GREETING_TITLE": "Welcome back",
	"GREETING_BODY": "Hello, {name}!",
	"UNREAD_COUNT": "{count, plural, one {# unread message} other {# unread messages}}"
}
i18n/de.json
{
	"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.

src/greeting/index.ts
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

  1. The user's languagede, when Discord is in German
  2. en-USYour source of truth
  3. 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

ExportSignatureWhat it is
messagesMessagesEvery en-US string without placeholders, read in the user's language on each access
formatformat(key, vars?) => stringA string with placeholders filled. vars is required when the key has placeholders, rejected otherwise
useMessages() => stringRe-renders on a language change. Returns the locale code
MessageKeytypeThe union of your en-US keys
Messages, FormatVarstypeThe 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:

PlaceholderType
{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.

src/components/unread-badge.tsx
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.
src/components/greeting.tsx
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.

src/unsuppress/delete-label.ts
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

ProblemResult
A file is not a JSON object, a value is not a string, or a string is invalid ICUBuild fails
@unbound-app/i18n imported without i18n/en-US.jsonBuild fails
A translation has a key en-US lacksWarning, key left out
A translation's placeholders differ from en-USWarning

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

  1. Copy i18n/en-US.json to the language's code, such as i18n/ja.json.
  2. 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.