Tsuji 5.0.0

Migrating

Taihei Tsuji 5.0.0 is the successor of dev.taihei::taihei-i18n 4.x under a new name, and a major release. Moving to it is a change of coordinates and imports first, and then a move to typed message families, one family at a time.

What changes

  • The dependency is dev.taihei::taihei-tsuji (and dev.taihei::taihei-tsuji-tools), where it was dev.taihei::taihei-i18n.
  • The package is dev.taihei.tsuji, where it was dev.taihei.i18n.
  • A library ships its catalogue files under META-INF/taihei-tsuji/.
  • The 4.x Message, which carried every translation, is replaced by typed message families. The 4.x type stays, unchanged and deprecated, as dev.taihei.tsuji.legacy.Message until 6.0.0.

The JSON of a message does not change, and neither do the keys of CoreMessages, so a taihei-i18n 4.x peer and a Taihei Tsuji 5 peer still exchange messages.

There is no taihei-i18n 5.0.0 and no relocation from the old coordinates. The two lines are different artifacts in different packages, so they can share a classpath: a library built against taihei-i18n 4.x keeps working beside Taihei Tsuji 5 with its own taihei-i18n jar. Its dev.taihei.i18n.Language and the dev.taihei.tsuji.Language of 5.x are different types, and cannot be passed for each other.

The 4.x line keeps its name and receives security fixes for 12 months after the release of Taihei Tsuji 5.0.0: see taihei-i18n 4.x.

Step 1: coordinates and imports

Change the dependency to dev.taihei::taihei-tsuji:5.0.0, then replace the package in every Scala file:

# every import of dev.taihei.i18n becomes dev.taihei.tsuji
grep -rlZ 'dev\.taihei\.i18n' --include='*.scala' . | xargs -0 perl -pi -e 's/dev\.taihei\.i18n/dev.taihei.tsuji/g'

Where 4.x messages are built, import the legacy Message explicitly. An explicit import wins over the wildcard's new Message. A 4.x file:

import dev.taihei.i18n.*
import dev.taihei.i18n.Language.*

object Account:
  val saved = Message.with0Args(English -> "Saved", Dutch -> "Opgeslagen")
  def hello(name: String): Message =
    Message.withNamedArgs(English -> "Hello {$name}", Dutch -> "Hallo {$name}")("name" -> name)

becomes, after the line above and one added import:

import dev.taihei.tsuji.*
import dev.taihei.tsuji.Language.*
import dev.taihei.tsuji.legacy.Message   // an explicit import wins over the wildcard

object Account:
  val saved = Message.with0Args(English -> "Saved", Dutch -> "Opgeslagen")
  def hello(name: String): Message =
    Message.withNamedArgs(English -> "Hello {$name}", Dutch -> "Hallo {$name}")("name" -> name)

Code that only builds and translates 4.x messages then compiles, with deprecation warnings, and works as before. Legacy messages and new ones are both Displayable, so they are shown side by side. To silence the warnings while you migrate, add this compiler option:

-Wconf:cat=deprecation&origin=dev\.taihei\.tsuji\.legacy\..*:s

Code that uses Message.of, Language.label, CoreMessages, the MessageArg union or the MessageFormat 2.0 internals needs the changes in the table below already in this step.

Step 2: families, one at a time

Then move the messages to families. The same messages as a family:

import dev.taihei.tsuji.*
import dev.taihei.tsuji.Language.*

object Account extends MessageFamily("account"):
  val saved = message("Saved", Dutch -> "Opgeslagen")
  val hello = message[(name: String)]("Hello {$name}", Dutch -> "Hallo {$name}")

given Catalogues = Catalogues(Seq(Account))

Account.saved.localize(Dutch)                // Opgeslagen
Account.hello(name = "Ada").localize(Dutch)  // Hallo Ada

This is where the compiler starts to help: a template that names an argument it does not have, or formats a date as a number, no longer compiles. Do it family by family.

4.x5.x
Message.with0Args(English -> "Saved", Dutch -> "Opgeslagen")val saved = message("Saved", Dutch -> "Opgeslagen") in a MessageFamily
Message.withNamedArgs(English -> "Hello {$name}")("name" -> n)val hello = message[(name: String)]("Hello {$name}"), then hello(name = n)
Message.with1Arg to with10Args and {$0}named arguments: message[(count: Int, query: String)]("{$count} results for {$query}")
message.translate(language), message.render(language)message.localize(language), with a given Catalogues
message.toString (English)message.toString (the family's source language)
Message.of(throwable)Text.of(throwable)
Message.of(string)Text(string, Language.English)
the union type MessageArgthe type class MessageArg[A]; the union is legacy.MessageArg
MessageFormat2.Parser, its AST, Contextinternal now: use MessageFormat2.format and MessageFormat2.validate
PlatformNumberFormatter, PlatformDateTimeFormatter, PlatformLocales, PlatformLanguageNamesinternal now: format through a template
Language.label, a legacy messageLanguage.label: Message, shown in any reader's language
CoreMessages.X.member(args)the same names, with named, typed arguments
CoreMessages.Exceptions.revisionMismatch(expected, actual)CoreMessages.Exceptions.revisionMismatchBetween(expected, actual)
CoreMessages.Validation.formatErrors(Map[String, List[Message]])formatErrors(Map[String, List[Displayable]]); each field error is shown in the reader's language
CoreMessages in Dutch and French on Scala.js, inline in 4.xcatalogue files now: export them (--scan dev.taihei.tsuji) and load them with CatalogueLoader, or a browser shows English
the case class Message(args, namedArgs, translationTemplates), copy, its fieldsonly on legacy.Message
message.nonEmptymessage.localize(language).nonEmpty
given JsonCodec[Message], needing nothingJsonEncoder[Message] as before; JsonDecoder and JsonCodec[Message] need a given Catalogues where they are summoned or derived
!!unknown message!!gone: the source language always has the key

Language has the API of 4.x apart from label, which now returns the new Message.

JSON during a rollout

Stored or exchanged 4.x messages stay readable in 5.x, as legacy messages with the key legacy, within the bounds of taihei-i18n 4.0.2: at most 1000 templates and 1000 arguments, none of them, nor an argument name, longer than 65536 characters.

A message that a 5.x writer produces is not readable by a 4.x reader. During a rollout, upgrade the readers (the clients) first, or keep writing legacy messages until they have.

Coming from edo-i18n or genius-i18n

taihei-i18n 4.x has the API of com.edoframework::edo-i18n:3.0.0 under its new name, with the package dev.taihei.i18n where it was com.edoframework.i18n. Coming from fish.genius::genius-i18n:2.0.0, the package was genius.i18n. Move to the newest taihei-i18n 4.0.x first, then to Taihei Tsuji 5.0.0 as above. Since 3.0.0 the library needs Scala 3.9 or later, uses zio-json 1.1 instead of 0.9, and is licensed under the Apache License 2.0; releases up to 2.0.0 stay under the MIT License.

Do not stay on 2.0.0, 3.0.0 or 4.0.0: parsing a long language tag can overflow the stack in them (advisory GHSA-45w8-x6c8-w4hg). Do not stay on 4.0.1 either: decoding or formatting a Message read from untrusted JSON can use unbounded memory and CPU in it and every earlier release (advisory GHSA-6h29-v54g-8337). taihei-i18n 4.0.2 has both fixes.

The migration notes in the README on GitHub