Localized Error Messages

Eventually you have to turn Error1 | Error2 | Error3 into something that is readable by the user.

The best way I have found so far is to use union-derivation.

Example

Defining the localization trait

import yantl.*
import io.github.irevive.union.derivation.*

enum LocaleEnum { case En }

trait LocalizedTextOfValue[A] {
  def text(a: A): LocaleEnum ?=> String
}
object LocalizedTextOfValue {
  /** Creates a new instance. */
  def of[A](localize: (A, LocaleEnum) => String): LocalizedTextOfValue[A] = new {
    override def text(value: A): LocaleEnum ?=> String = 
      (locale: LocaleEnum) ?=> localize(value, locale)
  }

  inline given derivedUnion[A](using IsUnion[A]): LocalizedTextOfValue[A] =
    UnionDerivation.derive[LocalizedTextOfValue, A]
}

extension [A](a: A) {
  def localized(using loc: LocalizedTextOfValue[A], locale: LocaleEnum): String = loc.text(a)
}

Defining the newtype

object Age extends Newtype.ValidatedOf(Validator.of(ValidatorRule.between(0L, 100L)))
type Age = Age.Type

Defining the error messages

given LocalizedTextOfValue[ValidatorRule.SmallerThan[Long]] = LocalizedTextOfValue.of {
  case (err, LocaleEnum.En) => s"Must be actually born."
}

given LocalizedTextOfValue[ValidatorRule.LargerThan[Long]] = LocalizedTextOfValue.of {
  case (err, LocaleEnum.En) => s"Sorry, too old. Must be under ${err.maximum}, was ${err.actual}."
}

Localizing the error message

given LocaleEnum = LocaleEnum.En

Age.make(-1).left.map(errors => errors.map(_.localized))
// res0: Either[Vector[String], Type] = Left(
//   value = Vector("Must be actually born.")
// )

Age.make(105).left.map(errors => errors.map(_.localized))
// res1: Either[Vector[String], Type] = Left(
//   value = Vector("Sorry, too old. Must be under 100, was 105.")
// )