i18n
NextVM's i18n system is integrated into the framework core, not bolted on. It works across server messages, client notifications, and NUI interfaces with type-safe translation keys and a fallback chain.
requires every user-facing string to use a translation key — not a hardcoded literal.
defineLocale
Each module ships its locale files under src/shared/locales/:
// modules/banking/src/shared/locales/en.ts
import { defineLocale } from '@nextvm/i18n'
export default defineLocale({
'banking.transfer_success': 'Transferred ${amount} to {target}.',
'banking.transfer_failed': 'Transfer failed: {reason}.',
'banking.insufficient_funds': 'Insufficient funds.',
'banking.balance': 'Cash: ${cash} — Bank: ${bank}',
})// modules/banking/src/shared/locales/de.ts
import { defineLocale } from '@nextvm/i18n'
export default defineLocale({
'banking.transfer_success': '{amount}€ an {target} überwiesen.',
'banking.transfer_failed': 'Überweisung fehlgeschlagen: {reason}.',
'banking.insufficient_funds': 'Nicht genug Geld.',
'banking.balance': 'Bargeld: {cash}€ — Bank: {bank}€',
})defineLocale is a typed identity function — it preserves the literal key types so other locales can be type-checked against the base via SameKeys<typeof en, typeof de>.
I18nService
import { I18nService } from '@nextvm/i18n'
const i18n = new I18nService({
defaultLocale: 'en',
onMissing: (key, locale) => {
logger.warn('missing translation', { key, locale })
},
})
// Register module locales at startup
i18n.registerTranslations('banking', 'en', en)
i18n.registerTranslations('banking', 'de', de)
// Set a player's locale (from their FiveM language setting)
i18n.setPlayerLocale(source, 'de')
// Resolve a key for a specific player
const msg = i18n.t(source, 'banking.transfer_success', {
amount: 500,
target: 'Tom',
})
// → '500€ an Tom überwiesen.'Fallback chain
If a key is missing in the player's locale, the resolver tries:
- The player's locale (e.g.
de) - The server's default locale (e.g.
en) - The hard-coded
enbaseline - The raw key as final fallback
Each fallback fires the onMissing callback so you can log + fix it. There are no silent $missing_key$ placeholders in production.
Interpolation
Both {name} and ${name} placeholders are supported:
i18n.translate('banking.balance', 'en', { cash: 100, bank: 500 })
// → 'Cash: $100 — Bank: $500'If a parameter is missing, the placeholder is left intact:
i18n.translate('banking.balance', 'en', { cash: 100 })
// → 'Cash: $100 — Bank: ${bank}'Type safety with SameKeys
@nextvm/i18n exports a type helper that fails compilation if a non-base locale is missing keys from the base:
import { defineLocale, type SameKeys } from '@nextvm/i18n'
const en = defineLocale({
'shop.title': 'Shop',
'shop.buy': 'Buy',
})
// This fails compile if `de` is missing any key from `en`
const de: SameKeys<typeof en, typeof de> = defineLocale({
'shop.title': 'Laden',
// ❌ TS error: 'shop.buy' is missing
})Build-time locale validation
nextvm build extracts locale keys from every defineLocale() call and validates that every key in the base locale (en) exists in every other locale. Missing keys produce warnings:
✓ @nextvm/banking (35ms)
⚠ locale 'de' missing key 'banking.new_field'You can fix these on the spot or accept them — the framework still ships them, falling back to en at runtime.
See also
@nextvm/i18npackage reference- [com/nextvm-official/nextvm/tree/main/docs/concept)