If these steps are done in order then Lucky should continue to compile and be usable with each change.
We’ll be using the Rosetta shard because it integrates well with Lucky, it handles key lookup at compile-time and it is very fast.
Summary:
After configuration you can apply translations using either:
r("tranlation.key.values").t
Or wherever you’d want to use a specific language:
Rosetta.with_locale(current_user.language) do
r("tranlation.key.values").t
end
This document assumes you are using the default Authentication - if not, you will need to make adjustments accordingly.
dependencies:
rosetta:
github: wout/rosetta
Require the shard in src/shards.cr
:
# src/shards.cr
# ...
require "rosetta"
And of course, install the shard:
shards install
After installing, run the init command to have Rosetta set up the required files, including translations for Lucky:
bin/rosetta --init --lucky
This command will generate:
1. An initializer at config/rosetta.cr
with the following content:
@[Rosetta::DefaultLocale(:en)]
@[Rosetta::AvailableLocales(:en)]
module Rosetta
end
Rosetta::Backend.load("config/rosetta")
Rosetta has an integration macro for Lucky to include the
Rosetta::Translatable
module everywhere translations are needed. It also
adds the necessary method overloads to make Lucky work seamlessly with the
underlying Rosetta::Translation
objects. Add the following line before
Rosetta::Backend.load
in config/rosetta.cr
, and you’re good to go:
# ...
Rosetta::Lucky.integrate
Rosetta::Backend.load("config/rosetta")
2. config/rosetta/rosetta.en.yml
This file contains localizations required by Rosetta. For every additional locale, you’ll need to copy and translate this file.
3. config/rosetta/avram.en.yml
This file contains translations for Avram’s validations. For every additional locale, you’ll need to copy and translate this file. Please consider contributing your translations.
4. config/locales/example.en.yml
An example locale file. Replace the contents of this file with the following to make it through this tutorial without compilation errors:
en:
api:
auth:
not_authenticated: "Not authenticated."
token_invalid: "The provided authentication token was incorrect."
token_missing: |
An authentication token is required. Please include a token in an 'auth_token' param or 'Authorization' header.
default:
button:
sign_out: "Sign out"
sign_up: "Sign up"
error:
incorrect_password: "is wrong"
invalid_email: "is not in our system"
field:
email: "Email"
language: "Language"
password: "Password"
password_confirmation: "Confirm password"
errors:
show_page:
helpful_link: "Try heading back to home"
page_title: "Something went wrong"
me:
show_page:
after_signin: "Change where you go after sign in: src/actions/home/index.cr"
auth_guides: "Check out the authentication guides"
modify_page: "Modify this page: src/pages/me/show_page.cr"
next_you_may_want_to: "Next, you may want to:"
page_title: "This is your profile"
user_email: "Email: %{email}"
password_reset_requests:
new_page:
page_title: "Request password reset"
password_resets:
new_page:
page_title: "Reset password"
sign_ups:
create:
success: "Thanks for signing up"
failure: "Couldn't sign you up"
new_page:
page_title: "Sign up"
sign_in_instead: "Sign in instead"
sign_ins:
create:
failure: "Sign in failed"
success: "You're now signed in"
delete:
success: "You have been signed out"
new_page:
page_title: "Sign in"
Tell Avram to use Rosetta’s backend to translate the default validation error messages:
# config/database.cr
# ...
Avram.configure do |settings|
# ...
settings.i18n_backend = Rosetta::AvramBackend.new
end
This setup will associate a language key with each user. Generate a migration using:
lucky gen.migration AddLanguageToUser
Edit the new migration file in db/migrations/
:
# db/migrations/20241112190759_add_language_to_user.cr
class AddLanguageToUser::V20241112190759 < Avram::Migrator::Migration::V1
def migrate
alter table_for(User) do
add language : String, default: "en"
end
end
def rollback
alter table_for(User) do
remove :language
end
end
end
Then run the migrations:
lucky db.migrate
# src/models/user.cr
class User < BaseModel
# ...
table do
column language : String
# ...
end
# ...
end
Create a new file at actions/mixins/set_language.cr
with the following
content:
module SetLanguage
macro included
before set_language
end
private def set_language
if language = current_user.try(&.language) || params.get?(:language)
Rosetta.locale = language
end
continue
end
end
And include it in your BrowserAction
:
# src/actions/browser_action.cr
abstract class BrowserAction < Lucky::Action
# ...
# Set the current language
include SetLanguage
# ...
end
This module tries to set current_user
‘s language. If there isn’t a
signed-in user, it tries to find a language
query parameter. If both
aren’t present, the Rosetta.default_locale
will be used, as configured in
Rosetta’s initializer (config/rosetta.cr
).
The SignUpUser
operation needs some changes:
# src/operations/sign_up_user.cr
class SignUpUser < User::SaveOperation
# ...
permit_columns email, language
# ...
before_save do
# ...
validate_inclusion_of language, in: Rosetta.available_locales
# ...
end
end
Make the error messages translatable in the SignInUser
operation:
# src/operations/sign_in_user.cr
class SignInUser < Avram::Operation
private def validate_credentials(user)
if user
unless Authentic.correct_password?(user, password.value.to_s)
password.add_error r("default.error.incorrect_password").t
end
else
# ...
email.add_error r("default.error.invalid_email").t
end
end
end
And do the same for the RequestPasswordReset
operation:
# src/operations/request_password_reset.cr
class RequestPasswordReset < Avram::Operation
def validate(user : User?)
# ...
if user.nil?
email.add_error r("default.error.invalid_email").t
end
end
end
Basic ideas:
Rosetta.locale
to define lang
attributes of the HTML document# src/pages/main_layout.cr
abstract class MainLayout
needs current_user : User
# ...
def page_title
r(".page_title").t
end
def render
# ...
html lang: Rosetta.locale do
# ...
end
end
private def render_signed_in_user
# ...
link r("default.button.sign_out").t, to: SignIns::Delete, flow_id: "sign-out-button"
end
end
Note: the locale key for
page_title
starts with a.
to tell Rosetta the prefix for this key should be derived from the current class. For example, in theMe::ShowPage
page, the locale key will resolve tome.show_page.page_title
. It’s an easy way to avoid having to define apage_title
method for every single page.
Do the same for AuthLayout
:
# src/pages/auth_layout.cr
abstract class AuthLayout
# ...
def page_title
r(".page_title").t
end
def render
# ...
html lang: Rosetta.locale do
# ...
end
end
end
And the Error::ShowPage
:
# src/pages/errors/show_page.cr
class Errors::ShowPage
def render
# ...
html lang: Rosetta.locale do
# ...
title r(".page_title").t
# ...
end
body do
div class: "container" do
#..
ul class: "helpful-links" do
li do
a r(".helpful_link").t, href: "/", class: "helpful-link"
end
end
end
end
end
# ...
end
Basic Idea:
You’ll need to style the select to your taste.
# src/pages/sign_ups/new_page.cr
class SignUps::NewPage < AuthLayout
# ...
def content
h1 r(".page_title").t
# ...
end
private def render_sign_up_form(op)
form_for SignUps::Create do
# ...
submit r("default.button.sign_up").t, flow_id: "sign-up-button"
end
link r(".sign_in_instead").t, to: SignIns::New
end
private def sign_up_fields(op)
mount Shared::Field,
attribute: op.email,
label_text: r("default.field.email").t, &.email_input(autofocus: "true")
mount Shared::Field,
attribute: op.password,
label_text: r("default.field.password").t, &.password_input
mount Shared::Field,
attribute: op.password_confirmation,
label_text: r("default.field.password_confirmation").t, &.password_input
mount Shared::Field,
attribute: op.language,
label_text: r("default.field.language").t, &.select_input do
options_for_select(op.language, [{"English", "en"}])
end
end
end
Add translations to the pages.
# src/pages/me/show_page.cr
class Me::ShowPage < MainLayout
def content
h1 r(".page_title").t
h3 r(".user_email").t(email: @current_user.email)
# ...
end
private def helpful_tips
h3 r(".next_you_may_want_to").t
ul do
# ...
li r(".modify_page").t
li r(".after_signin").t
end
end
private def link_to_authentication_guides
a r(".auth_guides").t, href: "https://luckyframework.org/guides/authentication"
end
end
Note: The
t
method for ther(".user_email")
translation takes an argument called%{email}
), Rosetta will require an argument with the same name, or raise an error at compile time.
Follow the same logic for the following files (as desired):
# src/pages/password_reset_requests/new_page.cr
# src/pages/password_resets/new_page.cr
# src/pages/sign_ins/new_page.cr
# src/pages/errors/show_page.cr
Translate flash messages.
# src/actions/sign_ins/create.cr
class SignIns::Create < BrowserAction
# ...
if authenticated_user
# ...
flash.success = r(".success").t
# ...
else
flash.failure = r(".failure").t
# ...
end
# ...
end
The same here:
# src/actions/sign_ups/create.cr
class SignUps::Create < BrowserAction
# ...
post "/sign_up" do
SignUpUser.create(params) do |operation, user|
if user
flash.success = r(".success").t
# ...
else
flash.failure = r(".failure").t
# ...
end
end
end
end
And here:
# src/actions/sign_ins/delete.cr
class SignIns::Delete < BrowserAction
delete "/sign_out" do
sign_out
flash.info = r(".success").t
redirect to: SignIns::New
end
end
Follow the same logic in these files:
# src/actions/password_resets/create.cr
# src/actions/password_reset_requests/create.cr
Similar to all previous steps, replace all untranslated strings:
# src/actions/mixins/api/auth/require_auth_token.cr
module Api::Auth::RequireAuthToken
private def auth_error_json
ErrorSerializer.new(
message: r("api.auth.not_authenticated").t,
details: auth_error_details
)
end
private def auth_error_details : String
if auth_token
r("api.auth.token_invalid").t
else
r("api.auth.token_missing").t
end
end
# ...
end