Skip to content

Commit

Permalink
Implement interface translations
Browse files Browse the repository at this point in the history
- Add locale column to Users
- Add Plug for setting Gettext locale
- Add instructions to CONTRIBUTING.md
- Add German translation
  • Loading branch information
wmnnd committed May 11, 2022
1 parent a3d6037 commit a5ffe23
Show file tree
Hide file tree
Showing 25 changed files with 2,993 additions and 80 deletions.
81 changes: 81 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Contributing to Keila

This document describes how to contribute code or translations to the Keila
project.

## Development Setup

Before you get started with developing Keila, you need to set up your development
environment.

### Setup with VS Code and [DevContainer](https://code.visualstudio.com/docs/remote/containers)
* Clone the repository:
`git clone https://github.com/pentacent/keila.git`
* Install the [Remote Container](https://github.com/microsoft/vscode-dev-containers)
extension in Visual Code
* Click on the ![Screenshot of DevContainer icon](.github/assets/devcontainer-button.png)
icon in the bottom left corner and select `Reopen in Container` or look for
`Reopen in Container` in the command panel (`Ctrl+Shift+P`)
* Wait for the containers to build and install all the dependencies needed to
run Keila, including [PostgreSQL](https://www.postgresql.org/)
and [Elixir](https://elixir-lang.org/install.html)
* Open a terminal from VS Code and proceed with the instructions from the
[Run Keila](#run-keila) section

### Setup with other editors
Start by cloning the repository: `git clone https://github.com/pentacent/keila.git`

#### Install Dependencies
[Install Elixir](https://elixir-lang.org/install.html) on your machine and launch
an an instance of [PostgreSQL](https://www.postgresql.org/).

#### Run Keila

* Install dependencies with `mix deps.get`
* Install dependencies and set up the database with `mix setup`
* Start Keila server with `mix phx.server`

Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.

## Contributing Code
Code contributions to Keila are welcome!

If you don’t know where to start, take a look at these [Good First Issues](https://github.com/pentacent/keila/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).

### Contribution Guidelines
Before you commit code to Keila, make sure to run `mix format` to ensure
consistent formatting.


## Translating Keila
Keila uses the [Gettext format](https://en.wikipedia.org/wiki/Gettext) for
translations of the interface.

Translation files are located in `priv/gettext`.

### Getting Started

Before you create a new translation or modify an existing translation, you need
to extract and update all strings from the code:
`mix gettext.extract --merge`

### Modify existing translations
Next you can use the editor of your choice to either edit the existing `.po`
files in `priv/gettext`. A convenient Open Source program for editing
translation files is [Poedit](https://poedit.net).

### Creating a new translation
1) Use the `.pot` templates in `priv/gettext` to create translation files for your
language . New translations should be stored in
`priv/gettext/:language/LC_MESSAGES`.

2) Find the line `config :keila, KeilaWeb.Gettext` in `config/config.exs` and
add the language code of the new language to the `:locales` list.

3) Find the line `def available_locales() do` in `lib/keila_web/gettext.ex` and
add a tuple with the native language name and the language code to the list
returned by the function.

## Finalizing your changes
Run `mix gettext.merge priv/gettext` to compile your changes. Make sure to
commit all changes to `.pot`, `.po` and the compiled `.mo`.
28 changes: 4 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ to SMTP.

![Screenshot of the Keila campaign editor showing the WYSIWYG editor and the default template](.github/assets/keila-20211203.jpg)

## Giving Keila a Try
## Give Keila a Try!

You can give a hosted version of Keila a try at [keila.io/try](https://www.keila.io/try).

Expand All @@ -28,31 +28,11 @@ in this repo.
Follow the [Installation Docs](https://keila.io/docs/installation)
for more details.

## Developing Keila
## Contributing

If you want to give Keila a try, here’s how to get it running from this
repository:
You can contribute to the Keila project with translations or code! Learn more
about how to contribute code or translations to Keila here: [CONTRIBUTING.md](CONTRIBUTING.md)

* [Install Elixir](https://elixir-lang.org/install.html) and make sure you have [PostgreSQL](https://www.postgresql.org/) running on your machine
* Clone the repository:
`git clone https://github.com/pentacent/keila.git`
* Install dependencies with `mix deps.get`
* Install dependencies and set up the database with `mix setup`
* Start Keila server with `mix phx.server`

Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.

## (Optional) Developing Keila inside a [DevContainer](https://code.visualstudio.com/docs/remote/containers)

Here’s how to get it running using DevContainer from Visual Code:

* Install [Remote Container](https://github.com/microsoft/vscode-dev-containers) extension in Visual Code
* Click on the bottom left corner with the icon below and choose `Reopen in Container`

![Screenshot of DevContainer icon](.github/assets/devcontainer-button.png)

* Wait for the containers to build and install all the dependencies needed to run Keila, including [PostgreSQL](https://www.postgresql.org/) and [Elixir](https://elixir-lang.org/install.html)
* Open a terminal inside the container and proceed to run the commands from [Developing Keila](#developing-keila) section

## The Name
Keila is the name of the elephant mascot of this project.
Expand Down
5 changes: 5 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ config :mime, :types, %{
"text/tab-separated-values" => ["tsv"]
}

# Configure locales
config :keila, KeilaWeb.Gettext,
default_locale: "en",
locales: ["de", "en"]

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs"
13 changes: 11 additions & 2 deletions lib/keila/auth/auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ defmodule Keila.Auth do
{:ok, Token.t()} | {:ok, User.t()} | {:error, Changeset.t(User.t())}
def update_user_email(id, params, url_fn \\ &default_url_function/1) do
user = Repo.get(User, id)
changeset = User.update_changeset(user, params)
changeset = User.update_email_changeset(user, params)

if changeset.valid? do
email = Changeset.get_change(changeset, :email)
Expand Down Expand Up @@ -460,7 +460,7 @@ defmodule Keila.Auth do
token = %Token{} ->
user = Repo.get(User, token.user_id)
params = %{email: token.data["email"]}
Repo.update(User.update_changeset(user, params))
Repo.update(User.update_email_changeset(user, params))

_ ->
:error
Expand Down Expand Up @@ -496,6 +496,15 @@ defmodule Keila.Auth do
end
end

@spec set_user_locale(User.id(), String.t()) ::
{:ok, User.t()} | {:error, Changeset.t(User.t())}
def set_user_locale(id, locale) do
id
|> get_user()
|> User.update_locale_changeset(%{locale: locale})
|> Repo.update()
end

@doc """
Creates a token for given `scope` and `user_id`.
Expand Down
26 changes: 9 additions & 17 deletions lib/keila/auth/schemas/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ defmodule Keila.Auth.User do
field(:password, :string, virtual: true)
field(:password_hash, :string)

field(:locale, :string)

field(:activated_at, :utc_datetime)

has_many(:user_groups, Keila.Auth.UserGroup)
Expand All @@ -19,29 +21,27 @@ defmodule Keila.Auth.User do
@spec creation_changeset(t() | Ecto.Changeset.data()) :: Ecto.Changeset.t(t)
def creation_changeset(struct \\ %__MODULE__{}, params) do
struct
|> cast(params, [:email, :password])
|> cast(params, [:email, :password, :locale])
|> validate_email()
|> validate_password()
end

@doc """
Changeset for User updates
"""
@spec update_changeset(t() | Ecto.Changeset.data()) :: Ecto.Changeset.t(t)
def update_changeset(struct \\ %__MODULE__{}, params) do
struct
|> cast(params, [:email, :password])
|> validate_email()
|> maybe_validate_password()
end

@spec update_email_changeset(t() | Ecto.Changeset.data()) :: Ecto.Changeset.t(t)
def update_email_changeset(struct \\ %__MODULE__{}, params) do
struct
|> cast(params, [:email])
|> validate_email()
end

@spec update_locale_changeset(t() | Ecto.Changeset.data()) :: Ecto.Changeset.t(t)
def update_locale_changeset(struct \\ %__MODULE__{}, params) do
struct
|> cast(params, [:locale])
end

@spec update_password_changeset(t() | Ecto.Changeset.data()) :: Ecto.Changeset.t(t)
def update_password_changeset(struct \\ %__MODULE__{}, params) do
struct
Expand All @@ -57,14 +57,6 @@ defmodule Keila.Auth.User do
|> validate_length(:email, max: 255)
|> unique_constraint(:email)
end

defp maybe_validate_password(changeset) do
case get_change(changeset, :password) do
nil -> changeset
_pw -> validate_password(changeset)
end
end

defp validate_password(changeset) do
changeset
|> validate_required([:password])
Expand Down
15 changes: 13 additions & 2 deletions lib/keila_web/controllers/account_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ defmodule KeilaWeb.AccountController do
end

@spec post_edit(Plug.Conn.t(), map()) :: Plug.Conn.t()
def post_edit(conn, params) do
params = Map.get(params, "user", %{})
def post_edit(conn, %{"user" => %{"password" => password}}) do
params = %{password: password}

case Auth.update_user_password(conn.assigns.current_user.id, params) do
{:ok, user} ->
Expand All @@ -24,6 +24,17 @@ defmodule KeilaWeb.AccountController do
end
end

def post_edit(conn, %{"user" => %{"locale" => locale}}) do
case Auth.set_user_locale(conn.assigns.current_user.id, locale) do
{:ok, _user} ->
conn
|> redirect(to: Routes.account_path(conn, :edit))

{:error, changeset} ->
render_edit(conn, changeset)
end
end

defp render_edit(conn, changeset) do
account = Accounts.get_user_account(conn.assigns.current_user.id)
credits = if account, do: Accounts.get_credits(account.id)
Expand Down
7 changes: 7 additions & 0 deletions lib/keila_web/gettext.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,11 @@ defmodule KeilaWeb.Gettext do
|> then(fn html -> {:safe, html} end)
end
end

def available_locales() do
[
{"English", "en"},
{"Deutsch", "de"}
]
end
end
68 changes: 68 additions & 0 deletions lib/keila_web/helpers/auth_session/put_locale_plug.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
defmodule KeilaWeb.PutLocalePlug do
@moduledoc """
Plug for setting the Gettext locale.
When the `:current_user` assign is available and has a locale set, the locale
is taken from the user’s settings. Otherwise the locales from the
`accept-language` header are used.
"""

alias Keila.Auth.User

@spec init(list()) :: list()
def init(_), do: []

@spec call(Plug.Conn.t(), list()) :: Plug.Conn.t()
def call(conn, _opts) do
case conn.assigns[:current_user] do
%User{locale: locale} when is_binary(locale) -> put_locale(locale)
_other -> put_locale_from_headers(conn)
end

conn
end

defp put_locale(locale) do
locales = Application.get_env(:keila, KeilaWeb.Gettext) |> Keyword.fetch!(:locales)

if locale in locales do
Gettext.put_locale(locale)
:ok
else
:error
end
end

defp put_locale_from_headers(conn) do
accepted_locales = get_accepted_locales(conn)

# We’re not actually trying to find this value, but this is the easiest
# way to run the `put_locale` side effect until we’ve found a locale that
# is supported
Enum.find(accepted_locales, fn locale ->
put_locale(locale) == :ok
end)
end

defp get_accepted_locales(conn) do
conn
|> Plug.Conn.get_req_header("accept-language")
|> Enum.join(",")
|> String.split(",")
|> Enum.map(&String.trim/1)
|> Enum.map(fn language ->
case String.split(language, ";q=") do
[language] ->
{language, 1.0}

[language, quality] ->
case Float.parse(quality) do
{quality, _} -> {language, quality}
:error -> {language, 1.0}
end
end
end)
|> Enum.sort_by(&elem(&1, 1), :desc)
|> Enum.map(&elem(&1, 0))
end
end
1 change: 1 addition & 0 deletions lib/keila_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ defmodule KeilaWeb.Router do
plug :put_secure_browser_headers
plug KeilaWeb.Meta.Plug
plug KeilaWeb.AuthSession.Plug
plug KeilaWeb.PutLocalePlug
end

# Non-authenticated Routes
Expand Down
25 changes: 25 additions & 0 deletions lib/keila_web/templates/account/edit.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,31 @@
</button>
</div>
</.form>
<.form
let={f}
for={@changeset}
action={ Routes.account_path(@conn, :post_edit)}
class="rounded shadow p-8 mt-8 max-w-5xl mx-auto flex flex-col gap-4 bg-gray-900 text-gray-50"
@change="setUnsavedReminder(true)"
>
<h2 class="text-3xl font-bold">
<%= gettext("Change Language") %>
</h2>
<p class="text-lg text-gray-200">
<%= gettext("Here you can change the language for the Keila interface.") %>
</p>
<div class="flex flex-col">
<%= label(f, :locale, gettext("Language")) %>
<%= with_validation(f, :locale) do %>
<%= select(f, :locale, KeilaWeb.Gettext.available_locales(), [class: "text-black"]) %>
<% end %>
</div>
<div class="flex justify-end mt-8">
<button class="button button--cta button--large" @click="setUnsavedReminder(false)">
<%= gettext("Update language settings") %>
</button>
</div>
</.form>
</div>

<script src="https://cdn.paddle.com/paddle/paddle.js"></script>
Expand Down
Loading

0 comments on commit a5ffe23

Please sign in to comment.