1. Intro
Working with multiple languages If these steps are done in order then Lucky should continue to compile (& be usable/testable) with each change.
TLDR: After configuration you can apply translations using either:
# simplest (requires user_lang or current_user to be defined)
t("tranlation.key.values")
# or where ever current user is available
I18n.t("tranlation.key.values", current_user.lang)
This document assumes you are using the default [Authentication](https://luckyframework.org/guides/authentication) - if not, you will need to make adjustments to the
user
in the Translator!
2. Add i18n shard
dependencies:
i18n:
github: vladfaust/i18n.cr
version: ~> 0.1.1
Add to the end of src/shards.cr
file with the new requirements
# src/shards.cr
# ...
require "i18n"
require "i18n/backends/yaml"
and of course install the shard
$ shards install
3. Add localization ymls
first make a locales folder:
$ mkdir config/locales
Then add at least one default localization: i.e. in this case English. Below is a list of the starting keys in a standard LUCKY project with authorization and web pages.
* Add additional langauges as needed in the same folder. Language files need to be named with the 2-letter
language code (ISO 639-1) https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes - so the sample file is named en.yml
* The yml file must start with it’s langauge code, i.e. in this example en:
(becareful, yml files are indentation sensitive)!
* Be sure that all lang yml files contain the same keys. If there’s no translation for that key, just leave the value blank. If you miss a language key you will see an error message like: MISSING: de.auth.sign_out
> NOTE: If you missed a key and get the above error YOU MUST RESTART LUCKY to reload its config.
# config/locales/en.yml
en:
action:
save_success: "The record has been saved"
update_success: "The record has been updated"
delete_success: "Deleted the record"
index_title: "All Records"
create_new: "New Record"
new: New
edit: Edit
delete: Delete
confirm: "Are you sure?"
update: Update
updating: "Updating..."
save: Save
saving: "Saving..."
back: Back
back_to_index: "Back to All"
auth:
sign_in: "Sign In"
sign_in_success: "You're now signed in"
sign_in_failure: "Sign in failed"
sign_up: "Sign up"
sign_up_success: "Thanks for signing up"
sign_up_failure: "Couldn't sign you up"
sign_out: "Sign Out"
signed_out: "You have been signed out"
pwd_update: "Update Password"
pwd_update_success: "Your password has been reset"
pwd_reset: "Password Reset"
pwd_reset_request: "Reset your Password"
pwd_reset_req_success: "You should receive an email on how to reset your password shortly"
auth_token:
not_authenticated: "Not Authenticated."
invalid: "The provided authentication token was incorrect."
missing: "An authentication token is required. Please include a token in an 'auth_token' param or 'Authorization' header."
default:
page_name: Welcome
error:
title: "Something went wrong"
try_home: "Try heading back to home"
locked_out: "Locked-out"
auth_incorrect: "is wrong"
form_not_valid: "It looks like the form is not valid"
not_in_system: "is not in our system"
user:
email: E-Mail
profile: Profile
next: "Next, you may want to"
auth_guides: "Check out the 'Authentication Guides'"
modify_page: "Modify this page"
after_signin: "Change where you go after sign in"
preferred_language: "Preferred Language"
4. Configure i18n within Lucky
The i18n needs to know the location of your language files. > NOTE: This config file is only executed at load, so all language changes require a server restart.
# config/i18n.cr
I18n.backend = I18n::Backends::YAML.new.tap do |backend|
backend.load_paths << Dir.current + "/config/locales"
backend.load
end
5. Add 'lang' to users table
This setup will assocatiate a language key with each user this language key is used when displaying information. Generate a migration using:
$ lucky gen.migration AddLanguageToUser
Edit the new migration file in db/migrations/
:
# db/migrations/#{Time.utc.to_s("%Y%m%d%H%I%S")}_add_language_to_user.cr
class AddLanguageToUser::V20191228100116 < Avram::Migrator::Migration::V1
def migrate
alter table_for(User) do
add lang : String, default: "en" # the appropriate default lang key
end
end
def rollback
alter table_for(User) do
remove :lang
end
end
end
Of course migrate
$ lucky db.migrate
6. Add lang column to User model
# src/models/user.cr
class User < BaseModel
# ...
table do
column lang : String
# ...
end
# ...
end
7. Create a Translator module
First create a location to extend your lucky system (I suggest mixins
):
$ mkdir src/mixins
$ touch src/mixins/translator.cr
# src/mixins/translator.cr
module Translator
LANGUAGE_DEFAULT = "en"
LANGUAGES_AVAILABLE = ["en", "de"]
LANGUAGES_SELECTOR_LIST = [{"English", "en"}, {"Deutsch", "de"}]
def t(key : String)
I18n.translate(key, user_lang)
end
def t(key : String, count : Int32)
I18n.translate(key, user_lang, count)
end
# in places where current_user / user isn't available be sure to override this method with
# `quick_def user_lang, LANGUAGE_DEFAULT`
def user_lang
current_user.try(&.lang) || LANGUAGE_DEFAULT
end
end
Add this module to the src/app.cr
so its available to Lucky files.
> NOTE: Put this at the top of this config file to be sure it is available to all aspect of Lucky!
# src/app.cr
require "./shards"
# Load the asset manifest in public/mix-manifest.json
Lucky::AssetHelpers.load_manifest
require "./mixins/translator"
# ...
8. Update Operations
SignUp Save Opoeration needs: - Update permitted columns (required for the signup form) - Update validations (will prevent run-time crashes)
# src/operations/sign_up_user.cr
class SignUpUser < User::SaveOperation
# ...
permit_columns email, lang
# ...
before_save do
# ...
validate_inclusion_of lang, in: Translator::LANGUAGES_AVAILABLE
# ...
end
end
Other Operation Files with translations need:
- Add include Translator
to the class
- Add quick_def user_lang
, LANGUAGE_DEFAULT for the failure error messages (ok since happy path messages are handled in other paths)
- Add translations: i.e. t("translation.keys")
* in cases where there is no user in the entire class override user_lang
with quick_def user_lang, LANGUAGE_DEFAULT
* in cases where the user login failed (or something like that) you can translate using: I18n.t("translation.keys", LANGUAGE_DEFAULT)
or override the user_lang
locally with user_lang = LANGUAGE_DEFAULT
- TODO: The translation module should use language settigns from the frontend (JS) first and fallback to the user or default setting.
Thus Sign_in would look like the situation with no user since the only messages it creates are when the login fails.
# src/operations/sign_in_user.cr
class SignInUser < Avram::Operation
# ...
include Translator
quick_def user_lang, LANGUAGE_DEFAULT
# ...
private def validate_credentials(user)
if user
unless Authentic.correct_password?(user, password.value.to_s)
password.add_error t("error.auth_incorrect")
end
else
# ...
email.add_error t("error.not_in_system")
end
end
end
Similarly, RequestPasswordReset only messages when the user can’t be found.
# src/operations/request_password_reset.cr
class RequestPasswordReset < Avram::Operation
# ...
include Translator
quick_def user_lang, LANGUAGE_DEFAULT
# ...
def validate(user : User?)
# ...
if user.nil?
email.add_error t("error.not_in_system")
end
end
end
9. Internationalize Templates
Basic ideas:
- Every Layout (abstract class) needs the include Translator
- Everywhere there is static text a translations can be added
# src/pages/main_layout.cr
abstract class MainLayout
include Translator
# ...
needs current_user : User
# make @current_user available as current_user
getter current_user
# ...
def page_title
t("default.page_name")
end
def render
# ...
html lang: user_lang do
# ...
end
end
private def render_signed_in_user
# ...
link t("auth.sign_out"), to: SignIns::Delete, flow_id: "sign-out-button"
end
end
AuthLayout needs updates and user_lang defined (since no user is available yet)
# src/pages/auth_layout.cr
abstract class AuthLayout
include Translator
# ...
# since user hasn't logged in yet - we set the user_lang to the default language
quick_def user_lang, LANGUAGE_DEFAULT
# ...
def page_title
t("default.page_name")
end
def render
# ...
html lang: user_lang do
# ...
end
end
end
Error Show Page also additinally needs user_lang defined.
# src/pages/errors/show_page.cr
class Errors::ShowPage
# ...
include Translator
# ...
# in error conditions we don't know if we have a current_user - so we use the default language
quick_def user_lang, LANGUAGE_DEFAULT
# ...
def render
# ...
html lang: user_lang do
# ...
title t("error.title")
# ...
end
end
# ...
end
10. Update Sign-up Form
Basic Idea: - Add translations - Add language choices to the sign-up form - You’ll need to style the select to your tastes.
# src/pages/sign_ups/new_page.cr
class SignUps::NewPage < AuthLayout
# ...
def content
h1 t("auth.sign_up")
# ...
end
private def render_sign_up_form(op)
form_for SignUps::Create do
# ...
submit t("auth.sign_up"), flow_id: "sign-up-button"
end
link t("auth.sign_in"), to: SignIns::New
end
private def sign_up_fields(op)
label_for op.lang, t("user.preferred_language")
select_input(op.lang) do
options_for_select(op.lang, LANGUAGES_SELECTOR_LIST)
end
# ...
end
end
11. Internationalize Pages
Add translations to the pages.
# src/pages/me/show_page.cr
class Me::ShowPage < MainLayout
def content
h1 t("me.profile")
h3 "\#{t("me.email")}: \#{@current_user.email}"
# ...
end
private def helpful_tips
h3 "\#{t("me.next")}:"
ul do
# ...
li "\#{t("me.modify_page")}: src/pages/me/show_page.cr"
li "\#{t("me.after_signin")}: src/actions/home/index.cr"
end
end
private def link_to_authentication_guides
link t("me.auth_guides"), to: "https://luckyframework.org/guides/authentication"
end
end
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
12. Internationalize Actions
Add include Translator
to the abstract class BrowserAction - this allows translations in flash messages too.
# src/actions/browser_action.cr
abstract class BrowserAction < Lucky::Action
include Translator
# ...
end
In these next two classes (Actions) there are cases where the user context may not be available - so assign user_lang
to the LANGUAGE_DEFAULT
)
# src/actions/sign_ins/create.cr
class SignIns::Create < BrowserAction
# ...
if authenticated_user
# ...
flash.success = t("auth.success")
# ...
else
# may be needed when user auth fails
user_lang = LANGUAGE_DEFAULT
flash.failure = t("auth.failure")
# ...
end
# ...
end
And the same here.
# src/actions/sign_ups/create.cr
class SignUps::Create < BrowserAction
# ...
route do
SignUpUser.create(params) do |operation, user|
if user
flash.success = t("auth.sign_up_success")
# ...
else
# when user signup fails we need a language preference
user_lang = LANGUAGE_DEFAULT
flash.failure = t("auth.sign_up_failure")
# ...
end
end
end
end
With SignIns::Delete (Sign-out) - put the flash assignment first so it has the user conext before the user session is gone.
# src/actions/sign_ins/delete.cr
class SignIns::Delete < BrowserAction
delete "/sign_out" do
# assign the flash before loosing the current_user
flash.info = t("auth.signed_out")
sign_out
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
13. Internationalize API Responses
If standard APIs responses need translation include Translator
here:
# src/actions/api_action.cr
abstract class ApiAction < Lucky::Action
include Translator
# ...
end
And here too for API Auth Responses
# src/actions/mixins/api/auth/require_auth_token.cr
module Api::Auth::RequireAuthToken
include Translator
# ...
private def auth_error_json
# since we have no valid user define `user_lang`
user_lang = LANGUAGE_DEFAULT
ErrorSerializer.new(
message: t("auth_token.not_authenticated"), details: auth_error_details
)
end
private def auth_error_details : String
# since we have no valid user define `user_lang`
user_lang = LANGUAGE_DEFAULT
if auth_token
t("auth_token.invalid")
else
t("auth_token.missing")
end
end
# ...
end