piko-lite-box 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/hanami-3.0.1/CHANGELOG.md +1701 -0
  3. data/hanami-3.0.1/LICENSE +20 -0
  4. data/hanami-3.0.1/README.md +89 -0
  5. data/hanami-3.0.1/hanami.gemspec +49 -0
  6. data/hanami-3.0.1/lib/hanami/app.rb +197 -0
  7. data/hanami-3.0.1/lib/hanami/boot.rb +5 -0
  8. data/hanami-3.0.1/lib/hanami/config/actions/content_security_policy.rb +140 -0
  9. data/hanami-3.0.1/lib/hanami/config/actions/cookies.rb +57 -0
  10. data/hanami-3.0.1/lib/hanami/config/actions/sessions.rb +83 -0
  11. data/hanami-3.0.1/lib/hanami/config/actions.rb +189 -0
  12. data/hanami-3.0.1/lib/hanami/config/assets.rb +84 -0
  13. data/hanami-3.0.1/lib/hanami/config/console.rb +79 -0
  14. data/hanami-3.0.1/lib/hanami/config/db.rb +35 -0
  15. data/hanami-3.0.1/lib/hanami/config/i18n.rb +138 -0
  16. data/hanami-3.0.1/lib/hanami/config/logger.rb +207 -0
  17. data/hanami-3.0.1/lib/hanami/config/null_config.rb +17 -0
  18. data/hanami-3.0.1/lib/hanami/config/router.rb +48 -0
  19. data/hanami-3.0.1/lib/hanami/config/views.rb +103 -0
  20. data/hanami-3.0.1/lib/hanami/config.rb +564 -0
  21. data/hanami-3.0.1/lib/hanami/constants.rb +62 -0
  22. data/hanami-3.0.1/lib/hanami/env.rb +52 -0
  23. data/hanami-3.0.1/lib/hanami/errors.rb +41 -0
  24. data/hanami-3.0.1/lib/hanami/extensions/action/i18n_helper.rb +64 -0
  25. data/hanami-3.0.1/lib/hanami/extensions/action/name_inferrer.rb +34 -0
  26. data/hanami-3.0.1/lib/hanami/extensions/action/slice_configured_action.rb +190 -0
  27. data/hanami-3.0.1/lib/hanami/extensions/action.rb +147 -0
  28. data/hanami-3.0.1/lib/hanami/extensions/db/repo.rb +108 -0
  29. data/hanami-3.0.1/lib/hanami/extensions/mailer/slice_configured_mailer.rb +134 -0
  30. data/hanami-3.0.1/lib/hanami/extensions/mailer.rb +28 -0
  31. data/hanami-3.0.1/lib/hanami/extensions/operation/slice_configured_db_operation.rb +90 -0
  32. data/hanami-3.0.1/lib/hanami/extensions/operation.rb +33 -0
  33. data/hanami-3.0.1/lib/hanami/extensions/router/errors.rb +58 -0
  34. data/hanami-3.0.1/lib/hanami/extensions/view/context.rb +298 -0
  35. data/hanami-3.0.1/lib/hanami/extensions/view/part.rb +92 -0
  36. data/hanami-3.0.1/lib/hanami/extensions/view/scope.rb +36 -0
  37. data/hanami-3.0.1/lib/hanami/extensions/view/slice_configured_context.rb +73 -0
  38. data/hanami-3.0.1/lib/hanami/extensions/view/slice_configured_helpers.rb +55 -0
  39. data/hanami-3.0.1/lib/hanami/extensions/view/slice_configured_part.rb +73 -0
  40. data/hanami-3.0.1/lib/hanami/extensions/view/slice_configured_view.rb +243 -0
  41. data/hanami-3.0.1/lib/hanami/extensions/view/standard_helpers.rb +26 -0
  42. data/hanami-3.0.1/lib/hanami/extensions/view.rb +38 -0
  43. data/hanami-3.0.1/lib/hanami/extensions.rb +30 -0
  44. data/hanami-3.0.1/lib/hanami/helpers/assets_helper.rb +804 -0
  45. data/hanami-3.0.1/lib/hanami/helpers/form_helper/form_builder.rb +1389 -0
  46. data/hanami-3.0.1/lib/hanami/helpers/form_helper/values.rb +75 -0
  47. data/hanami-3.0.1/lib/hanami/helpers/form_helper.rb +213 -0
  48. data/hanami-3.0.1/lib/hanami/helpers/i18n_helper.rb +241 -0
  49. data/hanami-3.0.1/lib/hanami/logger/rack_formatter.rb +73 -0
  50. data/hanami-3.0.1/lib/hanami/logger/sql_formatter.rb +80 -0
  51. data/hanami-3.0.1/lib/hanami/logger/sql_logger.rb +48 -0
  52. data/hanami-3.0.1/lib/hanami/middleware/assets.rb +21 -0
  53. data/hanami-3.0.1/lib/hanami/middleware/content_security_policy_nonce.rb +53 -0
  54. data/hanami-3.0.1/lib/hanami/middleware/public_errors_app.rb +75 -0
  55. data/hanami-3.0.1/lib/hanami/middleware/render_errors.rb +90 -0
  56. data/hanami-3.0.1/lib/hanami/port.rb +45 -0
  57. data/hanami-3.0.1/lib/hanami/prepare.rb +5 -0
  58. data/hanami-3.0.1/lib/hanami/provider/source.rb +16 -0
  59. data/hanami-3.0.1/lib/hanami/provider_registrar.rb +28 -0
  60. data/hanami-3.0.1/lib/hanami/providers/assets.rb +28 -0
  61. data/hanami-3.0.1/lib/hanami/providers/db/adapter.rb +75 -0
  62. data/hanami-3.0.1/lib/hanami/providers/db/adapters.rb +66 -0
  63. data/hanami-3.0.1/lib/hanami/providers/db/config.rb +44 -0
  64. data/hanami-3.0.1/lib/hanami/providers/db/gateway.rb +87 -0
  65. data/hanami-3.0.1/lib/hanami/providers/db/sql_adapter.rb +100 -0
  66. data/hanami-3.0.1/lib/hanami/providers/db.rb +325 -0
  67. data/hanami-3.0.1/lib/hanami/providers/db_logging.rb +19 -0
  68. data/hanami-3.0.1/lib/hanami/providers/i18n/backend.rb +373 -0
  69. data/hanami-3.0.1/lib/hanami/providers/i18n/locale/en.yml +57 -0
  70. data/hanami-3.0.1/lib/hanami/providers/i18n.rb +114 -0
  71. data/hanami-3.0.1/lib/hanami/providers/inflector.rb +17 -0
  72. data/hanami-3.0.1/lib/hanami/providers/logger.rb +41 -0
  73. data/hanami-3.0.1/lib/hanami/providers/mailers.rb +101 -0
  74. data/hanami-3.0.1/lib/hanami/providers/rack.rb +47 -0
  75. data/hanami-3.0.1/lib/hanami/providers/relations.rb +31 -0
  76. data/hanami-3.0.1/lib/hanami/providers/routes.rb +29 -0
  77. data/hanami-3.0.1/lib/hanami/rake_tasks.rb +54 -0
  78. data/hanami-3.0.1/lib/hanami/routes.rb +146 -0
  79. data/hanami-3.0.1/lib/hanami/settings/composite_store.rb +53 -0
  80. data/hanami-3.0.1/lib/hanami/settings/env_store.rb +32 -0
  81. data/hanami-3.0.1/lib/hanami/settings.rb +234 -0
  82. data/hanami-3.0.1/lib/hanami/setup.rb +6 -0
  83. data/hanami-3.0.1/lib/hanami/slice/router.rb +280 -0
  84. data/hanami-3.0.1/lib/hanami/slice/routes_helper.rb +37 -0
  85. data/hanami-3.0.1/lib/hanami/slice/routing/middleware/stack.rb +216 -0
  86. data/hanami-3.0.1/lib/hanami/slice/routing/resolver.rb +88 -0
  87. data/hanami-3.0.1/lib/hanami/slice/view_name_inferrer.rb +63 -0
  88. data/hanami-3.0.1/lib/hanami/slice.rb +1239 -0
  89. data/hanami-3.0.1/lib/hanami/slice_configurable.rb +72 -0
  90. data/hanami-3.0.1/lib/hanami/slice_name.rb +111 -0
  91. data/hanami-3.0.1/lib/hanami/slice_registrar.rb +215 -0
  92. data/hanami-3.0.1/lib/hanami/universal_logger.rb +250 -0
  93. data/hanami-3.0.1/lib/hanami/version.rb +45 -0
  94. data/hanami-3.0.1/lib/hanami/web/rack_logger.rb +126 -0
  95. data/hanami-3.0.1/lib/hanami/web/welcome.html.erb +584 -0
  96. data/hanami-3.0.1/lib/hanami/web/welcome.rb +46 -0
  97. data/hanami-3.0.1/lib/hanami.rb +276 -0
  98. data/piko-lite-box.gemspec +11 -0
  99. metadata +137 -0
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Helpers
5
+ module FormHelper
6
+ # Values from params and form helpers.
7
+ #
8
+ # It's responsible to populate input values with data coming from params
9
+ # and inline values specified via form helpers like `text_field`.
10
+ #
11
+ # @since 2.1.0
12
+ # @api private
13
+ class Values
14
+ # @since 2.1.0
15
+ # @api private
16
+ GET_SEPARATOR = "."
17
+
18
+ # @api private
19
+ # @since 2.1.0
20
+ attr_reader :csrf_token
21
+
22
+ # @since 2.1.0
23
+ # @api private
24
+ def initialize(values: {}, params: {}, csrf_token: nil)
25
+ @values = values.to_h
26
+ @params = params.to_h
27
+ @csrf_token = csrf_token
28
+ end
29
+
30
+ # Returns the value (if present) for the given key.
31
+ # Nested values are expressed with an array if symbols.
32
+ #
33
+ # @since 2.1.0
34
+ # @api private
35
+ def get(*keys)
36
+ get_from_params(*keys) || get_from_values(*keys)
37
+ end
38
+
39
+ private
40
+
41
+ # @since 2.1.0
42
+ # @api private
43
+ def get_from_params(*keys)
44
+ keys.map! { |key| /\A\d+\z/.match?(key.to_s) ? key.to_s.to_i : key }
45
+ @params.dig(*keys)
46
+ end
47
+
48
+ # @since 2.1.0
49
+ # @api private
50
+ def get_from_values(*keys)
51
+ head, *tail = *keys
52
+ result = @values[head]
53
+
54
+ tail.each do |k|
55
+ break if result.nil?
56
+
57
+ result = dig(result, k)
58
+ end
59
+
60
+ result
61
+ end
62
+
63
+ # @since 2.1.0
64
+ # @api private
65
+ def dig(base, key)
66
+ case base
67
+ when ::Hash then base[key]
68
+ when Array then base[key.to_s.to_i]
69
+ when ->(r) { r.respond_to?(key) } then base.public_send(key)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/view"
4
+
5
+ module Hanami
6
+ module Helpers
7
+ # Helper methods for generating HTML forms.
8
+ #
9
+ # These helpers will be automatically available in your view templates, part classes and scope
10
+ # classes.
11
+ #
12
+ # This module provides one primary method: {#form_for}, yielding an HTML form builder. This
13
+ # integrates with request params and template locals to populate the form with appropriate
14
+ # values.
15
+ #
16
+ # @api public
17
+ # @since 2.1.0
18
+ module FormHelper
19
+ require_relative "form_helper/form_builder"
20
+
21
+ # Default HTTP method for form
22
+ #
23
+ # @since 2.1.0
24
+ # @api private
25
+ DEFAULT_METHOD = "POST"
26
+
27
+ # Default charset
28
+ #
29
+ # @since 2.1.0
30
+ # @api private
31
+ DEFAULT_CHARSET = "utf-8"
32
+
33
+ # CSRF Token session key
34
+ #
35
+ # This name of this key is shared with the hanami and hanami-action gems.
36
+ #
37
+ # @since 2.1.0
38
+ # @api private
39
+ CSRF_TOKEN = :_csrf_token
40
+
41
+ include Hanami::View::Helpers::TagHelper
42
+
43
+ # Yields a form builder for constructing an HTML form and returns the resulting form string.
44
+ #
45
+ # See {FormHelper::FormBuilder} for the methods for building the form's fields.
46
+ #
47
+ # @overload form_for(base_name, url, values: _form_for_values, params: _form_for_params, **attributes)
48
+ # Builds the form using the given base name for all fields.
49
+ #
50
+ # @param base_name [String] the base
51
+ # @param url [String] the URL for submitting the form
52
+ # @param values [Hash] values to be used for populating form field values; optional,
53
+ # defaults to the template's locals or to a part's `{name => self}`
54
+ # @param params [Hash] request param values to be used for populating form field values;
55
+ # these are used in preference over the `values`; optional, defaults to the current
56
+ # request's params
57
+ # @param attributes [Hash] the HTML attributes for the form tag
58
+ # @yieldparam [FormHelper::FormBuilder] f the form builder
59
+ #
60
+ # @overload form_for(url, values: _form_for_values, params: _form_for_params, **attributes)
61
+ # @param url [String] the URL for submitting the form
62
+ # @param values [Hash] values to be used for populating form field values; optional,
63
+ # defaults to the template's locals or to a part's `{name => self}`
64
+ # @param params [Hash] request param values to be used for populating form field values;
65
+ # these are used in preference over the `values`; optional, defaults to the current
66
+ # request's params
67
+ # @param attributes [Hash] the HTML attributes for the form tag
68
+ # @yieldparam [FormHelper::FormBuilder] f the form builder
69
+ #
70
+ # @return [String] the form HTML
71
+ #
72
+ # @see FormHelper
73
+ # @see FormHelper::FormBuilder
74
+ #
75
+ # @example Basic usage
76
+ # <%= form_for("book", "/books", class: "form-horizontal") do |f| %>
77
+ # <div>
78
+ # <%= f.label "title" %>
79
+ # <%= f.text_field "title", class: "form-control" %>
80
+ # </div>
81
+ #
82
+ # <%= f.submit "Create" %>
83
+ # <% end %>
84
+ #
85
+ # =>
86
+ # <form action="/books" method="POST" accept-charset="utf-8" class="form-horizontal">
87
+ # <input type="hidden" name="_csrf_token" value="920cd5bfaecc6e58368950e790f2f7b4e5561eeeab230aa1b7de1b1f40ea7d5d">
88
+ # <div>
89
+ # <label for="book-title">Title</label>
90
+ # <input type="text" name="book[title]" id="book-title" value="Test Driven Development">
91
+ # </div>
92
+ #
93
+ # <button type="submit">Create</button>
94
+ # </form>
95
+ #
96
+ # @example Without base name
97
+ #
98
+ # <%= form_for("/books", class: "form-horizontal") do |f| %>
99
+ # <div>
100
+ # <%= f.label "books.title" %>
101
+ # <%= f.text_field "books.title", class: "form-control" %>
102
+ # </div>
103
+ #
104
+ # <%= f.submit "Create" %>
105
+ # <% end %>
106
+ #
107
+ # =>
108
+ # <form action="/books" method="POST" accept-charset="utf-8" class="form-horizontal">
109
+ # <input type="hidden" name="_csrf_token" value="920cd5bfaecc6e58368950e790f2f7b4e5561eeeab230aa1b7de1b1f40ea7d5d">
110
+ # <div>
111
+ # <label for="book-title">Title</label>
112
+ # <input type="text" name="book[title]" id="book-title" value="Test Driven Development">
113
+ # </div>
114
+ #
115
+ # <button type="submit">Create</button>
116
+ # </form>
117
+ #
118
+ # @example Method override
119
+ # <%= form_for("/books/123", method: :put) do |f|
120
+ # <%= f.text_field "book.title" %>
121
+ # <%= f.submit "Update" %>
122
+ # <% end %>
123
+ #
124
+ # =>
125
+ # <form action="/books/123" accept-charset="utf-8" method="POST">
126
+ # <input type="hidden" name="_method" value="PUT">
127
+ # <input type="hidden" name="_csrf_token" value="920cd5bfaecc6e58368950e790f2f7b4e5561eeeab230aa1b7de1b1f40ea7d5d">
128
+ # <input type="text" name="book[title]" id="book-title" value="Test Driven Development">
129
+ #
130
+ # <button type="submit">Update</button>
131
+ # </form>
132
+ #
133
+ # @example Overriding values
134
+ # <%= form_for("/songs", values: {song: {title: "Envision"}}) do |f| %>
135
+ # <%= f.text_field "song.title" %>
136
+ # <%= f.submit "Create" %>
137
+ # <%= end %>
138
+ #
139
+ # =>
140
+ # <form action="/songs" accept-charset="utf-8" method="POST">
141
+ # <input type="hidden" name="_csrf_token" value="920cd5bfaecc6e58368950e790f2f7b4e5561eeeab230aa1b7de1b1f40ea7d5d">
142
+ # <input type="text" name="song[title]" id="song-title" value="Envision">
143
+ #
144
+ # <button type="submit">Create</button>
145
+ # </form>
146
+ #
147
+ # @api public
148
+ # @since 2.1.0
149
+ def form_for(base_name, url = nil, values: _form_for_values, params: _form_for_params, **attributes)
150
+ url, base_name = base_name, nil if url.nil?
151
+
152
+ values = Values.new(values: values, params: params, csrf_token: _form_csrf_token)
153
+
154
+ builder = FormBuilder.new(
155
+ base_name: base_name,
156
+ values: values,
157
+ inflector: _context.inflector,
158
+ form_attributes: attributes
159
+ )
160
+
161
+ content = (block_given? ? yield(builder) : "").html_safe
162
+
163
+ builder.call(content, action: url, **attributes)
164
+ end
165
+
166
+ # Returns CSRF meta tags for use via unobtrusive JavaScript (UJS) libraries.
167
+ #
168
+ # @return [String, nil] the tags, if a CSRF token is available, or nil
169
+ #
170
+ # @example
171
+ # csrf_meta_tags
172
+ #
173
+ # =>
174
+ # <meta name="csrf-param" content="_csrf_token">
175
+ # <meta name="csrf-token" content="4a038be85b7603c406dcbfad4b9cdf91ec6ca138ed6441163a07bb0fdfbe25b5">
176
+ #
177
+ # @api public
178
+ # @since 2.1.0
179
+ def csrf_meta_tags
180
+ return unless (token = _form_csrf_token)
181
+
182
+ tag.meta(name: "csrf-param", content: CSRF_TOKEN) +
183
+ tag.meta(name: "csrf-token", content: token)
184
+ end
185
+
186
+ # @api private
187
+ # @since 2.1.0
188
+ def _form_for_values
189
+ if respond_to?(:_locals) # Scope
190
+ _locals
191
+ elsif respond_to?(:_name) # Part
192
+ {_name => self}
193
+ else
194
+ {}
195
+ end
196
+ end
197
+
198
+ # @api private
199
+ # @since 2.1.0
200
+ def _form_for_params
201
+ _context.request.params
202
+ end
203
+
204
+ # @since 2.1.0
205
+ # @api private
206
+ def _form_csrf_token
207
+ return unless _context.request.session_enabled?
208
+
209
+ _context.csrf_token
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi/escape"
4
+
5
+ module Hanami
6
+ module Helpers
7
+ # View-layer translation and localization helpers.
8
+ #
9
+ # These helpers are automatically available in your view templates, part classes, and scope
10
+ # classes when the `i18n` gem is bundled. They provide `translate`/`t`, `translate!`/`t!`, and
11
+ # `localize`/`l`, sourcing the i18n backend from the view context and expanding relative
12
+ # (leading-dot) translation keys against the currently-rendering template name.
13
+ #
14
+ # The shared `translate`/`localize` logic lives in {Methods}, which is also included by the
15
+ # action-layer i18n helper ({Hanami::Extensions::Action::I18nHelper}). This module supplies
16
+ # the two view-specific concrete implementations of {Methods}'s abstract hooks: {#_i18n}
17
+ # returns the i18n backend from the view context, and {#_resolve_i18n_key} expands relative
18
+ # keys against the template name.
19
+ #
20
+ # @example Basic translation
21
+ # <%= translate("messages.welcome") %>
22
+ # # => "Welcome"
23
+ #
24
+ # @example HTML-safe translation
25
+ # # en.yml
26
+ # # greeting_html: "Hello, <strong>%{name}</strong>!"
27
+ # <%= translate("greeting_html", name: "<script>") %>
28
+ # # => "Hello, <strong>&lt;script&gt;</strong>!" (marked HTML-safe)
29
+ #
30
+ # @example Missing translation
31
+ # <%= translate("missing.key") %>
32
+ # # => '<span class="translation_missing" title="...">missing.key</span>'
33
+ #
34
+ # @example Relative key lookup
35
+ # # In app/templates/users/index.html.erb:
36
+ # <%= translate(".title") %>
37
+ # # Looks up "users.index.title"
38
+ #
39
+ # # In app/templates/users/_form.html.erb (a partial):
40
+ # <%= translate(".label") %>
41
+ # # Looks up "users._form.label"
42
+ #
43
+ # @api public
44
+ # @since 3.0.0
45
+ module I18nHelper
46
+ # Shared `translate` / `localize` (and shorthand) helper methods used by both view-layer
47
+ # consumers and action-layer consumers.
48
+ #
49
+ # **This module is abstract.** Including modules must override two private hooks:
50
+ #
51
+ # - {#_i18n} — returns the i18n backend to delegate to.
52
+ # - {#_resolve_i18n_key} — expands relative (leading-dot) keys against the consumer's
53
+ # context, and is a no-op for absolute keys.
54
+ #
55
+ # @api public
56
+ # @since 3.0.0
57
+ module Methods
58
+ # Matches keys whose final segment is `html` or whose final segment ends in `_html`. These
59
+ # translated values are treated as HTML-safe and any string interpolation values are
60
+ # HTML-escaped before substitution.
61
+ #
62
+ # @api private
63
+ HTML_SAFE_TRANSLATION_KEY = /(\b|_)html\z/
64
+
65
+ # Translates the given key using the slice's i18n backend.
66
+ #
67
+ # When the key's final segment is `html` or ends in `_html`, the result is marked HTML-safe
68
+ # and any string interpolation values are HTML-escaped first.
69
+ #
70
+ # When a translation is missing and neither `:default` nor `:raise` was supplied, returns a
71
+ # `<span class="translation_missing">` element containing the missing key, useful for
72
+ # spotting missing translations during development.
73
+ #
74
+ # When the key begins with a `.`, it is treated as relative to the consumer's context and
75
+ # expanded by {#_resolve_i18n_key}.
76
+ #
77
+ # @param key [String, Symbol] the translation key to look up
78
+ # @param options [Hash] translation options forwarded to the backend (`:locale`, `:scope`,
79
+ # `:default`, `:count`, `:raise`, etc.), plus any interpolation values
80
+ #
81
+ # @return [String] the translated string (marked HTML-safe when hanami-view is bundled and
82
+ # the key ends in `_html` or `html`)
83
+ #
84
+ # @api public
85
+ # @since 3.0.0
86
+ def translate(key, **options)
87
+ key = _resolve_i18n_key(key)
88
+
89
+ html_safe = _html_safe_translation_key?(key)
90
+
91
+ options = _escape_translation_options(options) if html_safe
92
+
93
+ result =
94
+ if options.key?(:default) || options[:raise]
95
+ _i18n.translate(key, **options)
96
+ else
97
+ begin
98
+ _i18n.translate(key, **options, raise: true)
99
+ rescue ::I18n::MissingTranslationData => exception
100
+ return _missing_translation_markup(key, exception)
101
+ end
102
+ end
103
+
104
+ html_safe ? _i18n_mark_html_safe(result.to_s) : result
105
+ end
106
+
107
+ # @api public
108
+ # @since 3.0.0
109
+ alias_method :t, :translate
110
+
111
+ # Translates the given key, raising an exception if the translation is missing.
112
+ #
113
+ # @param (see #translate)
114
+ #
115
+ # @return [String] the translated string (marked HTML-safe when hanami-view is bundled and
116
+ # the key ends in `_html` or `html`)
117
+ #
118
+ # @raise [I18n::MissingTranslationData] if the translation is missing
119
+ #
120
+ # @api public
121
+ # @since 3.0.0
122
+ def translate!(key, **options)
123
+ translate(key, **options, raise: true)
124
+ end
125
+
126
+ # @api public
127
+ # @since 3.0.0
128
+ alias_method :t!, :translate!
129
+
130
+ # Localizes the given object (e.g. a date, time, or number) using the slice's i18n backend.
131
+ #
132
+ # @param object [Date, Time, DateTime, Numeric] the object to localize
133
+ # @param options [Hash] localization options forwarded to the backend (`:locale`, `:format`,
134
+ # etc.)
135
+ #
136
+ # @return [String] the localized string
137
+ #
138
+ # @api public
139
+ # @since 3.0.0
140
+ def localize(object, **options)
141
+ _i18n.localize(object, **options)
142
+ end
143
+
144
+ # @api public
145
+ # @since 3.0.0
146
+ alias_method :l, :localize
147
+
148
+ private
149
+
150
+ # Returns the i18n backend the helper methods delegate to.
151
+ #
152
+ # Including modules must override this method to return an I18n backend instance.
153
+ #
154
+ # @return [Hanami::Providers::I18n::Backend]
155
+ def _i18n
156
+ raise NoMethodError, "#{self.class} must implement #_i18n to return an i18n backend"
157
+ end
158
+
159
+ # Resolves the given key, returning it unchanged by default.
160
+ #
161
+ # Override this hook to expand relative (leading-dot) keys against a context.
162
+ #
163
+ # @param key [String, Symbol]
164
+ #
165
+ # @return [String, Symbol] the resolved key
166
+ def _resolve_i18n_key(key)
167
+ key
168
+ end
169
+
170
+ def _html_safe_translation_key?(key)
171
+ HTML_SAFE_TRANSLATION_KEY.match?(key.to_s)
172
+ end
173
+
174
+ def _escape_translation_options(options)
175
+ options.each_with_object({}) do |(key, value), result|
176
+ result[key] =
177
+ if ::I18n::RESERVED_KEYS.include?(key) || !value.is_a?(String) || _i18n_html_safe?(value)
178
+ value
179
+ else
180
+ _i18n_html_escape(value)
181
+ end
182
+ end
183
+ end
184
+
185
+ def _missing_translation_markup(key, error)
186
+ title = _i18n_html_escape(error.message)
187
+ body = _i18n_html_escape(key.to_s)
188
+ _i18n_mark_html_safe(%(<span class="translation_missing" title="#{title}">#{body}</span>))
189
+ end
190
+
191
+ # Escapes `value` for HTML. Prefers `Hanami::View::Helpers::EscapeHelper#escape_html` when
192
+ # Hanami View's EscapeHelper is included, falling back to stdlib `CGI.escapeHTML` so the
193
+ # helper works in API-only apps that don't bundle hanami-view.
194
+ def _i18n_html_escape(value)
195
+ if respond_to?(:escape_html)
196
+ escape_html(value)
197
+ else
198
+ CGI.escapeHTML(value.to_s)
199
+ end
200
+ end
201
+
202
+ # Marks the given string as HTML-safe when Hanami View is loaded, otherwise returns the
203
+ # string unchanged.
204
+ #
205
+ # The HTML-safe marker is only meaningful to Hanami View's template rendering, so there's
206
+ # nothing to do in its absence.
207
+ def _i18n_mark_html_safe(str)
208
+ str.respond_to?(:html_safe) ? str.html_safe : str
209
+ end
210
+
211
+ def _i18n_html_safe?(value)
212
+ value.respond_to?(:html_safe?) && value.html_safe?
213
+ end
214
+ end
215
+
216
+ include Methods
217
+
218
+ private
219
+
220
+ def _i18n
221
+ _context.i18n
222
+ end
223
+
224
+ def _resolve_i18n_key(key)
225
+ return key unless key.to_s.start_with?(".")
226
+
227
+ template_name = _context.current_template_name
228
+
229
+ unless template_name
230
+ raise(
231
+ ::I18n::ArgumentError,
232
+ "Cannot use relative translation key #{key.inspect} outside of a template render. " \
233
+ "Use an absolute key (without a leading dot) instead."
234
+ )
235
+ end
236
+
237
+ "#{template_name.tr("/", ".")}#{key}"
238
+ end
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/logger"
4
+
5
+ module Hanami
6
+ module Logger
7
+ # Rack request log formatter for Dry Logger.
8
+ #
9
+ # Formats rack request log entries with colorized output for HTTP verbs, status codes, and
10
+ # request paths, making it easier to visually scan logs in development.
11
+ #
12
+ # HTTP verbs each have a distinct color. Status codes follow traffic-light coloring (2xx green,
13
+ # 3xx cyan, 4xx yellow, 5xx red), and the request path echoes the status color so both signals
14
+ # reinforce each other at a glance.
15
+ #
16
+ # Colorization is only active when `colorize: true` is set in the logger options (the default
17
+ # in development).
18
+ #
19
+ # @api private
20
+ class RackFormatter < Dry::Logger::Formatters::String
21
+ RACK_TEMPLATE = <<~TEXT
22
+ [%<progname>s] [%<severity>s] [%<time>s] \
23
+ %<verb>s %<status>s %<elapsed>s %<ip>s %<path>s %<length>s %<payload>s
24
+ %<params>s
25
+ TEXT
26
+
27
+ VERB_COLORS = {
28
+ "GET" => :green,
29
+ "POST" => :yellow,
30
+ "PUT" => :blue,
31
+ "PATCH" => :blue,
32
+ "DELETE" => :red,
33
+ "HEAD" => :cyan
34
+ }.freeze
35
+
36
+ Colors = Dry::Logger::Formatters::Colors
37
+ private_constant :Colors
38
+
39
+ def initialize(**options)
40
+ super
41
+ @template = Dry::Logger::Formatters::Template[RACK_TEMPLATE]
42
+ end
43
+
44
+ private
45
+
46
+ def format_values(entry)
47
+ return super unless colorize?
48
+
49
+ status_color = status_color(entry.to_h[:status])
50
+
51
+ super.merge(
52
+ verb: Colors.call(VERB_COLORS.fetch(entry.to_h[:verb].to_s.upcase, :gray), entry.to_h[:verb]),
53
+ status: Colors.call(status_color, entry.to_h[:status]),
54
+ path: Colors.call(status_color, entry.to_h[:path])
55
+ )
56
+ end
57
+
58
+ def format_params(value)
59
+ value unless value.empty?
60
+ end
61
+
62
+ def status_color(status)
63
+ case status.to_i
64
+ when 200..299 then :green
65
+ when 300..399 then :cyan
66
+ when 400..499 then :yellow
67
+ when 500..599 then :red
68
+ else :gray
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/logger"
4
+
5
+ module Hanami
6
+ module Logger
7
+ # SQL query log formatter for Dry Logger.
8
+ #
9
+ # Formats SQL query log entries with a template that mirrors the structure of the built-in rack
10
+ # log formatter, providing consistent visual formatting across both HTTP request and database
11
+ # query logs.
12
+ #
13
+ # For example, in development, SQL logs alongside Rack logs:
14
+ #
15
+ # [my_app] [INFO] [2026-03-04 10:15:32] SQL sqlite 1.234ms SELECT * FROM users
16
+ # [my_app] [INFO] [2026-03-04 10:15:32] GET 200 1ms 127.0.0.1 /users -
17
+ #
18
+ # In production, the default JSON formatter handles SQL entries automatically via the structured
19
+ # payload.
20
+ #
21
+ # Supports colorization via Dry Logger's template color tags. When `colorize: true` is set in
22
+ # the logger options (the default in development), the "SQL" label is colorized, and severity
23
+ # is colorized per-level by the parent formatter (e.g. INFO => magenta, ERROR => red).
24
+ #
25
+ # When colorization is enabled and the "rouge" gem is available, SQL queries are syntax
26
+ # highlighted using Rouge's SQL lexer. This is a soft dependency; if Rouge is not installed,
27
+ # queries output as plain, unhighlighted text.
28
+ #
29
+ # The Rouge theme defaults to "pastie" and can be customized by setting the
30
+ # `HANAMI_LOG_SYNTAX_THEME` environment variable to any Rouge theme name. See
31
+ # `Rouge::Theme.registry` for available themes.
32
+ #
33
+ # @see Hanami::Logger::SQLLogger
34
+ #
35
+ # @api private
36
+ class SQLFormatter < Dry::Logger::Formatters::String
37
+ SQL_TEMPLATE = <<~TEXT
38
+ [%<progname>s] [%<severity>s] [%<time>s] SQL %<db>s %<elapsed>s%<elapsed_unit>s %<query>s
39
+ TEXT
40
+
41
+ SQL_TEMPLATE_COLORIZED = <<~TEXT
42
+ [%<progname>s] [%<severity>s] [%<time>s] <blue>SQL</blue> %<db>s %<elapsed>s%<elapsed_unit>s %<query>s
43
+ TEXT
44
+
45
+ def initialize(**options)
46
+ super
47
+ @template = Dry::Logger::Formatters::Template[
48
+ colorize? ? SQL_TEMPLATE_COLORIZED : SQL_TEMPLATE
49
+ ]
50
+ @sql_colorizer = build_sql_colorizer if colorize?
51
+ end
52
+
53
+ private
54
+
55
+ def format_query(value)
56
+ if @sql_colorizer
57
+ @sql_colorizer.call(value)
58
+ else
59
+ value
60
+ end
61
+ end
62
+
63
+ def build_sql_colorizer
64
+ begin
65
+ require "rouge"
66
+ rescue LoadError
67
+ return nil
68
+ end
69
+
70
+ theme_name = ENV.fetch("HANAMI_LOG_SYNTAX_THEME", "pastie")
71
+ theme_class = Rouge::Theme.find(theme_name) || Rouge::Themes::Pastie
72
+ formatter = Rouge::Formatters::Terminal256.new(theme_class.new)
73
+
74
+ lexer = Rouge::Lexers::SQL.new
75
+
76
+ ->(sql) { formatter.format(lexer.lex(sql)) }
77
+ end
78
+ end
79
+ end
80
+ end