Skip to content

Internationalization (i18n)

@sentinel-password/core has pluggable message templates. Validators emit a stable MessageCode (e.g. 'length.tooShort') plus a params object of interpolation values, then render the user-facing string through a fallback chain you control:

  1. options.formatMessage(code, params, defaultMessage) if provided (highest precedence — integrates with react-intl, i18next, FormatJS/ICU, etc.)
  2. options.messages?.[code] template if provided ({placeholder} tokens are substituted)
  3. Built-in English default

If neither option is set, you get the legacy English strings — backwards-compatible with existing apps using the lookup-table workaround below.

The codes

MessageCodeParamsDefault English template
length.tooShort{ minLength }Password must be at least {minLength} characters
length.tooLong{ maxLength }Password must be at most {maxLength} characters
characterTypes.missing{ missing, missingTypes }Password must contain at least one {missing}
repetition.tooMany{ maxRepeatedChars }Password contains too many repeated characters (max {maxRepeatedChars})
sequential.found{}Password contains sequential characters (e.g., abc, 123)
keyboardPattern.found{}Password contains common keyboard patterns
commonPassword.found{}Password is too common. Please choose a more unique password.
personalInfo.found{}Password contains personal information

Codes are part of the public API and stable across patch and minor releases. The default English strings remain stable too, so the lookup-table pattern documented below keeps working.

For characterTypes.missing, two params are provided:

  • params.missing — joined English ('uppercase letter, digit') used by the default template.
  • params.missingTypes — raw comma-separated tokens ('uppercase,digit') — split it to translate type names individually.

Pattern 1 — messages: template map

Best for static locales with a small set of fixed messages. Pass partial overrides keyed by MessageCode; missing codes fall back to English.

typescript
import { validatePassword, type MessageCode } from '@sentinel-password/core'

const es: Partial<Record<MessageCode, string>> = {
  'length.tooShort': 'La contraseña debe tener al menos {minLength} caracteres',
  'length.tooLong': 'La contraseña no debe superar los {maxLength} caracteres',
  'characterTypes.missing': 'La contraseña debe contener al menos un {missing}',
  'repetition.tooMany': 'Demasiados caracteres repetidos (máx. {maxRepeatedChars})',
  'sequential.found': 'La contraseña contiene caracteres secuenciales (ej. abc, 123)',
  'keyboardPattern.found': 'La contraseña contiene patrones de teclado',
  'commonPassword.found': 'La contraseña es demasiado común',
  'personalInfo.found': 'La contraseña contiene información personal',
}

const result = validatePassword(password, {
  minLength: 12,
  messages: es,
})

// result.feedback.warning   → "La contraseña debe tener al menos 12 caracteres"
// result.feedback.suggestions → fully localized strings

Template strings may include {placeholder} tokens — they're substituted with the matching params value. Unknown placeholders are left intact so missing data surfaces clearly during development.

Pattern 2 — formatMessage: callback

Best for integrating with an i18n library you already use (react-intl, i18next, lingui, FormatJS), or when you need pluralization, gender, or ICU-style formatting.

typescript
import { useIntl } from 'react-intl'
import { validatePassword } from '@sentinel-password/core'

function useValidate() {
  const intl = useIntl()

  return (password: string) =>
    validatePassword(password, {
      minLength: 12,
      requireUppercase: true,
      formatMessage: (code, params, defaultMessage) => {
        // Reuse react-intl's message catalog. Falls back to English if a
        // translation is missing for this code in the active locale.
        return intl.formatMessage(
          { id: `sentinelPassword.${code}`, defaultMessage },
          params
        )
      },
    })
}

Re-localizing missing character types

For characterTypes.missing, use params.missingTypes to translate each type name individually instead of working with the joined English string:

typescript
const typeNames: Record<string, Record<string, string>> = {
  es: { uppercase: 'mayúscula', lowercase: 'minúscula', digit: 'dígito', symbol: 'símbolo' },
}

validatePassword(password, {
  formatMessage: (code, params, defaultMessage) => {
    if (code === 'characterTypes.missing') {
      const tokens = (params.missingTypes as string).split(',')
      const localized = tokens.map((t) => typeNames.es[t] ?? t).join(', ')
      return `La contraseña debe contener al menos un ${localized}`
    }
    return defaultMessage
  },
})

Pluralization

The built-in template engine is a literal {placeholder} substitution — it does not handle plurals or gender. For locales that need them (Russian, Polish, Arabic, etc.), use formatMessage with an ICU-capable library:

typescript
import { IntlMessageFormat } from 'intl-messageformat'

validatePassword(password, {
  formatMessage: (code, params, defaultMessage) => {
    const messageById: Record<string, string> = {
      'length.tooShort':
        '{minLength, plural, one {Минимум # символ} few {Минимум # символа} other {Минимум # символов}}',
    }
    const tmpl = messageById[code]
    if (!tmpl) return defaultMessage
    return new IntlMessageFormat(tmpl, 'ru').format(params) as string
  },
})

Strength labels

result.strength is a typed enum ('very-weak' | 'weak' | 'medium' | 'strong' | 'very-strong'), not a user-facing message. Translate it in your UI layer — the library doesn't and shouldn't:

typescript
const strengthLabels = {
  'very-weak': 'Muy débil',
  weak: 'Débil',
  medium: 'Media',
  strong: 'Fuerte',
  'very-strong': 'Muy fuerte',
} as const

<span>{strengthLabels[result.strength]}</span>

React layer

@sentinel-password/react's usePasswordValidator hook accepts the same options (UsePasswordValidatorOptions extends ValidatorOptions) — messages and formatMessage thread through to core unchanged:

typescript
const { result } = usePasswordValidator({
  minLength: 12,
  messages: es,
  // ...or formatMessage
})

PasswordInput from @sentinel-password/react-components accepts a nested validatorOptions prop (so it doesn't collide with the HTML input's minLength / maxLength attributes) plus four optional props for the visibility toggle's English text:

tsx
import { PasswordInput } from '@sentinel-password/react-components'

<PasswordInput
  label="Contraseña"
  validatorOptions={{
    minLength: 12,
    messages: { 'length.tooShort': 'Mínimo {minLength} caracteres' },
    // or formatMessage: (code, params, defaultMessage) => intl.formatMessage(...)
  }}
  toggleShowText="Mostrar"
  toggleHideText="Ocultar"
  toggleShowLabel="Mostrar contraseña"
  toggleHideLabel="Ocultar contraseña"
/>

The aria-live region renders the resolved string, so screen readers announce the localized message on each validation pass.

Legacy: lookup-table workaround

The previous workaround — mapping English message strings to translations — still works because default English templates are stable across patch and minor releases. Prefer messages / formatMessage for new code, but you don't need to migrate existing apps:

typescript
const lookup: Record<string, string> = {
  'Password must be at least 8 characters': 'La contraseña debe tener al menos 8 caracteres',
  // ...
}

const result = validatePassword(password)
const localized = result.feedback.suggestions.map((m) => lookup[m] ?? m)

See Also

  • Configuration — full options reference
  • Validators — the canonical list of validators
  • Core APIValidatorOptions, MessageCode, MessageFormatter, DEFAULT_TEMPLATES

Released under the MIT License.