changed README.md
 
@@ -102,7 +102,7 @@ by adding `kanta` to your list of dependencies in `mix.exs`:
102
102
```elixir
103
103
def deps do
104
104
[
105
- {:kanta, "~> 0.3.0"},
105
+ {:kanta, "~> 0.3.1"},
106
106
{:gettext, git: "[email protected]:ravensiris/gettext.git", branch: "runtime-gettext"}
107
107
]
108
108
end
 
@@ -180,9 +180,9 @@ In the `application.ex` file of our project, we add Kanta and its configuration
180
180
181
181
## Kanta UI
182
182
183
- Inside your `router.ex` file we need to connect the Kanta panel using the kanta_dashboard macro.
183
+ Inside your `router.ex` file we need to connect the Kanta panel using the kanta_dashboard macro.
184
184
185
- ```elixir
185
+ ```elixir
186
186
import KantaWeb.Router
187
187
188
188
scope "/" do
 
@@ -226,7 +226,7 @@ Not all of us are polyglots, and sometimes we need the help of machine translati
226
226
227
227
```elixir
228
228
# mix.exs
229
- defp deps
229
+ defp deps do
230
230
...
231
231
{:kanta_deep_l_plugin, "~> 0.1.1"}
232
232
end
 
@@ -241,6 +241,35 @@ config :kanta,
241
241
]
242
242
```
243
243
244
+ ## KantaSync
245
+
246
+ The [KantaSync plugin](https://github.com/curiosum-dev/kanta_sync_plugin) allows you to synchronize translations between your production and staging/dev environments. It ensures that any changes made to translations in one are reflected in the others, helping you maintain consistency across different stages of development.
247
+
248
+ ```elixir
249
+ # mix.exs
250
+ defp deps do
251
+ ...
252
+ {:kanta_sync_plugin, "~> 0.1.0"}
253
+ end
254
+ ```
255
+
256
+ You need to have Kanta API configured by using kanta_api macro.
257
+
258
+ ```elixir
259
+ # router.ex
260
+ import KantaWeb.Router
261
+
262
+ scope "/" do
263
+ kanta_api("/kanta-api")
264
+ end
265
+ ```
266
+
267
+ ### Authorization
268
+
269
+ Set `KANTA_SECRET_TOKEN` environment variable for restricting API access. It should be generated with `mix phx.gen.secret 256` and both environments must have the same `KANTA_SECRET_TOKEN` environment variables.
270
+
271
+ You can also disable default authorization mechanism and use your own, by passing `disable_api_authorization: true` option into Kanta's config.
272
+
244
273
## PO Writer
245
274
246
275
Kanta was created to allow easy management of static text translations in the application, however, for various reasons like wanting a backup or parallel use of other tools like TMS etc. you may want to overwrite .po files with translations entered in Kanta. To install it append `{:kanta_po_writer_plugin, git: "https://github.com/curiosum-dev/kanta_po_writer_plugin"}` to your `deps` list. Currently, it's not on Hex because it's in a pre-release version. Then add `Kanta.Plugins.POWriter` to the list of plugins, and new functions will appear in the Kanta UI to allow writing to .po files.
changed hex_metadata.config
 
@@ -1,6 +1,6 @@
1
1
{<<"links">>,[{<<"GitHub">>,<<"https://github.com/curiosum-dev/kanta">>}]}.
2
2
{<<"name">>,<<"kanta">>}.
3
- {<<"version">>,<<"0.3.0">>}.
3
+ {<<"version">>,<<"0.3.1">>}.
4
4
{<<"description">>,
5
5
<<"User-friendly translations manager for Elixir/Phoenix projects.">>}.
6
6
{<<"elixir">>,<<"~> 1.14">>}.
 
@@ -13,9 +13,11 @@
13
13
<<"lib/kanta/migrations/postgresql/v01.ex">>,
14
14
<<"lib/kanta/migrations/postgresql/v02.ex">>,
15
15
<<"lib/kanta/migrations/postgresql.ex">>,<<"lib/kanta/translations.ex">>,
16
- <<"lib/kanta/cache.ex">>,<<"lib/kanta/types.ex">>,
17
- <<"lib/kanta/validator.ex">>,<<"lib/kanta/config.ex">>,
18
- <<"lib/kanta/utils">>,<<"lib/kanta/utils/module_utils.ex">>,
16
+ <<"lib/kanta/cache.ex">>,<<"lib/kanta/types.ex">>,<<"lib/kanta/specs">>,
17
+ <<"lib/kanta/specs/schemata_spec.ex">>,<<"lib/kanta/validator.ex">>,
18
+ <<"lib/kanta/config.ex">>,<<"lib/kanta/utils">>,
19
+ <<"lib/kanta/utils/database_populator.ex">>,
20
+ <<"lib/kanta/utils/get_schemata.ex">>,<<"lib/kanta/utils/module_utils.ex">>,
19
21
<<"lib/kanta/query.ex">>,<<"lib/kanta/migration.ex">>,
20
22
<<"lib/kanta/gettext">>,<<"lib/kanta/gettext/repo.ex">>,
21
23
<<"lib/kanta/po_files">>,
 
@@ -33,6 +35,7 @@
33
35
<<"lib/kanta/translations/plural_translation/finders">>,
34
36
<<"lib/kanta/translations/plural_translation/finders/get_plural_translation.ex">>,
35
37
<<"lib/kanta/translations/plural_translation/finders/list_plural_translations.ex">>,
38
+ <<"lib/kanta/translations/plural_translation/finders/list_translated_plural_translations.ex">>,
36
39
<<"lib/kanta/translations/plural_translation/plural_translation_spec.ex">>,
37
40
<<"lib/kanta/translations/messages">>,
38
41
<<"lib/kanta/translations/messages/message_spec.ex">>,
 
@@ -71,6 +74,7 @@
71
74
<<"lib/kanta/translations/singular_translation/singular_translations.ex">>,
72
75
<<"lib/kanta/translations/singular_translation/finders">>,
73
76
<<"lib/kanta/translations/singular_translation/finders/get_singular_translation.ex">>,
77
+ <<"lib/kanta/translations/singular_translation/finders/list_translated_singular_translations.ex">>,
74
78
<<"lib/kanta/translations/singular_translation/finders/list_singular_translations.ex">>,
75
79
<<"lib/kanta/translations/singular_translation/singular_translation_spec.ex">>,
76
80
<<"lib/kanta/translations/locale.ex">>,<<"lib/kanta_web">>,
 
@@ -92,6 +96,7 @@
92
96
<<"lib/kanta_web/components/shared/select/select.html.heex">>,
93
97
<<"lib/kanta_web/components/shared/select/select.ex">>,
94
98
<<"lib/kanta_web/plugs">>,<<"lib/kanta_web/plugs/redirect.ex">>,
99
+ <<"lib/kanta_web/plugs/api_auth_plug.ex">>,
95
100
<<"lib/kanta_web/plugs/assets.ex">>,<<"lib/kanta_web/live">>,
96
101
<<"lib/kanta_web/live/dashboard">>,
97
102
<<"lib/kanta_web/live/dashboard/dashboard_live">>,
 
@@ -142,6 +147,14 @@
142
147
<<"lib/kanta_web/live/translations/translation_form_live/components/singular_translation_form/singular_translation_form.html.heex">>,
143
148
<<"lib/kanta_web/templates">>,<<"lib/kanta_web/templates/layouts">>,
144
149
<<"lib/kanta_web/templates/layouts/dashboard.html.heex">>,
150
+ <<"lib/kanta_web/controllers">>,<<"lib/kanta_web/controllers/api">>,
151
+ <<"lib/kanta_web/controllers/api/kanta_api_controller.ex">>,
152
+ <<"lib/kanta_web/controllers/api/plural_translations_controller.ex">>,
153
+ <<"lib/kanta_web/controllers/api/contexts_controller.ex">>,
154
+ <<"lib/kanta_web/controllers/api/domains_controller.ex">>,
155
+ <<"lib/kanta_web/controllers/api/singular_translations_controller.ex">>,
156
+ <<"lib/kanta_web/controllers/api/locales_controller.ex">>,
157
+ <<"lib/kanta_web/controllers/api/messages_controller.ex">>,
145
158
<<"lib/kanta_web/views">>,<<"lib/kanta_web/views/layout_view.ex">>,
146
159
<<"lib/kanta.ex">>,<<"priv">>,<<"priv/iso639.json">>,<<"dist">>,
147
160
<<"dist/css">>,<<"dist/css/app.css">>,<<"dist/js">>,<<"dist/js/app.js">>,
changed lib/kanta/config.ex
 
@@ -7,14 +7,16 @@ defmodule Kanta.Config do
7
7
otp_name: atom(),
8
8
repo: module(),
9
9
endpoint: module(),
10
- plugins: false | [module() | {module() | Keyword.t()}]
10
+ plugins: false | [module() | {module() | Keyword.t()}],
11
+ disable_api_authorization: boolean()
11
12
}
12
13
13
14
defstruct name: Kanta,
14
15
otp_name: nil,
15
16
repo: nil,
16
17
endpoint: nil,
17
- plugins: []
18
+ plugins: [],
19
+ disable_api_authorization: false
18
20
19
21
alias Kanta.Validator
20
22
 
@@ -71,6 +73,15 @@ defmodule Kanta.Config do
71
73
end
72
74
end
73
75
76
+ defp validate_opt(_opts, {:disable_api_authorization, disable_api_authorization}) do
77
+ if is_boolean(disable_api_authorization) do
78
+ :ok
79
+ else
80
+ {:error,
81
+ "expected :disable_api_authorization to be a boolean, got: #{inspect(disable_api_authorization)}"}
82
+ end
83
+ end
84
+
74
85
defp validate_opt(_opts, option) do
75
86
{:unknown, option, __MODULE__}
76
87
end
changed lib/kanta/query.ex
 
@@ -34,10 +34,21 @@ defmodule Kanta.Query do
34
34
Repo.get_repo().one(query, opts)
35
35
end
36
36
37
- def paginate(query, page \\ 1, per_page \\ 15)
38
- def paginate(query, nil, nil), do: paginate(query, 1, 15)
37
+ @default_page_size 100
38
+ @minimum_per_page 10
39
+
40
+ @spec paginate(Ecto.Query.t(), integer() | nil, integer() | nil) :: map()
41
+ @spec paginate(Ecto.Query.t(), integer() | nil) :: map()
42
+ @spec paginate(Ecto.Query.t()) :: map()
43
+
44
+ def paginate(query, page \\ 1, per_page \\ @default_page_size)
39
45
40
46
def paginate(query, page, per_page) do
47
+ page = if is_number(page), do: max(page, 1), else: 1
48
+
49
+ per_page =
50
+ if is_number(per_page), do: max(per_page, @minimum_per_page), else: @default_page_size
51
+
41
52
%{
42
53
entries: entries,
43
54
page_number: page_number,
 
@@ -51,7 +62,7 @@ defmodule Kanta.Query do
51
62
caller: self(),
52
63
module: Repo.get_repo(),
53
64
page_number: page || 1,
54
- page_size: per_page || 15,
65
+ page_size: per_page || @default_page_size,
55
66
options: []
56
67
}
57
68
)
added lib/kanta/specs/schemata_spec.ex
 
@@ -0,0 +1,7 @@
1
+ defmodule Kanta.Specs.SchemataSpec do
2
+ @moduledoc false
3
+
4
+ @type schema() :: {String.t(), %{schema: atom(), conflict_target: atom() | [atom()]}}
5
+
6
+ @type t() :: [schema()]
7
+ end
changed lib/kanta/translations/context.ex
 
@@ -8,8 +8,13 @@ defmodule Kanta.Translations.Context do
8
8
9
9
alias Kanta.Translations.Message
10
10
11
+ @required_fields ~w(name)a
12
+ @optional_fields ~w(description color)a
13
+
11
14
@type t() :: Kanta.Translations.ContextSpec.t()
12
15
16
+ @derive {Jason.Encoder, only: [:id] ++ @required_fields ++ @optional_fields}
17
+
13
18
schema "kanta_contexts" do
14
19
field :name, :string
15
20
field :description, :string
 
@@ -22,7 +27,7 @@ defmodule Kanta.Translations.Context do
22
27
23
28
def changeset(struct, params) do
24
29
struct
25
- |> cast(params, [:name, :description, :color])
26
- |> validate_required([:name])
30
+ |> cast(params, @required_fields ++ @optional_fields)
31
+ |> validate_required(@required_fields)
27
32
end
28
33
end
changed lib/kanta/translations/domain.ex
 
@@ -8,8 +8,13 @@ defmodule Kanta.Translations.Domain do
8
8
9
9
alias Kanta.Translations.Message
10
10
11
+ @required_fields ~w(name)a
12
+ @optional_fields ~w(description color)a
13
+
11
14
@type t() :: Kanta.Translations.DomainSpec.t()
12
15
16
+ @derive {Jason.Encoder, only: [:id] ++ @required_fields ++ @optional_fields}
17
+
13
18
schema "kanta_domains" do
14
19
field :name, :string
15
20
field :description, :string
 
@@ -22,7 +27,7 @@ defmodule Kanta.Translations.Domain do
22
27
23
28
def changeset(struct, params) do
24
29
struct
25
- |> cast(params, [:name, :description, :color])
26
- |> validate_required([:name])
30
+ |> cast(params, @required_fields ++ @optional_fields)
31
+ |> validate_required(@required_fields)
27
32
end
28
33
end
changed lib/kanta/translations/locale.ex
 
@@ -7,11 +7,13 @@ defmodule Kanta.Translations.Locale do
7
7
import Ecto.Changeset
8
8
alias Kanta.Translations.SingularTranslation
9
9
10
- @all_fields ~w(iso639_code name plurals_header native_name family wiki_url colors)a
11
10
@required_fields ~w(iso639_code name native_name)a
11
+ @optional_fields ~w(plurals_header family wiki_url colors)a
12
12
13
13
@type t() :: Kanta.Translations.LocaleSpec.t()
14
14
15
+ @derive {Jason.Encoder, only: [:id] ++ @required_fields ++ @optional_fields}
16
+
15
17
schema "kanta_locales" do
16
18
field :iso639_code, :string
17
19
field :name, :string
 
@@ -29,7 +31,7 @@ defmodule Kanta.Translations.Locale do
29
31
30
32
def changeset(struct, params) do
31
33
struct
32
- |> cast(params, @all_fields)
34
+ |> cast(params, @required_fields ++ @optional_fields)
33
35
|> validate_required(@required_fields)
34
36
end
35
37
end
changed lib/kanta/translations/locale/finders/list_locales.ex
 
@@ -9,6 +9,7 @@ defmodule Kanta.Translations.Locale.Finders.ListLocales do
9
9
10
10
def find(params \\ []) do
11
11
base()
12
+ |> order_by(:id)
12
13
|> filter_query(params[:filter])
13
14
|> preload_resources(params[:preloads] || [])
14
15
|> paginate(params[:page], params[:per_page])
changed lib/kanta/translations/message.ex
 
@@ -8,10 +8,12 @@ defmodule Kanta.Translations.Message do
8
8
9
9
alias Kanta.Translations.{Context, Domain, PluralTranslation, SingularTranslation}
10
10
11
+ @required_fields ~w(msgid message_type)a
12
+ @optional_fields ~w(domain_id context_id)a
13
+
11
14
@type t() :: Kanta.Translations.MessageSpec.t()
12
15
13
- @all_fields ~w(msgid message_type domain_id context_id)a
14
- @required_fields ~w(msgid message_type)a
16
+ @derive {Jason.Encoder, only: [:id] ++ @required_fields ++ @optional_fields}
15
17
16
18
schema "kanta_messages" do
17
19
field :msgid, :string
 
@@ -28,7 +30,7 @@ defmodule Kanta.Translations.Message do
28
30
29
31
def changeset(struct, params) do
30
32
struct
31
- |> cast(params, @all_fields)
33
+ |> cast(params, @required_fields ++ @optional_fields)
32
34
|> validate_required(@required_fields)
33
35
end
34
36
end
changed lib/kanta/translations/messages/finders/list_messages.ex
 
@@ -13,13 +13,16 @@ defmodule Kanta.Translations.Messages.Finders.ListMessages do
13
13
@available_filters ~w(domain_id context_id)
14
14
15
15
def find(params \\ []) do
16
+ filters = params[:filter] || %{}
17
+ query_filters = Map.take(filters, @available_filters)
18
+
16
19
base()
17
- |> filter_query(Map.take(params[:filter], @available_filters))
18
- |> not_translated_query(params[:filter])
19
- |> search_subquery([locale_id: params[:filter]["locale_id"]], params[:search])
20
+ |> filter_query(query_filters)
21
+ |> not_translated_query(filters)
22
+ |> search_subquery(filters, params[:search])
20
23
|> distinct(true)
21
24
|> preload_resources(params[:preloads] || [])
22
- |> paginate(String.to_integer(params[:page] || "1"), params[:per_page])
25
+ |> paginate(params[:page], params[:per_page])
23
26
end
24
27
25
28
defp not_translated_query(query, %{"locale_id" => locale_id, "not_translated" => "true"}) do
 
@@ -41,6 +44,12 @@ defmodule Kanta.Translations.Messages.Finders.ListMessages do
41
44
defp search_subquery(query, _, nil), do: query
42
45
defp search_subquery(query, _, ""), do: query
43
46
47
+ defp search_subquery(query, %{"locale_id" => locale_id}, search) do
48
+ search_subquery(query, [locale_id: locale_id], search)
49
+ end
50
+
51
+ defp search_subquery(query, filter, _) when is_map(filter), do: query
52
+
44
53
defp search_subquery(query, filter, search) do
45
54
sub =
46
55
base()
changed lib/kanta/translations/plural_translation.ex
 
@@ -8,11 +8,13 @@ defmodule Kanta.Translations.PluralTranslation do
8
8
9
9
alias Kanta.Translations.{Locale, Message}
10
10
11
- @all_fields ~w(nplural_index original_text translated_text locale_id message_id)a
12
11
@required_fields ~w(nplural_index message_id locale_id)a
12
+ @optional_fields ~w(original_text translated_text)a
13
13
14
14
@type t() :: Kanta.Translations.PluralTranslationSpec.t()
15
15
16
+ @derive {Jason.Encoder, only: [:id] ++ @required_fields ++ @optional_fields}
17
+
16
18
schema "kanta_plural_translations" do
17
19
field :nplural_index, :integer
18
20
field :original_text, :string
 
@@ -26,7 +28,7 @@ defmodule Kanta.Translations.PluralTranslation do
26
28
27
29
def changeset(struct, attrs \\ %{}) do
28
30
struct
29
- |> cast(attrs, @all_fields)
31
+ |> cast(attrs, @required_fields ++ @optional_fields)
30
32
|> validate_required(@required_fields)
31
33
|> foreign_key_constraint(:locale_id)
32
34
|> foreign_key_constraint(:message_id)
added lib/kanta/translations/plural_translation/finders/list_translated_plural_translations.ex
 
@@ -0,0 +1,23 @@
1
+ defmodule Kanta.Translations.PluralTranslations.Finders.ListTranslatedPluralTranslations do
2
+ @moduledoc """
3
+ Query module aka Finder responsible for listing translated plural translations
4
+ """
5
+
6
+ use Kanta.Query,
7
+ module: Kanta.Translations.PluralTranslation,
8
+ binding: :plural_translation
9
+
10
+ alias Kanta.Repo
11
+
12
+ def find do
13
+ base()
14
+ |> translated_query()
15
+ |> Repo.get_repo().all()
16
+ end
17
+
18
+ defp translated_query(query) do
19
+ from(pt in query,
20
+ where: not is_nil(pt.translated_text) and pt.translated_text != ""
21
+ )
22
+ end
23
+ end
changed lib/kanta/translations/singular_translation.ex
 
@@ -7,11 +7,13 @@ defmodule Kanta.Translations.SingularTranslation do
7
7
import Ecto.Changeset
8
8
alias Kanta.Translations.{Locale, Message}
9
9
10
- @all_fields ~w(original_text translated_text locale_id message_id)a
11
10
@required_fields ~w(message_id locale_id)a
11
+ @optional_fields ~w(original_text translated_text)a
12
12
13
13
@type t() :: Kanta.Translations.SingularTranslationSpec.t()
14
14
15
+ @derive {Jason.Encoder, only: [:id] ++ @required_fields ++ @optional_fields}
16
+
15
17
schema "kanta_singular_translations" do
16
18
field :original_text, :string
17
19
field :translated_text, :string
 
@@ -24,7 +26,7 @@ defmodule Kanta.Translations.SingularTranslation do
24
26
25
27
def changeset(struct, attrs \\ %{}) do
26
28
struct
27
- |> cast(attrs, @all_fields)
29
+ |> cast(attrs, @required_fields ++ @optional_fields)
28
30
|> validate_required(@required_fields)
29
31
|> foreign_key_constraint(:locale_id)
30
32
|> foreign_key_constraint(:message_id)
added lib/kanta/translations/singular_translation/finders/list_translated_singular_translations.ex
 
@@ -0,0 +1,23 @@
1
+ defmodule Kanta.Translations.SingularTranslations.Finders.ListTranslatedSingularTranslations do
2
+ @moduledoc """
3
+ Query module aka Finder responsible for listing translated singular translations
4
+ """
5
+
6
+ use Kanta.Query,
7
+ module: Kanta.Translations.SingularTranslation,
8
+ binding: :singular_translation
9
+
10
+ alias Kanta.Repo
11
+
12
+ def find do
13
+ base()
14
+ |> translated_query()
15
+ |> Repo.get_repo().all()
16
+ end
17
+
18
+ defp translated_query(query) do
19
+ from(st in query,
20
+ where: not is_nil(st.translated_text) and st.translated_text != ""
21
+ )
22
+ end
23
+ end
added lib/kanta/utils/database_populator.ex
 
@@ -0,0 +1,43 @@
1
+ defmodule Kanta.Utils.DatabasePopulator do
2
+ @moduledoc false
3
+
4
+ import Ecto.Changeset
5
+
6
+ alias Kanta.Repo
7
+ alias Kanta.Utils.GetSchemata
8
+
9
+ @resource_name_to_schema GetSchemata.call()
10
+ |> Map.new()
11
+
12
+ @spec call(atom(), String.t(), [map()]) :: no_return()
13
+ def call(repo \\ Repo.get_repo(), resource_name, entries) do
14
+ schema = @resource_name_to_schema[resource_name]
15
+
16
+ entries
17
+ |> Enum.each(&populate(repo, schema, &1))
18
+ end
19
+
20
+ defp populate(repo, %{schema: schema, conflict_target: conflict_target}, entry) do
21
+ schema
22
+ |> struct()
23
+ |> change(entry |> keys_to_atoms())
24
+ |> repo.insert!(on_conflict: :replace_all, conflict_target: conflict_target)
25
+ end
26
+
27
+ defp keys_to_atoms(map) do
28
+ Map.new(map, &reduce_keys_to_atoms/1)
29
+ end
30
+
31
+ defp reduce_keys_to_atoms({"message_type", "singular"}) do
32
+ {:message_type, :singular}
33
+ end
34
+
35
+ defp reduce_keys_to_atoms({"message_type", "plural"}) do
36
+ {:message_type, :plural}
37
+ end
38
+
39
+ defp reduce_keys_to_atoms({key, val}) when is_map(val),
40
+ do: {String.to_existing_atom(key), keys_to_atoms(val)}
41
+
42
+ defp reduce_keys_to_atoms({key, val}), do: {String.to_existing_atom(key), val}
43
+ end
added lib/kanta/utils/get_schemata.ex
 
@@ -0,0 +1,28 @@
1
+ defmodule Kanta.Utils.GetSchemata do
2
+ @moduledoc false
3
+
4
+ alias Kanta.Specs.SchemataSpec
5
+
6
+ alias Kanta.Translations.{
7
+ Context,
8
+ Domain,
9
+ Locale,
10
+ Message,
11
+ PluralTranslation,
12
+ SingularTranslation
13
+ }
14
+
15
+ @schemata [
16
+ {"contexts", %{schema: Context, conflict_target: [:name]}},
17
+ {"domains", %{schema: Domain, conflict_target: [:name]}},
18
+ {"locales", %{schema: Locale, conflict_target: [:iso639_code]}},
19
+ {"messages", %{schema: Message, conflict_target: [:id]}},
20
+ {"singular_translations", %{schema: SingularTranslation, conflict_target: [:id]}},
21
+ {"plural_translations", %{schema: PluralTranslation, conflict_target: [:id]}}
22
+ ]
23
+
24
+ @spec call :: SchemataSpec.t()
25
+ def call do
26
+ @schemata
27
+ end
28
+ end
added lib/kanta_web/controllers/api/contexts_controller.ex
 
@@ -0,0 +1,23 @@
1
+ defmodule KantaWeb.Api.ContextsController do
2
+ @moduledoc false
3
+ use KantaWeb, :controller
4
+
5
+ alias Kanta.Translations.Contexts.Finders.ListContexts
6
+ alias Kanta.Utils.DatabasePopulator
7
+
8
+ def index(conn, params) do
9
+ page = params |> Map.get("page", "1") |> String.to_integer()
10
+
11
+ conn
12
+ |> put_status(200)
13
+ |> json(ListContexts.find(page: page))
14
+ end
15
+
16
+ def update(conn, %{"entries" => entries}) do
17
+ DatabasePopulator.call("contexts", entries)
18
+
19
+ conn
20
+ |> put_status(200)
21
+ |> json(%{status: "OK"})
22
+ end
23
+ end
added lib/kanta_web/controllers/api/domains_controller.ex
 
@@ -0,0 +1,23 @@
1
+ defmodule KantaWeb.Api.DomainsController do
2
+ @moduledoc false
3
+ use KantaWeb, :controller
4
+
5
+ alias Kanta.Translations.Domains.Finders.ListDomains
6
+ alias Kanta.Utils.DatabasePopulator
7
+
8
+ def index(conn, params) do
9
+ page = params |> Map.get("page", "1") |> String.to_integer()
10
+
11
+ conn
12
+ |> put_status(200)
13
+ |> json(ListDomains.find(page: page))
14
+ end
15
+
16
+ def update(conn, %{"entries" => entries}) do
17
+ DatabasePopulator.call("domains", entries)
18
+
19
+ conn
20
+ |> put_status(200)
21
+ |> json(%{status: "OK"})
22
+ end
23
+ end
added lib/kanta_web/controllers/api/kanta_api_controller.ex
 
@@ -0,0 +1,9 @@
1
+ defmodule KantaWeb.Api.KantaApiController do
2
+ use KantaWeb, :controller
3
+
4
+ def index(conn, _params) do
5
+ conn
6
+ |> put_status(200)
7
+ |> json(%{status: "OK"})
8
+ end
9
+ end
added lib/kanta_web/controllers/api/locales_controller.ex
 
@@ -0,0 +1,23 @@
1
+ defmodule KantaWeb.Api.LocalesController do
2
+ @moduledoc false
3
+ use KantaWeb, :controller
4
+
5
+ alias Kanta.Translations.Locale.Finders.ListLocales
6
+ alias Kanta.Utils.DatabasePopulator
7
+
8
+ def index(conn, params) do
9
+ page = params |> Map.get("page", "1") |> String.to_integer()
10
+
11
+ conn
12
+ |> put_status(200)
13
+ |> json(ListLocales.find(page: page))
14
+ end
15
+
16
+ def update(conn, %{"entries" => entries}) do
17
+ DatabasePopulator.call("locales", entries)
18
+
19
+ conn
20
+ |> put_status(200)
21
+ |> json(%{status: "OK"})
22
+ end
23
+ end
added lib/kanta_web/controllers/api/messages_controller.ex
 
@@ -0,0 +1,23 @@
1
+ defmodule KantaWeb.Api.MessagesController do
2
+ @moduledoc false
3
+ use KantaWeb, :controller
4
+
5
+ alias Kanta.Translations.Messages.Finders.ListMessages
6
+ alias Kanta.Utils.DatabasePopulator
7
+
8
+ def index(conn, params) do
9
+ page = params |> Map.get("page", "1") |> String.to_integer()
10
+
11
+ conn
12
+ |> put_status(200)
13
+ |> json(ListMessages.find(page: page))
14
+ end
15
+
16
+ def update(conn, %{"entries" => entries}) do
17
+ DatabasePopulator.call("messages", entries)
18
+
19
+ conn
20
+ |> put_status(200)
21
+ |> json(%{status: "OK"})
22
+ end
23
+ end
added lib/kanta_web/controllers/api/plural_translations_controller.ex
 
@@ -0,0 +1,23 @@
1
+ defmodule KantaWeb.Api.PluralTranslationsController do
2
+ @moduledoc false
3
+ use KantaWeb, :controller
4
+
5
+ alias Kanta.Translations.PluralTranslations.Finders.ListPluralTranslations
6
+ alias Kanta.Utils.DatabasePopulator
7
+
8
+ def index(conn, params) do
9
+ page = params |> Map.get("page", "1") |> String.to_integer()
10
+
11
+ conn
12
+ |> put_status(200)
13
+ |> json(ListPluralTranslations.find(page: page))
14
+ end
15
+
16
+ def update(conn, %{"entries" => entries}) do
17
+ DatabasePopulator.call("plural_translations", entries)
18
+
19
+ conn
20
+ |> put_status(200)
21
+ |> json(%{status: "OK"})
22
+ end
23
+ end
added lib/kanta_web/controllers/api/singular_translations_controller.ex
 
@@ -0,0 +1,23 @@
1
+ defmodule KantaWeb.Api.SingularTranslationsController do
2
+ @moduledoc false
3
+ use KantaWeb, :controller
4
+
5
+ alias Kanta.Translations.SingularTranslations.Finders.ListSingularTranslations
6
+ alias Kanta.Utils.DatabasePopulator
7
+
8
+ def index(conn, params) do
9
+ page = params |> Map.get("page", "1") |> String.to_integer()
10
+
11
+ conn
12
+ |> put_status(200)
13
+ |> json(ListSingularTranslations.find(page: page))
14
+ end
15
+
16
+ def update(conn, %{"entries" => entries}) do
17
+ DatabasePopulator.call("singular_translations", entries)
18
+
19
+ conn
20
+ |> put_status(200)
21
+ |> json(%{status: "OK"})
22
+ end
23
+ end
added lib/kanta_web/plugs/api_auth_plug.ex
 
@@ -0,0 +1,62 @@
1
+ defmodule KantaWeb.APIAuthPlug do
2
+ @moduledoc false
3
+
4
+ import Plug.Conn
5
+
6
+ @kanta_secret_token "KANTA_SECRET_TOKEN"
7
+
8
+ def init(_opts), do: %{}
9
+
10
+ def call(conn, _opts) do
11
+ if api_authorization_disabled?() or is_bearer_token_valid?(conn) do
12
+ conn
13
+ else
14
+ conn
15
+ |> send_resp(
16
+ 401,
17
+ "Incorrect authorization Bearer token."
18
+ )
19
+ |> halt()
20
+ end
21
+ end
22
+
23
+ defp is_bearer_token_valid?(conn) do
24
+ with {:ok, token} <- extract_bearer_token(conn),
25
+ true <- is_secret_token_matching?(token) do
26
+ true
27
+ else
28
+ _ -> false
29
+ end
30
+ end
31
+
32
+ defp is_secret_token_matching?(token) do
33
+ secret_token_env =
34
+ @kanta_secret_token
35
+ |> System.get_env()
36
+
37
+ if is_nil(secret_token_env) do
38
+ false
39
+ else
40
+ sha256(secret_token_env) == token
41
+ end
42
+ end
43
+
44
+ defp extract_bearer_token(conn) do
45
+ case get_req_header(conn, "authorization") do
46
+ ["Bearer " <> token] ->
47
+ {:ok, token}
48
+
49
+ _ ->
50
+ :error
51
+ end
52
+ end
53
+
54
+ defp sha256(token) do
55
+ :crypto.hash(:sha256, token)
56
+ |> Base.encode64()
57
+ end
58
+
59
+ defp api_authorization_disabled? do
60
+ Kanta.config().disable_api_authorization
61
+ end
62
+ end
changed lib/kanta_web/router.ex
 
@@ -4,7 +4,7 @@ defmodule KantaWeb.Router do
4
4
# deps/phoenix/lib/phoenix/router.ex:2:no_return Function call/2 has no local return.
5
5
@dialyzer {:no_return, {:call, 2}}
6
6
7
- defmacro kanta_dashboard(path, opts \\ []) do
7
+ defmacro kanta_dashboard(path \\ "/kanta", opts \\ []) do
8
8
opts =
9
9
if Macro.quoted_literal?(opts) do
10
10
Macro.prewalk(opts, &expand_alias(&1, __CALLER__))
 
@@ -71,6 +71,32 @@ defmodule KantaWeb.Router do
71
71
end
72
72
end
73
73
74
+ defmacro kanta_api(path \\ "/kanta-api") do
75
+ quote bind_quoted: binding() do
76
+ pipeline :kanta_api_pipeline do
77
+ plug :accepts, ["json"]
78
+ plug KantaWeb.APIAuthPlug
79
+ end
80
+
81
+ scope path, alias: false, as: false do
82
+ scope "/", KantaWeb.Api do
83
+ pipe_through :kanta_api_pipeline
84
+ get "/", KantaApiController, :index
85
+
86
+ resources "/contexts", ContextsController, only: [:index, :update]
87
+ resources "/domains", DomainsController, only: [:index, :update]
88
+ resources "/locales", LocalesController, only: [:index, :update]
89
+ resources "/messages", MessagesController, only: [:index, :update]
90
+
91
+ resources "/singular_translations", SingularTranslationsController,
92
+ only: [:index, :update]
93
+
94
+ resources "/plural_translations", PluralTranslationsController, only: [:index, :update]
95
+ end
96
+ end
97
+ end
98
+ end
99
+
74
100
defp expand_alias({:__aliases__, _, _} = alias, env),
75
101
do: Macro.expand(alias, %{env | function: {:kanta_dashboard, 2}})
changed mix.exs
 
@@ -6,7 +6,7 @@ defmodule Kanta.MixProject do
6
6
app: :kanta,
7
7
description: "User-friendly translations manager for Elixir/Phoenix projects.",
8
8
package: package(),
9
- version: "0.3.0",
9
+ version: "0.3.1",
10
10
elixir: "~> 1.14",
11
11
elixirc_options: [
12
12
warnings_as_errors: true