Plurals

Babel provides first class support for plurals with the Quantities class.

en.conf
sourcebicycles = {
  0 = "There are no bicycles in Beijing"
  1 = "There is one bicycle in Beijing"
  "*" = "There are 9 million bicycles in Beijing"
}
import cats.effect._
import cats.effect.unsafe.implicits.global
import cats.syntax.all._
import io.taig.babel._
import io.taig.babel.generic.auto._

final case class I18n(bicycles: Quantities[String])

val i18n = Loader
  .default[IO]
  .load("plurals", Set(Locales.en))
  .map(Decoder[I18n].decodeAll)
  .rethrow
  .map(_.withFallback(Locales.en))
  .flatMap(_.liftTo[IO](new IllegalStateException("Translations for en missing")))
  .map(_.apply(Locales.en))
  .unsafeRunSync()
// i18n: I18n = I18n(
//   bicycles = Quantities(
//     default = "There are 9 million bicycles in Beijing",
//     quantities = List(
//       Element(
//         quantity = Exact(value = 0),
//         value = "There are no bicycles in Beijing"
//       ),
//       Element(
//         quantity = Exact(value = 1),
//         value = "There is one bicycle in Beijing"
//       )
//     )
//   )
// )
i18n.bicycles(0)
// res0: String = "There are no bicycles in Beijing"
i18n.bicycles(1)
// res1: String = "There is one bicycle in Beijing"
i18n.bicycles(100)
// res2: String = "There are 9 million bicycles in Beijing"

Using plurals with arguments

Of course, you will most likely want to reference the given quantity in your translations, which was painfully omitted in the example above. This can be achieved by combining the StringFormat feature with Quantities.

en.conf
sourcebicycles = {
  0 = "There are no bicycles in Beijing"
  1 = "There is one bicycle in Beijing"
  "*" = "There are {0} bicycles in Beijing"
}
import cats.effect._
import cats.effect.unsafe.implicits.global
import cats.syntax.all._
import io.taig.babel._
import io.taig.babel.generic.auto._

final case class I18n(bicycles: Quantities[StringFormat1])

val i18n = Loader
  .default[IO]
  .load("plurals-arguments", Set(Locales.en))
  .map(Decoder[I18n].decodeAll)
  .rethrow
  .map(_.withFallback(Locales.en))
  .flatMap(_.liftTo[IO](new IllegalStateException("Translations for en missing")))
  .map(_.apply(Locales.en))
  .unsafeRunSync()
// i18n: I18n = I18n({0: "There are no bicycles in Beijing", 1: "There is one bicycle in Beijing", *: "There are {0} bicycles in Beijing"})
i18n.bicycles(0)
// res4: StringFormat1 = There are no bicycles in Beijing
i18n.bicycles(100)("100")
// res5: String = "There are 100 bicycles in Beijing"
i18n.bicycles(9000000)("9 million")
// res6: String = "There are 9 million bicycles in Beijing"

Plural ranges

Some languages have very complex plural rules. These can be mapped with quantity ranges.

en.conf
sourceranges = {
  1 = "Exact"
  "10-20" = "Range 10 to 20"
  "*" = "Fallback"
}
import cats.effect._
import cats.effect.unsafe.implicits.global
import cats.syntax.all._
import io.taig.babel._
import io.taig.babel.generic.auto._

final case class I18n(ranges: Quantities[String])

val i18n = Loader
  .default[IO]
  .load("plurals-ranges", Set(Locales.en))
  .map(Decoder[I18n].decodeAll)
  .rethrow
  .map(_.withFallback(Locales.en))
  .flatMap(_.liftTo[IO](new IllegalStateException("Translations for en missing")))
  .map(_.apply(Locales.en))
  .unsafeRunSync()
// i18n: I18n = I18n({1: "Exact", 10-20: "Range 10 to 20", *: "Fallback"})
i18n.ranges(0)
// res8: String = "Fallback"
i18n.ranges(1)
// res9: String = "Exact"
i18n.ranges(9)
// res10: String = "Fallback"
i18n.ranges(10)
// res11: String = "Range 10 to 20"
i18n.ranges(11)
// res12: String = "Range 10 to 20"
i18n.ranges(20)
// res13: String = "Range 10 to 20"
i18n.ranges(21)
// res14: String = "Fallback"