拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Play Framework 2.6 I18N API 迁移完全指南:Messages 接口化、MessagesProvider 与隐式转换重构

Play Framework 2.6 I18N API 迁移完全指南Messages 接口化、MessagesProvider 与隐式转换重构【免费下载链接】playframeworkThe Community Maintained High Velocity Web Framework For Java and Scala.项目地址: https://gitcode.com/gh_mirrors/pl/playframework导读Play Framework 2.6 对play.i18nJava与play.api.i18nScala两套国际化 API 进行了系统性重构Messages从具体的 case class / 具体类升级为 trait / 接口新增MessagesProvider统一消息提供抽象并将I18nSupport的隐式转换改为依赖请求Request上下文。本文基于官方迁移文档 MessagesMigration26.md结合当前仓库源码逐项讲解这些变更的动机、破坏性影响与升级路径读完你可以将 Play 2.5 及更早版本中依赖隐式Lang、隐式Messages的控制器与模板代码平滑迁移到 2.6 的新 API。迁移概览为什么 I18N API 要重构2.6 之前I18N 相关 API 存在两个明显问题隐式作用域过于“宽泛”Lang.defaultLang曾经是一个implicit值任何没有显式隐式Lang的代码都会静默地回退到 JVM 默认 Locale导致在控制器中忘记声明implicit request时Messages会错误地使用defaultLang而不是请求的语言隐式转换链太长、歧义太多从Request到Messages需要经过多步隐式转换在同时存在隐式Request和隐式Lang时容易产生混淆。2.6 的解决方案是把Messages抽象为接口/trait引入MessagesProvider作为模板与表单助手的统一隐式参数并让I18nSupport的类型增强request.messages/request.lang取代隐式转换。迁移文档的原文定位是“对团队扩展现有 I18N API 时提供参考”但对普通用户而言绝大多数变更都是透明的只有少数隐式用法需要主动修改。Java API 迁移Messages 重构为接口Java 侧play.i18n包将Messages重构为接口并提供实现类MessagesImpl。从当前仓库的源码可见这一结构的最终形态Messages.java 定义为public interface Messages extends MessagesProvider其中MessagesProvider是 Scala 侧play.api.i18n.MessagesProvider的 Java 视图MessagesImpl.java 实现该接口接口内部定义了apply/at/isDefinedAt/asScala等方法其中messages()默认委托给asScala()实现 Java 与 Scala 双向互转。这次改动对普通调用方完全透明你仍然通过MessagesApi获取消息实例。它主要影响的是那些自行实现Messages逻辑、扩展 I18N API的团队——现在他们应当实现接口而不是继承具体类。静态废弃方法移除play.i18n.Messages上带deprecated标记的静态方法已在 2.6.x 中移除。官方迁移文档明确说明这些方法在MessagesApi实例上都有等价方法。升级时只需要把形如Messages.get(...)的静态调用改为通过注入的MessagesApiJava 侧为play.i18n.MessagesApi实例调用。从源码看Java 的MessagesApi通过play.i18n.MessagesApi包装 Scala 的play.api.i18n.MessagesApi见 Messages.scala 中asJava方法因此 Java 代码拿到的是同一个消息解析引擎。Scala API 迁移移除隐式默认 Lang2.6 之前Lang伴生对象中存在一个隐式值object Lang { implicit lazy val defaultLang: Lang Lang(java.util.Locale.getDefault) }2.6 中该隐式被移除object Lang { lazy val defaultLang: Lang Lang(java.util.Locale.getDefault) }当前仓库 Langs.scala 中的注释完整复述了这一变更动机defaultLang此前会参与隐式作用域解析如果局部作用域找不到Lang就会静默采用 JVM 默认 Locale一旦请求没有声明为 implicit就会错误地使用defaultLang而非请求的语言从而产生国际化 bug。升级动作任何依赖该隐式值的代码例如直接写implicitly[Lang]却期望得到默认语言必须改为显式引用Lang.defaultLang。Messages 从 case class 重构为 traitScala 侧play.api.i18n.Messages由 case class 改为 trait原 case class 更名为MessagesImpl。当前仓库 Messages.scala 展示了最终形态trait Messages extends MessagesProviderL255-L316声明lang、apply、translate、isDefinedAt、asJava等抽象成员并且messages默认返回自身即每个Messages天然是一个MessagesProvidercase class MessagesImpl(lang: Lang, messagesApi: MessagesApi) extends MessagesL187-L243承载语言上下文与MessagesApi引用所有翻译委托给messagesApi。由于 trait 不暴露Product特性case class 才有copy/equals等设计意图是让Messages只保留“按语言取消息”的最小接口具体实现细节由MessagesImpl封装。I18nSupport 隐式转换的变化2.5.x 中I18nSupport通过一串隐式转换允许在没有声明隐式请求的情况下使用“默认语言”的Messagesdef 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) }这段代码在 2.6 中不再成立。官方迁移文档指出I18nSupport的隐式转换现在要求隐式Request或RequestHeader在作用域内以便正确确定请求的首选语言。从当前仓库 I18nSupport.scala 可以看到新签名trait I18nSupport extends I18NSupportLowPriorityImplicits { def messagesApi: MessagesApi implicit def request2Messages(implicit request: RequestHeader): Messages messagesApi.preferred(request) }request2Messages需要隐式RequestHeader才能工作。因此下面这种“裸” Actiondef index Action { }必须改成携带隐式请求def index Action { implicit request }这样 i18n 支持才能读取请求的语言结合Accept-Language头、PLAY_LANGCookie 与play.i18n.langs配置让校验错误提示与表单验证信息以用户的语言呈现。更平滑的 I18nSupport从 ControllerComponents 获取 MessagesApi2.5 中混入I18nSupport的控制器必须显式声明val messagesApi: MessagesApi。2.6 起ControllerComponents本身就包含一个MessagesApi实例并由AbstractController暴露给子类。因此控制器可以这样写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 - sUser is ${user}!) } ) } }注意其中两种不同的取消息方式index中依赖request2messages隐式转换让模板的隐式MessagesProvider自动从请求推导showMessage中直接使用类型增强request.messages(hello.world)。request.messages 与 request.lang 类型增强I18nSupport现在附带类型增强type enrichment为RequestHeader添加messages与lang两个方法。源码位于 I18nSupport.scalaimplicit class RequestWithMessagesApi(request: RequestHeader) { def messages(implicit messagesApi: MessagesApi): Messages messagesApi.preferred(request) def lang(implicit messagesApi: MessagesApi): Lang messagesApi.preferred(request).lang }引入方式有两种控制器混入trait I18nSupport——此时包含request2messages隐式转换import I18nSupport._——此方式不包含request2Messages隐式转换只引入类型增强避免隐式转换带来的歧义。迁移文档特别提醒import I18nSupport._的版本不含request2messages如果你的模板依赖该隐式转换把请求转换为Messages则需要显式传参或改用request.messages。MessagesProvider统一消息提供抽象MessagesProvider 的设计2.6 新增MessagesProvidertrait仅暴露一个messages成员trait MessagesProvider { def messages: Messages }当前仓库中定义于 Messages.scala。MessagesImpl同时实现Messages与MessagesProvider其messages默认返回自身def messages: Messages this。设计动机迁移文档原话如果继续使用隐式Messages在Request等其他类型也需要作为隐式参数的场景下就必须引入各种类型的隐式转换容易造成混乱。MessagesProvider用一个统一的隐式参数解决了这个问题——Request、模板、控制器各取所需。模板助手全面切换到 MessagesProvider所有表单模板助手form helpers的隐式参数从直接的Messages改为MessagesProvider。例如inputText.scala.html的签名(field: play.api.data.Field, args: (Symbol,Any)*)(implicit handler: FieldConstructor, messagesProvider: play.api.i18n.MessagesProvider)当前仓库中 inputText.scala.html 的声明与此一致。表单助手需要MessagesProvider是因为它们要输出与请求语言一致的校验错误消息。配套地Messages对象提供了基于MessagesProvider的便捷方法Messages.scaladef apply(key: String, args: Any*)(implicit provider: MessagesProvider): String def apply(keys: Seq[String], args: Any*)(implicit provider: MessagesProvider): String def isDefinedAt(key: String)(implicit provider: MessagesProvider): Boolean这意味着Messages(hello.world)这类调用现在也接受MessagesProvider隐式参数进一步减少了作用域中需要同时存在的隐式值。关于如何向表单助手传入MessagesProvider可进一步参考 ScalaForms.md 的 Passing MessagesProvider to Form Helpers 一节。MessagesRequest 与 MessagesAbstractController为了让控制器代码更简洁2.6 还提供了“i18n 感知”的请求类型与控制器基类。相关实现集中在 MessagesRequest.scala。使用 MessagesActionBuilderMessagesRequest是一个WrappedRequest同时实现MessagesProvider并借助PreferredMessagesProvider惰性计算首选语言的消息class MessagesRequestA extends WrappedRequest(request) with PreferredMessagesProvider with MessagesRequestHeader其中lazy val messages: Messages messagesApi.preferred(self)见 MessagesRequest.scala。使用MessagesActionBuilder获取MessagesRequestclass 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 } }由于MessagesRequest本身是MessagesProvider只要把它声明为 implicit模板中的表单助手就能直接使用无需额外提供Messages。使用 MessagesAbstractController更省事的方式是继承MessagesAbstractController它把默认的Action替换为提供MessagesRequest的版本class MyController Inject() ( mcc: MessagesControllerComponents ) extends MessagesAbstractController(mcc) { def index Action { implicit request: MessagesRequest[AnyContent] Ok(sThe messages are ${request.messages}) } }源码层面MessagesBaseController.Action是这样组合出来的MessagesRequest.scaladef Action: ActionBuilder[MessagesRequest, AnyContent] { controllerComponents.messagesActionBuilder.compose(controllerComponents.actionBuilder) }MessagesControllerComponents在普通ControllerComponents基础上增加了messagesActionBuilder成员。结合 CSRF 的完整示例当表单同时涉及 CSRF 时假设已禁用 CSRF filter、改为在 Action 上显式使用 tokenMessagesRequest的价值尤为明显模板只需一个隐式MessagesRequestHeader即可同时满足 CSRF 字段与表单助手对MessagesProvider的需求。class MyController Inject() ( addToken: CSRFAddToken, checkToken: CSRFCheck, mcc: MessagesControllerComponents ) extends MessagesAbstractController(mcc) { 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 { Action { implicit request userForm.bindFromRequest.fold( formWithErrors { play.api.Logger.info(sunsuccessful user submission) BadRequest(views.html.formpage(formWithErrors)) }, user { play.api.Logger.info(ssuccessful user submission ${user}) Redirect(routes.MyController.index()).flashing(success - sUser is ${user}!) } ) } } }对应的formpage.scala.html(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 typesubmit valueSUBMIT/ }迁移文档特别指出因为模板用不到请求体这里使用MessagesRequestHeader而非MessagesRequest[_]即可它仅要求RequestHeader MessagesProvider能力见 MessagesRequest.scala。DefaultMessagesApi单元测试与功能测试无参实例化与原始 MapMessagesApi的默认实现是DefaultMessagesApi。2.5 时代它需要直接接收Configuration和Environment在表单/测试场景下很不顺手。2.6 起为了单元测试方便DefaultMessagesApi可以不传参数实例化直接接收一个原始 Map。从当前仓库源码看构造器参数带有默认值Messages.scalaclass DefaultMessagesApi Inject() ( val messages: Map[String, Map[String, String]] Map.empty, langs: Langs new DefaultLangs(), val langCookieName: String PLAY_LANG, ... ) extends MessagesApi单元测试示例迁移文档原文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!)))这个测试验证了关键链路DefaultMessagesApi从原始 Map 加载error.min消息preferred(request)依据请求推导语言表单校验失败后errorsAsJson把英文消息minimum!编码进 JSON 错误响应。WithApplication 功能测试涉及完整配置的功能测试最佳实践是使用WithApplication并从注入器取出MessagesApiimport play.api.test.{ PlaySpecification, WithApplication } import play.api.i18n._ class MessagesSpec extends PlaySpecification { 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!) } } }这段测试同时验证了 Java 与 Scala 两套MessagesApi的等价性constraint.email均解析为 Email以及应用自定义消息对内置约束消息的覆盖能力。仓库中 MessagesSpec.scala 提供了同类断言的实际用例例如直接new DefaultMessagesApi(testMessages, langs)构造后验证preferred行为。如果确实需要自定义配置迁移文档建议把配置值放进GuiceApplicationBuilder而不是直接使用DefaultMessagesApiProviderGuiceApplicationBuilder() .configure(play.i18n.langs - Seq(en, fr)) .build()这样可以利用标准 DI 装配流程见 I18nModule.scala 中的Langs - DefaultLangsProvider、MessagesApi - DefaultMessagesApiProvider绑定同时仍可通过app.injector.instanceOf[MessagesApi]获取实例。2.6 中的废弃 API 清单迁移文档列出以下被废弃的入口升级时应逐一替换废弃项替代方案play.api.i18n.Messages.Implicits.applicationMessagesApi不再依赖隐式Application的显式注入play.api.i18n.Messages.Implicits.applicationMessages同上改用显式MessagesApiplay.api.mvc.Controller.request2lang使用request.lang类型增强原实现底层依赖全局ApplicationI18nSupport.request2Messages隐式转换迁移到I18NSupportLowPriorityImplicits.request2Messages推荐改用更清晰的request.messages类型增强I18NSupportLowPriorityImplicits.lang2Messages迁移到LangImplicits.lang2MessagesI18nSupport.scala避免隐式Request与隐式Lang同时在作用域时产生歧义最后一条的源码印证LangImplicits现在是一个独立 trait要求显式def messagesApi: MessagesApi并通过implicit def lang2Messages(implicit lang: Lang): Messages从隐式Lang构造Messages。只有在明确“从隐式 Lang 生成 Messages”的需求时才应扩展该 trait。迁移检查清单升级到 Play 2.6 时对照以下清单逐一排查隐式 Lang删除对隐式Lang.defaultLang的依赖需要默认语言时显式写Lang.defaultLang裸 Action所有在I18nSupport作用域内使用Messages/模板的 Action改为Action { implicit request ... }模板隐式参数自定义模板若直接接收隐式Messages改为接收隐式MessagesProvider或MessagesRequestHeader与inputText等内建助手保持一致控制器基类优先使用MessagesAbstractControllerMessagesControllerComponents或注入MessagesActionBuilder让 Action 直接产出MessagesRequest类型增强用request.messages/request.lang/request.withLang(lang)/request.withoutLang取代旧隐式转换与request2lang测试单元测试用无参new DefaultMessagesApi(Map(...))集成测试用WithApplication 注入的MessagesApi需要自定义配置时通过GuiceApplicationBuilder配置play.i18n.langs等键废弃 API替换 废弃清单 中列出的所有旧入口特别是I18nSupport相关隐式转换与Messages.Implicits系列。完成上述调整后表单校验、模板渲染与 CSRF 组合场景中的消息解析将完全由请求语言驱动彻底消除 2.5 时代“拿不到隐式请求就退回 JVM 默认语言”的国际化隐患。【免费下载链接】playframeworkThe Community Maintained High Velocity Web Framework For Java and Scala.项目地址: https://gitcode.com/gh_mirrors/pl/playframework创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门