changed CHANGELOG.md
 
@@ -0,0 +1,311 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
5
+ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
6
+
7
+ ## [v0.4.0]
8
+ ### Other
9
+ - feat: :sparkles: add versioce for version bumping
10
+ - Merge pull request #62 from curiosum-dev/feature/ui-translation-improvements
11
+
12
+ Improve UX when editing many translations
13
+ - Fix
14
+ - Merge branch develop into feature/ui-translation-improvements
15
+ - Merge pull request #59 from curiosum-dev/feature/add_support_for_multiple_apps
16
+
17
+ Add support for multiple apps
18
+ - Add missing @moduledoc
19
+ - Merge branch develop into feature/add_support_for_multiple_apps
20
+ - Merge pull request #61 from curiosum-dev/feature/support-nested-scopes
21
+
22
+ Add support for nested scopes and different main path
23
+ - Merge branch develop into feature/support-nested-scopes
24
+ - Trigger workflow
25
+ - Merge pull request #60 from curiosum-dev/fix/multiple-locales-for-messages-fixes
26
+
27
+ Fix translation preloads to preload only necessary data
28
+ - Merge branch develop into fix/multiple-locales-for-messages-fixes
29
+ - Trigger workflow
30
+ - Merge pull request #56 from curiosum-dev/fix/multiline-translations
31
+
32
+ Add support for multi-line msgids
33
+ - Merge branch develop into fix/multiline-translations
34
+ - Trigger workflow
35
+ - Merge pull request #52 from curiosum-dev/fix/parse_params_to_avoid_500s
36
+
37
+ Parse params and add support for different ID types
38
+ - Refactor translations_live.ex
39
+ - Trigger workflow
40
+ - Extract logic of `parse_filters/1`s reduce to `parse_filter/2`
41
+ - Return error when `id_parse_function` provided with MFA with invalid arity
42
+ - Add support for different ID types
43
+ - Parse params and and redirects
44
+ - Merge pull request #80 from curiosum-dev/feature/improve-ci
45
+
46
+ Feature/improve ci
47
+ - Bump credo and dialyxir versions
48
+ - Suppress warning about router.ex
49
+ - Bump uri_query version
50
+ - Bump ecto versions
51
+ - Use Ubuntu 24.04 LTS
52
+ - Set env
53
+ - Remove unsupported OTP version
54
+ - Add supported Elixir versions
55
+ - Fix naming across file
56
+ - Fix naming
57
+ - Improve CI
58
+ - Merge pull request #77 from curiosum-dev/feat/remove-devenv
59
+
60
+ ❄️ Remove devenv
61
+ - Merge pull request #79 from curiosum-dev/fix/align-with-main
62
+
63
+ Fix/align with main
64
+ - Remove autogenerated dummy test
65
+ - Fix credo
66
+ - ❄️ Remove devenv
67
+ - Add chevrons to `Icons` module
68
+ - Add `Colors` module
69
+ - Improve readability of `Router`
70
+ - Add `dashboard_path` helper to verified routes
71
+ - Restore params after saving a translation
72
+ - Add a way to clear all filters at once
73
+ - Fixes to migrations
74
+ - Fix to `application_source_form_live`
75
+ - Add a way to create new application source
76
+ - Add support for nested scopes and different main path
77
+ - Fix translation preloads to preload only necessary data
78
+ - Changes to make import export plugin work
79
+ - Replace `parse_msgid/1` with just `Enum.join`
80
+ - Add support for multiline msgids
81
+ - Add application source
82
+ - Add support for SQLite3 (#51)
83
+
84
+ * Add support for SQLite3
85
+
86
+ * Fix current version
87
+ - Recover filters from params on mount in the translations list (#49)
88
+
89
+ * Improve UX when using filters
90
+
91
+ * Properly find index in Select
92
+ - 🐛 Fix pagination failing to parse int (#44)
93
+ - Support translations during compilation (#48)
94
+
95
+ * Support translations during compilation
96
+
97
+ * Remove Logger
98
+
99
+ * Fix credo issues
100
+
101
+ * Add doc to `compiling?` function
102
+ - Support for newer phoenix_html 4 with phoenix_html_helpers because newer version has a little different api (#47)
103
+
104
+ * html_helpers
105
+
106
+ Signed-off-by: Jan Jakůbek <[email protected]>
107
+
108
+ * delete_use_PhoenixHtml
109
+
110
+ Signed-off-by: Jan Jakůbek <[email protected]>
111
+
112
+ ---------
113
+
114
+ Signed-off-by: Jan Jakůbek <[email protected]>
115
+ - Merge pull request #42 from curiosum-dev/feature/child-lv-components
116
+
117
+ Dashboard allow child live view components
118
+ - 📝 Add docs to DashboardLive
119
+ - 💫 Support child LV dashboard components
120
+ - Merge pull request #41 from curiosum-dev/feature/stricter-po-file-search
121
+
122
+ 🚦 Search for PO files only in priv/gettext
123
+ - Merge pull request #40 from oliver-kriska/feature/multi-line-msgstr
124
+
125
+ PO Extractor - allow to import multi-lines msgstr
126
+ - 🚦 Search for PO files only in priv/gettext
127
+ - Update README.md
128
+ - PO Extractor - allow to import multi-lines msgstr
129
+ - Merge pull request #38 from curiosum-dev/chore/kanta-sync-readme-plugins-list-update
130
+
131
+ Update README.md
132
+ - Update README.md
133
+
134
+ Adds Kanta sync to the plugins list in README Table of contents
135
+
136
+ ## [0.3.1]
137
+ ### Other
138
+ - Merge pull request #37 from curiosum-dev/release/0.3.1
139
+
140
+ Release/0.3.1
141
+ - Merge branch main into release/0.3.1
142
+ - Merge pull request #36 from curiosum-dev/feature/api-endpoint
143
+
144
+ Feature/api endpoint
145
+ - Add optional API authorization, resolve minor naming issues
146
+ - Add @moduledoc to APIAuthPlug
147
+ - Add API endpoints
148
+ - Merge branch develop into feature/api-endpoint
149
+ - Create API scaffolding
150
+
151
+ ## [0.3.0]
152
+ ### Other
153
+ - Merge pull request #35 from curiosum-dev/release/0.3.0
154
+
155
+ Release/0.3.0
156
+ - Update phoenix_live_view to 0.20
157
+ - Update Kanta version to 0.3.0
158
+ - Merge pull request #34 from curiosum-dev/fix/js-error-on-liveview-page-change
159
+
160
+ Fix JS error on LiveView page change
161
+ - Get rid of deprecated live_component/2
162
+ - Fix JS error on LiveView page change
163
+ - Merge pull request #32 from FranciscoLira/fix/router-import-missing
164
+
165
+ Fix/router import missing
166
+ - Merge pull request #21 from curiosum-dev/feature/improved-translations-search
167
+
168
+ Improve translations search
169
+ - Require opts keyword list in join_resource/3
170
+ - Improve efficiency of messages list query
171
+ - Merge branch develop into feature/improved-translations-search
172
+ - fix:README missing import in router
173
+ - Improve translations search
174
+
175
+ ## [0.2.2]
176
+ ### Other
177
+ - Merge pull request #30 from curiosum-dev/release/0.2.2
178
+
179
+ Release/0.2.2
180
+ - Merge branch main into release/0.2.2
181
+ - Merge pull request #29 from curiosum-dev/chore/update-powriter-information-in-readme
182
+
183
+ Update information about POWriter
184
+ - Update information about POWriter
185
+ - Merge pull request #27 from curiosum-dev/feature/readme-and-sidebar-badges
186
+
187
+ Add badges
188
+ - Add badges
189
+
190
+ ## [0.2.1]
191
+ ### Other
192
+ - Add badges (#28)
193
+ - Merge pull request #26 from curiosum-dev/chore/update-readme
194
+
195
+ Update README to match Kanta version
196
+ - Update README to match Kanta version
197
+
198
+ ## [0.2.0]
199
+ ### Other
200
+ - Merge pull request #25 from curiosum-dev/release/0.2.0
201
+
202
+ Release/0.2.0
203
+ - Merge pull request #24 from curiosum-dev/feature/set-docs-entry-and-bump-mix-version
204
+
205
+ Set docs entry and bump mix version
206
+ - Set docs entry and bump mix version
207
+ - Merge pull request #23 from curiosum-dev/fix/plural-translations-form
208
+
209
+ Improve UI and fix plural translations form
210
+ - Use truncate CSS prop instead of String.slice
211
+ - Improve UI and plural translations form
212
+ - Merge pull request #22 from curiosum-dev/chore/update-gettext-repo
213
+
214
+ Update gettext repo
215
+ - Update gettext repo
216
+ - Merge pull request #20 from curiosum-dev/chore/update-demo-link
217
+
218
+ Update demo link in README
219
+ - Update demo link in README
220
+ - Merge pull request #18 from curiosum-dev/feature/plugin-docs
221
+
222
+ Feature/plugin docs
223
+ - Module names consistency, explicit ArgumentError rescue
224
+ - Add dialyzer, fix credo and dialyzer warnings
225
+ - Update "How to write plugins?" tutorial
226
+ - Plugin docs, specs and conditional components rendering
227
+ - 🐛 Hotfix wrong type
228
+
229
+ It was a Map all along
230
+ - Merge pull request #14 from curiosum-dev/bugfix/prerelease-fixes
231
+
232
+ Bugfix/prerelease fixes
233
+ - 🔧 Fallback to public prefix for messages
234
+ - 🔧 Pass on_mount option to live_session opts
235
+ - 🐛 Use https instead ssh connection for gettext
236
+ - 🐛 Fix down migration order
237
+ - 🐛 Fix prefix() calls into prefix
238
+ - 🔧 Pass down opts to respective up&down migrations
239
+ - 🐛 Add limit 1
240
+ - ❄ Add devenv
241
+ - fix: multitenancy projects issues
242
+ - feat: rework plural messages handling
243
+ - Merge pull request #12 from curiosum-dev/feature/kanta-plugins-extraction
244
+
245
+ feat: extract DeepL plugin to the external package
246
+ - feat: extract DeepL plugin to the external package
247
+ - Merge pull request #11 from curiosum-dev/fix/phoenix-verified-url
248
+
249
+ fix: change unverified url to unverified path
250
+ - fix: change unverified url to unverified path
251
+ - Merge pull request #10 from curiosum-dev/fix/phoenix-verified-paths
252
+
253
+ fix: Phoenix VerifiedRoutes issues & missing views
254
+ - fix: Phoenix VerifiedRoutes issues & missing views
255
+ - Merge pull request #8 from curiosum-dev/fix/kanta-in-mix-release-setup
256
+
257
+ fix: Kanta in a mix release projects
258
+ - fix: Kanta in a mix release projects
259
+ - Merge tag 0.1.0 into develop
260
+
261
+ Initial version of Kanta
262
+
263
+ ## [0.1.0]
264
+ ### Other
265
+ - Merge branch release/0.1.0 into main
266
+ - chore: bump version
267
+ - docs: update readme
268
+ - feat: basic rwd
269
+ - feat: dark mode
270
+ - feat: add messages filters
271
+ - feat: plugins management
272
+ - feat: small refactoring
273
+ - fix: paths resolution
274
+ - feat: add gettext contexts & fix plural translations
275
+ - feat: mainly UI adjusts
276
+ - Merge pull request #6 from curiosum-dev/1-fix_github_dependency_on_gettext_fork
277
+
278
+ Fix github dependency on gettext fork (#1)
279
+ - Fix github dependency on gettext fork (#1)
280
+ - Merge tag 0.0.1-rc1 into develop
281
+
282
+ First release candidate of Kanta translations manager.
283
+ - Merge branch release/0.0.1-rc1
284
+ - chore: update package version
285
+ - Merge pull request #4 from curiosum-dev/feature/KNT-2
286
+
287
+ fix: cached translations in UI
288
+ - fix: cached translations in UI
289
+ - Merge pull request #3 from curiosum-dev/feature/KNT-1
290
+
291
+ KNT-1 mimic LiveDashboard mechanism in Kanta
292
+ - fix: LV redirect loop
293
+ - feat: reverse engineering of live dashboard (WIP)
294
+ - feat: adjust gettext repo for plural messages handling
295
+ - feat: add form for plural translation updates
296
+ - feat: add form for singular translation updates
297
+ - Merge pull request #1 from curiosum-dev/feature/petal_init
298
+
299
+ PETAL stack init & db structure rework
300
+ - feat: rework translation listings
301
+ - feat: add plural translations
302
+ - feat: add nebulex as a caching tool
303
+ - feat: add sample pages in petal
304
+ - feat: add basic configuration for semi-PETAL stack
305
+ - chore: update deps & readme
306
+ - Change translations to singular translations
307
+ - Create PopulateCacheWithStoredDataService
308
+ - Proof of concept
309
+ - Create file structure
310
+ - First commit
311
+
changed README.md
 
@@ -61,6 +61,7 @@
61
61
<ul>
62
62
<li><a href="#po-writer">PO Writer</a></li>
63
63
<li><a href="#deepl">DeepL</a></li>
64
+ <li><a href="#kantasync">Translation synchronization</a></li>
64
65
</ul>
65
66
</li>
66
67
<li><a href="#roadmap">Roadmap</a></li>
 
@@ -102,7 +103,7 @@ by adding `kanta` to your list of dependencies in `mix.exs`:
102
103
```elixir
103
104
def deps do
104
105
[
105
- {:kanta, "~> 0.3.1"},
106
+ {:kanta, "~> 0.4.0"},
106
107
{:gettext, git: "[email protected]:ravensiris/gettext.git", branch: "runtime-gettext"}
107
108
]
108
109
end
 
@@ -336,6 +337,8 @@ Distributed under the MIT License. See `LICENSE.txt` for more information.
336
337
337
338
## Contact
338
339
340
+ [Curiosum](https://curiosum.com)
341
+
339
342
Michał Buszkiewicz - [email protected]
340
343
341
344
Krzysztof Janiec - [email protected]
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.1">>}.
3
+ {<<"version">>,<<"0.4.0">>}.
4
4
{<<"description">>,
5
5
<<"User-friendly translations manager for Elixir/Phoenix projects.">>}.
6
6
{<<"elixir">>,<<"~> 1.14">>}.
 
@@ -12,15 +12,21 @@
12
12
<<"lib/kanta/migrations/postgresql">>,
13
13
<<"lib/kanta/migrations/postgresql/v01.ex">>,
14
14
<<"lib/kanta/migrations/postgresql/v02.ex">>,
15
- <<"lib/kanta/migrations/postgresql.ex">>,<<"lib/kanta/translations.ex">>,
15
+ <<"lib/kanta/migrations/postgresql/v03.ex">>,
16
+ <<"lib/kanta/migrations/postgresql.ex">>,
17
+ <<"lib/kanta/migrations/sqlite3.ex">>,<<"lib/kanta/migrations/sqlite3">>,
18
+ <<"lib/kanta/migrations/sqlite3/v01.ex">>,
19
+ <<"lib/kanta/migrations/sqlite3/v02.ex">>,<<"lib/kanta/translations.ex">>,
16
20
<<"lib/kanta/cache.ex">>,<<"lib/kanta/types.ex">>,<<"lib/kanta/specs">>,
17
21
<<"lib/kanta/specs/schemata_spec.ex">>,<<"lib/kanta/validator.ex">>,
18
22
<<"lib/kanta/config.ex">>,<<"lib/kanta/utils">>,
19
23
<<"lib/kanta/utils/database_populator.ex">>,
20
- <<"lib/kanta/utils/get_schemata.ex">>,<<"lib/kanta/utils/module_utils.ex">>,
24
+ <<"lib/kanta/utils/get_schemata.ex">>,
25
+ <<"lib/kanta/utils/param_parsers.ex">>,<<"lib/kanta/utils/colors.ex">>,
26
+ <<"lib/kanta/utils/compilation.ex">>,<<"lib/kanta/utils/module_utils.ex">>,
21
27
<<"lib/kanta/query.ex">>,<<"lib/kanta/migration.ex">>,
22
28
<<"lib/kanta/gettext">>,<<"lib/kanta/gettext/repo.ex">>,
23
- <<"lib/kanta/po_files">>,
29
+ <<"lib/kanta/schema.ex">>,<<"lib/kanta/po_files">>,
24
30
<<"lib/kanta/po_files/messages_extractor_agent.ex">>,
25
31
<<"lib/kanta/po_files/handlers">>,
26
32
<<"lib/kanta/po_files/handlers/messages_extractor.ex">>,
 
@@ -30,6 +36,7 @@
30
36
<<"lib/kanta/po_files/services/extract_plural_translation.ex">>,
31
37
<<"lib/kanta/application.ex">>,<<"lib/kanta/repo.ex">>,
32
38
<<"lib/kanta/translations">>,
39
+ <<"lib/kanta/translations/application_source.ex">>,
33
40
<<"lib/kanta/translations/plural_translation">>,
34
41
<<"lib/kanta/translations/plural_translation/plural_translations.ex">>,
35
42
<<"lib/kanta/translations/plural_translation/finders">>,
 
@@ -40,6 +47,7 @@
40
47
<<"lib/kanta/translations/messages">>,
41
48
<<"lib/kanta/translations/messages/message_spec.ex">>,
42
49
<<"lib/kanta/translations/messages/finders">>,
50
+ <<"lib/kanta/translations/messages/finders/list_all_messages.ex">>,
43
51
<<"lib/kanta/translations/messages/finders/get_message.ex">>,
44
52
<<"lib/kanta/translations/messages/finders/list_messages.ex">>,
45
53
<<"lib/kanta/translations/messages/messages.ex">>,
 
@@ -58,9 +66,16 @@
58
66
<<"lib/kanta/translations/context">>,
59
67
<<"lib/kanta/translations/context/contexts.ex">>,
60
68
<<"lib/kanta/translations/context/finders">>,
69
+ <<"lib/kanta/translations/context/finders/list_all_contexts.ex">>,
61
70
<<"lib/kanta/translations/context/finders/get_context.ex">>,
62
71
<<"lib/kanta/translations/context/finders/list_contexts.ex">>,
63
72
<<"lib/kanta/translations/context/context_spec.ex">>,
73
+ <<"lib/kanta/translations/application_source">>,
74
+ <<"lib/kanta/translations/application_source/application_sources.ex">>,
75
+ <<"lib/kanta/translations/application_source/finders">>,
76
+ <<"lib/kanta/translations/application_source/finders/get_application_source.ex">>,
77
+ <<"lib/kanta/translations/application_source/finders/list_application_sources.ex">>,
78
+ <<"lib/kanta/translations/application_source/application_source_spec.ex">>,
64
79
<<"lib/kanta/translations/plural_translation.ex">>,
65
80
<<"lib/kanta/translations/singular_translation.ex">>,
66
81
<<"lib/kanta/translations/domain.ex">>,
 
@@ -70,6 +85,7 @@
70
85
<<"lib/kanta/translations/domain/finders">>,
71
86
<<"lib/kanta/translations/domain/finders/list_domains.ex">>,
72
87
<<"lib/kanta/translations/domain/finders/get_domain.ex">>,
88
+ <<"lib/kanta/translations/domain/finders/list_all_domains.ex">>,
73
89
<<"lib/kanta/translations/singular_translation">>,
74
90
<<"lib/kanta/translations/singular_translation/singular_translations.ex">>,
75
91
<<"lib/kanta/translations/singular_translation/finders">>,
 
@@ -106,6 +122,9 @@
106
122
<<"lib/kanta_web/live/translations/domain_live">>,
107
123
<<"lib/kanta_web/live/translations/domain_live/domain_live.html.heex">>,
108
124
<<"lib/kanta_web/live/translations/domain_live/domain_live.ex">>,
125
+ <<"lib/kanta_web/live/translations/application_source_form_live">>,
126
+ <<"lib/kanta_web/live/translations/application_source_form_live/application_source_form_live.html.heex">>,
127
+ <<"lib/kanta_web/live/translations/application_source_form_live/application_source_form_live.ex">>,
109
128
<<"lib/kanta_web/live/translations/translations_live">>,
110
129
<<"lib/kanta_web/live/translations/translations_live/translations_live.ex">>,
111
130
<<"lib/kanta_web/live/translations/translations_live/components">>,
 
@@ -119,6 +138,13 @@
119
138
<<"lib/kanta_web/live/translations/locales_live">>,
120
139
<<"lib/kanta_web/live/translations/locales_live/locales_live.ex">>,
121
140
<<"lib/kanta_web/live/translations/locales_live/locales_live.html.heex">>,
141
+ <<"lib/kanta_web/live/translations/application_sources_live">>,
142
+ <<"lib/kanta_web/live/translations/application_sources_live/application_sources_live.html.heex">>,
143
+ <<"lib/kanta_web/live/translations/application_sources_live/components">>,
144
+ <<"lib/kanta_web/live/translations/application_sources_live/components/application_sources_table">>,
145
+ <<"lib/kanta_web/live/translations/application_sources_live/components/application_sources_table/application_sources_table.html.heex">>,
146
+ <<"lib/kanta_web/live/translations/application_sources_live/components/application_sources_table/application_sources_table.ex">>,
147
+ <<"lib/kanta_web/live/translations/application_sources_live/application_sources_live.ex">>,
122
148
<<"lib/kanta_web/live/translations/contexts_live">>,
123
149
<<"lib/kanta_web/live/translations/contexts_live/components">>,
124
150
<<"lib/kanta_web/live/translations/contexts_live/components/contexts_table">>,
 
@@ -155,6 +181,7 @@
155
181
<<"lib/kanta_web/controllers/api/singular_translations_controller.ex">>,
156
182
<<"lib/kanta_web/controllers/api/locales_controller.ex">>,
157
183
<<"lib/kanta_web/controllers/api/messages_controller.ex">>,
184
+ <<"lib/kanta_web/controllers/api/application_sources_controller.ex">>,
158
185
<<"lib/kanta_web/views">>,<<"lib/kanta_web/views/layout_view.ex">>,
159
186
<<"lib/kanta.ex">>,<<"priv">>,<<"priv/iso639.json">>,<<"dist">>,
160
187
<<"dist/css">>,<<"dist/css/app.css">>,<<"dist/js">>,<<"dist/js/app.js">>,
 
@@ -168,12 +195,12 @@
168
195
[{<<"name">>,<<"ecto">>},
169
196
{<<"app">>,<<"ecto">>},
170
197
{<<"optional">>,false},
171
- {<<"requirement">>,<<"~> 3.10">>},
198
+ {<<"requirement">>,<<"~> 3.12">>},
172
199
{<<"repository">>,<<"hexpm">>}],
173
200
[{<<"name">>,<<"ecto_sql">>},
174
201
{<<"app">>,<<"ecto_sql">>},
175
202
{<<"optional">>,false},
176
- {<<"requirement">>,<<"~> 3.10">>},
203
+ {<<"requirement">>,<<"~> 3.12">>},
177
204
{<<"repository">>,<<"hexpm">>}],
178
205
[{<<"name">>,<<"phoenix">>},
179
206
{<<"app">>,<<"phoenix">>},
 
@@ -190,6 +217,16 @@
190
217
{<<"optional">>,false},
191
218
{<<"requirement">>,<<"~> 0.20">>},
192
219
{<<"repository">>,<<"hexpm">>}],
220
+ [{<<"name">>,<<"phoenix_html">>},
221
+ {<<"app">>,<<"phoenix_html">>},
222
+ {<<"optional">>,false},
223
+ {<<"requirement">>,<<"~> 4.0">>},
224
+ {<<"repository">>,<<"hexpm">>}],
225
+ [{<<"name">>,<<"phoenix_html_helpers">>},
226
+ {<<"app">>,<<"phoenix_html_helpers">>},
227
+ {<<"optional">>,false},
228
+ {<<"requirement">>,<<"~> 1.0">>},
229
+ {<<"repository">>,<<"hexpm">>}],
193
230
[{<<"name">>,<<"tailwind">>},
194
231
{<<"app">>,<<"tailwind">>},
195
232
{<<"optional">>,false},
 
@@ -223,6 +260,16 @@
223
260
[{<<"name">>,<<"uri_query">>},
224
261
{<<"app">>,<<"uri_query">>},
225
262
{<<"optional">>,false},
226
- {<<"requirement">>,<<"~> 0.1.1">>},
263
+ {<<"requirement">>,<<"~> 0.2">>},
264
+ {<<"repository">>,<<"hexpm">>}],
265
+ [{<<"name">>,<<"versioce">>},
266
+ {<<"app">>,<<"versioce">>},
267
+ {<<"optional">>,false},
268
+ {<<"requirement">>,<<"~> 2.0.0">>},
269
+ {<<"repository">>,<<"hexpm">>}],
270
+ [{<<"name">>,<<"git_cli">>},
271
+ {<<"app">>,<<"git_cli">>},
272
+ {<<"optional">>,false},
273
+ {<<"requirement">>,<<"~> 0.3.0">>},
227
274
{<<"repository">>,<<"hexpm">>}]]}.
228
275
{<<"build_tools">>,[<<"mix">>]}.
changed lib/kanta/config.ex
 
@@ -8,7 +8,8 @@ defmodule Kanta.Config do
8
8
repo: module(),
9
9
endpoint: module(),
10
10
plugins: false | [module() | {module() | Keyword.t()}],
11
- disable_api_authorization: boolean()
11
+ disable_api_authorization: boolean(),
12
+ id_parse_function: mfa() | (term() -> {:ok, term()} | term())
12
13
}
13
14
14
15
defstruct name: Kanta,
 
@@ -16,7 +17,8 @@ defmodule Kanta.Config do
16
17
repo: nil,
17
18
endpoint: nil,
18
19
plugins: [],
19
- disable_api_authorization: false
20
+ disable_api_authorization: false,
21
+ id_parse_function: {Kanta.Utils.ParamParsers, :default_id_parser, 1}
20
22
21
23
alias Kanta.Validator
22
24
 
@@ -82,6 +84,29 @@ defmodule Kanta.Config do
82
84
end
83
85
end
84
86
87
+ defp validate_opt(_opts, {:id_parse_function, {module, function, 1} = id_parse_function}) do
88
+ if Code.ensure_loaded?(module) and Kernel.function_exported?(module, function, 1) do
89
+ :ok
90
+ else
91
+ {:error,
92
+ "expected :id_parse_function to be a function with arity of 1, got: #{inspect(id_parse_function)}"}
93
+ end
94
+ end
95
+
96
+ defp validate_opt(_opts, {:id_parse_function, {_module, _function, _arity} = id_parse_function}) do
97
+ {:error,
98
+ "expected :id_parse_function to be a function with arity of 1, got: #{inspect(id_parse_function)}"}
99
+ end
100
+
101
+ defp validate_opt(_opts, {:id_parse_function, id_parse_function}) do
102
+ if is_function(id_parse_function, 1) do
103
+ :ok
104
+ else
105
+ {:error,
106
+ "expected :id_parse_function to be a function with arity of 1, got: #{inspect(id_parse_function)}"}
107
+ end
108
+ end
109
+
85
110
defp validate_opt(_opts, option) do
86
111
{:unknown, option, __MODULE__}
87
112
end
changed lib/kanta/gettext/repo.ex
 
@@ -1,4 +1,6 @@
1
1
defmodule Kanta.Gettext.Repo do
2
+ alias Kanta.Utils.Compilation
3
+
2
4
alias Kanta.Translations.{
3
5
Context,
4
6
Domain,
 
@@ -15,6 +17,14 @@ defmodule Kanta.Gettext.Repo do
15
17
end
16
18
17
19
def get_translation(locale, domain, msgctxt, msgid, opts) do
20
+ if Compilation.compiling?() do
21
+ msgid
22
+ else
23
+ do_get_translation(locale, domain, msgctxt, msgid, opts)
24
+ end
25
+ end
26
+
27
+ defp do_get_translation(locale, domain, msgctxt, msgid, opts) do
18
28
default_locale = Application.get_env(:kanta, :default_locale) || "en"
19
29
20
30
with {:ok, %Locale{id: locale_id}} <-
 
@@ -27,7 +37,8 @@ defmodule Kanta.Gettext.Repo do
27
37
filter: [
28
38
msgid: msgid,
29
39
context_id: context_id,
30
- domain_id: domain_id
40
+ domain_id: domain_id,
41
+ application_source_id: nil
31
42
]
32
43
),
33
44
{:ok, %SingularTranslation{translated_text: text}} <-
 
@@ -39,7 +50,7 @@ defmodule Kanta.Gettext.Repo do
39
50
) do
40
51
if is_nil(text) do
41
52
if locale != default_locale do
42
- get_translation(default_locale, domain, msgctxt, msgid, opts)
53
+ do_get_translation(default_locale, domain, msgctxt, msgid, opts)
43
54
else
44
55
:not_found
45
56
end
 
@@ -61,6 +72,30 @@ defmodule Kanta.Gettext.Repo do
61
72
plural_form,
62
73
opts
63
74
) do
75
+ if Compilation.compiling?() do
76
+ if plural_form == 1, do: msgid, else: msgid_plural
77
+ else
78
+ do_get_plural_translation(
79
+ locale,
80
+ domain,
81
+ msgctxt,
82
+ msgid,
83
+ msgid_plural,
84
+ plural_form,
85
+ opts
86
+ )
87
+ end
88
+ end
89
+
90
+ defp do_get_plural_translation(
91
+ locale,
92
+ domain,
93
+ msgctxt,
94
+ msgid,
95
+ msgid_plural,
96
+ plural_form,
97
+ opts
98
+ ) do
64
99
default_locale = Application.get_env(:kanta, :default_locale) || "en"
65
100
66
101
with {:ok, %Locale{id: locale_id, plurals_header: plurals_header}} <-
 
@@ -73,7 +108,8 @@ defmodule Kanta.Gettext.Repo do
73
108
filter: [
74
109
msgid: msgid_plural,
75
110
context_id: context_id,
76
- domain_id: domain_id
111
+ domain_id: domain_id,
112
+ application_source_id: nil
77
113
]
78
114
),
79
115
{:ok, plurals_options} <- Expo.PluralForms.parse(plurals_header),
 
@@ -88,7 +124,7 @@ defmodule Kanta.Gettext.Repo do
88
124
) do
89
125
if is_nil(text) do
90
126
if locale != default_locale do
91
- get_plural_translation(
127
+ do_get_plural_translation(
92
128
default_locale,
93
129
domain,
94
130
msgctxt,
changed lib/kanta/migration.ex
 
@@ -193,6 +193,7 @@ defmodule Kanta.Migration do
193
193
defp migrator do
194
194
case repo().__adapter__() do
195
195
Ecto.Adapters.Postgres -> Kanta.Migrations.Postgresql
196
+ Ecto.Adapters.SQLite3 -> Kanta.Migrations.SQLite3
196
197
end
197
198
end
198
199
end
changed lib/kanta/migrations/postgresql.ex
 
@@ -6,7 +6,7 @@ defmodule Kanta.Migrations.Postgresql do
6
6
use Ecto.Migration
7
7
8
8
@initial_version 1
9
- @current_version 2
9
+ @current_version 3
10
10
@default_prefix "public"
11
11
12
12
@doc false
changed lib/kanta/migrations/postgresql/v01.ex
 
@@ -5,6 +5,8 @@ defmodule Kanta.Migrations.Postgresql.V01 do
5
5
6
6
use Ecto.Migration
7
7
8
+ alias Kanta.Utils.Colors
9
+
8
10
@default_prefix "public"
9
11
@kanta_locales "kanta_locales"
10
12
@kanta_domains "kanta_domains"
 
@@ -56,7 +58,7 @@ defmodule Kanta.Migrations.Postgresql.V01 do
56
58
create_if_not_exists table(@kanta_domains) do
57
59
add(:name, :string)
58
60
add(:description, :text)
59
- add(:color, :string, null: false, default: "#7E37D8")
61
+ add(:color, :string, null: false, default: Colors.default_color())
60
62
timestamps()
61
63
end
62
64
 
@@ -67,7 +69,7 @@ defmodule Kanta.Migrations.Postgresql.V01 do
67
69
create_if_not_exists table(@kanta_contexts) do
68
70
add(:name, :string)
69
71
add(:description, :text)
70
- add(:color, :string, null: false, default: "#7E37D8")
72
+ add(:color, :string, null: false, default: Colors.default_color())
71
73
timestamps()
72
74
end
added lib/kanta/migrations/postgresql/v03.ex
 
@@ -0,0 +1,81 @@
1
+ defmodule Kanta.Migrations.Postgresql.V03 do
2
+ @moduledoc """
3
+ Kanta V3 Migrations
4
+ """
5
+
6
+ use Ecto.Migration
7
+
8
+ alias Kanta.Utils.Colors
9
+
10
+ @kanta_application_sources "kanta_application_sources"
11
+ @kanta_messages "kanta_messages"
12
+
13
+ def up(opts) do
14
+ Kanta.Migration.up(version: 2)
15
+
16
+ [
17
+ &up_application_sources/1,
18
+ &up_kanta_messages/1
19
+ ]
20
+ |> Enum.each(&apply(&1, [opts]))
21
+ end
22
+
23
+ def down(opts) do
24
+ [
25
+ &down_application_sources/1,
26
+ &down_kanta_messages/1
27
+ ]
28
+ |> Enum.each(&apply(&1, [opts]))
29
+
30
+ Kanta.Migration.down(version: 2)
31
+ end
32
+
33
+ def up_application_sources(_opts) do
34
+ create_if_not_exists table(@kanta_application_sources) do
35
+ add(:name, :string)
36
+ add(:description, :text)
37
+ add(:color, :string, null: false, default: Colors.default_color())
38
+ timestamps()
39
+ end
40
+
41
+ create_if_not_exists unique_index(@kanta_application_sources, [:name])
42
+ end
43
+
44
+ def up_kanta_messages(_opts) do
45
+ alter table(@kanta_messages) do
46
+ add(:application_source_id, references(@kanta_application_sources), null: true)
47
+ end
48
+
49
+ drop unique_index(@kanta_messages, [:context_id, :domain_id, :msgid])
50
+
51
+ create_if_not_exists unique_index(
52
+ @kanta_messages,
53
+ [
54
+ :application_source_id,
55
+ :context_id,
56
+ :domain_id,
57
+ :msgid
58
+ ],
59
+ nulls_distinct: false
60
+ )
61
+ end
62
+
63
+ def down_application_sources(_opts) do
64
+ drop table(@kanta_application_sources)
65
+ end
66
+
67
+ def down_kanta_messages(_opts) do
68
+ drop unique_index(@kanta_messages, [
69
+ :application_source_id,
70
+ :context_id,
71
+ :domain_id,
72
+ :msgid
73
+ ])
74
+
75
+ create_if_not_exists unique_index(@kanta_messages, [:context_id, :domain_id, :msgid])
76
+
77
+ alter table(@kanta_messages) do
78
+ remove(:application_source_id)
79
+ end
80
+ end
81
+ end
added lib/kanta/migrations/sqlite3.ex
 
@@ -0,0 +1,81 @@
1
+ defmodule Kanta.Migrations.SQLite3 do
2
+ @moduledoc false
3
+
4
+ @behaviour Kanta.Migration
5
+
6
+ use Ecto.Migration
7
+
8
+ @initial_version 1
9
+ @current_version 2
10
+
11
+ @doc false
12
+ def initial_version, do: @initial_version
13
+
14
+ @doc false
15
+ def current_version, do: @current_version
16
+
17
+ @impl Kanta.Migration
18
+ def up(opts) do
19
+ opts = with_defaults(opts, @current_version)
20
+ initial = migrated_version(opts)
21
+
22
+ cond do
23
+ initial == 0 ->
24
+ change(@initial_version..opts.version, :up, opts)
25
+
26
+ initial < opts.version ->
27
+ change((initial + 1)..opts.version, :up, opts)
28
+
29
+ true ->
30
+ :ok
31
+ end
32
+ end
33
+
34
+ @impl Kanta.Migration
35
+ def down(opts) do
36
+ opts = with_defaults(opts, @initial_version)
37
+ initial = max(migrated_version(opts), @initial_version)
38
+
39
+ if initial >= opts.version do
40
+ change(initial..opts.version, :down, opts)
41
+ end
42
+ end
43
+
44
+ @impl Kanta.Migration
45
+ def migrated_version(opts) do
46
+ opts = with_defaults(opts, @initial_version)
47
+
48
+ repo = Map.get_lazy(opts, :repo, fn -> repo() end)
49
+ query = "PRAGMA user_version"
50
+
51
+ case repo.query(query, [], log: false) do
52
+ {:ok, %{rows: [[version]]}} when is_integer(version) -> version
53
+ _ -> 0
54
+ end
55
+ end
56
+
57
+ defp change(range, direction, opts) do
58
+ for index <- range do
59
+ pad_idx = String.pad_leading(to_string(index), 2, "0")
60
+
61
+ [__MODULE__, "V#{pad_idx}"]
62
+ |> Module.concat()
63
+ |> apply(direction, [opts])
64
+ end
65
+
66
+ case direction do
67
+ :up -> record_version(opts, Enum.max(range))
68
+ :down -> record_version(opts, Enum.min(range) - 1)
69
+ end
70
+ end
71
+
72
+ defp record_version(_opts, 0), do: :ok
73
+
74
+ defp record_version(_opts, version) do
75
+ execute "PRAGMA user_version = #{version}"
76
+ end
77
+
78
+ defp with_defaults(opts, version) do
79
+ Enum.into(opts, %{version: version})
80
+ end
81
+ end
added lib/kanta/migrations/sqlite3/v01.ex
 
@@ -0,0 +1,151 @@
1
+ defmodule Kanta.Migrations.SQLite3.V01 do
2
+ @moduledoc false
3
+
4
+ use Ecto.Migration
5
+
6
+ alias Kanta.Utils.Colors
7
+
8
+ @kanta_locales "kanta_locales"
9
+ @kanta_domains "kanta_domains"
10
+ @kanta_contexts "kanta_contexts"
11
+ @kanta_messages "kanta_messages"
12
+ @kanta_singular_translations "kanta_singular_translations"
13
+ @kanta_plural_translations "kanta_plural_translations"
14
+
15
+ def up(opts) do
16
+ [
17
+ &up_locales/1,
18
+ &up_contexts/1,
19
+ &up_domains/1,
20
+ &up_messages/1,
21
+ &up_singular_translations/1,
22
+ &up_plural_translations/1
23
+ ]
24
+ |> Enum.each(&apply(&1, [opts]))
25
+ end
26
+
27
+ def down(opts) do
28
+ [
29
+ &down_plural_translations/1,
30
+ &down_singular_translations/1,
31
+ &down_messages/1,
32
+ &down_domains/1,
33
+ &down_contexts/1,
34
+ &down_locales/1
35
+ ]
36
+ |> Enum.each(&apply(&1, [opts]))
37
+ end
38
+
39
+ defp up_locales(_opts) do
40
+ create_if_not_exists table(@kanta_locales) do
41
+ add(:iso639_code, :string)
42
+ add(:name, :string)
43
+ add(:native_name, :string)
44
+ add(:family, :string)
45
+ add(:wiki_url, :string)
46
+ add(:colors, {:array, :string})
47
+ add(:plurals_header, :string)
48
+ timestamps()
49
+ end
50
+
51
+ create_if_not_exists unique_index(@kanta_locales, [:iso639_code])
52
+ end
53
+
54
+ defp up_domains(_opts) do
55
+ create_if_not_exists table(@kanta_domains) do
56
+ add(:name, :string)
57
+ add(:description, :text)
58
+ add(:color, :string, null: false, default: Colors.default_color())
59
+ timestamps()
60
+ end
61
+
62
+ create_if_not_exists unique_index(@kanta_domains, [:name])
63
+ end
64
+
65
+ defp up_contexts(_opts) do
66
+ create_if_not_exists table(@kanta_contexts) do
67
+ add(:name, :string)
68
+ add(:description, :text)
69
+ add(:color, :string, null: false, default: Colors.default_color())
70
+ timestamps()
71
+ end
72
+
73
+ create_if_not_exists unique_index(@kanta_contexts, [:name])
74
+ end
75
+
76
+ defp up_messages(_opts) do
77
+ create_if_not_exists table(@kanta_messages) do
78
+ add(:msgid, :text)
79
+
80
+ add(:message_type, :string,
81
+ null: false,
82
+ check: %{name: "message_type_check", expr: "message_type IN ('singular', 'plural')"}
83
+ )
84
+
85
+ add(:domain_id, references(@kanta_domains), null: true)
86
+ add(:context_id, references(@kanta_contexts), null: true)
87
+ timestamps()
88
+ end
89
+
90
+ execute "ALTER TABLE #{@kanta_messages} ADD COLUMN searchable TEXT AS (msgid) VIRTUAL"
91
+
92
+ create_if_not_exists unique_index(@kanta_messages, [:context_id, :domain_id, :msgid])
93
+ end
94
+
95
+ defp up_singular_translations(_opts) do
96
+ create_if_not_exists table(@kanta_singular_translations) do
97
+ add(:original_text, :text)
98
+ add(:translated_text, :text, null: true)
99
+ add(:locale_id, references(@kanta_locales))
100
+ add(:message_id, references(@kanta_messages))
101
+ timestamps()
102
+ end
103
+
104
+ create_if_not_exists unique_index(@kanta_singular_translations, [:locale_id, :message_id])
105
+
106
+ execute "ALTER TABLE #{@kanta_singular_translations} ADD COLUMN searchable TEXT AS (translated_text) VIRTUAL"
107
+ end
108
+
109
+ defp up_plural_translations(_opts) do
110
+ create_if_not_exists table(@kanta_plural_translations) do
111
+ add(:nplural_index, :integer)
112
+ add(:original_text, :text)
113
+ add(:translated_text, :text, null: true)
114
+ add(:locale_id, references(@kanta_locales))
115
+ add(:message_id, references(@kanta_messages))
116
+ timestamps()
117
+ end
118
+
119
+ create_if_not_exists unique_index(@kanta_plural_translations, [
120
+ :locale_id,
121
+ :message_id,
122
+ :nplural_index
123
+ ])
124
+
125
+ execute "ALTER TABLE #{@kanta_plural_translations} ADD COLUMN searchable TEXT AS (translated_text) VIRTUAL"
126
+ end
127
+
128
+ defp down_locales(_opts) do
129
+ drop table(@kanta_locales)
130
+ end
131
+
132
+ defp down_domains(_opts) do
133
+ drop table(@kanta_domains)
134
+ end
135
+
136
+ defp down_contexts(_opts) do
137
+ drop table(@kanta_contexts)
138
+ end
139
+
140
+ defp down_messages(_opts) do
141
+ drop table(@kanta_messages)
142
+ end
143
+
144
+ defp down_singular_translations(_opts) do
145
+ drop table(@kanta_singular_translations)
146
+ end
147
+
148
+ defp down_plural_translations(_opts) do
149
+ drop table(@kanta_plural_translations)
150
+ end
151
+ end
added lib/kanta/migrations/sqlite3/v02.ex
 
@@ -0,0 +1,78 @@
1
+ defmodule Kanta.Migrations.SQLite3.V02 do
2
+ @moduledoc """
3
+ Kanta V2 Migrations
4
+ """
5
+
6
+ use Ecto.Migration
7
+ alias Kanta.Utils.Colors
8
+
9
+ @kanta_application_sources "kanta_application_sources"
10
+ @kanta_messages "kanta_messages"
11
+
12
+ def up(opts) do
13
+ [
14
+ &up_application_sources/1,
15
+ &up_kanta_messages/1
16
+ ]
17
+ |> Enum.each(&apply(&1, [opts]))
18
+ end
19
+
20
+ def down(opts) do
21
+ [
22
+ &down_application_sources/1,
23
+ &down_kanta_messages/1
24
+ ]
25
+ |> Enum.each(&apply(&1, [opts]))
26
+ end
27
+
28
+ def up_application_sources(_opts) do
29
+ create_if_not_exists table(@kanta_application_sources) do
30
+ add(:name, :string)
31
+ add(:description, :text)
32
+ add(:color, :string, null: false, default: Colors.default_color())
33
+ timestamps()
34
+ end
35
+
36
+ create_if_not_exists unique_index(@kanta_application_sources, [:name])
37
+ end
38
+
39
+ def up_kanta_messages(_opts) do
40
+ alter table(@kanta_messages) do
41
+ add(:application_source_id, references(@kanta_application_sources), null: true)
42
+ end
43
+
44
+ drop unique_index(@kanta_messages, [:context_id, :domain_id, :msgid])
45
+
46
+ create_if_not_exists unique_index(
47
+ @kanta_messages,
48
+ [
49
+ :application_source_id,
50
+ :context_id,
51
+ :domain_id,
52
+ :msgid
53
+ ]
54
+ )
55
+ end
56
+
57
+ def down_application_sources(_opts) do
58
+ drop table(@kanta_application_sources)
59
+ end
60
+
61
+ def down_kanta_messages(_opts) do
62
+ drop unique_index(
63
+ @kanta_messages,
64
+ [
65
+ :application_source_id,
66
+ :context_id,
67
+ :domain_id,
68
+ :msgid
69
+ ]
70
+ )
71
+
72
+ create_if_not_exists unique_index(@kanta_messages, [:context_id, :domain_id, :msgid])
73
+
74
+ alter table(@kanta_messages) do
75
+ remove(:application_source_id)
76
+ end
77
+ end
78
+ end
changed lib/kanta/po_files/handlers/messages_extractor.ex
 
@@ -16,8 +16,8 @@ defmodule Kanta.POFiles.MessagesExtractor do
16
16
]
17
17
18
18
priv = :code.priv_dir(opts[:otp_name])
19
- all_po_files = po_files_in_priv(priv)
20
- known_po_files = known_po_files(all_po_files, opts)
19
+ priv_gettext_po_files = po_files_in_priv(priv)
20
+ known_po_files = known_po_files(priv_gettext_po_files, opts)
21
21
22
22
extract_translations(known_po_files)
23
23
end
 
@@ -35,35 +35,35 @@ defmodule Kanta.POFiles.MessagesExtractor do
35
35
36
36
messages
37
37
|> Stream.map(fn
38
- %Expo.Message.Singular{msgctxt: nil, msgid: [msgid], msgstr: [text]} ->
38
+ %Expo.Message.Singular{msgctxt: nil, msgid: msgid, msgstr: texts} ->
39
39
ExtractSingularTranslation.call(%{
40
- msgid: msgid,
40
+ msgid: Enum.join(msgid),
41
41
locale_name: locale,
42
42
domain_name: domain,
43
- original_text: text
43
+ original_text: Enum.join(texts)
44
44
})
45
45
46
- %Expo.Message.Singular{msgctxt: [msgctxt], msgid: [msgid], msgstr: [text]} ->
46
+ %Expo.Message.Singular{msgctxt: [msgctxt], msgid: msgid, msgstr: texts} ->
47
47
ExtractSingularTranslation.call(%{
48
- msgid: msgid,
48
+ msgid: Enum.join(msgid),
49
49
context_name: msgctxt,
50
50
locale_name: locale,
51
51
domain_name: domain,
52
- original_text: text
52
+ original_text: Enum.join(texts)
53
53
})
54
54
55
- %Expo.Message.Plural{msgctxt: nil, msgid_plural: [msgid], msgstr: plurals_map} ->
55
+ %Expo.Message.Plural{msgctxt: nil, msgid_plural: msgid, msgstr: plurals_map} ->
56
56
ExtractPluralTranslation.call(%{
57
- msgid: msgid,
57
+ msgid: Enum.join(msgid),
58
58
locale_name: locale,
59
59
domain_name: domain,
60
60
plurals_map: plurals_map,
61
61
plurals_header: plurals_header
62
62
})
63
63
64
- %Expo.Message.Plural{msgctxt: [msgctxt], msgid_plural: [msgid], msgstr: plurals_map} ->
64
+ %Expo.Message.Plural{msgctxt: [msgctxt], msgid_plural: msgid, msgstr: plurals_map} ->
65
65
ExtractPluralTranslation.call(%{
66
- msgid: msgid,
66
+ msgid: Enum.join(msgid),
67
67
context_name: msgctxt,
68
68
locale_name: locale,
69
69
domain_name: domain,
 
@@ -82,6 +82,7 @@ defmodule Kanta.POFiles.MessagesExtractor do
82
82
83
83
defp po_files_in_priv(priv) do
84
84
priv
85
+ |> Path.join("gettext")
85
86
|> Path.join(@po_wildcard)
86
87
|> Path.wildcard()
87
88
end
changed lib/kanta/query.ex
 
@@ -44,7 +44,7 @@ defmodule Kanta.Query do
44
44
def paginate(query, page \\ 1, per_page \\ @default_page_size)
45
45
46
46
def paginate(query, page, per_page) do
47
- page = if is_number(page), do: max(page, 1), else: 1
47
+ page = parse_page(page)
48
48
49
49
per_page =
50
50
if is_number(per_page), do: max(per_page, @minimum_per_page), else: @default_page_size
 
@@ -61,7 +61,7 @@ defmodule Kanta.Query do
61
61
%Scrivener.Config{
62
62
caller: self(),
63
63
module: Repo.get_repo(),
64
- page_number: page || 1,
64
+ page_number: page,
65
65
page_size: per_page || @default_page_size,
66
66
options: []
67
67
}
 
@@ -337,10 +337,20 @@ defmodule Kanta.Query do
337
337
def search_query(query, search_term) do
338
338
repo = Repo.get_repo()
339
339
340
- if Postgresql.migrated_version(%{repo: repo}) >= 2 do
341
- search_query_fuzzy(query, search_term)
342
- else
343
- search_query_legacy(query, search_term)
340
+ case repo.__adapter__() do
341
+ Ecto.Adapters.Postgres ->
342
+ if Postgresql.migrated_version(%{repo: repo}) >= 2 do
343
+ search_query_fuzzy(query, search_term)
344
+ else
345
+ search_query_legacy(query, search_term)
346
+ end
347
+
348
+ _ ->
349
+ or_where(
350
+ query,
351
+ [{unquote(opts[:binding]), resource}],
352
+ like(resource.searchable, ^"%#{search_term}%")
353
+ )
344
354
end
345
355
end
346
356
 
@@ -417,6 +427,29 @@ defmodule Kanta.Query do
417
427
end
418
428
419
429
defoverridable join_resource: 3
430
+
431
+ @spec parse_page(page :: any()) :: integer()
432
+ defp parse_page(page) when is_binary(page) do
433
+ case Integer.parse(page) do
434
+ {n, _} ->
435
+ parse_page(n)
436
+
437
+ :error ->
438
+ 1
439
+ end
440
+ end
441
+
442
+ defp parse_page(page) when is_integer(page) do
443
+ max(page, 1)
444
+ end
445
+
446
+ defp parse_page(page) when is_float(page) do
447
+ page
448
+ |> floor()
449
+ |> parse_page()
450
+ end
451
+
452
+ defp parse_page(_), do: 1
420
453
end
421
454
end
422
455
end
added lib/kanta/schema.ex
 
@@ -0,0 +1,13 @@
1
+ defmodule Kanta.Schema do
2
+ @moduledoc false
3
+
4
+ defmacro __using__(_opts) do
5
+ quote do
6
+ use Ecto.Schema
7
+
8
+ @primary_key {:id, Application.compile_env(:kanta, :schema_id_type, :id),
9
+ autogenerate: true}
10
+ @foreign_key_type Application.compile_env(:kanta, :schema_id_type, :id)
11
+ end
12
+ end
13
+ end
changed lib/kanta/translations.ex
 
@@ -4,6 +4,7 @@ defmodule Kanta.Translations do
4
4
"""
5
5
6
6
alias Kanta.Translations.{
7
+ ApplicationSources,
7
8
Contexts,
8
9
Domains,
9
10
Locales,
 
@@ -12,34 +13,49 @@ defmodule Kanta.Translations do
12
13
SingularTranslations
13
14
}
14
15
16
+ # APPLICATION SOURCES
17
+ defdelegate list_application_sources(params \\ []), to: ApplicationSources
18
+ defdelegate get_application_source(params), to: ApplicationSources
19
+ defdelegate create_application_source(attrs, opts \\ []), to: ApplicationSources
20
+ defdelegate change_application_source(attrs, params \\ %{}), to: ApplicationSources
21
+ defdelegate application_sources_empty?(), to: ApplicationSources
22
+
23
+ defdelegate update_application_source(application_source, attrs, opts \\ []),
24
+ to: ApplicationSources
25
+
15
26
# CONTEXTS
16
27
defdelegate list_contexts(params \\ []), to: Contexts
28
+ defdelegate list_all_contexts(params \\ []), to: Contexts
17
29
defdelegate get_context(params), to: Contexts
18
- defdelegate create_context(params), to: Contexts
30
+ defdelegate create_context(params, opts \\ []), to: Contexts
19
31
20
32
# DOMAINS
21
33
defdelegate list_domains(params \\ []), to: Domains
34
+ defdelegate list_all_domains(params \\ []), to: Domains
22
35
defdelegate get_domain(params \\ []), to: Domains
23
- defdelegate create_domain(attrs), to: Domains
36
+ defdelegate create_domain(attrs, opts \\ []), to: Domains
24
37
25
38
# MESSAGES
26
39
defdelegate list_messages(params \\ []), to: Messages
40
+ defdelegate list_all_messages(params \\ []), to: Messages
27
41
defdelegate get_message(params \\ []), to: Messages
28
42
defdelegate get_messages_count(), to: Messages
29
- defdelegate create_message(attrs), to: Messages
43
+ defdelegate create_message(attrs, opts \\ []), to: Messages
30
44
31
45
# LOCALES
32
46
defdelegate list_locales(params \\ []), to: Locales
33
47
defdelegate get_locale(params \\ []), to: Locales
34
- defdelegate update_locale(locale, attrs), to: Locales
48
+ defdelegate update_locale(locale, attrs, opts \\ []), to: Locales
35
49
36
50
# TRANSLATIONS
37
51
defdelegate list_plural_translations(params), to: PluralTranslations
38
52
defdelegate get_plural_translation(params), to: PluralTranslations
39
- defdelegate create_plural_translation(attrs), to: PluralTranslations
40
- defdelegate update_plural_translation(translation, attrs), to: PluralTranslations
53
+ defdelegate create_plural_translation(attrs, opts \\ []), to: PluralTranslations
54
+ defdelegate update_plural_translation(translation, attrs, opts \\ []), to: PluralTranslations
41
55
42
56
defdelegate get_singular_translation(params), to: SingularTranslations
43
- defdelegate create_singular_translation(attrs), to: SingularTranslations
44
- defdelegate update_singular_translation(translation, attrs), to: SingularTranslations
57
+ defdelegate create_singular_translation(attrs, opts \\ []), to: SingularTranslations
58
+
59
+ defdelegate update_singular_translation(translation, attrs, opts \\ []),
60
+ to: SingularTranslations
45
61
end
added lib/kanta/translations/application_source.ex
 
@@ -0,0 +1,34 @@
1
+ defmodule Kanta.Translations.ApplicationSource do
2
+ @moduledoc """
3
+ Application source DB model used when dealing with multiple apps, for example mobile app
4
+ """
5
+
6
+ use Ecto.Schema
7
+ import Ecto.Changeset
8
+
9
+ alias Kanta.Translations.Message
10
+
11
+ @required_fields ~w(name)a
12
+ @optional_fields ~w(description color)a
13
+
14
+ @type t() :: Kanta.Translations.ApplicationSourceSpec.t()
15
+
16
+ @derive {Jason.Encoder, only: [:id] ++ @required_fields ++ @optional_fields}
17
+
18
+ schema "kanta_application_sources" do
19
+ field :name, :string
20
+ field :description, :string
21
+ field :color, :string
22
+
23
+ has_many :messages, Message
24
+
25
+ timestamps()
26
+ end
27
+
28
+ def changeset(struct, params) do
29
+ struct
30
+ |> cast(params, @required_fields ++ @optional_fields)
31
+ |> validate_required(@required_fields)
32
+ |> unique_constraint([:name])
33
+ end
34
+ end
added lib/kanta/translations/application_source/application_source_spec.ex
 
@@ -0,0 +1,18 @@
1
+ defmodule Kanta.Translations.ApplicationSourceSpec do
2
+ @moduledoc """
3
+ Includes type specs for application source.
4
+ """
5
+
6
+ alias Kanta.Translations.{ApplicationSource, Message}
7
+ alias Kanta.Types
8
+
9
+ @type t() :: %ApplicationSource{
10
+ id: Types.field(Types.id()),
11
+ name: Types.field(String.t()),
12
+ description: Types.field(String.t()),
13
+ color: Types.field(String.t()),
14
+ messages: [Message.t()],
15
+ inserted_at: Types.field(NaiveDateTime.t()),
16
+ updated_at: Types.field(NaiveDateTime.t())
17
+ }
18
+ end
added lib/kanta/translations/application_source/application_sources.ex
 
@@ -0,0 +1,43 @@
1
+ defmodule Kanta.Translations.ApplicationSources do
2
+ @moduledoc """
3
+ ApplicationSources Kanta subcontext
4
+ """
5
+
6
+ alias Kanta.Translations.ApplicationSource
7
+
8
+ alias Kanta.Translations.ApplicationSources.Finders.{
9
+ GetApplicationSource,
10
+ ListApplicationSources
11
+ }
12
+
13
+ def list_application_sources(params \\ []) do
14
+ ListApplicationSources.find(params)
15
+ end
16
+
17
+ def get_application_source(params) do
18
+ GetApplicationSource.find(params)
19
+ end
20
+
21
+ def create_application_source(attrs, opts \\ []) do
22
+ %ApplicationSource{}
23
+ |> ApplicationSource.changeset(attrs)
24
+ |> Kanta.Repo.get_repo().insert(opts)
25
+ end
26
+
27
+ def update_application_source(%ApplicationSource{} = application_source, attrs, opts \\ []) do
28
+ application_source
29
+ |> ApplicationSource.changeset(attrs)
30
+ |> Kanta.Repo.get_repo().update(opts)
31
+ end
32
+
33
+ def change_application_source(%ApplicationSource{} = application_source, params \\ %{}) do
34
+ ApplicationSource.changeset(application_source, params)
35
+ end
36
+
37
+ def application_sources_empty? do
38
+ %{entries: application_sources, metadata: _application_sources_metadata} =
39
+ list_application_sources(page: 1, per_page: 1)
40
+
41
+ Enum.empty?(application_sources)
42
+ end
43
+ end
added lib/kanta/translations/application_source/finders/get_application_source.ex
 
@@ -0,0 +1,47 @@
1
+ defmodule Kanta.Translations.ApplicationSources.Finders.GetApplicationSource do
2
+ @moduledoc """
3
+ Query module aka Finder responsible for finding application source
4
+ """
5
+
6
+ use Kanta.Query,
7
+ module: Kanta.Translations.ApplicationSource,
8
+ binding: :domain
9
+
10
+ alias Kanta.Cache
11
+ alias Kanta.Translations.ApplicationSource
12
+
13
+ def find(params \\ []) do
14
+ cache_key = Cache.generate_cache_key("application_source", params)
15
+
16
+ with {:error, _, :not_cached} <- find_in_cache(cache_key),
17
+ {:ok, %ApplicationSource{} = application_source} <- find_in_database(params) do
18
+ Cache.put(cache_key, application_source)
19
+
20
+ {:ok, application_source}
21
+ else
22
+ {:ok, %ApplicationSource{} = application_source} -> {:ok, application_source}
23
+ {:error, _, :not_found} -> {:error, :application_source, :not_found}
24
+ end
25
+ end
26
+
27
+ defp find_in_cache(cache_key) do
28
+ case Cache.get(cache_key) do
29
+ nil ->
30
+ {:error, :application_source, :not_cached}
31
+
32
+ cached_application_source ->
33
+ {:ok, cached_application_source}
34
+ end
35
+ end
36
+
37
+ defp find_in_database(params) do
38
+ base()
39
+ |> filter_query(params[:filter])
40
+ |> preload_resources(params[:preloads] || [])
41
+ |> one()
42
+ |> case do
43
+ %ApplicationSource{} = application_source -> {:ok, application_source}
44
+ _ -> {:error, :application_source, :not_found}
45
+ end
46
+ end
47
+ end
added lib/kanta/translations/application_source/finders/list_application_sources.ex
 
@@ -0,0 +1,16 @@
1
+ defmodule Kanta.Translations.ApplicationSources.Finders.ListApplicationSources do
2
+ @moduledoc """
3
+ Query module aka Finder responsible for listing application sources
4
+ """
5
+
6
+ use Kanta.Query,
7
+ module: Kanta.Translations.ApplicationSource,
8
+ binding: :domain
9
+
10
+ def find(params \\ []) do
11
+ base()
12
+ |> filter_query(params[:filter])
13
+ |> preload_resources(params[:preloads] || [])
14
+ |> paginate(params[:page], params[:per_page])
15
+ end
16
+ end
changed lib/kanta/translations/context.ex
 
@@ -3,7 +3,7 @@ defmodule Kanta.Translations.Context do
3
3
Gettext Context DB model
4
4
"""
5
5
6
- use Ecto.Schema
6
+ use Kanta.Schema
7
7
import Ecto.Changeset
8
8
9
9
alias Kanta.Translations.Message
changed lib/kanta/translations/context/contexts.ex
 
@@ -4,19 +4,23 @@ defmodule Kanta.Translations.Contexts do
4
4
"""
5
5
6
6
alias Kanta.Translations.Context
7
- alias Kanta.Translations.Contexts.Finders.{GetContext, ListContexts}
7
+ alias Kanta.Translations.Contexts.Finders.{GetContext, ListAllContexts, ListContexts}
8
8
9
9
def list_contexts(params \\ []) do
10
10
ListContexts.find(params)
11
11
end
12
12
13
+ def list_all_contexts(params \\ []) do
14
+ ListAllContexts.find(params)
15
+ end
16
+
13
17
def get_context(params) do
14
18
GetContext.find(params)
15
19
end
16
20
17
- def create_context(attrs) do
21
+ def create_context(attrs, opts \\ []) do
18
22
%Context{}
19
23
|> Context.changeset(attrs)
20
- |> Kanta.Repo.get_repo().insert()
24
+ |> Kanta.Repo.get_repo().insert(opts)
21
25
end
22
26
end
added lib/kanta/translations/context/finders/list_all_contexts.ex
 
@@ -0,0 +1,18 @@
1
+ defmodule Kanta.Translations.Contexts.Finders.ListAllContexts do
2
+ @moduledoc """
3
+ Query module aka Finder responsible for listing all gettext contexts
4
+ """
5
+
6
+ use Kanta.Query,
7
+ module: Kanta.Translations.Context,
8
+ binding: :context
9
+
10
+ def find(params \\ []) do
11
+ repo = Kanta.Repo.get_repo()
12
+
13
+ base()
14
+ |> filter_query(params[:filter])
15
+ |> preload_resources(params[:preloads] || [])
16
+ |> repo.all()
17
+ end
18
+ end
changed lib/kanta/translations/domain.ex
 
@@ -3,7 +3,7 @@ defmodule Kanta.Translations.Domain do
3
3
Gettext domain DB model
4
4
"""
5
5
6
- use Ecto.Schema
6
+ use Kanta.Schema
7
7
import Ecto.Changeset
8
8
9
9
alias Kanta.Translations.Message
changed lib/kanta/translations/domain/domains.ex
 
@@ -6,19 +6,23 @@ defmodule Kanta.Translations.Domains do
6
6
alias Kanta.Repo
7
7
8
8
alias Kanta.Translations.Domain
9
- alias Kanta.Translations.Domains.Finders.{GetDomain, ListDomains}
9
+ alias Kanta.Translations.Domains.Finders.{GetDomain, ListAllDomains, ListDomains}
10
10
11
11
def list_domains(params \\ []) do
12
12
ListDomains.find(params)
13
13
end
14
14
15
+ def list_all_domains(params \\ []) do
16
+ ListAllDomains.find(params)
17
+ end
18
+
15
19
def get_domain(params) do
16
20
GetDomain.find(params)
17
21
end
18
22
19
- def create_domain(attrs) do
23
+ def create_domain(attrs, opts \\ []) do
20
24
%Domain{}
21
25
|> Domain.changeset(attrs)
22
- |> Repo.get_repo().insert()
26
+ |> Repo.get_repo().insert(opts)
23
27
end
24
28
end
added lib/kanta/translations/domain/finders/list_all_domains.ex
 
@@ -0,0 +1,18 @@
1
+ defmodule Kanta.Translations.Domains.Finders.ListAllDomains do
2
+ @moduledoc """
3
+ Query module aka Finder responsible for listing all gettext domains
4
+ """
5
+
6
+ use Kanta.Query,
7
+ module: Kanta.Translations.Domain,
8
+ binding: :domain
9
+
10
+ def find(params \\ []) do
11
+ repo = Kanta.Repo.get_repo()
12
+
13
+ base()
14
+ |> filter_query(params[:filter])
15
+ |> preload_resources(params[:preloads] || [])
16
+ |> repo.all()
17
+ end
18
+ end
changed lib/kanta/translations/locale.ex
 
@@ -3,7 +3,7 @@ defmodule Kanta.Translations.Locale do
3
3
Locale DB model
4
4
"""
5
5
6
- use Ecto.Schema
6
+ use Kanta.Schema
7
7
import Ecto.Changeset
8
8
alias Kanta.Translations.SingularTranslation
changed lib/kanta/translations/locale/locales.ex
 
@@ -16,8 +16,8 @@ defmodule Kanta.Translations.Locales do
16
16
GetLocale.find(params)
17
17
end
18
18
19
- def update_locale(locale, attrs \\ %{}) do
19
+ def update_locale(locale, attrs \\ %{}, opts \\ []) do
20
20
Locale.changeset(locale, attrs)
21
- |> Repo.get_repo().update()
21
+ |> Repo.get_repo().update(opts)
22
22
end
23
23
end
changed lib/kanta/translations/message.ex
 
@@ -3,17 +3,24 @@ defmodule Kanta.Translations.Message do
3
3
Gettext message DB model
4
4
"""
5
5
6
- use Ecto.Schema
6
+ use Kanta.Schema
7
7
import Ecto.Changeset
8
8
9
- alias Kanta.Translations.{Context, Domain, PluralTranslation, SingularTranslation}
9
+ alias Kanta.Translations.{
10
+ ApplicationSource,
11
+ Context,
12
+ Domain,
13
+ PluralTranslation,
14
+ SingularTranslation
15
+ }
10
16
11
17
@required_fields ~w(msgid message_type)a
12
- @optional_fields ~w(domain_id context_id)a
18
+ @optional_fields ~w(domain_id context_id application_source_id)a
19
+ @relations ~w(domain context singular_translations plural_translations)a
13
20
14
21
@type t() :: Kanta.Translations.MessageSpec.t()
15
22
16
- @derive {Jason.Encoder, only: [:id] ++ @required_fields ++ @optional_fields}
23
+ @derive {Jason.Encoder, only: [:id] ++ @required_fields ++ @optional_fields ++ @relations}
17
24
18
25
schema "kanta_messages" do
19
26
field :msgid, :string
 
@@ -21,6 +28,7 @@ defmodule Kanta.Translations.Message do
21
28
22
29
belongs_to :domain, Domain
23
30
belongs_to :context, Context
31
+ belongs_to :application_source, ApplicationSource
24
32
25
33
has_many :singular_translations, SingularTranslation
26
34
has_many :plural_translations, PluralTranslation
changed lib/kanta/translations/messages/finders/get_message.ex
 
@@ -46,10 +46,15 @@ defmodule Kanta.Translations.Messages.Finders.GetMessage do
46
46
%Message{} = message -> {:ok, message}
47
47
_ -> {:error, :message, :not_found}
48
48
end
49
- |> database_fallback_public_prefix(params, opts)
49
+ |> database_fallback_public_prefix(params, opts, Repo.get_repo().__adapter__())
50
50
end
51
51
52
- defp database_fallback_public_prefix({:error, :message, :not_found} = result, params, repo_opts) do
52
+ defp database_fallback_public_prefix(
53
+ {:error, :message, :not_found} = result,
54
+ params,
55
+ repo_opts,
56
+ Ecto.Adapters.Postgres
57
+ ) do
53
58
if public_prefix?(repo_opts) do
54
59
result
55
60
else
 
@@ -58,7 +63,7 @@ defmodule Kanta.Translations.Messages.Finders.GetMessage do
58
63
end
59
64
end
60
65
61
- defp database_fallback_public_prefix(result, _, _), do: result
66
+ defp database_fallback_public_prefix(result, _, _, _), do: result
62
67
63
68
defp public_prefix?(repo_opts) do
64
69
config_prefix = Repo.get_repo().default_options(:all) |> Keyword.get(:prefix, :unset)
added lib/kanta/translations/messages/finders/list_all_messages.ex
 
@@ -0,0 +1,22 @@
1
+ defmodule Kanta.Translations.Messages.Finders.ListAllMessages do
2
+ @moduledoc """
3
+ Query module aka Finder responsible for listing all gettext messages
4
+ """
5
+
6
+ use Kanta.Query,
7
+ module: Kanta.Translations.Message,
8
+ binding: :message
9
+
10
+ @available_filters ~w(domain_id context_id application_source_id)
11
+
12
+ def find(params \\ []) do
13
+ repo = Kanta.Repo.get_repo()
14
+ filters = params[:filter] || %{}
15
+ query_filters = Map.take(filters, @available_filters)
16
+
17
+ base()
18
+ |> filter_query(query_filters)
19
+ |> preload_resources(params[:preloads] || [])
20
+ |> repo.all()
21
+ end
22
+ end
changed lib/kanta/translations/messages/finders/list_messages.ex
 
@@ -10,7 +10,7 @@ defmodule Kanta.Translations.Messages.Finders.ListMessages do
10
10
alias Kanta.Translations.PluralTranslations.Finders.ListPluralTranslations
11
11
alias Kanta.Translations.SingularTranslations.Finders.ListSingularTranslations
12
12
13
- @available_filters ~w(domain_id context_id)
13
+ @available_filters ~w(domain_id context_id application_source_id)
14
14
15
15
def find(params \\ []) do
16
16
filters = params[:filter] || %{}
changed lib/kanta/translations/messages/message_spec.ex
 
@@ -3,13 +3,23 @@ defmodule Kanta.Translations.MessageSpec do
3
3
Includes type specs for message.
4
4
"""
5
5
6
- alias Kanta.Translations.{Context, Domain, Message, PluralTranslation, SingularTranslation}
6
+ alias Kanta.Translations.{
7
+ ApplicationSource,
8
+ Context,
9
+ Domain,
10
+ Message,
11
+ PluralTranslation,
12
+ SingularTranslation
13
+ }
14
+
7
15
alias Kanta.Types
8
16
9
17
@type t() :: %Message{
10
18
id: Types.field(Types.id()),
11
19
msgid: Types.field(String.t()),
12
20
message_type: :singular | :plural,
21
+ application_source: Types.field(ApplicationSource.t()),
22
+ application_source_id: Types.field(Types.id()),
13
23
domain: Types.field(Domain.t()),
14
24
domain_id: Types.field(Types.id()),
15
25
context: Types.field(Context.t()),
changed lib/kanta/translations/messages/messages.ex
 
@@ -7,12 +7,16 @@ defmodule Kanta.Translations.Messages do
7
7
8
8
alias Kanta.Translations.Message
9
9
10
- alias Kanta.Translations.Messages.Finders.{GetMessage, ListMessages}
10
+ alias Kanta.Translations.Messages.Finders.{GetMessage, ListAllMessages, ListMessages}
11
11
12
12
def list_messages(params \\ []) do
13
13
ListMessages.find(params)
14
14
end
15
15
16
+ def list_all_messages(params \\ []) do
17
+ ListAllMessages.find(params)
18
+ end
19
+
16
20
def get_message(params \\ []) do
17
21
GetMessage.find(params)
18
22
end
 
@@ -21,8 +25,8 @@ defmodule Kanta.Translations.Messages do
21
25
Repo.get_repo().aggregate(Message, :count)
22
26
end
23
27
24
- def create_message(attrs) do
25
- %Message{} |> Message.changeset(attrs) |> Repo.get_repo().insert()
28
+ def create_message(attrs, opts \\ []) do
29
+ %Message{} |> Message.changeset(attrs) |> Repo.get_repo().insert(opts)
26
30
end
27
31
28
32
def update_message(message, attrs) do
changed lib/kanta/translations/plural_translation.ex
 
@@ -3,7 +3,7 @@ defmodule Kanta.Translations.PluralTranslation do
3
3
Plural translation DB model
4
4
"""
5
5
6
- use Ecto.Schema
6
+ use Kanta.Schema
7
7
import Ecto.Changeset
8
8
9
9
alias Kanta.Translations.{Locale, Message}
changed lib/kanta/translations/plural_translation/plural_translations.ex
 
@@ -20,10 +20,10 @@ defmodule Kanta.Translations.PluralTranslations do
20
20
GetPluralTranslation.find(params)
21
21
end
22
22
23
- def create_plural_translation(attrs) do
23
+ def create_plural_translation(attrs, opts \\ []) do
24
24
attrs
25
25
|> then(&PluralTranslation.changeset(%PluralTranslation{}, &1))
26
- |> Repo.get_repo().insert()
26
+ |> Repo.get_repo().insert(opts)
27
27
|> case do
28
28
{:ok, plural_translation} ->
29
29
cache_key =
 
@@ -43,9 +43,9 @@ defmodule Kanta.Translations.PluralTranslations do
43
43
end
44
44
end
45
45
46
- def update_plural_translation(translation, attrs) do
46
+ def update_plural_translation(translation, attrs, opts \\ []) do
47
47
PluralTranslation.changeset(translation, attrs)
48
- |> Repo.get_repo().update()
48
+ |> Repo.get_repo().update(opts)
49
49
|> case do
50
50
{:ok, translation} ->
51
51
cache_key =
changed lib/kanta/translations/singular_translation.ex
 
@@ -3,7 +3,7 @@ defmodule Kanta.Translations.SingularTranslation do
3
3
Singular translation DB model
4
4
"""
5
5
6
- use Ecto.Schema
6
+ use Kanta.Schema
7
7
import Ecto.Changeset
8
8
alias Kanta.Translations.{Locale, Message}
changed lib/kanta/translations/singular_translation/singular_translations.ex
 
@@ -13,10 +13,10 @@ defmodule Kanta.Translations.SingularTranslations do
13
13
GetSingularTranslation.find(params)
14
14
end
15
15
16
- def create_singular_translation(attrs) do
16
+ def create_singular_translation(attrs, opts \\ []) do
17
17
attrs
18
18
|> then(&SingularTranslation.changeset(%SingularTranslation{}, &1))
19
- |> Repo.get_repo().insert()
19
+ |> Repo.get_repo().insert(opts)
20
20
|> case do
21
21
{:ok, singular_translation} ->
22
22
cache_key =
 
@@ -35,9 +35,9 @@ defmodule Kanta.Translations.SingularTranslations do
35
35
end
36
36
end
37
37
38
- def update_singular_translation(translation, attrs) do
38
+ def update_singular_translation(translation, attrs, opts \\ []) do
39
39
SingularTranslation.changeset(translation, attrs)
40
- |> Repo.get_repo().update()
40
+ |> Repo.get_repo().update(opts)
41
41
|> case do
42
42
{:ok, translation} ->
43
43
cache_key =
added lib/kanta/utils/colors.ex
 
@@ -0,0 +1,10 @@
1
+ defmodule Kanta.Utils.Colors do
2
+ @moduledoc """
3
+ Color definitions
4
+ """
5
+
6
+ @default_color "#7E37D8"
7
+
8
+ @spec default_color() :: binary()
9
+ def default_color(), do: @default_color
10
+ end
added lib/kanta/utils/compilation.ex
 
@@ -0,0 +1,14 @@
1
+ defmodule Kanta.Utils.Compilation do
2
+ @moduledoc false
3
+
4
+ @doc """
5
+ Returns `true` if it is run during compilation.
6
+
7
+ This function is used to handle translation messages during compilation time in macros and module attributes.
8
+ """
9
+ @spec compiling?() :: boolean()
10
+ def compiling? do
11
+ Code.ensure_loaded?(Code) &&
12
+ Code.can_await_module_compilation?()
13
+ end
14
+ end
changed lib/kanta/utils/database_populator.ex
 
@@ -39,5 +39,8 @@ defmodule Kanta.Utils.DatabasePopulator do
39
39
defp reduce_keys_to_atoms({key, val}) when is_map(val),
40
40
do: {String.to_existing_atom(key), keys_to_atoms(val)}
41
41
42
+ defp reduce_keys_to_atoms({key, val}) when is_list(val),
43
+ do: {String.to_existing_atom(key), Enum.map(val, &keys_to_atoms/1)}
44
+
42
45
defp reduce_keys_to_atoms({key, val}), do: {String.to_existing_atom(key), val}
43
46
end
changed lib/kanta/utils/get_schemata.ex
 
@@ -4,6 +4,7 @@ defmodule Kanta.Utils.GetSchemata do
4
4
alias Kanta.Specs.SchemataSpec
5
5
6
6
alias Kanta.Translations.{
7
+ ApplicationSource,
7
8
Context,
8
9
Domain,
9
10
Locale,
 
@@ -13,6 +14,7 @@ defmodule Kanta.Utils.GetSchemata do
13
14
}
14
15
15
16
@schemata [
17
+ {"application_sources", %{schema: ApplicationSource, conflict_target: [:name]}},
16
18
{"contexts", %{schema: Context, conflict_target: [:name]}},
17
19
{"domains", %{schema: Domain, conflict_target: [:name]}},
18
20
{"locales", %{schema: Locale, conflict_target: [:iso639_code]}},
added lib/kanta/utils/param_parsers.ex
 
@@ -0,0 +1,29 @@
1
+ defmodule Kanta.Utils.ParamParsers do
2
+ @moduledoc false
3
+
4
+ def default_id_parser(id) do
5
+ case Integer.parse(id) do
6
+ {id, _} -> {:ok, id}
7
+ _ -> :error
8
+ end
9
+ end
10
+
11
+ def parse_page(page) do
12
+ case Integer.parse(page) do
13
+ {page, _} -> page
14
+ _ -> 1
15
+ end
16
+ end
17
+
18
+ def parse_id_filter(id) do
19
+ run_parse_function(Kanta.config().id_parse_function, id)
20
+ end
21
+
22
+ defp run_parse_function(parse_function, id) when is_function(parse_function, 1) do
23
+ parse_function.(id)
24
+ end
25
+
26
+ defp run_parse_function({module, parse_function, 1}, id) do
27
+ apply(module, parse_function, [id])
28
+ end
29
+ end
changed lib/kanta_web.ex
 
@@ -99,8 +99,9 @@ defmodule KantaWeb do
99
99
quote do
100
100
@endpoint Application.compile_env(:kanta, :endpoint)
101
101
102
- # Use all HTML functionality (forms, tags, etc)
103
- use Phoenix.HTML
102
+ import Phoenix.HTML
103
+ import Phoenix.HTML.Form
104
+ use PhoenixHTMLHelpers
104
105
105
106
# Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
106
107
import Phoenix.Component
 
@@ -110,6 +111,7 @@ defmodule KantaWeb do
110
111
111
112
import Kanta.Utils.ModuleUtils
112
113
114
+ alias KantaWeb.Components.Icons
113
115
alias KantaWeb.Router.Helpers, as: Routes
114
116
unquote(verified_routes())
115
117
end
 
@@ -121,6 +123,33 @@ defmodule KantaWeb do
121
123
endpoint: Application.compile_env(:kanta, :endpoint),
122
124
router: KantaWeb.Router,
123
125
statics: KantaWeb.static_paths()
126
+
127
+ def dashboard_path(%Phoenix.LiveView.Socket{} = socket),
128
+ do: socket.router.__kanta_dashboard_prefix__()
129
+
130
+ def dashboard_path(%Plug.Conn{} = conn),
131
+ do: conn.private.phoenix_router.__kanta_dashboard_prefix__()
132
+
133
+ def dashboard_path(%Phoenix.LiveView.Socket{} = socket, "/" <> path),
134
+ do: dashboard_path(socket, path)
135
+
136
+ def dashboard_path(%Phoenix.LiveView.Socket{} = socket, path) do
137
+ unverified_path(
138
+ socket,
139
+ Kanta.Router,
140
+ socket.router.__kanta_dashboard_prefix__() <> "/" <> path
141
+ )
142
+ end
143
+
144
+ def dashboard_path(%Plug.Conn{} = conn, "/" <> path), do: dashboard_path(conn, path)
145
+
146
+ def dashboard_path(%Plug.Conn{} = conn, path) do
147
+ unverified_path(
148
+ conn,
149
+ Kanta.Router,
150
+ conn.private.phoenix_router.__kanta_dashboard_prefix__() <> "/" <> path
151
+ )
152
+ end
124
153
end
125
154
end
changed lib/kanta_web/components/shared/icons.ex
 
@@ -230,4 +230,56 @@ defmodule KantaWeb.Components.Icons do
230
230
</svg>
231
231
"""
232
232
end
233
+
234
+ def computer(assigns) do
235
+ attrs = assigns_to_attributes(assigns)
236
+ assigns = assign(assigns, :attrs, attrs)
237
+
238
+ ~H"""
239
+ <svg {@attrs}
240
+ xmlns="https://www.w3.org/2000/svg"
241
+ width="24"
242
+ height="24"
243
+ viewBox="0 0 24 24"
244
+ fill="none"
245
+ stroke="currentColor"
246
+ stroke-width="2"
247
+ stroke-linecap="round"
248
+ stroke-linejoin="round"
249
+ >
250
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25"></path>
251
+ </svg>
252
+
253
+ """
254
+ end
255
+
256
+ def chevron_left(assigns) do
257
+ attrs = assigns_to_attributes(assigns)
258
+ assigns = assign(assigns, :attrs, attrs)
259
+
260
+ ~H"""
261
+ <svg {@attrs}
262
+ xmlns="https://www.w3.org/2000/svg"
263
+ viewBox="0 0 20 20"
264
+ fill="currentColor"
265
+ >
266
+ <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
267
+ </svg>
268
+ """
269
+ end
270
+
271
+ def chevron_right(assigns) do
272
+ attrs = assigns_to_attributes(assigns)
273
+ assigns = assign(assigns, :attrs, attrs)
274
+
275
+ ~H"""
276
+ <svg {@attrs}
277
+ xmlns="https://www.w3.org/2000/svg"
278
+ viewBox="0 0 20 20"
279
+ fill="currentColor"
280
+ >
281
+ <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
282
+ </svg>
283
+ """
284
+ end
233
285
end
changed lib/kanta_web/components/shared/select/select.ex
 
@@ -15,7 +15,8 @@ defmodule KantaWeb.Components.Shared.Select do
15
15
if is_nil(field) do
16
16
List.first(options)
17
17
else
18
- Enum.find(options, &(&1.value == value_to_integer(field.value))) || List.first(options)
18
+ Enum.find(options, &(parse_select_value(&1.value) == field.value)) ||
19
+ List.first(options)
19
20
end
20
21
else
21
22
assigns.selected_option
 
@@ -40,12 +41,7 @@ defmodule KantaWeb.Components.Shared.Select do
40
41
}
41
42
end
42
43
43
- defp value_to_integer(nil), do: nil
44
- defp value_to_integer(""), do: nil
45
-
46
- defp value_to_integer(value) do
47
- String.to_integer(value)
48
- rescue
49
- _ in ArgumentError -> nil
50
- end
44
+ defp parse_select_value(nil), do: nil
45
+ defp parse_select_value(""), do: nil
46
+ defp parse_select_value(value), do: to_string(value)
51
47
end
changed lib/kanta_web/components/shared/select/select.html.heex
 
@@ -1,7 +1,7 @@
1
1
<div
2
2
id={"#{@id}-wrapper"}
3
3
phx-hook="Select"
4
- x-data={"{ open: false, idx: -1, selectedIdx: #{Enum.find_index(@options, & &1.value == @field.value) || 0}, max: #{length(@options) - 1} }"}
4
+ x-data={"{ open: false, idx: -1, selectedIdx: #{Enum.find_index(@options, & parse_select_value(&1.value) == @field.value) || 0}, max: #{length(@options) - 1} }"}
5
5
x-init={"() => { $watch('selectedIdx', val => $dispatch('selected-change', { selectedIdx: val, id: '##{@id}' }) ) }"}
6
6
x-on:reset="open = false"
7
7
>
changed lib/kanta_web/components/shared/toggle/toggle.html.heex
 
@@ -2,7 +2,7 @@
2
2
id={"#{@id}-wrapper"}
3
3
class="flex items-center"
4
4
phx-hook="Toggle"
5
- x-data="{ on: false }"
5
+ x-data={"{ on: #{@default_value} }"}
6
6
x-init={"() => { $watch('on', val => $dispatch('toggle-change', { id: '##{@id}', state: val }) ) }"}
7
7
>
8
8
<%= hidden_input @form, @field.name %>
added lib/kanta_web/controllers/api/application_sources_controller.ex
 
@@ -0,0 +1,23 @@
1
+ defmodule KantaWeb.Api.ApplicationSourcesController do
2
+ @moduledoc false
3
+ use KantaWeb, :controller
4
+
5
+ alias Kanta.Translations
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(Translations.list_application_sources(page: page))
14
+ end
15
+
16
+ def update(conn, %{"entries" => entries}) do
17
+ DatabasePopulator.call("application_sources", entries)
18
+
19
+ conn
20
+ |> put_status(200)
21
+ |> json(%{status: "OK"})
22
+ end
23
+ end
changed lib/kanta_web/live/dashboard/dashboard_live/dashboard_live.html.heex
 
@@ -47,6 +47,9 @@
47
47
<%= if plugin_name |> Module.concat(DashboardComponent) |> module_exists?() do %>
48
48
<.live_component module={Module.concat(plugin_name, DashboardComponent)} id={plugin_name} />
49
49
<% end %>
50
+ <%= if plugin_name |> Module.concat(DashboardLive) |> module_exists?() do %>
51
+ <%= live_render(@socket, Module.concat(plugin_name, DashboardLive), id: plugin_name) %>
52
+ <% end %>
50
53
<% end %>
51
54
</div>
52
55
<% end %>
added lib/kanta_web/live/translations/application_source_form_live/application_source_form_live.ex
 
@@ -0,0 +1,85 @@
1
+ defmodule KantaWeb.Translations.ApplicationSourceFormLive do
2
+ use KantaWeb, :live_view
3
+
4
+ alias Kanta.Translations
5
+ alias Kanta.Translations.ApplicationSource
6
+
7
+ def mount(%{"id" => application_source_id}, _session, socket) do
8
+ socket =
9
+ case get_application_source(application_source_id) do
10
+ nil ->
11
+ redirect(socket, to: dashboard_path(socket, "/application_sources"))
12
+
13
+ application_source ->
14
+ form = Translations.change_application_source(application_source)
15
+
16
+ socket
17
+ |> assign(:form, to_form(form))
18
+ |> assign(:application_source, application_source)
19
+ end
20
+
21
+ {:ok, socket}
22
+ end
23
+
24
+ def mount(_params, _session, socket) do
25
+ form = Translations.change_application_source(%ApplicationSource{})
26
+ socket = assign(socket, :form, to_form(form))
27
+
28
+ {:ok, socket}
29
+ end
30
+
31
+ def handle_event("validate", %{"application_source" => attrs}, socket) do
32
+ {action, application_source} =
33
+ if Map.has_key?(socket.assigns, :application_source) do
34
+ {:update, socket.assigns.application_source}
35
+ else
36
+ {:insert, %ApplicationSource{}}
37
+ end
38
+
39
+ form =
40
+ application_source
41
+ |> Translations.change_application_source(attrs)
42
+ |> Map.put(:action, action)
43
+
44
+ socket = assign(socket, form: to_form(form))
45
+
46
+ {:noreply, socket}
47
+ end
48
+
49
+ def handle_event(
50
+ "submit",
51
+ %{"application_source" => attrs},
52
+ %{assigns: %{application_source: application_source}} = socket
53
+ ) do
54
+ socket =
55
+ case Translations.update_application_source(application_source, attrs) do
56
+ {:ok, _application_source} ->
57
+ push_redirect(socket, to: dashboard_path(socket, "/application_sources"))
58
+
59
+ {:error, changeset} ->
60
+ assign(socket, :form, to_form(changeset))
61
+ end
62
+
63
+ {:noreply, socket}
64
+ end
65
+
66
+ def handle_event("submit", %{"application_source" => attrs}, socket) do
67
+ socket =
68
+ case Translations.create_application_source(attrs) do
69
+ {:ok, _application_source} ->
70
+ push_redirect(socket, to: dashboard_path(socket, "/application_sources"))
71
+
72
+ {:error, changeset} ->
73
+ assign(socket, :form, to_form(changeset))
74
+ end
75
+
76
+ {:noreply, socket}
77
+ end
78
+
79
+ defp get_application_source(id) do
80
+ case Translations.get_application_source(filter: [id: id]) do
81
+ {:ok, application_source} -> application_source
82
+ {:error, _, _reason} -> nil
83
+ end
84
+ end
85
+ end
added lib/kanta_web/live/translations/application_source_form_live/application_source_form_live.html.heex
 
@@ -0,0 +1,71 @@
1
+ <div>
2
+ <div class="mb-6">
3
+ <div>
4
+ <div>
5
+ <nav class="sm:hidden" aria-label="Back">
6
+ <.link navigate={dashboard_path(@socket, "/application_sources")} class="flex items-center text-sm font-medium text-slate-400 hover:text-slate-200">
7
+ <Icons.chevron_left class="flex-shrink-0 -ml-1 mr-1 h-5 w-5 text-slate-500" aria-hidden="true" />
8
+ Back
9
+ </.link>
10
+ </nav>
11
+ <nav class="hidden sm:flex mb-2" aria-label="Breadcrumb">
12
+ <ol class="flex items-center space-x-4">
13
+ <li>
14
+ <div>
15
+ <.link patch={"/application_sources"} class="cursor-pointer text-sm font-medium text-slate-400 hover:text-slate-200">Application Sources</.link>
16
+ </div>
17
+ </li>
18
+ </ol>
19
+ </nav>
20
+ </div>
21
+ <div class="mt-2 md:flex md:items-center md:justify-between">
22
+ <div class="flex-1 min-w-0">
23
+ <h2 class="text-xl font-bold leading-7 text-slate-600 dark:text-content-light sm:text-2xl sm:truncate">
24
+ <%= if Map.has_key?(assigns, :application_source), do: "Updating application source", else: "Creating application source" %>
25
+ </h2>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ <.form for={@form} phx-change="validate" phx-submit="submit" class="bg-white dark:bg-stone-900 shadow rounded-md mt-4 px-4 py-4 space-y-4">
31
+ <div class="pb-5 border-b border-slate-200 sm:flex sm:items-center sm:justify-between">
32
+ <h3 class="text-lg leading-6 font-medium text-primary-dark dark:text-accent-dark">
33
+ Application Source
34
+ </h3>
35
+ </div>
36
+ <div class="grid grid-cols-2 gap-4 lg:gap-8">
37
+ <div class="col-span-2 space-y-4">
38
+ <div>
39
+ <label for={@form[:name].id} class="block text-sm font-bold text-slate-700 dark:text-content-light">Name</label>
40
+ <input type="text" name={@form[:name].name} id={@form[:name].id} value={@form[:name].value} class="bg-slate-100 dark:bg-stone-700 shadow-sm focus:ring-primary focus:dark:ring-accent-dark focus:border-primary focus:dark:border-accent-dark block w-full sm:text-sm border-slate-300 rounded-md" />
41
+ <p :for={{error, _} <- @form[:name].errors} class="mt-3 text-sm leading-6 text-rose-600">
42
+ <%= error %>
43
+ </p>
44
+ </div>
45
+ <div>
46
+ <label for={@form[:description].id} class="block text-sm font-bold text-slate-700 dark:text-content-light">Description</label>
47
+ <div class="mt-1">
48
+ <textarea type="text" name={@form[:description].name} id={@form[:description].id} class="bg-slate-100 dark:bg-stone-700 shadow-sm focus:ring-primary focus:dark:ring-accent-dark focus:border-primary focus:dark:border-accent-dark block w-full sm:text-sm border-slate-300 rounded-md"><%= @form[:description].value %></textarea>
49
+ </div>
50
+ <p :for={{error, _} <- @form[:description].errors} class="mt-3 text-sm leading-6 text-rose-600">
51
+ <%= error %>
52
+ </p>
53
+ </div>
54
+ <div>
55
+ <label for={@form[:color].id} class="block text-sm font-bold text-slate-700 dark:text-content-light">Color</label>
56
+ <div class="mt-1">
57
+ <input type="color" name={@form[:color].name} id={@form[:color].id} value={@form[:color].value} class="bg-white dark:bg-base-dark text-content-dark dark:text-content-light shadow-sm focus:ring-primary focus:dark:ring-accent-dark focus:border-primary focus:dark:border-accent-dark block w-full sm:text-sm border-slate-300 rounded-md" />
58
+ </div>
59
+ <p :for={{error, _} <- @form[:color].errors} class="mt-3 text-sm leading-6 text-rose-600">
60
+ <%= error %>
61
+ </p>
62
+ </div>
63
+ </div>
64
+ <div class="col-span-2">
65
+ <button type="submit" class="w-full flex items-center justify-center px-4 py-4 mt-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary dark:bg-accent-dark hover:bg-primary-dark hover:dark:bg-accent-light focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-800 focus:ring-primary focus:dark:ring-accent-dark">
66
+ Save
67
+ </button>
68
+ </div>
69
+ </div>
70
+ </.form>
71
+ </div>
added lib/kanta_web/live/translations/application_sources_live/application_sources_live.ex
 
@@ -0,0 +1,36 @@
1
+ defmodule KantaWeb.Translations.ApplicationSourcesLive do
2
+ use KantaWeb, :live_view
3
+
4
+ alias Kanta.Translations
5
+ alias KantaWeb.Translations.ApplicationSourcesTable
6
+
7
+ alias KantaWeb.Components.Shared.Pagination
8
+
9
+ def mount(_params, _session, socket) do
10
+ %{entries: application_sources, metadata: application_sources_metadata} =
11
+ Translations.list_application_sources()
12
+
13
+ socket =
14
+ socket
15
+ |> assign(:application_sources, application_sources)
16
+ |> assign(:application_sources_metadata, application_sources_metadata)
17
+
18
+ {:ok, socket}
19
+ end
20
+
21
+ def handle_event("navigate", %{"to" => to}, socket) do
22
+ {:noreply, push_redirect(socket, to: "/kanta" <> to)}
23
+ end
24
+
25
+ def handle_event("page_changed", %{"index" => page_number}, socket) do
26
+ %{entries: application_sources, metadata: application_sources_metadata} =
27
+ Translations.list_application_sources(page: String.to_integer(page_number))
28
+
29
+ socket =
30
+ socket
31
+ |> assign(:application_sources, application_sources)
32
+ |> assign(:application_sources_metadata, application_sources_metadata)
33
+
34
+ {:noreply, socket}
35
+ end
36
+ end
added lib/kanta_web/live/translations/application_sources_live/application_sources_live.html.heex
 
@@ -0,0 +1,18 @@
1
+ <div>
2
+ <div class="mt-2 md:flex md:items-center md:justify-between">
3
+ <div class="flex-1 min-w-0">
4
+ <h2 class="text-2xl font-bold leading-7 text-primary-dark dark:text-accent-dark sm:text-3xl sm:truncate">
5
+ Application Sources
6
+ </h2>
7
+ </div>
8
+ </div>
9
+ </div>
10
+ <div class="mt-4">
11
+ <div class="flex gap-2 justify-end">
12
+ <.link navigate={dashboard_path(@socket, "/application_sources/new")} class="font-semibold text-primary-dark dark:text-accent-dark">
13
+ Create application source
14
+ </.link>
15
+ </div>
16
+ <.live_component module={ApplicationSourcesTable} id="application_sources-table" application_sources={@application_sources} />
17
+ <Pagination.render metadata={@application_sources_metadata} on_page_change="page_changed" />
18
+ </div>
added lib/kanta_web/live/translations/application_sources_live/components/application_sources_table/application_sources_table.ex
 
@@ -0,0 +1,15 @@
1
+ defmodule KantaWeb.Translations.ApplicationSourcesTable do
2
+ @moduledoc """
3
+ Application sources table component
4
+ """
5
+
6
+ use KantaWeb, :live_component
7
+
8
+ def update(socket, assigns) do
9
+ {:ok, assign(assigns, socket)}
10
+ end
11
+
12
+ def handle_event("edit_application_source", %{"id" => id}, socket) do
13
+ {:noreply, push_navigate(socket, to: dashboard_path(socket, "/application_sources/#{id}"))}
14
+ end
15
+ end
added lib/kanta_web/live/translations/application_sources_live/components/application_sources_table/application_sources_table.html.heex
 
@@ -0,0 +1,48 @@
1
+ <div class="bg-base-light dark:bg-base-dark py-6">
2
+ <div class="w-full">
3
+ <div class="flex flex-col">
4
+ <div class="-my-2 overflow-x-auto">
5
+ <div class="py-2 align-middle inline-block min-w-full">
6
+ <div class="shadow overflow-hidden border-b border-slate-200 sm:rounded-lg">
7
+ <table class="min-w-full w-full divide-y divide-slate-200">
8
+ <thead class="bg-slate-50 dark:bg-stone-900">
9
+ <tr>
10
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-content-light uppercase tracking-wider">
11
+ Name
12
+ </th>
13
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-content-light uppercase tracking-wider">
14
+ Description
15
+ </th>
16
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-content-light uppercase tracking-wider">
17
+ Color
18
+ </th>
19
+ </tr>
20
+ </thead>
21
+ <tbody class="bg-white dark:bg-stone-800 divide-y divide-slate-200">
22
+ <%= for application_source <- @application_sources do %>
23
+ <tr class="cursor-pointer hover:bg-slate-50 hover:dark:bg-base-dark transition-all" phx-click="edit_application_source" phx-value-id={application_source.id} phx-target={@myself}>
24
+ <td class="flex px-6 py-4">
25
+ <div class="text-sm font-medium text-primary-dark dark:text-accent-dark truncate"><%= String.slice(application_source.name, 0..30) %></div>
26
+ </td>
27
+ <td>
28
+ <div class="text-sm font-medium text-primary-dark dark:text-accent-dark truncate">
29
+ <%= application_source.description %>
30
+ </div>
31
+ </td>
32
+ <td>
33
+ <div class="text-sm font-medium text-primary-dark dark:text-accent-dark truncate">
34
+ <span class="inline-flex items-center px-2.5 py-1 rounded-full uppercase text-xs border border-primary text-white font-bold" style={"background-color:#{application_source.color};"}>
35
+ <%= application_source.color %>
36
+ </span>
37
+ </div>
38
+ </td>
39
+ </tr>
40
+ <% end %>
41
+ </tbody>
42
+ </table>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </div>
\ No newline at end of file
changed lib/kanta_web/live/translations/context_live/context_live.ex
 
@@ -1,18 +1,25 @@
1
1
defmodule KantaWeb.Translations.ContextLive do
2
2
use KantaWeb, :live_view
3
3
4
+ import Kanta.Utils.ParamParsers, only: [parse_id_filter: 1]
5
+
4
6
alias Kanta.Translations
5
7
alias Kanta.Translations.Context
6
8
7
9
def mount(%{"id" => id}, _session, socket) do
8
- context =
9
- case Translations.get_context(filter: [id: id]) do
10
- {:ok, %Context{} = context} -> context
11
- {:error, _, _reason} -> nil
10
+ socket =
11
+ case get_context(id) do
12
+ {:ok, %Context{} = context} -> assign(socket, :context, context)
13
+ {:error, _, _reason} -> redirect(socket, to: "/kanta/contexts")
12
14
end
13
15
14
- socket = socket |> assign(:context, context)
15
-
16
16
{:ok, socket}
17
17
end
18
+
19
+ defp get_context(id) do
20
+ case parse_id_filter(id) do
21
+ {:ok, id} -> Translations.get_context(filter: [id: id])
22
+ _ -> {:error, :id, :invalid}
23
+ end
24
+ end
18
25
end
changed lib/kanta_web/live/translations/context_live/context_live.html.heex
 
@@ -1,11 +1,8 @@
1
1
<div>
2
2
<div>
3
3
<nav class="sm:hidden" aria-label="Back">
4
- <.link navigate={unverified_path(@socket, Kanta.Router, "/kanta/contexts")} class="flex items-center text-sm font-medium text-slate-500 hover:text-slate-700">
5
- <!-- Heroicon name: solid/chevron-left -->
6
- <svg class="flex-shrink-0 -ml-1 mr-1 h-5 w-5 text-slate-400" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
7
- <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
8
- </svg>
4
+ <.link navigate={dashboard_path(@socket, "/contexts")} class="flex items-center text-sm font-medium text-slate-500 hover:text-slate-700">
5
+ <Icons.chevron_left class="flex-shrink-0 -ml-1 mr-1 h-5 w-5 text-slate-400" aria-hidden="true" />
9
6
Back
10
7
</.link>
11
8
</nav>
 
@@ -13,15 +10,12 @@
13
10
<ol class="flex items-center space-x-4">
14
11
<li>
15
12
<div>
16
- <.link navigate={unverified_path(@socket, Kanta.Router, "/kanta/contexts")} class="text-sm font-medium text-slate-500 hover:text-slate-700">Contexts</.link>
13
+ <.link navigate={dashboard_path(@socket, "/contexts")} class="text-sm font-medium text-slate-500 hover:text-slate-700">Contexts</.link>
17
14
</div>
18
15
</li>
19
16
<li>
20
17
<div class="flex items-center">
21
- <!-- Heroicon name: solid/chevron-right -->
22
- <svg class="flex-shrink-0 h-5 w-5 text-slate-400" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
23
- <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
24
- </svg>
18
+ <Icons.chevron_right class="flex-shrink-0 h-5 w-5 text-slate-400" aria-hidden="true" />
25
19
<.link navigate="#" aria-current="page" class="ml-4 text-sm font-medium text-slate-500 hover:text-slate-700"><%= @context.name %></.link>
26
20
</div>
27
21
</li>
 
@@ -35,4 +29,4 @@
35
29
</h2>
36
30
</div>
37
31
</div>
38
- </div>
\ No newline at end of file
32
+ </div>
changed lib/kanta_web/live/translations/contexts_live/components/contexts_table/contexts_table.ex
 
@@ -10,14 +10,6 @@ defmodule KantaWeb.Translations.ContextsTable do
10
10
end
11
11
12
12
def handle_event("edit_context", %{"id" => id}, socket) do
13
- {:noreply,
14
- push_navigate(socket,
15
- to:
16
- unverified_path(
17
- socket,
18
- Kanta.Router,
19
- "/kanta/contexts/#{id}"
20
- )
21
- )}
13
+ {:noreply, push_navigate(socket, to: dashboard_path(socket, "/contexts/#{id}"))}
22
14
end
23
15
end
changed lib/kanta_web/live/translations/contexts_live/contexts_live.ex
 
@@ -18,7 +18,7 @@ defmodule KantaWeb.Translations.ContextsLive do
18
18
end
19
19
20
20
def handle_event("navigate", %{"to" => to}, socket) do
21
- {:noreply, push_redirect(socket, to: "/kanta" <> to)}
21
+ {:noreply, push_redirect(socket, to: dashboard_path(socket) <> to)}
22
22
end
23
23
24
24
def handle_event("page_changed", %{"index" => page_number}, socket) do
changed lib/kanta_web/live/translations/domain_live/domain_live.ex
 
@@ -1,18 +1,25 @@
1
1
defmodule KantaWeb.Translations.DomainLive do
2
2
use KantaWeb, :live_view
3
3
4
+ import Kanta.Utils.ParamParsers, only: [parse_id_filter: 1]
5
+
4
6
alias Kanta.Translations
5
7
alias Kanta.Translations.Domain
6
8
7
9
def mount(%{"id" => id}, _session, socket) do
8
- domain =
9
- case Translations.get_domain(filter: [id: id]) do
10
- {:ok, %Domain{} = domain} -> domain
11
- {:error, _, _reason} -> nil
10
+ socket =
11
+ case get_domain(id) do
12
+ {:ok, %Domain{} = domain} -> assign(socket, :domain, domain)
13
+ {:error, _, _reason} -> redirect(socket, to: "/kanta/domains")
12
14
end
13
15
14
- socket = socket |> assign(:domain, domain)
15
-
16
16
{:ok, socket}
17
17
end
18
+
19
+ defp get_domain(id) do
20
+ case parse_id_filter(id) do
21
+ {:ok, id} -> Translations.get_domain(filter: [id: id])
22
+ _ -> {:error, :id, :invalid}
23
+ end
24
+ end
18
25
end
changed lib/kanta_web/live/translations/domain_live/domain_live.html.heex
 
@@ -1,11 +1,8 @@
1
1
<div>
2
2
<div>
3
3
<nav class="sm:hidden" aria-label="Back">
4
- <.link navigate={unverified_path(@socket, Kanta.Router, "/kanta/domains")} class="flex items-center text-sm font-medium text-slate-500 hover:text-slate-700">
5
- <!-- Heroicon name: solid/chevron-left -->
6
- <svg class="flex-shrink-0 -ml-1 mr-1 h-5 w-5 text-slate-400" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
7
- <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
8
- </svg>
4
+ <.link navigate={dashboard_path(@socket, "/domains")} class="flex items-center text-sm font-medium text-slate-500 hover:text-slate-700">
5
+ <Icons.chevron_left class="flex-shrink-0 -ml-1 mr-1 h-5 w-5 text-slate-400" aria-hidden="true" />
9
6
Back
10
7
</.link>
11
8
</nav>
 
@@ -13,15 +10,12 @@
13
10
<ol class="flex items-center space-x-4">
14
11
<li>
15
12
<div>
16
- <.link navigate={unverified_path(@socket, Kanta.Router, "/kanta/domains")} class="text-sm font-medium text-slate-500 hover:text-slate-700">Domains</.link>
13
+ <.link navigate={dashboard_path(@socket, "/domains")} class="text-sm font-medium text-slate-500 hover:text-slate-700">Domains</.link>
17
14
</div>
18
15
</li>
19
16
<li>
20
17
<div class="flex items-center">
21
- <!-- Heroicon name: solid/chevron-right -->
22
- <svg class="flex-shrink-0 h-5 w-5 text-slate-400" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
23
- <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
24
- </svg>
18
+ <Icons.chevron_right class="flex-shrink-0 h-5 w-5 text-slate-400" aria-hidden="true" />
25
19
<.link navigate="#" aria-current="page" class="ml-4 text-sm font-medium text-slate-500 hover:text-slate-700"><%= @domain.name %></.link>
26
20
</div>
27
21
</li>
 
@@ -35,4 +29,4 @@
35
29
</h2>
36
30
</div>
37
31
</div>
38
- </div>
\ No newline at end of file
32
+ </div>
changed lib/kanta_web/live/translations/domains_live/components/domains_table/domains_table.ex
 
@@ -10,14 +10,6 @@ defmodule KantaWeb.Translations.DomainsTable do
10
10
end
11
11
12
12
def handle_event("edit_domain", %{"id" => id}, socket) do
13
- {:noreply,
14
- push_navigate(socket,
15
- to:
16
- unverified_path(
17
- socket,
18
- Kanta.Router,
19
- "/kanta/domains/#{id}"
20
- )
21
- )}
13
+ {:noreply, push_navigate(socket, to: dashboard_path(socket, "/domains/#{id}"))}
22
14
end
23
15
end
changed lib/kanta_web/live/translations/domains_live/domains_live.ex
 
@@ -18,7 +18,7 @@ defmodule KantaWeb.Translations.DomainsLive do
18
18
end
19
19
20
20
def handle_event("navigate", %{"to" => to}, socket) do
21
- {:noreply, push_redirect(socket, to: "/kanta" <> to)}
21
+ {:noreply, push_redirect(socket, to: dashboard_path(socket) <> to)}
22
22
end
23
23
24
24
def handle_event("page_changed", %{"index" => page_number}, socket) do
changed lib/kanta_web/live/translations/locales_live/locales_live.html.heex
 
@@ -10,7 +10,7 @@
10
10
<div class="col-span-1 my-1 w-full">
11
11
<div>
12
12
<div class="w-full">
13
- <.link navigate={unverified_path(@socket, Kanta.Router, "/kanta/locales/#{locale.id}/translations")}>
13
+ <.link navigate={dashboard_path(@socket, "/locales/#{locale.id}/translations")}>
14
14
<div class="w-full bg-white dark:bg-stone-900 dark:border dark:border-accent-dark transition-all hover:bg-slate-100 overflow-hidden shadow rounded-lg items-center cursor-pointer">
15
15
<div class="flex flex-row justify-start px-4 py-5 sm:p-6 font-medium text-md text-slate-700">
16
16
<div class="relative w-12 h-12">
changed lib/kanta_web/live/translations/translation_form_live/components/plural_translation_form/plural_translation_form.ex
 
@@ -65,11 +65,7 @@ defmodule KantaWeb.Translations.PluralTranslationForm do
65
65
{:noreply,
66
66
push_redirect(socket,
67
67
to:
68
- unverified_path(
69
- socket,
70
- Kanta.Router,
71
- "/kanta/locales/#{locale.id}/translations"
72
- )
68
+ dashboard_path(socket, "/locales/#{locale.id}/translations" <> get_query(socket.assigns))
73
69
)}
74
70
end
75
71
 
@@ -80,4 +76,11 @@ defmodule KantaWeb.Translations.PluralTranslationForm do
80
76
|> Map.fetch!(index)
81
77
|> Enum.join(", ")
82
78
end
79
+
80
+ defp get_query(%{filters: nil}), do: ""
81
+
82
+ defp get_query(%{filters: filters}) do
83
+ query = UriQuery.params(filters)
84
+ "?" <> URI.encode_query(query)
85
+ end
83
86
end
changed lib/kanta_web/live/translations/translation_form_live/components/plural_translation_form/plural_translation_form.html.heex
 
@@ -3,10 +3,8 @@
3
3
<div>
4
4
<div>
5
5
<nav class="sm:hidden" aria-label="Back">
6
- <.link navigate={unverified_path(@socket, Kanta.Router, "/kanta/locales/#{@locale.id}/translations")} class="flex items-center text-sm font-medium text-slate-400 hover:text-slate-200">
7
- <svg class="flex-shrink-0 -ml-1 mr-1 h-5 w-5 text-slate-500" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
8
- <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"></path>
9
- </svg>
6
+ <.link navigate={dashboard_path(@socket, "/locales/#{@locale.id}/translations")} class="flex items-center text-sm font-medium text-slate-400 hover:text-slate-200">
7
+ <Icons.chevron_left class="flex-shrink-0 -ml-1 mr-1 h-5 w-5 text-slate-500" aria-hidden="true" />
10
8
Back
11
9
</.link>
12
10
</nav>
 
@@ -14,22 +12,18 @@
14
12
<ol class="flex items-center space-x-4">
15
13
<li>
16
14
<div>
17
- <.link patch={"/kanta/locales"} class="cursor-pointer text-sm font-medium text-slate-400 hover:text-slate-200">Locales</.link>
15
+ <.link patch={"#{dashboard_path(@socket)}/locales"} class="cursor-pointer text-sm font-medium text-slate-400 hover:text-slate-200">Locales</.link>
18
16
</div>
19
17
</li>
20
18
<li>
21
19
<div class="flex items-center">
22
- <svg class="flex-shrink-0 h-5 w-5 text-slate-500" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
23
- <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
24
- </svg>
25
- <.link patch={"/kanta/locales/#{@locale.id}/translations"} class="cursor-pointer ml-4 text-sm font-medium text-slate-400 hover:text-slate-200"><%= @locale.native_name %></.link>
20
+ <Icons.chevron_right class="flex-shrink-0 h-5 w-5 text-slate-500" aria-hidden="true" />
21
+ <.link patch={"#{dashboard_path(@socket)}/locales/#{@locale.id}/translations"} class="cursor-pointer ml-4 text-sm font-medium text-slate-400 hover:text-slate-200"><%= @locale.native_name %></.link>
26
22
</div>
27
23
</li>
28
24
<li>
29
25
<div class="flex items-center">
30
- <svg class="flex-shrink-0 h-5 w-5 text-slate-500" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
31
- <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
32
- </svg>
26
+ <Icons.chevron_right class="flex-shrink-0 h-5 w-5 text-slate-500" aria-hidden="true" />
33
27
<a href="#" aria-current="page" class="cursor-pointer ml-4 text-sm font-medium text-slate-400 hover:text-slate-200 truncate"><%= @message.msgid %></a>
34
28
</div>
35
29
</li>
 
@@ -45,7 +39,7 @@
45
39
</div>
46
40
</div>
47
41
</div>
48
- <.live_component module={Tabs} id="tabs" tabs={@tabs} current_url={"/kanta/locales/#{@locale.id}/translations/#{@message.id}"} current_tab={@current_tab} />
42
+ <.live_component module={Tabs} id="tabs" tabs={@tabs} current_url={"#{dashboard_path(@socket)}/locales/#{@locale.id}/translations/#{@message.id}"} current_tab={@current_tab} />
49
43
<.form for={@form} phx-change="validate" phx-submit="submit" phx-target={@myself} class="bg-white dark:bg-stone-900 shadow rounded-md mt-4 px-4 py-4 space-y-4">
50
44
<div class="pb-5 border-b border-slate-200 sm:flex sm:items-center sm:justify-between">
51
45
<h3 class="text-lg leading-6 font-medium text-primary-dark dark:text-accent-dark">
changed lib/kanta_web/live/translations/translation_form_live/components/singular_translation_form/singular_translation_form.ex
 
@@ -31,11 +31,14 @@ defmodule KantaWeb.Translations.SingularTranslationForm do
31
31
{:noreply,
32
32
push_redirect(socket,
33
33
to:
34
- unverified_path(
35
- socket,
36
- Kanta.Router,
37
- "/kanta/locales/#{locale.id}/translations"
38
- )
34
+ dashboard_path(socket, "/locales/#{locale.id}/translations" <> get_query(socket.assigns))
39
35
)}
40
36
end
37
+
38
+ defp get_query(%{filters: nil}), do: ""
39
+
40
+ defp get_query(%{filters: filters}) do
41
+ query = UriQuery.params(filters)
42
+ "?" <> URI.encode_query(query)
43
+ end
41
44
end
changed lib/kanta_web/live/translations/translation_form_live/components/singular_translation_form/singular_translation_form.html.heex
 
@@ -3,10 +3,8 @@
3
3
<div>
4
4
<div>
5
5
<nav class="sm:hidden" aria-label="Back">
6
- <.link navigate={unverified_path(@socket, Kanta.Router, "/kanta/locales/#{@locale.id}/translations")} class="flex items-center text-sm font-medium text-slate-400 hover:text-slate-200">
7
- <svg class="flex-shrink-0 -ml-1 mr-1 h-5 w-5 text-slate-500" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
8
- <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"></path>
9
- </svg>
6
+ <.link navigate={dashboard_path(@socket, "/locales/#{@locale.id}/translations")} class="flex items-center text-sm font-medium text-slate-400 hover:text-slate-200">
7
+ <Icons.chevron_left class="flex-shrink-0 -ml-1 mr-1 h-5 w-5 text-slate-500" aria-hidden="true" />
10
8
Back
11
9
</.link>
12
10
</nav>
 
@@ -14,22 +12,18 @@
14
12
<ol class="flex items-center space-x-4">
15
13
<li>
16
14
<div>
17
- <.link patch={"/kanta/locales"} class="cursor-pointer text-sm font-medium text-slate-400 hover:text-slate-200">Locales</.link>
15
+ <.link patch={"#{dashboard_path(@socket)}/locales"} class="cursor-pointer text-sm font-medium text-slate-400 hover:text-slate-200">Locales</.link>
18
16
</div>
19
17
</li>
20
18
<li>
21
19
<div class="flex items-center">
22
- <svg class="flex-shrink-0 h-5 w-5 text-slate-500" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
23
- <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
24
- </svg>
25
- <.link patch={"/kanta/locales/#{@locale.id}/translations"} class="cursor-pointer ml-4 text-sm font-medium text-slate-400 hover:text-slate-200"><%= @locale.native_name %></.link>
20
+ <Icons.chevron_right class="flex-shrink-0 h-5 w-5 text-slate-500" aria-hidden="true" />
21
+ <.link patch={"#{dashboard_path(@socket)}/locales/#{@locale.id}/translations"} class="cursor-pointer ml-4 text-sm font-medium text-slate-400 hover:text-slate-200"><%= @locale.native_name %></.link>
26
22
</div>
27
23
</li>
28
24
<li>
29
25
<div class="flex items-center">
30
- <svg class="flex-shrink-0 h-5 w-5 text-slate-500" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
31
- <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
32
- </svg>
26
+ <Icons.chevron_right class="flex-shrink-0 h-5 w-5 text-slate-500" aria-hidden="true" />
33
27
<a href="#" aria-current="page" class="cursor-pointer ml-4 text-sm font-medium text-slate-400 hover:text-slate-200"><%= String.slice(@message.msgid, 0..30) %></a>
34
28
</div>
35
29
</li>
changed lib/kanta_web/live/translations/translation_form_live/translation_form_live.ex
 
@@ -1,6 +1,8 @@
1
1
defmodule KantaWeb.Translations.TranslationFormLive do
2
2
use KantaWeb, :live_view
3
3
4
+ import Kanta.Utils.ParamParsers, only: [parse_id_filter: 1]
5
+
4
6
alias Kanta.Translations
5
7
alias Kanta.Translations.Message
6
8
 
@@ -14,6 +16,7 @@ defmodule KantaWeb.Translations.TranslationFormLive do
14
16
translation={@translations}
15
17
message={@message}
16
18
locale={@locale}
19
+ filters={@filters}
17
20
/>
18
21
"""
19
22
end
 
@@ -35,22 +38,25 @@ defmodule KantaWeb.Translations.TranslationFormLive do
35
38
locale={@locale}
36
39
current_tab={@tab}
37
40
current_tab_index={String.to_integer(@tab) - 1}
41
+ filters={@filters}
38
42
/>
39
43
"""
40
44
end
41
45
42
- def mount(%{"message_id" => message_id, "locale_id" => locale_id}, _session, socket) do
46
+ def mount(%{"message_id" => message_id, "locale_id" => locale_id} = params, _session, socket) do
43
47
socket =
44
- with {:ok, locale} <- Translations.get_locale(filter: [id: locale_id]),
45
- {:ok, message} <- Translations.get_message(filter: [id: message_id]),
48
+ with {:ok, locale} <- get_locale(locale_id),
49
+ {:ok, message} <- get_message(message_id),
46
50
{:ok, translations} <- get_translations(message, locale) do
47
51
socket
48
52
|> assign(:locale, locale)
49
53
|> assign(:message, message)
50
54
|> assign(:translations, translations)
55
+ else
56
+ _ -> redirect(socket, to: "/kanta/locales/#{locale_id}/translations")
51
57
end
52
58
53
- {:ok, socket}
59
+ {:ok, assign(socket, :filters, Map.get(params, "filters"))}
54
60
end
55
61
56
62
def handle_params(%{"tab" => tab}, _uri, socket) do
 
@@ -116,4 +122,18 @@ defmodule KantaWeb.Translations.TranslationFormLive do
116
122
end
117
123
end
118
124
end
125
+
126
+ defp get_locale(locale_id) do
127
+ case parse_id_filter(locale_id) do
128
+ {:ok, id} -> Translations.get_locale(filter: [id: id])
129
+ _ -> {:error, :id, :invalid}
130
+ end
131
+ end
132
+
133
+ defp get_message(message_id) do
134
+ case parse_id_filter(message_id) do
135
+ {:ok, id} -> Translations.get_message(filter: [id: id])
136
+ _ -> {:error, :id, :invalid}
137
+ end
138
+ end
119
139
end
changed lib/kanta_web/live/translations/translations_live/components/filters_bar/filters_bar.ex
 
@@ -13,13 +13,25 @@ defmodule KantaWeb.Translations.Components.FiltersBar do
13
13
%{entries: contexts, metadata: _contexts_metadata} = Translations.list_contexts()
14
14
%{entries: domains, metadata: _domains_metadata} = Translations.list_domains()
15
15
16
+ %{entries: application_sources, metadata: _application_sources_metadata} =
17
+ Translations.list_application_sources()
18
+
19
+ {col_span, grid_cols} =
20
+ if assigns.application_sources_empty?,
21
+ do: {"col-span-5", "grid-cols-5"},
22
+ else: {"col-span-6", "grid-cols-6"}
23
+
16
24
socket =
17
25
socket
18
26
|> assign(:contexts, contexts)
19
27
|> assign(:domains, domains)
28
+ |> assign(:application_sources, application_sources)
29
+ |> assign(:col_span, col_span)
30
+ |> assign(:grid_cols, grid_cols)
20
31
|> assign(:filters, %{
21
32
domain: nil,
22
- context: nil
33
+ context: nil,
34
+ application_source: nil
23
35
})
24
36
25
37
{:ok, assign(socket, assigns)}
changed lib/kanta_web/live/translations/translations_live/components/filters_bar/filters_bar.html.heex
 
@@ -1,6 +1,7 @@
1
- <div >
2
- <.form :let={form} for={@filters} phx-change="change" class="grid grid-cols-5 gap-2">
3
- <div class="col-span-5 sm:col-span-5 xl:col-span-2">
1
+ <div>
2
+ <.form :let={form} for={@filters} phx-change="change" class={"grid #{@grid_cols} gap-2"}>
3
+ <.link navigate={dashboard_path(@socket, "/locales/#{@locale_id}/translations")} class="absolute -top-1 right-0 font-semibold text-primary-dark dark:text-accent-dark">Clear filters</.link>
4
+ <div class={"#{@col_span} sm:#{@col_span} xl:col-span-2"}>
4
5
<SearchInput.render
5
6
type="text"
6
7
id={form["search"].id}
 
@@ -10,7 +11,7 @@
10
11
label="Search"
11
12
/>
12
13
</div>
13
- <div class="col-span-5 sm:col-span-2 xl:col-span-1">
14
+ <div class={"#{@col_span} sm:col-span-2 xl:col-span-1"}>
14
15
<.live_component
15
16
form={form}
16
17
module={Select}
 
@@ -20,7 +21,7 @@
20
21
options={[%{color: "#c3c3c3", label: "All", value: nil}] ++ Enum.map(@domains, & %{color: &1.color, label: &1.name, value: &1.id})}
21
22
/>
22
23
</div>
23
- <div class="col-span-5 sm:col-span-2 xl:col-span-1">
24
+ <div class={"#{@col_span} sm:col-span-2 xl:col-span-1"}>
24
25
<.live_component
25
26
form={form}
26
27
module={Select}
 
@@ -30,14 +31,27 @@
30
31
options={[%{color: "#c3c3c3", label: "All", value: nil}] ++ Enum.map(@contexts, & %{color: &1.color, label: &1.name, value: &1.id})}
31
32
/>
32
33
</div>
33
- <div class="col-span-5 sm:col-span-1 xl:col-span-1 mt-4 xl:mt-7">
34
+ <%= if not Enum.empty?(@application_sources) do %>
35
+ <div class={"#{@col_span} sm:col-span-2 xl:col-span-1"}>
36
+ <.live_component
37
+ form={form}
38
+ module={Select}
39
+ id={"application_source_id"}
40
+ label="Application"
41
+ field={form["application_source_id"]}
42
+ options={[%{color: "#c3c3c3", label: "All", value: nil}] ++ Enum.map(@application_sources, & %{color: &1.color, label: &1.name, value: &1.id})}
43
+ />
44
+ </div>
45
+ <% end %>
46
+ <div class={"#{@col_span} sm:col-span-1 xl:col-span-1 mt-4 xl:mt-7"}>
34
47
<.live_component
35
48
form={form}
36
49
module={Toggle}
37
50
id={"not_translated"}
51
+ default_value={@not_translated_default}
38
52
label="Not translated"
39
53
field={form["not_translated"]}
40
54
/>
41
55
</div>
42
56
</.form>
43
- </div>
\ No newline at end of file
57
+ </div>
changed lib/kanta_web/live/translations/translations_live/components/messages_table/messages_table.ex
 
@@ -7,23 +7,29 @@ defmodule KantaWeb.Translations.Components.MessagesTable do
7
7
8
8
alias Kanta.Translations.{Message, SingularTranslation}
9
9
10
+ @available_params ~w(page search filter)
11
+ @params_in_filter ~w(domain_id context_id not_translated)
12
+
10
13
def update(socket, assigns) do
11
14
{:ok, assign(assigns, socket)}
12
15
end
13
16
14
17
def handle_event("edit_message", %{"id" => id}, socket) do
18
+ params = get_filter_params_from_assigns(socket.assigns)
19
+ query = UriQuery.params(params)
20
+
15
21
{:noreply,
16
22
push_navigate(socket,
17
23
to:
18
- unverified_path(
24
+ dashboard_path(
19
25
socket,
20
- Kanta.Router,
21
- "/kanta/locales/#{socket.assigns.locale.id}/translations/#{id}"
26
+ "/locales/#{socket.assigns.locale.id}/translations/#{id}?" <>
27
+ URI.encode_query(query)
22
28
)
23
29
)}
24
30
end
25
31
26
- def is_translated(%Message{message_type: :singular} = message, locale, source) do
32
+ def translated?(%Message{message_type: :singular} = message, locale, source) do
27
33
case Enum.find(message.singular_translations, &(&1.locale_id == locale.id)) do
28
34
nil ->
29
35
false
 
@@ -37,18 +43,18 @@ defmodule KantaWeb.Translations.Components.MessagesTable do
37
43
end
38
44
end
39
45
40
- def is_translated(%Message{message_type: :plural} = message, locale, source) do
46
+ def translated?(%Message{message_type: :plural} = message, locale, source) do
41
47
case Enum.filter(message.plural_translations, &(&1.locale_id == locale.id)) do
42
48
[] ->
43
49
false
44
50
45
51
translations ->
46
52
translations
47
- |> Enum.map(&is_plural_form_translated(&1, source))
53
+ |> Enum.map(&plural_form_translated?(&1, source))
48
54
end
49
55
end
50
56
51
- defp is_plural_form_translated(translation, source) do
57
+ defp plural_form_translated?(translation, source) do
52
58
case get_in(translation, [Access.key!(source)]) do
53
59
nil ->
54
60
false
 
@@ -135,4 +141,19 @@ defmodule KantaWeb.Translations.Components.MessagesTable do
135
141
defp truncate_translation(text) do
136
142
if String.length(text) > 45, do: String.slice(text, 0..45) <> "... ", else: text
137
143
end
144
+
145
+ defp get_filter_params_from_assigns(%{filters: filters}) do
146
+ filter =
147
+ filters
148
+ |> Map.take(@params_in_filter)
149
+ |> Map.reject(fn {_, value} -> is_nil(value) or value == "" end)
150
+
151
+ params =
152
+ filters
153
+ |> Map.take(@available_params)
154
+ |> Map.put("filter", filter)
155
+ |> Map.reject(fn {_, value} -> is_nil(value) or value == "" end)
156
+
157
+ %{"filters" => params}
158
+ end
138
159
end
changed lib/kanta_web/live/translations/translations_live/components/messages_table/messages_table.html.heex
 
@@ -22,6 +22,11 @@
22
22
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-content-light uppercase tracking-wider">
23
23
Context
24
24
</th>
25
+ <%= if not @application_sources_empty? do %>
26
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-content-light uppercase tracking-wider">
27
+ Application
28
+ </th>
29
+ <% end %>
25
30
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-content-light uppercase tracking-wider">
26
31
Type
27
32
</th>
 
@@ -36,7 +41,7 @@
36
41
<td class="px-6 py-4">
37
42
<div class={[
38
43
"text-md font-medium tracking-wide",
39
- "#{if is_translated(message, @locale, :original_text), do: "text-green-700 dark:text-green-500", else: "text-red-700 dark:text-red-500"}"
44
+ "#{if translated?(message, @locale, :original_text), do: "text-green-700 dark:text-green-500", else: "text-red-700 dark:text-red-500"}"
40
45
]}>
41
46
<%= original_text(assigns, message) %>
42
47
</div>
 
@@ -44,14 +49,14 @@
44
49
<td class="px-6 py-4">
45
50
<div class={[
46
51
"text-md font-medium tracking-wide",
47
- "#{if is_translated(message, @locale, :translated_text), do: "text-green-700 dark:text-green-500", else: "text-red-700 dark:text-red-500"}"
52
+ "#{if translated?(message, @locale, :translated_text), do: "text-green-700 dark:text-green-500", else: "text-red-700 dark:text-red-500"}"
48
53
]}>
49
54
<%= translated_text(assigns, message) %>
50
55
</div>
51
56
</td>
52
57
<td class="px-6 py-4">
53
58
<div class="text-sm font-medium text-primary-dark truncate">
54
- <span class={"inline-flex items-center px-2.5 py-1 rounded-full uppercase text-xs border border-primary dark:border-accent-dark #{if is_nil(message.domain), do: "text-primary-dark dark:text-accent-dark", else: "text-white"} font-bold"} style={"background-color:#{message.domain.color};"}>
59
+ <span class={"inline-flex items-center px-2.5 py-1 rounded-full uppercase text-xs border border-primary dark:border-accent-dark #{if is_nil(message.domain), do: "text-primary-dark dark:text-accent-dark", else: "text-white"} font-bold"} style={"background-color:#{unless is_nil(message.domain), do: message.domain.color, else: "transparent"};"}>
55
60
<%= if is_nil(message.domain), do: "None", else: message.domain.name %>
56
61
</span>
57
62
</div>
 
@@ -63,6 +68,15 @@
63
68
</span>
64
69
</div>
65
70
</td>
71
+ <%= if not @application_sources_empty? do %>
72
+ <td class="px-6 py-4">
73
+ <div class="text-sm font-medium text-primary-dark truncate">
74
+ <span class={"inline-flex items-center px-2.5 py-1 rounded-full uppercase text-xs border border-primary dark:border-accent-dark #{if is_nil(message.application_source), do: "text-primary-dark dark:text-accent-dark", else: "text-white"} font-bold"} style={"background-color:#{unless is_nil(message.application_source), do: message.application_source.color, else: "transparent"};"}>
75
+ <%= if is_nil(message.application_source), do: "None", else: message.application_source.name %>
76
+ </span>
77
+ </div>
78
+ </td>
79
+ <% end %>
66
80
<td class="px-6 py-4">
67
81
<span class="inline-flex items-center px-2.5 py-1 rounded-full uppercase text-xs border border-primary dark:border-accent-dark text-primary-dark dark:text-accent-dark font-bold">
68
82
<%= message.message_type %>
 
@@ -77,4 +91,4 @@
77
91
</div>
78
92
</div>
79
93
</div>
80
- </div>
\ No newline at end of file
94
+ </div>
changed lib/kanta_web/live/translations/translations_live/translations_live.ex
 
@@ -1,41 +1,57 @@
1
1
defmodule KantaWeb.Translations.TranslationsLive do
2
2
use KantaWeb, :live_view
3
3
4
+ import Kanta.Utils.ParamParsers
5
+
4
6
alias Kanta.Translations
7
+ alias Kanta.Translations.SingularTranslations.Finders.ListSingularTranslations
8
+ alias Kanta.Translations.PluralTranslations.Finders.ListPluralTranslations
5
9
alias KantaWeb.Translations.Components.{FiltersBar, MessagesTable}
6
10
7
11
alias KantaWeb.Components.Shared.Pagination
8
12
9
- @available_filters ~w(domain_id context_id search not_translated page)
13
+ @available_filters ~w(application_source_id domain_id context_id search not_translated page)
14
+ @available_params ~w(page search filter)
15
+ @params_in_filter ~w(application_source_id domain_id context_id not_translated)
16
+ @ids_to_parse ~w(application_source_id domain_id context_id locale_id)
10
17
11
- def mount(%{"locale_id" => locale_id}, _session, socket) do
18
+ def mount(%{"locale_id" => locale_id} = params, _session, socket) do
12
19
socket =
13
- case Translations.get_locale(filter: [id: locale_id]) do
20
+ case get_locale(locale_id) do
14
21
{:ok, locale} ->
15
22
socket
16
23
|> assign(:locale, locale)
17
- |> assign(:filters, %{})
24
+ |> assign(:application_sources_empty?, Translations.application_sources_empty?())
25
+ |> assign(get_assigns_from_params(params))
18
26
19
27
_ ->
20
28
socket
29
+ |> redirect(to: "/kanta/locales")
21
30
end
22
31
23
32
{:ok, socket}
24
33
end
25
34
26
35
def handle_params(%{"locale_id" => locale_id} = params, _location, socket) do
36
+ preload_filters = %{"locale_id" => locale_id}
37
+ singular_translation_query = ListSingularTranslations.filter_query(preload_filters)
38
+ plural_translation_query = ListPluralTranslations.filter_query(preload_filters)
39
+
27
40
%{entries: messages, metadata: messages_metadata} =
28
41
Translations.list_messages(
29
42
[]
30
- |> Keyword.merge(filter: Map.put(params["filter"] || %{}, "locale_id", locale_id))
31
43
|> Keyword.merge(search: params["search"] || "")
32
- |> Keyword.merge(page: params["page"] || "1")
44
+ |> Keyword.merge(page: parse_page(params["page"] || "1"))
45
+ |> Keyword.merge(
46
+ filter: parse_filters(Map.put(params["filter"] || %{}, "locale_id", locale_id))
47
+ )
33
48
|> Keyword.merge(
34
49
preloads: [
50
+ :application_source,
35
51
:context,
36
52
:domain,
37
- :singular_translations,
38
- :plural_translations
53
+ singular_translations: singular_translation_query,
54
+ plural_translations: plural_translation_query
39
55
]
40
56
)
41
57
)
 
@@ -49,6 +65,7 @@ defmodule KantaWeb.Translations.TranslationsLive do
49
65
end
50
66
51
67
def handle_event("change", filters, socket) do
68
+ filters = Map.put(filters, "page", "1")
52
69
query = UriQuery.params(format_filters(Map.merge(socket.assigns.filters, filters)))
53
70
54
71
socket = socket |> assign(:filters, Map.merge(socket.assigns.filters, filters))
 
@@ -56,13 +73,13 @@ defmodule KantaWeb.Translations.TranslationsLive do
56
73
{:noreply,
57
74
push_patch(socket,
58
75
to:
59
- "/kanta/locales/#{socket.assigns.locale.id}/translations?" <>
76
+ "#{dashboard_path(socket)}/locales/#{socket.assigns.locale.id}/translations?" <>
60
77
URI.encode_query(query)
61
78
)}
62
79
end
63
80
64
81
def handle_event("navigate", %{"to" => to}, socket) do
65
- {:noreply, push_redirect(socket, to: "/kanta" <> to)}
82
+ {:noreply, push_redirect(socket, to: dashboard_path(socket) <> to)}
66
83
end
67
84
68
85
def handle_event("page_changed", %{"index" => page_number}, socket) do
 
@@ -70,50 +87,92 @@ defmodule KantaWeb.Translations.TranslationsLive do
70
87
socket
71
88
|> assign(
72
89
:filters,
73
- Map.merge(socket.assigns.filters, %{"page" => String.to_integer(page_number)})
90
+ Map.merge(socket.assigns.filters, %{"page" => parse_page(page_number)})
74
91
)
75
92
76
93
query =
77
94
UriQuery.params(
78
- format_filters(
79
- Map.merge(socket.assigns.filters, %{"page" => String.to_integer(page_number)})
80
- )
95
+ format_filters(Map.merge(socket.assigns.filters, %{"page" => parse_page(page_number)}))
81
96
)
82
97
83
98
{:noreply,
84
99
push_patch(socket,
85
100
to:
86
- "/kanta/locales/#{socket.assigns.locale.id}/translations?" <>
101
+ "#{dashboard_path(socket)}/locales/#{socket.assigns.locale.id}/translations?" <>
87
102
URI.encode_query(query)
88
103
)}
89
104
end
90
105
106
+ defp get_locale(id) do
107
+ case parse_id_filter(id) do
108
+ {:ok, id} -> Translations.get_locale(filter: [id: id])
109
+ _ -> {:error, :id, :invalid}
110
+ end
111
+ end
112
+
91
113
defp format_filters(filters) do
92
114
filters
93
115
|> Map.take(@available_filters)
94
116
|> Enum.reject(fn {_, value} -> is_nil(value) or value == "" end)
95
- |> Enum.reduce([filter: %{}, search: "", page: "1"], fn {key, value}, acc ->
117
+ |> Enum.reduce([filter: %{}, search: "", page: "1"], &update_filters_acc/2)
118
+ end
119
+
120
+ defp update_filters_acc({"search", value}, acc), do: Keyword.put(acc, :search, value)
121
+ defp update_filters_acc({"page", value}, acc), do: Keyword.put(acc, :page, value)
122
+
123
+ defp update_filters_acc({"not_translated", value}, acc) do
124
+ Keyword.put(acc, :filter, Map.put(acc[:filter] || %{}, "not_translated", value))
125
+ end
126
+
127
+ defp update_filters_acc({key, value}, acc) do
128
+ case parse_id_filter(value) do
129
+ {:ok, id} -> Keyword.put(acc, :filter, Map.put(acc[:filter] || %{}, key, id))
130
+ _ -> acc
131
+ end
132
+ end
133
+
134
+ defp get_assigns_from_params(params) do
135
+ params
136
+ |> Map.take(@available_params)
137
+ |> Enum.reduce(%{}, fn {key, value}, acc ->
96
138
case key do
97
- "search" ->
98
- Keyword.put(acc, :search, value)
99
-
100
- "page" ->
101
- Keyword.put(acc, :page, value)
102
-
103
- "not_translated" ->
104
- Keyword.put(
105
- acc,
106
- :filter,
107
- Map.put(acc[:filter] || %{}, "not_translated", value)
108
- )
139
+ "filter" ->
140
+ values = Map.take(value, @params_in_filter)
141
+ Map.merge(acc, values)
109
142
110
143
filter_key ->
111
- Keyword.put(
112
- acc,
113
- :filter,
114
- Map.put(acc[:filter] || %{}, filter_key, String.to_integer(value))
115
- )
144
+ Map.put(acc, filter_key, value)
116
145
end
117
146
end)
147
+ |> then(fn filters ->
148
+ %{
149
+ not_translated_default: get_not_translated_default_value(params),
150
+ filters: filters
151
+ }
152
+ end)
153
+ end
154
+
155
+ defp get_not_translated_default_value(%{"filter" => filter}) do
156
+ case filter["not_translated"] do
157
+ "true" -> true
158
+ _ -> false
159
+ end
160
+ end
161
+
162
+ defp get_not_translated_default_value(_), do: false
163
+
164
+ defp parse_filters(filters) do
165
+ Enum.reduce(filters, %{}, &parse_filter/2)
166
+ end
167
+
168
+ defp parse_filter({key, value}, acc) when key in @ids_to_parse do
169
+ case parse_id_filter(value) do
170
+ {:ok, id} -> Map.put(acc, key, id)
171
+ _ -> acc
172
+ end
173
+ end
174
+
175
+ defp parse_filter({key, value}, acc) do
176
+ Map.put(acc, key, value)
118
177
end
119
178
end
changed lib/kanta_web/live/translations/translations_live/translations_live.html.heex
 
@@ -2,11 +2,8 @@
2
2
<div>
3
3
<div>
4
4
<nav class="sm:hidden" aria-label="Back">
5
- <.link navigate={unverified_path(@socket, Kanta.Router, "/kanta/locales")} class="flex items-center text-sm font-medium text-slate-500 hover:text-slate-700">
6
- <!-- Heroicon name: solid/chevron-left -->
7
- <svg class="flex-shrink-0 -ml-1 mr-1 h-5 w-5 text-slate-400 dark:text-content-light" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
8
- <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
9
- </svg>
5
+ <.link navigate={dashboard_path(@socket, "/locales")} class="flex items-center text-sm font-medium text-slate-500 hover:text-slate-700">
6
+ <Icons.chevron_left class="flex-shrink-0 -ml-1 mr-1 h-5 w-5 text-slate-400 dark:text-content-light" aria-hidden="true" />
10
7
Back
11
8
</.link>
12
9
</nav>
 
@@ -14,15 +11,12 @@
14
11
<ol class="flex items-center space-x-4">
15
12
<li>
16
13
<div>
17
- <.link navigate={unverified_path(@socket, Kanta.Router, "/kanta/locales")} class="text-sm font-medium text-slate-500 dark:text-content-light hover:text-slate-700 dark:hover:text-white">Locales</.link>
14
+ <.link navigate={dashboard_path(@socket, "/locales")} class="text-sm font-medium text-slate-500 dark:text-content-light hover:text-slate-700 dark:hover:text-white">Locales</.link>
18
15
</div>
19
16
</li>
20
17
<li>
21
18
<div class="flex items-center">
22
- <!-- Heroicon name: solid/chevron-right -->
23
- <svg class="flex-shrink-0 h-5 w-5 text-slate-400" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
24
- <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
25
- </svg>
19
+ <Icons.chevron_right class="flex-shrink-0 h-5 w-5 text-slate-400" aria-hidden="true" />
26
20
<.link navigate="#" aria-current="page" class="ml-4 text-sm font-medium text-slate-500 dark:text-content-light hover:text-slate-700 dark:hover:text-white"><%= @locale.native_name %></.link>
27
21
</div>
28
22
</li>
 
@@ -38,7 +32,7 @@
38
32
</div>
39
33
</div>
40
34
<div class="mt-4">
41
- <.live_component module={FiltersBar} filters={@filters} id="filters-bar" />
42
- <.live_component module={MessagesTable} id="messages-table" messages={@messages} locale={@locale} />
35
+ <.live_component module={FiltersBar} filters={@filters} not_translated_default={@not_translated_default} application_sources_empty?={@application_sources_empty?} locale_id={@locale.id} id="filters-bar" />
36
+ <.live_component module={MessagesTable} messages={@messages} filters={@filters} locale={@locale} application_sources_empty?={@application_sources_empty?} id="messages-table" />
43
37
<Pagination.render metadata={@messages_metadata} on_page_change="page_changed" />
44
- </div>
\ No newline at end of file
38
+ </div>
changed lib/kanta_web/plugs/api_auth_plug.ex
 
@@ -8,7 +8,7 @@ defmodule KantaWeb.APIAuthPlug do
8
8
def init(_opts), do: %{}
9
9
10
10
def call(conn, _opts) do
11
- if api_authorization_disabled?() or is_bearer_token_valid?(conn) do
11
+ if api_authorization_disabled?() or bearer_token_valid?(conn) do
12
12
conn
13
13
else
14
14
conn
 
@@ -20,16 +20,16 @@ defmodule KantaWeb.APIAuthPlug do
20
20
end
21
21
end
22
22
23
- defp is_bearer_token_valid?(conn) do
23
+ defp bearer_token_valid?(conn) do
24
24
with {:ok, token} <- extract_bearer_token(conn),
25
- true <- is_secret_token_matching?(token) do
25
+ true <- secret_token_matching?(token) do
26
26
true
27
27
else
28
28
_ -> false
29
29
end
30
30
end
31
31
32
- defp is_secret_token_matching?(token) do
32
+ defp secret_token_matching?(token) do
33
33
secret_token_env =
34
34
@kanta_secret_token
35
35
|> System.get_env()
changed lib/kanta_web/router.ex
 
@@ -25,13 +25,23 @@ defmodule KantaWeb.Router do
25
25
get "/css-:md5", KantaWeb.Assets, :css, as: :kanta_dashboard_asset
26
26
get "/js-:md5", KantaWeb.Assets, :js, as: :kanta_dashboard_asset
27
27
28
- redirect("/", "/kanta/dashboard", :permanent)
28
+ redirect(
29
+ "/",
30
+ "#{KantaWeb.Router.internal_dashboard_scoped_path(path)}/dashboard",
31
+ :permanent
32
+ )
29
33
30
34
scope "/", KantaWeb do
31
35
scope "/dashboard", Dashboard do
32
36
live "/", DashboardLive, :index, route_opts
33
37
end
34
38
39
+ scope "/application_sources", Translations do
40
+ live "/", ApplicationSourcesLive, :index, route_opts
41
+ live "/new", ApplicationSourceFormLive, :index, route_opts
42
+ live "/:id", ApplicationSourceFormLive, :index, route_opts
43
+ end
44
+
35
45
scope "/contexts", Translations do
36
46
live "/", ContextsLive, :index, route_opts
37
47
live "/:id", ContextLive, :index, route_opts
 
@@ -57,17 +67,13 @@ defmodule KantaWeb.Router do
57
67
end
58
68
end
59
69
60
- if Code.ensure_loaded?(Phoenix.VerifiedRoutes) do
61
- quote do
62
- unquote(scope)
70
+ quote do
71
+ unquote(scope)
63
72
64
- unless Module.get_attribute(__MODULE__, :kanta_dashboard_prefix) do
65
- @kanta_dashboard_prefix Phoenix.Router.scoped_path(__MODULE__, path)
66
- def __kanta_dashboard_prefix__, do: @kanta_dashboard_prefix
67
- end
73
+ unless Module.get_attribute(__MODULE__, :kanta_dashboard_prefix) do
74
+ @kanta_dashboard_prefix KantaWeb.Router.internal_dashboard_scoped_path(path)
75
+ def __kanta_dashboard_prefix__, do: @kanta_dashboard_prefix
68
76
end
69
- else
70
- scope
71
77
end
72
78
end
73
79
 
@@ -83,6 +89,7 @@ defmodule KantaWeb.Router do
83
89
pipe_through :kanta_api_pipeline
84
90
get "/", KantaApiController, :index
85
91
92
+ resources "/applications", ApplicationSourcesController, only: [:index, :update]
86
93
resources "/contexts", ContextsController, only: [:index, :update]
87
94
resources "/domains", DomainsController, only: [:index, :update]
88
95
resources "/locales", LocalesController, only: [:index, :update]
 
@@ -97,6 +104,34 @@ defmodule KantaWeb.Router do
97
104
end
98
105
end
99
106
107
+ defmacro internal_dashboard_scoped_path(path) do
108
+ if Code.ensure_loaded?(Phoenix.VerifiedRoutes) do
109
+ quote do
110
+ Phoenix.Router.scoped_path(__MODULE__, unquote(path))
111
+ end
112
+ else
113
+ quote do
114
+ __MODULE__
115
+ |> Module.get_attribute(:phoenix_top_scopes)
116
+ |> Map.fetch!(:path)
117
+ |> KantaWeb.Router.append_last_path(unquote(path))
118
+ |> Enum.join("/")
119
+ |> String.replace_prefix("", "/")
120
+ end
121
+ end
122
+ end
123
+
124
+ @spec append_last_path(list(), binary()) :: list()
125
+ def append_last_path(paths, "/" <> path), do: append_last_path(paths, path)
126
+
127
+ def append_last_path(paths, path) do
128
+ if List.last(paths) == path do
129
+ paths
130
+ else
131
+ paths ++ [path]
132
+ end
133
+ end
134
+
100
135
defp expand_alias({:__aliases__, _, _} = alias, env),
101
136
do: Macro.expand(alias, %{env | function: {:kanta_dashboard, 2}})
changed lib/kanta_web/templates/layouts/dashboard.html.heex
 
@@ -39,26 +39,30 @@
39
39
<div class="flex-1 flex flex-col pt-5 pb-4 overflow-y-auto">
40
40
<div class="flex justify-start items-center mb-4">
41
41
<Logo.render class="ml-4 w-10 h-10 text-primary-dark dark:text-accent-light" />
42
- <div class="ml-4 text-4xl text-primary-dark dark:text-accent-light fotn-medium">Kanta</div>
42
+ <div class="ml-4 text-4xl text-primary-dark dark:text-accent-light font-medium">Kanta</div>
43
43
</div>
44
44
<div class="border-b border-slate-50 opacity-50" />
45
45
<nav class="mt-5 flex-1 px-2 space-y-3">
46
- <%= link to: unverified_path(@conn, Kanta.Router, "/kanta/dashboard"), class: "bg-white dark:bg-base-dark transition-all hover:bg-slate-100 dark:hover:bg-stone-800 border-slate-300 dark:border-accent-dark shadow-md text-primary-dark dark:text-accent-dark group flex items-center px-2 py-2 text-sm font-semibold rounded-md" do %>
46
+ <%= link to: dashboard_path(@conn, "/dashboard"), class: "bg-white dark:bg-base-dark transition-all hover:bg-slate-100 dark:hover:bg-stone-800 border-slate-300 dark:border-accent-dark shadow-md text-primary-dark dark:text-accent-dark group flex items-center px-2 py-2 text-sm font-semibold rounded-md" do %>
47
47
<Icons.inspect class="mr-3 flex-shrink-0 h-6 w-6 text-primary-dark dark:text-accent-dark" />
48
48
Dashboard
49
49
<% end %>
50
- <%= link to: unverified_path(@conn, Kanta.Router, "/kanta/locales"), class: "bg-white dark:bg-base-dark transition-all hover:bg-slate-100 dark:hover:bg-stone-800 border-slate-300 dark:border-accent-dark shadow-md text-primary-dark dark:text-accent-dark group flex items-center px-2 py-2 text-sm font-semibold rounded-md" do %>
50
+ <%= link to: dashboard_path(@conn, "/locales"), class: "bg-white dark:bg-base-dark transition-all hover:bg-slate-100 dark:hover:bg-stone-800 border-slate-300 dark:border-accent-dark shadow-md text-primary-dark dark:text-accent-dark group flex items-center px-2 py-2 text-sm font-semibold rounded-md" do %>
51
51
<Icons.languages class="mr-3 flex-shrink-0 h-6 w-6 text-primary-dark dark:text-accent-dark" />
52
52
Locales
53
53
<% end %>
54
- <%= link to: unverified_path(@conn, Kanta.Router, "/kanta/domains"), class: "bg-white dark:bg-base-dark transition-all hover:bg-slate-100 dark:hover:bg-stone-800 border-slate-300 dark:border-accent-dark shadow-md text-primary-dark dark:text-accent-dark group flex items-center px-2 py-2 text-sm font-semibold rounded-md" do %>
54
+ <%= link to: dashboard_path(@conn, "/domains"), class: "bg-white dark:bg-base-dark transition-all hover:bg-slate-100 dark:hover:bg-stone-800 border-slate-300 dark:border-accent-dark shadow-md text-primary-dark dark:text-accent-dark group flex items-center px-2 py-2 text-sm font-semibold rounded-md" do %>
55
55
<Icons.album class="mr-3 flex-shrink-0 h-6 w-6 text-primary-dark dark:text-accent-dark" />
56
56
Domains
57
57
<% end %>
58
- <%= link to: unverified_path(@conn, Kanta.Router, "/kanta/contexts"), class: "bg-white dark:bg-base-dark transition-all hover:bg-slate-100 dark:hover:bg-stone-800 border-slate-300 dark:border-accent-dark shadow-md text-primary-dark dark:text-accent-dark group flex items-center px-2 py-2 text-sm font-semibold rounded-md" do %>
58
+ <%= link to: dashboard_path(@conn, "/contexts"), class: "bg-white dark:bg-base-dark transition-all hover:bg-slate-100 dark:hover:bg-stone-800 border-slate-300 dark:border-accent-dark shadow-md text-primary-dark dark:text-accent-dark group flex items-center px-2 py-2 text-sm font-semibold rounded-md" do %>
59
59
<Icons.box class="mr-3 flex-shrink-0 h-6 w-6 text-primary-dark dark:text-accent-dark" />
60
60
Contexts
61
61
<% end %>
62
+ <%= link to: dashboard_path(@conn, "/application_sources"), class: "bg-white dark:bg-base-dark transition-all hover:bg-slate-100 dark:hover:bg-stone-800 border-slate-300 dark:border-accent-dark shadow-md text-primary-dark dark:text-accent-dark group flex items-center px-2 py-2 text-sm font-semibold rounded-md" do %>
63
+ <Icons.computer class="mr-3 flex-shrink-0 h-6 w-6 text-primary-dark dark:text-accent-dark" />
64
+ Applications
65
+ <% end %>
62
66
</nav>
63
67
</div>
64
68
 
@@ -79,7 +83,7 @@
79
83
</div>
80
84
</div>
81
85
</div>
82
- <div x-data={"{ open: false}"} class="relative flex flex-col w-0 flex-1 overflow-hidden">
86
+ <div x-data={"{ open: false }"} class="relative flex flex-col w-0 flex-1 overflow-hidden">
83
87
<div class="md:hidden pl-1 pt-1 sm:pl-3 sm:pt-3">
84
88
<button @click="open = !open" class="-ml-0.5 -mt-0.5 h-12 w-12 inline-flex items-center justify-center rounded-md text-slate-500 dark:text-content-light hover:text-primary-dark hover:dark:text-accent-dark focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary focus:dark:ring-accent-dark">
85
89
<span class="sr-only">Open sidebar</span>
 
@@ -103,19 +107,19 @@
103
107
</div>
104
108
<div class="border-b border-slate-50 opacity-50" />
105
109
<nav class="mt-5 flex-1 px-2 pb-2 space-y-3">
106
- <%= link to: unverified_path(@conn, Kanta.Router, "/kanta/dashboard"), class: "bg-white dark:bg-base-dark transition-all hover:bg-slate-100 dark:hover:bg-stone-800 border-slate-300 dark:border-accent-dark shadow-md text-primary-dark dark:text-accent-dark group flex items-center px-2 py-2 text-sm font-semibold rounded-md" do %>
110
+ <%= link to: dashboard_path(@conn, "/dashboard"), class: "bg-white dark:bg-base-dark transition-all hover:bg-slate-100 dark:hover:bg-stone-800 border-slate-300 dark:border-accent-dark shadow-md text-primary-dark dark:text-accent-dark group flex items-center px-2 py-2 text-sm font-semibold rounded-md" do %>
107
111
<Icons.inspect class="mr-3 flex-shrink-0 h-6 w-6 text-primary-dark dark:text-accent-dark" />
108
112
Dashboard
109
113
<% end %>
110
- <%= link to: unverified_path(@conn, Kanta.Router, "/kanta/locales"), class: "bg-white dark:bg-base-dark transition-all hover:bg-slate-100 dark:hover:bg-stone-800 border-slate-300 dark:border-accent-dark shadow-md text-primary-dark dark:text-accent-dark group flex items-center px-2 py-2 text-sm font-semibold rounded-md" do %>
114
+ <%= link to: dashboard_path(@conn, "/locales"), class: "bg-white dark:bg-base-dark transition-all hover:bg-slate-100 dark:hover:bg-stone-800 border-slate-300 dark:border-accent-dark shadow-md text-primary-dark dark:text-accent-dark group flex items-center px-2 py-2 text-sm font-semibold rounded-md" do %>
111
115
<Icons.languages class="mr-3 flex-shrink-0 h-6 w-6 text-primary-dark dark:text-accent-dark" />
112
116
Locales
113
117
<% end %>
114
- <%= link to: unverified_path(@conn, Kanta.Router, "/kanta/domains"), class: "bg-white dark:bg-base-dark transition-all hover:bg-slate-100 dark:hover:bg-stone-800 border-slate-300 dark:border-accent-dark shadow-md text-primary-dark dark:text-accent-dark group flex items-center px-2 py-2 text-sm font-semibold rounded-md" do %>
118
+ <%= link to: dashboard_path(@conn, "/domains"), class: "bg-white dark:bg-base-dark transition-all hover:bg-slate-100 dark:hover:bg-stone-800 border-slate-300 dark:border-accent-dark shadow-md text-primary-dark dark:text-accent-dark group flex items-center px-2 py-2 text-sm font-semibold rounded-md" do %>
115
119
<Icons.album class="mr-3 flex-shrink-0 h-6 w-6 text-primary-dark dark:text-accent-dark" />
116
120
Domains
117
121
<% end %>
118
- <%= link to: unverified_path(@conn, Kanta.Router, "/kanta/contexts"), class: "bg-white dark:bg-base-dark transition-all hover:bg-slate-100 dark:hover:bg-stone-800 border-slate-300 dark:border-accent-dark shadow-md text-primary-dark dark:text-accent-dark group flex items-center px-2 py-2 text-sm font-semibold rounded-md" do %>
122
+ <%= link to: dashboard_path(@conn, "/contexts"), class: "bg-white dark:bg-base-dark transition-all hover:bg-slate-100 dark:hover:bg-stone-800 border-slate-300 dark:border-accent-dark shadow-md text-primary-dark dark:text-accent-dark group flex items-center px-2 py-2 text-sm font-semibold rounded-md" do %>
119
123
<Icons.box class="mr-3 flex-shrink-0 h-6 w-6 text-primary-dark dark:text-accent-dark" />
120
124
Contexts
121
125
<% end %>
changed lib/kanta_web/views/layout_view.ex
 
@@ -21,7 +21,7 @@ defmodule KantaWeb.LayoutView do
21
21
def asset_path(conn, asset) when asset in [:css, :js] do
22
22
hash = KantaWeb.Assets.current_hash(asset)
23
23
24
- prefix = conn.private.phoenix_router.__kanta_dashboard_prefix__()
24
+ prefix = dashboard_path(conn)
25
25
26
26
Phoenix.VerifiedRoutes.unverified_path(
27
27
conn,
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.1",
9
+ version: "0.4.0",
10
10
elixir: "~> 1.14",
11
11
elixirc_options: [
12
12
warnings_as_errors: true
 
@@ -35,25 +35,29 @@ defmodule Kanta.MixProject do
35
35
defp deps do
36
36
[
37
37
{:expo, "~> 0.3"},
38
- {:ecto, "~> 3.10"},
39
- {:ecto_sql, "~> 3.10"},
38
+ {:ecto, "~> 3.12"},
39
+ {:ecto_sql, "~> 3.12"},
40
40
{:phoenix, "~> 1.7.0"},
41
41
{:phoenix_view, "~> 2.0"},
42
42
{:phoenix_live_view, "~> 0.20"},
43
+ {:phoenix_html, "~> 4.0"},
44
+ {:phoenix_html_helpers, "~> 1.0"},
43
45
{:tailwind, "~> 0.2", runtime: Mix.env() == :dev},
44
46
{:jason, "~> 1.0"},
45
47
{:nebulex, "~> 2.5"},
46
48
{:shards, "~> 1.0"},
47
49
{:scrivener, "~> 2.0"},
48
50
{:scrivener_ecto, "~> 2.0"},
49
- {:uri_query, "~> 0.1.1"},
51
+ {:uri_query, "~> 0.2"},
50
52
# DEV
53
+ {:versioce, "~> 2.0.0"},
54
+ {:git_cli, "~> 0.3.0"},
51
55
{:esbuild, "~> 0.7", only: :dev},
52
- {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
56
+ {:credo, "~> 1.7.7", only: [:dev, :test], runtime: false},
53
57
{:mix_audit, "~> 2.0", only: [:dev, :test], runtime: false},
54
58
{:gettext, github: "ravensiris/gettext", branch: "runtime-gettext", only: [:dev, :test]},
55
59
{:ex_doc, "~> 0.27", only: :dev, runtime: false},
56
- {:dialyxir, "~> 1.3", only: :dev, runtime: false}
60
+ {:dialyxir, "~> 1.4", only: :dev, runtime: false}
57
61
]
58
62
end