scala Tutorial

Building an Expense Tracker with Scala 3 - Part 5: Internationalization & Testing

In this final part of the backend series, we will make our application accessible to a wider audience by adding Internationalization (I18n) support and ensure its reliability with Automated Tests.

Internationalization (I18n)

Internationalization allows your app to adapt to different languages and regions without changing the code. Play Framework has built-in support for this.

We already enabled it in conf/application.conf in Part 1:

play.i18n.langs = ["en", "es"]

Messages Files

Play looks for messages in conf/messages.xxx files. Let's create English and Spanish translations.

# conf/messages.en
hello.world = Hello World!
expense.tracker = Expense Tracker
# conf/messages.es
hello.world = ¡Hola Mundo!
expense.tracker = Rastreador de Gastos

Using Messages in Controllers

To use these messages, we inject MessagesApi or use the I18nSupport trait in our controller. The I18nSupport trait adds helper methods to look up messages based on the current request's Accept-Language header.

// app/controllers/HomeController.scala
package controllers

import play.api.mvc.*
import play.api.i18n.I18nSupport

class HomeController(cc: ControllerComponents) extends AbstractController(cc) with I18nSupport {
  def index = Action { implicit request =>
    // messagesApi("key") looks up the message.
    // "implicit request" is needed so Play knows which language the user prefers.
    Ok(messagesApi("hello.world"))
  }
}

Now, if you request GET / with Accept-Language: es, you will see "¡Hola Mundo!". If you send Accept-Language: en (or nothing), you'll see "Hello World!".

Testing

Testing is crucial for any production application. It gives you confidence to refactor and deploy. Play uses ScalaTest by default, which is a powerful and flexible testing framework.

Unit Testing Controllers

Let's write a simple test for our HomeController. We'll use GuiceOneAppPerTest (even though we use Macwire, this helper is useful for integration tests) or simply instantiate the controller directly for unit tests.

Here is a unit test where we stub the components:

// test/controllers/HomeControllerSpec.scala
package controllers

import org.scalatestplus.play.*
import org.scalatestplus.play.guice.*
import play.api.test.*
import play.api.test.Helpers.*

class HomeControllerSpec extends PlaySpec with GuiceOneAppPerTest {

  "HomeController GET" should {

    "render the index page" in {
      // We create a fake request
      val request = FakeRequest(GET, "/")
      
      // We instantiate the controller with stubbed components
      val controller = new HomeController(stubControllerComponents())
      val home = controller.index().apply(request)

      // We assert the status and content
      status(home) mustBe OK
      contentAsString(home) must include ("Hello World!")
    }
  }
}

To run the tests, execute:

sbt test

Conclusion

Congratulations! You have built a complete Expense Tracker API with Scala 3. You've learned about:

  • Project Setup & Macwire DI: Setting up a modern Scala environment.
  • Database access: Using MySQL & Docker with JDBC.
  • Authentication: Securing your API with JWT.
  • CRUD operations: Managing Expenses and Categories.
  • Internationalization & Testing: Polishing and verifying your app.

In the next series, we will build a modern React Frontend to interact with this API, bringing your backend to life!