* Backend routes for choosing username in OAuth flow * fix up oauth flow routes * improve URL-related OAuth code * Use user-provided callback addr instead of SELF_ADDR * Revert "Use user-provided callback addr instead of SELF_ADDR" This reverts commit7ea0635d86. * fix flow * fix: backend response for create oauth account * feat: new auth flow (#5840) * update auth with new designs * refactor: auth.js to auth.ts * refactor: componentize auth pages * fix: auth pages height * feat: initial implementation of new sign-in oauth * fix create account flow * fix checkbox * remove hard coded username * implement create user validation endpoint and add more specific error responses * feat: implement under 13 DOB guard and email/password validation route * fix: TOCTOU issue * refactor: pnpm prepr * fix: make sure staging uses staging * fix: hcaptcha styles * fix: copy * remove: auth/welcome page as its no longer used * refactor: bring root page card styles into individual components and use tailwind * fix: account settings modals to use new modal and fix lots of bad styles * refactor: pnpm prepr * feat: implement last signed in indicator * fix: append number when generated name from email is taken * refactor: pnpm prepr * fix: last sign in badge color * fix: qa issues * refactor: pnpm prepr * fix: hover effect on native date picker * chore: temp staging undo * Revert "chore: temp staging undo" This reverts commitcad6bd4f92. * feat: handle app create account * fix: last signed in style * fix: add initOnMounted for SSR race * refactor: use typescript * refactor: pnpm prepr * refactor: use typescript for reset-password * refactor: convert verify-email to use typescript * refactor: convert authorize.vue to use typescript * fix: authorize.vue error states * feat: small style updates * feat: implement date picker component * feat: improve UX and styles for range select * refactor: pnpm prepr * fix: range select border styles * feat: implement date picker component in create account * feat: implement preserve date for date picker * update rust toolchain * increase recursion limit * fix: date picker can be null * fix: calculate age based on user's timezone * fix: number input icons color * fix: date picker icons * feat: improve styles * fix: add width on date * fix: hover color bad on number input * fix lints * feat: add default date open view * fmt * fix: account.vue * fix: remove default date to open 13 years ago * fix: edit copy on info banner * fix: cannot hover over project card tooltip items (#6071) fix: cannot hover over project cards * feat: improve add dependency flow (#6075) * fix: shadow on nav * feat: improve add dependency flow * feat: update suggested dependency style * feat: update dependency rows to use version number and update styles * feat: implement combobox select searched text on focus * feat: add Tabs.vue * feat: update nav tabs to use tabs * feat: improve project search dropdown * fix: dependency search not clearing inbound query * fix: combobox no options open state bug * feat: improve dependency project and version search * fix: open modrinth project links in the app (#6072) * pin tanstack versions + set pnpm min age to 7 days * squash commits * fix: 2 factor auth enter code screen styles * update copy * update copy * improve reset password * feat: update sign in screen * fix: unused import * Merge branch 'main' into boris/dev-908-backend-changes * Revert "Merge branch 'main' into boris/dev-908-backend-changes" This reverts commitb9b03796e3. * fix: add stroke * feat: add passkey support (#6375) * feat: add passkey backend * feat: passkey frontend * invalidate sessions on compromised passkey * chore: run sqlx prepare * fix: make passkey button use both collumns to prevent empty space * fix: correctly verify max passkeys in finish route * fix: use structs for response * fix: add rp name default * style: use web::Json * fmt * feat: improve manage passkeys UI * fix copy * pnpm prepr --------- Co-authored-by: tdgao <mr.trumgao@gmail.com> Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com> Co-authored-by: Michael H. <michael@iptables.sh> Co-authored-by: Calum H. (IMB11) <contact@cal.engineer> Co-authored-by: Calum H. <calum@modrinth.com> Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com> Co-authored-by: DeDiamondPro <67508414+DeDiamondPro@users.noreply.github.com>
6.0 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}}'
Writing Translation-Friendly Strings
ICU gives you powerful tools (plurals, selects, nested expressions), but translators in other languages face constraints that English doesn't have:
- 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 ifitemTypeis 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.
Guidelines
- Use
selectfor content types, not bare variables. When a variable represents different content types (mod, shader, modpack, etc.), pass a key and use ICUselectso translators can write type-specific forms:
// Bad — translators can't inflect around a pre-rendered noun
'Delete {count} {itemType}'
// Good — translators can write entirely different phrases per 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.
-
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.
-
Don't concatenate translated strings. Never build a sentence by joining multiple
formatMessagecalls — the word order may be wrong in other languages. Put the entire sentence in one message. -
Keep variables semantic. Pass
contentType: 'mod'(a key), notcontentType: 'Mod'(a pre-rendered display string). Translators can then map each key to the correct form in their language. -
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.
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 definitionsuseVIntl— composable providingformatMessageIntlFormatted— component for rich-text messagesnormalizeChildren— 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/error.vue