Files
Modrinth-plus/standards/frontend/INTERNATIONALIZATION.md
Calum H. d0c7575a23 feat: move notion docs to standards folder (#5590)
* feat: move notion docs to standards folder

* fix: remove skills mention (automatic now)
2026-03-16 17:30:05 +00:00

3.7 KiB

Internationalization (i18n)

All user-visible strings in Vue SFCs must use the localization system from @modrinth/ui. No hard-coded English strings should appear in templates or script — everything comes from formatMessage or <IntlFormatted>.

Translatable Strings

User-visible strings include: inner text, alt attributes, placeholder attributes, button labels, dropdown option labels, notification messages, etc.

Dynamic expressions ({{ user.name }}) and HTML tags are not translatable strings — only static human-readable text.

Message Definitions

Messages are defined with defineMessage or defineMessages from @modrinth/ui in <script setup>. Each message has a unique id and a defaultMessage containing the English string:

const messages = defineMessages({
	welcomeTitle: { id: 'auth.welcome.title', defaultMessage: 'Welcome' },
	welcomeDescription: { id: 'auth.welcome.description', defaultMessage: "You're now part of the community…" },
})

Message ids should be descriptive and stable (e.g. error.generic.default.title). Group related messages together with defineMessages.

Rendering Messages

Use useVIntl() from @modrinth/ui for simple string formatting:

const { formatMessage } = useVIntl()
<button>{{ formatMessage(messages.welcomeTitle) }}</button>
{{ formatMessage(messages.greeting, { name: user.name }) }}

ICU Message Format

Dynamic values use ICU placeholders in defaultMessage:

  • Variables: 'Hello, {name}!'
  • Numbers/dates/times: '{price, number, ::currency/USD}'
  • Plurals/selects: '{count, plural, one {# message} other {# messages}}'

Rich-Text Messages

When a message contains links or markup, wrap the relevant ranges with named tags in defaultMessage:

"By creating an account, you agree to our <terms-link>Terms</terms-link> and <privacy-link>Privacy Policy</privacy-link>."

Render with the <IntlFormatted> component using named slots:

<IntlFormatted :message-id="messages.tosLabel">
	<template #terms-link="{ children }">
		<NuxtLink to="/terms">
			<component :is="() => children" />
		</NuxtLink>
	</template>
	<template #privacy-link="{ children }">
		<NuxtLink to="/privacy">
			<component :is="() => children" />
		</NuxtLink>
	</template>
</IntlFormatted>

For simple emphasis ('Welcome to <strong>Modrinth</strong>!'):

<template #strong="{ children }">
	<strong><component :is="() => children" /></strong>
</template>

For complex child handling, use normalizeChildren from @modrinth/ui:

<template #bold="{ children }">
	<strong><component :is="() => normalizeChildren(children)" /></strong>
</template>

Vue/ICU Delimiter Collisions

If an ICU placeholder ends right before }} in a Vue template, insert a space (} }) to avoid parsing issues.

Imports

All i18n utilities come from @modrinth/ui:

  • defineMessage / defineMessages — message definitions
  • useVIntl — composable providing formatMessage
  • IntlFormatted — component for rich-text messages
  • normalizeChildren — helper for complex rich-text slot children

Reference Examples

  • Variables and plurals: apps/frontend/src/pages/frog.vue
  • Rich-text with link tags: apps/frontend/src/pages/auth/welcome.vue and apps/frontend/src/error.vue