mirror of
https://github.com/modrinth/code.git
synced 2026-08-28 02:24:56 +00:00
chore: cleanup standards (#6970)
* chore: cleanup standards & skills * remove: figma mcp doc, not needed anymore
This commit is contained in:
@@ -1,40 +1,51 @@
|
||||
- [Internationalization (i18n)](#internationalization-i18n)
|
||||
- [Translatable Strings](#translatable-strings)
|
||||
- [Message Definitions](#message-definitions)
|
||||
- [Rendering Messages](#rendering-messages)
|
||||
- [ICU Message Format](#icu-message-format)
|
||||
- [Writing Translation-Friendly Strings](#writing-translation-friendly-strings)
|
||||
- [Rich-Text Messages](#rich-text-messages)
|
||||
- [Vue/ICU Delimiter Collisions](#vueicu-delimiter-collisions)
|
||||
- [Internationalization](#internationalization)
|
||||
- [Translatable strings](#translatable-strings)
|
||||
- [Message definitions](#message-definitions)
|
||||
- [Render messages](#render-messages)
|
||||
- [ICU message format](#icu-message-format)
|
||||
- [Write strings for translation](#write-strings-for-translation)
|
||||
- [Rich-text messages](#rich-text-messages)
|
||||
- [Vue and ICU delimiter conflicts](#vue-and-icu-delimiter-conflicts)
|
||||
- [Imports](#imports)
|
||||
- [Reference Examples](#reference-examples)
|
||||
- [Reference examples](#reference-examples)
|
||||
|
||||
# Internationalization (i18n)
|
||||
# Internationalization
|
||||
|
||||
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>`.
|
||||
Use the `@modrinth/ui` localization system for all user-visible strings in Vue single-file components (SFCs).
|
||||
|
||||
Do not put hard-coded English text in templates or scripts. Get all user-visible text from `formatMessage` or `<IntlFormatted>`.
|
||||
|
||||
## Translatable Strings
|
||||
|
||||
User-visible strings include: inner text, `alt` attributes, `placeholder` attributes, button labels, dropdown option labels, notification messages, etc.
|
||||
Translate these user-visible items:
|
||||
|
||||
Dynamic expressions (`{{ user.name }}`) and HTML tags are not translatable strings — only static human-readable text.
|
||||
- Inner text.
|
||||
- `alt` and `placeholder` attributes.
|
||||
- Button and dropdown-option labels.
|
||||
- Notification and error messages.
|
||||
|
||||
Do not translate dynamic expressions, HTML tag names, CSS classes, internal identifiers, or log messages.
|
||||
|
||||
In `{{ user.name }}`, only the static text around the expression needs translation.
|
||||
|
||||
## 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:
|
||||
Use `defineMessage` or `defineMessages` from `@modrinth/ui` in `<script setup>`.
|
||||
|
||||
Give each message a unique `id`. Put the English text in `defaultMessage`:
|
||||
|
||||
```ts
|
||||
const messages = defineMessages({
|
||||
welcomeTitle: { id: 'auth.welcome.title', defaultMessage: 'Welcome' },
|
||||
welcomeDescription: { id: 'auth.welcome.description', defaultMessage: "You're now part of the community…" },
|
||||
welcomeDescription: { id: 'auth.welcome.description', defaultMessage: 'You are now part of the community.' },
|
||||
})
|
||||
```
|
||||
|
||||
Message `id`s should be descriptive and stable (e.g. `error.generic.default.title`). Group related messages together with `defineMessages`.
|
||||
Use descriptive, stable message IDs, such as `error.generic.default.title`. Put related messages in one `defineMessages` object.
|
||||
|
||||
## Rendering Messages
|
||||
## Render Messages
|
||||
|
||||
Use `useVIntl()` from `@modrinth/ui` for simple string formatting:
|
||||
Use `useVIntl()` from `@modrinth/ui` to format simple strings:
|
||||
|
||||
```ts
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -47,51 +58,63 @@ const { formatMessage } = useVIntl()
|
||||
|
||||
## ICU Message Format
|
||||
|
||||
Dynamic values use ICU placeholders in `defaultMessage`:
|
||||
Use ICU placeholders for dynamic values in `defaultMessage`:
|
||||
|
||||
- **Variables:** `'Hello, {name}!'`
|
||||
- **Numbers/dates/times:** `'{price, number, ::currency/USD}'`
|
||||
- **Plurals/selects:** `'{count, plural, one {# message} other {# messages}}'`
|
||||
- Variable: `'Hello, {name}!'`
|
||||
- Number, date, or time: `'{price, number, ::currency/USD}'`
|
||||
- Plural or selection: `'{count, plural, one {# message} other {# messages}}'`
|
||||
|
||||
## Writing Translation-Friendly Strings
|
||||
## Write Strings for Translation
|
||||
|
||||
ICU gives you powerful tools (plurals, selects, nested expressions), but translators in other languages face constraints that English doesn't have:
|
||||
ICU supports plurals, selections, and nested expressions. Languages can have different grammar rules.
|
||||
|
||||
- **Word order varies by language.** Don't assume `{action} {noun}` works everywhere — some languages need `{noun} {action}` or require prepositions between them.
|
||||
- **Plurals aren't just "add an s".** Many languages change internal parts of a word or phrase for pluralization, not just the ending. A simple `{count} {itemType}` breaks if `itemType` is always singular.
|
||||
- **Grammatical gender affects surrounding words.** Articles, adjectives, and verbs may change based on whether a noun is masculine or feminine. If a variable like `{contentType}` can be "shader" or "mod", translators may need to inflect surrounding text differently for each.
|
||||
- Word order changes between languages. Do not assume that `{action} {noun}` operates in all languages.
|
||||
- Plural forms can change a complete word or phrase. Do not only add an `s` to make a plural.
|
||||
- Grammatical gender can change articles, adjectives, and verbs. Give translators a separate branch for each content type.
|
||||
|
||||
### Guidelines
|
||||
|
||||
1. **Use `select` for content types, not bare variables.** When a variable represents different content types (mod, shader, modpack, etc.), pass a key and use ICU `select` so translators can write type-specific forms:
|
||||
1. Use `select` for content types. Do not use a bare variable for a content type.
|
||||
|
||||
```
|
||||
// Bad — translators can't inflect around a pre-rendered noun
|
||||
Pass a content-type key. Then, use ICU `select` so translators can write a specific form for each type:
|
||||
|
||||
```text
|
||||
// Incorrect. Translators cannot change the grammar around this rendered noun.
|
||||
'Delete {count} {itemType}'
|
||||
|
||||
// Good — translators can write entirely different phrases per type
|
||||
// Correct. Translators can write a different phrase for each type.
|
||||
'Delete {count} {contentType, select, mod {{count, plural, one {mod} other {mods}}} shader {{count, plural, one {shader} other {shaders}}} other {items}}'
|
||||
```
|
||||
|
||||
This lets translators write entirely different noun forms per branch, which many languages require.
|
||||
This structure lets translators write different noun forms in each branch.
|
||||
|
||||
2. **Prefer separate messages over complex ICU when branches diverge significantly.** If the singular and plural versions of a string are structurally different (not just a noun change), use two separate message IDs rather than one complex ICU expression.
|
||||
2. Use separate messages when ICU branches have different sentence structures.
|
||||
|
||||
3. **Don't concatenate translated strings.** Never build a sentence by joining multiple `formatMessage` calls — the word order may be wrong in other languages. Put the entire sentence in one message.
|
||||
If singular and plural text have different structures, use two message IDs. Do not make one complex ICU expression.
|
||||
|
||||
4. **Keep variables semantic.** Pass `contentType: 'mod'` (a key), not `contentType: 'Mod'` (a pre-rendered display string). Translators can then map each key to the correct form in their language.
|
||||
3. Do not join translated strings.
|
||||
|
||||
5. **Test with long strings.** German and Finnish words can be 2-3x longer than English equivalents. Ensure UI layouts don't break with longer text.
|
||||
Do not make a sentence from multiple `formatMessage` calls. Put the complete sentence in one message.
|
||||
|
||||
4. Use semantic variable values.
|
||||
|
||||
Pass `contentType: 'mod'` as a key. Do not pass `contentType: 'Mod'` as rendered text.
|
||||
|
||||
The translator can map each key to the correct form.
|
||||
|
||||
5. Test the UI with long strings.
|
||||
|
||||
Some translated words can be two or three times longer than the English words. Make sure that the layout remains correct.
|
||||
|
||||
## Rich-Text Messages
|
||||
|
||||
When a message contains links or markup, wrap the relevant ranges with named tags in `defaultMessage`:
|
||||
When a message contains links or markup, put named tags around the applicable text in `defaultMessage`:
|
||||
|
||||
```
|
||||
"By creating an account, you agree to our <terms-link>Terms</terms-link> and <privacy-link>Privacy Policy</privacy-link>."
|
||||
```text
|
||||
"When you create an account, you agree to the <terms-link>Terms</terms-link> and <privacy-link>Privacy Policy</privacy-link>."
|
||||
```
|
||||
|
||||
Render with the `<IntlFormatted>` component using named slots:
|
||||
Use named slots in `<IntlFormatted>` to render the tags:
|
||||
|
||||
```vue
|
||||
<IntlFormatted :message-id="messages.tosLabel">
|
||||
@@ -108,7 +131,11 @@ Render with the `<IntlFormatted>` component using named slots:
|
||||
</IntlFormatted>
|
||||
```
|
||||
|
||||
For simple emphasis (`'Welcome to <strong>Modrinth</strong>!'`):
|
||||
Use this pattern for simple emphasis:
|
||||
|
||||
```text
|
||||
'Welcome to <strong>Modrinth</strong>!'
|
||||
```
|
||||
|
||||
```vue
|
||||
<template #strong="{ children }">
|
||||
@@ -116,7 +143,7 @@ For simple emphasis (`'Welcome to <strong>Modrinth</strong>!'`):
|
||||
</template>
|
||||
```
|
||||
|
||||
For complex child handling, use `normalizeChildren` from `@modrinth/ui`:
|
||||
Use `normalizeChildren` from `@modrinth/ui` for complex child content:
|
||||
|
||||
```vue
|
||||
<template #bold="{ children }">
|
||||
@@ -124,20 +151,20 @@ For complex child handling, use `normalizeChildren` from `@modrinth/ui`:
|
||||
</template>
|
||||
```
|
||||
|
||||
## Vue/ICU Delimiter Collisions
|
||||
## Vue and ICU Delimiter Conflicts
|
||||
|
||||
If an ICU placeholder ends right before `}}` in a Vue template, insert a space (`} }`) to avoid parsing issues.
|
||||
If an ICU placeholder ends immediately before `}}`, add a space. Use `} }` to prevent a Vue parser error.
|
||||
|
||||
## Imports
|
||||
|
||||
All i18n utilities come from `@modrinth/ui`:
|
||||
Get all internationalization utilities 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
|
||||
- `defineMessage` and `defineMessages`: Define messages.
|
||||
- `useVIntl`: Supplies `formatMessage`.
|
||||
- `IntlFormatted`: Renders rich-text messages.
|
||||
- `normalizeChildren`: Normalizes complex rich-text slot children.
|
||||
|
||||
## Reference Examples
|
||||
|
||||
- Variables and plurals: `apps/frontend/src/pages/frog.vue`
|
||||
- Rich-text with link tags: `apps/frontend/src/error.vue`
|
||||
- Variables and plurals: `apps/frontend/src/pages/frog.vue`.
|
||||
- Rich text with link tags: `apps/frontend/src/error.vue`.
|
||||
|
||||
Reference in New Issue
Block a user