Skip to content

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/:

typescript
// 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}',
})
typescript
// 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

typescript
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:

  1. The player's locale (e.g. de)
  2. The server's default locale (e.g. en)
  3. The hard-coded en baseline
  4. 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:

typescript
i18n.translate('banking.balance', 'en', { cash: 100, bank: 500 })
// → 'Cash: $100 — Bank: $500'

If a parameter is missing, the placeholder is left intact:

typescript
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:

typescript
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

Released under the LGPL-3.0 License.