§I18N API Migration
There are a number of changes to the I18N API to make working with messages and languages easier to use, particularly with forms and templates.
§Java API
§Refactored Messages API to interfaces
The play.i18n
package has changed to make access to Messages
easier. These changes should be transparent to the user, but are provided here for teams extending the I18N API.
Messages
is now an interface, and there is a MessagesImpl
class that implements that interface.
§Deprecated / Removed Methods
The static deprecated methods in play.i18n.Messages
have been removed in 2.6.x, as there are equivalent methods on the MessagesApi
instance.
§Scala API
§Removed Implicit Default Lang
The Lang
singleton object has a defaultLang
that points to the JVM default Locale. Pre 2.6.x, defaultLang
was an implicit value, with the result that it could be used in implicit scope resolution if no Lang
was found in local scope. This setting was too general and resulted in bugs where defaultLang
was being used instead of a request’s locale, if the request was not declared as implicit.
As a result, the implicit has been removed, and so what was:
object Lang {
implicit lazy val defaultLang: Lang = Lang(java.util.Locale.getDefault)
}
is now:
object Lang {
lazy val defaultLang: Lang = Lang(java.util.Locale.getDefault)
}
Any code that was relying on this implicit should use Lang.defaultLang
explicitly.
§Refactored Messages API to traits
The play.api.i18n
package has changed to make access to Messages
instances easier and reduce the number of implicits in play. These changes should be transparent to the user, but are provided here for teams extending the I18N API.
Messages
is now a trait (rather than a case class). The case class is now MessagesImpl
, which implements Messages
.
§I18nSupport Implicit Conversion
If you are upgrading directly from Play 2.5 to Play 2.6, you should know that I18nSupport
support has changed in 2.6.x. In 2.5.x, it was possible through a series of implicits to use a “language default” Messages
instance if the request was not declared to be in implicit scope:
def listWidgets = Action {
val lang = implicitly[Lang] // Uses Lang.defaultLang
val messages = implicitly[Messages] // Uses I18nSupport.lang2messages(Lang.defaultLang)
// implicit parameter messages: Messages in requiresMessages template, but no request!
val content = views.html.requiresMessages(form)
Ok(content)
}
The I18nSupport
implicit conversion now requires an implicit request or request header in scope in order to correctly determine the preferred locale and language for the request.
This means if you have the following:
def index = Action {
}
You need to change it to:
def index = Action { implicit request =>
}
This will allow i18n support to see the request’s locale and provide error messages and validation alerts in the user’s language.
§Smoother I18nSupport
Using a form inside a controller is a smoother experience in 2.6.x. ControllerComponents
contains a MessagesApi
instance, which is exposed by AbstractController
. This means that the I18nSupport
trait does not require an explicit val messagesApi: MessagesApi
declaration, as it did in Play 2.5.x.
class FormController @Inject()(components: ControllerComponents)
extends AbstractController(components) with I18nSupport {
import play.api.data.validation.Constraints._
val userForm = Form(
mapping(
"name" -> text.verifying(nonEmpty),
"age" -> number.verifying(min(0), max(100))
)(UserData.apply)(UserData.unapply)
)
def index = Action { implicit request =>
// use request2messages implicit conversion method
Ok(views.html.user(userForm))
}
def showMessage = Action { request =>
// uses type enrichment
Ok(request.messages("hello.world"))
}
def userPost = Action { implicit request =>
userForm.bindFromRequest.fold(
formWithErrors => {
BadRequest(views.html.user(formWithErrors))
},
user => {
Redirect(routes.FormController.index()).flashing("success" -> s"User is ${user}!")
}
)
}
}
Note there is now also type enrichment in I18nSupport
which adds request.messages
and request.lang
. This can be added either by extending from I18nSupport
, or by import I18nSupport._
. The import version does not contain the request2messages
implicit conversion.
§MessagesProvider trait
A new MessagesProvider
trait is available, which exposes a Messages
instance.
trait MessagesProvider {
def messages: Messages
}
MessagesImpl
implements Messages
and MessagesProvider
, and returns itself by default.
All the template helpers now take MessagesProvider
as an implicit parameter, rather than a straight Messages
object, i.e. inputText.scala.html
takes the following:
@(field: play.api.data.Field, args: (Symbol,Any)*)(implicit handler: FieldConstructor, messagesProvider: play.api.i18n.MessagesProvider)
The benefit to using a MessagesProvider
is that otherwise, if you used implicit Messages
, you would have to introduce implicit conversions from other types like Request
in places where those implicits could be confusing.
To assist, there’s MessagesRequest
, which is a WrappedRequest
that implements MessagesProvider
and provides the preferred language.
You can access a MessagesRequest
by using a MessagesActionBuilder
:
class MyController @Inject()(
messagesAction: MessagesActionBuilder,
cc: ControllerComponents
) extends AbstractController(cc) {
def index = messagesAction { implicit request: MessagesRequest[AnyContent] =>
Ok(views.html.formTemplate(form)) // twirl template with form builders
}
}
Or you can even replace the default Action
with MessagesActionBuilder
by swapping out the components:
case class MyControllerComponents @Inject()(
messagesActionBuilder: MessagesAction,
actionBuilder: DefaultActionBuilder,
parsers: PlayBodyParsers,
messagesApi: MessagesApi,
langs: Langs,
fileMimeTypes: FileMimeTypes,
executionContext: scala.concurrent.ExecutionContext
) extends ControllerComponents
abstract class MyAbstractController @Inject()(
protected val controllerComponents: MyControllerComponents
) extends ControllerHelpers {
// Same as AbstractController, but MessagesRequest instead of Request
def Action: ActionBuilder[MessagesRequest, AnyContent] = {
controllerComponents.messagesActionBuilder.compose(controllerComponents.actionBuilder)
}
def parse: PlayBodyParsers = controllerComponents.parsers
def defaultExecutionContext: ExecutionContext = controllerComponents.executionContext
implicit def messagesApi: MessagesApi = controllerComponents.messagesApi
implicit def supportedLangs: Langs = controllerComponents.langs
implicit def fileMimeTypes: FileMimeTypes = controllerComponents.fileMimeTypes
}
By setting up your own abstract controller as a base and extending your controllers from it, you can provide richer functionality without having to inject functionality directly. This is a direct replacement for AbstractController
, and lets you redefine Action
with your own types:
class MyController @Inject() (
addToken: CSRFAddToken,
checkToken: CSRFCheck,
components: MyControllerComponents
) extends MyAbstractController(components) {
import play.api.data.Form
import play.api.data.Forms._
val userForm = Form(
mapping(
"name" -> text,
"age" -> number
)(UserData.apply)(UserData.unapply)
)
def index = addToken {
Action { implicit request =>
Ok(views.html.formpage(userForm))
}
}
def userPost = checkToken {
// Same as AbstractController, only using MessagesRequest[AnyContent]
Action { implicit request =>
userForm.bindFromRequest.fold(
formWithErrors => {
play.api.Logger.info(s"unsuccessful user submission")
BadRequest(views.html.formpage(formWithErrors))
},
user => {
play.api.Logger.info(s"successful user submission ${user}")
Redirect(routes.MessagesController.index()).flashing("success" -> s"User is ${user}!")
}
)
}
}
}
This is also useful for passing around a single implicit request, especially when CSRF checks are involved:
@(userForm: Form[UserData])(implicit request: MessagesRequestHeader)
@helper.form(action = routes.MyController.userPost()) {
@views.html.helper.CSRF.formField
@helper.inputText(userForm("name"))
@helper.inputText(userForm("age"))
<input type="submit" value="SUBMIT"/>
}
Please see passing messages to form helpers for more details.
§DefaultMessagesApi component
The default implementation of MessagesApi
is DefaultMessagesApi
. DefaultMessagesApi
used to take Configuration
and Environment
directly, which made it awkward to deal with in forms. For unit testing purposes, DefaultMessagesApi
can be instantiated without arguments, and will take a raw map.
import play.api.data.Forms._
import play.api.data._
import play.api.i18n._
val messagesApi = new DefaultMessagesApi(
Map("en" ->
Map("error.min" -> "minimum!")
)
)
implicit val request = {
play.api.test.FakeRequest("POST", "/")
.withFormUrlEncodedBody("name" -> "Play", "age" -> "-1")
}
implicit val messages = messagesApi.preferred(request)
def errorFunc(badForm: Form[UserData]) = {
BadRequest(badForm.errorsAsJson)
}
def successFunc(userData: UserData) = {
Redirect("/").flashing("success" -> "success form!")
}
val result = Future.successful(form.bindFromRequest().fold(errorFunc, successFunc))
Json.parse(contentAsString(result)) must beEqualTo(Json.obj("age" -> Json.arr("minimum!")))
For functional tests that involve configuration, the best option is to use WithApplication
and pull in an injected MessagesApi
:
import play.api.test.{ PlaySpecification, WithApplication }
import play.api.mvc.Controller
import play.api.i18n._
class MessagesSpec extends PlaySpecification with Controller {
sequential
implicit val lang = Lang("en-US")
"Messages" should {
"provide default messages" in new WithApplication(_.requireExplicitBindings()) {
val messagesApi = app.injector.instanceOf[MessagesApi]
val javaMessagesApi = app.injector.instanceOf[play.i18n.MessagesApi]
val msg = messagesApi("constraint.email")
val javaMsg = javaMessagesApi.get(new play.i18n.Lang(lang), "constraint.email")
msg must ===("Email")
msg must ===(javaMsg)
}
"permit default override" in new WithApplication(_.requireExplicitBindings()) {
val messagesApi = app.injector.instanceOf[MessagesApi]
val msg = messagesApi("constraint.required")
msg must ===("Required!")
}
}
}
If you need to customize the configuration, it’s better to add configuration values into the GuiceApplicationBuilder rather than use the DefaultMessagesApiProvider directly.
§Deprecated Methods
play.api.i18n.Messages.Implicits.applicationMessagesApi
and play.api.i18n.Messages.Implicits.applicationMessages
have been deprecated, because they rely on an implicit Application
instance.
The play.api.mvc.Controller.request2lang
method has been deprecated, because it was using a global Application
under the hood.
The play.api.i18n.I18nSupport.request2Messages
implicit conversion method has been moved to I18NSupportLowPriorityImplicits.request2Messages
, and deprecated in favor of request.messages
type enrichment, which is clearer overall.
The I18NSupportLowPriorityImplicits.lang2Messages
implicit conversion has been moved out to LangImplicits.lang2Messages
, because of confusion when both implicit Request and a Lang were in scope. Please extend the play.api.i18n.LangImplicits
trait specifically if you want to create a Messages
from an implicit Lang
.
Next: WS Migration