hanami 3.0.0.rc1 → 3.0.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -2
- data/hanami.gemspec +1 -1
- data/lib/hanami/app.rb +12 -2
- data/lib/hanami/config/i18n.rb +6 -6
- data/lib/hanami/extensions/action/i18n_helper.rb +64 -0
- data/lib/hanami/extensions/action/name_inferrer.rb +34 -0
- data/lib/hanami/extensions/action/slice_configured_action.rb +30 -1
- data/lib/hanami/extensions/action.rb +18 -5
- data/lib/hanami/extensions/mailer/slice_configured_mailer.rb +14 -0
- data/lib/hanami/extensions/view/context.rb +1 -1
- data/lib/hanami/helpers/i18n_helper.rb +197 -132
- data/lib/hanami/logger/sql_formatter.rb +5 -5
- data/lib/hanami/providers/i18n/backend.rb +23 -19
- data/lib/hanami/providers/logger.rb +23 -1
- data/lib/hanami/settings/composite_store.rb +2 -2
- data/lib/hanami/slice.rb +32 -1
- data/lib/hanami/slice_configurable.rb +1 -3
- data/lib/hanami/universal_logger.rb +11 -11
- data/lib/hanami/version.rb +1 -1
- data/lib/hanami/web/rack_logger.rb +1 -1
- metadata +7 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 53f2d51070211158a50d0adfd0e4326c2647cd8dfb81135d74b5c09449213b24
|
|
4
|
+
data.tar.gz: f8c19c0c73096809f15176ed90c358be89dfda92adb8c418e1863c902b7835dc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a0b02ef4b8832c4de4d7ba33077ccde81a5412a1483e942fcf3ac8d179c6b5a4dc1ad23c2c597ef50cfb77f84034c6045679051c7c70e7bcc0c9ccd43f7414f0
|
|
7
|
+
data.tar.gz: 82fe7c7f69fc77c9a4cce6017a3b3e3dea90e3eb55aabc58205532c77711caab93cf2bc1824177a1d1840d5d723da12c7337a7a158390043fad9d7e5f88c7ea3
|
data/CHANGELOG.md
CHANGED
|
@@ -37,7 +37,55 @@ A complete Hanami app is composed of multiple gems. For a complete overview of c
|
|
|
37
37
|
|
|
38
38
|
### Security
|
|
39
39
|
|
|
40
|
-
[unreleased]: https://github.com/hanami/hanami/compare/v3.0.0
|
|
40
|
+
[unreleased]: https://github.com/hanami/hanami/compare/v3.0.0...HEAD
|
|
41
|
+
|
|
42
|
+
## [3.0.0] - 2026-06-30
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- Integrate hanami-mailer gem when bundled. (@timriley in #1597, #1600, #1606, #1609)
|
|
47
|
+
|
|
48
|
+
Load templates from `templates/mailers/`. Register a `"mailers.delivery_method"`, which is either an SMTP mailer when `SMTP_ADDRESS`, `SMTP_PORT`, `SMTP_USERNAME`, `SMTP_PASSWORD`, `SMTP_AUTHENTICATION` env vars are present. These can also be prefixed with a slice name. Register a test delivery always in the test env, and also in other envs when the env vars are absent.
|
|
49
|
+
- Integrate i18n gem when bundled. (@timriley in #1562, #1589, #1590, #1591, #1592, #1593)
|
|
50
|
+
|
|
51
|
+
Register an `"i18n"` component in each slice, which may be configured via `config.i18n` or a dedicated `:i18n` provider. Load translations from `config/i18n/` within each slice. Load shared translations from `config/i18n/shared/` at the app-level only. Bundle default English translations for `#localize`. Make helpers available in views and actions, which also prefix relative keys (with a leading ".") with their own dot-delimited template or action names.
|
|
52
|
+
- Wrap the configured app logger with `Hanami::UniversalLogger`, to provide a consistent logging interface regardless of the underlying logger. The app can now be depended upon to support (1) structured logging via keyword args passed to log methods, and (2) tagged logging via `#tagged`. (@timriley in #1567, #1568, #1608)
|
|
53
|
+
- Allow the built-in logger to be further configured in a `config/providers/logger.rb` provider file, useful for calling arbitrary methods on the logger, like adding backends. (@timriley in #1608)
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
Hanami.app.configure_provider :logger do
|
|
57
|
+
before :start do
|
|
58
|
+
logger.add_backend(
|
|
59
|
+
stream: Hanami.app.root.join("log", "payments.log"),
|
|
60
|
+
log_if: -> entry { entry.tag?(:payments) }
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
- Support `HANAMI_LOG_LEVEL` env var to set the log level (e.g. `HANAMI_LOG_LEVEL=warn bundle exec hanami server`). Takes precedence over `config.logger.level` set in `config/app.rb`. (@cllns in #1580)
|
|
66
|
+
- Add `config.db.log_level` setting, for changing the log level for SQL logs. (@katafrakt in #1587)
|
|
67
|
+
- New setting `:default_template_engine` that sets which template engine should be used by default when doing `hanami generate`. (@katafrakt in #1564)
|
|
68
|
+
- Add `Hanami::Settings::CompositeStore`, which can be used to chain setting lookups from multiple stores. (@aaronmallen in #1572)
|
|
69
|
+
- Add `Hanami::Slice.with_slices`, returning the slice and all its nested slices. (@timriley in #1604)
|
|
70
|
+
|
|
71
|
+
### Changed
|
|
72
|
+
|
|
73
|
+
- Default to memoizing all components, except in test env. Opt out for some or all components via `config.no_memoize`. (@timriley in #1573, #1599)
|
|
74
|
+
- Colorize logs by default in development env. (@timriley in #1566)
|
|
75
|
+
- Emit structured log entries for SQL queries, formatted consistently with request logs. (@timriley in #1569)
|
|
76
|
+
- Syntax highlight SQL in logs when the rouge gem is bundled. (@timriley in #1570, #1607)
|
|
77
|
+
- Colorize web request logs. (@timriley in #1571)
|
|
78
|
+
- Change default log level for SQL (and other database) statements from `:info` to `:debug`. (@katafrakt in #1595)
|
|
79
|
+
- Raise a helpful error message when the `Slice.call` Rack entrypoint is called and no routes are available. (@sandbergja in #1586)
|
|
80
|
+
- Apply extensions to hanami-action gem rather than hanami-controller (which is now retired). (@cllns in #1582)
|
|
81
|
+
- Redesign the new app welcome screen to match our new Hanakai visuals. (@makenosound in #1598)
|
|
82
|
+
- Require Ruby 3.3 or newer.
|
|
83
|
+
|
|
84
|
+
### Removed
|
|
85
|
+
|
|
86
|
+
- Remove default body parsing middleware. This functionality has moved into Hanami Action. (@timriley in #1575)
|
|
87
|
+
|
|
88
|
+
[3.0.0]: https://github.com/hanami/hanami/compare/v2.3.2...v3.0.0
|
|
41
89
|
|
|
42
90
|
## [3.0.0.rc1] - 2026-06-16
|
|
43
91
|
|
|
@@ -46,7 +94,7 @@ A complete Hanami app is composed of multiple gems. For a complete overview of c
|
|
|
46
94
|
- Integrate hanami-mailer gem when bundled. (@timriley in #1597, #1600)
|
|
47
95
|
|
|
48
96
|
Load templates from `templates/mailers/`. Register a `"mailers.delivery_method"`, which is either an SMTP mailer when `SMTP_ADDRESS`, `SMTP_PORT`, `SMTP_USERNAME`, `SMTP_PASSWORD`, `SMTP_AUTHENTICATION` env vars are present. These can also be prefixed with a slice name. Register a test delivery always in the test env, and also in other envs when the env vars are absent.
|
|
49
|
-
- Integrate i18n gem when bundled. (@timriley in #1562, #1589, #1590, #1591, #1592)
|
|
97
|
+
- Integrate i18n gem when bundled. (@timriley in #1562, #1589, #1590, #1591, #1592, #1593)
|
|
50
98
|
|
|
51
99
|
Register an `"i18n"` component in each slice, which may be configured via `config.i18n` or a dedicated `:i18n` provider. Load translations from `config/i18n/` within each slice. Load shared translations from `config/i18n/shared/` at the app-level only. Bundle default English translations for `#localize`. Make helpers available in views and actions, which also prefix relative keys (with a leading ".") with their own dot-delimited template or action names.
|
|
52
100
|
- Wrap the configured app logger with `Hanami::UniversalLogger`, to provide a consistent logging interface regardless of the underlying logger. The app can now be depended upon to support (1) structured logging via keyword args passed to log methods, and (2) tagged logging via `#tagged`. (@timriley in #1567, #1568)
|
data/hanami.gemspec
CHANGED
|
@@ -38,7 +38,7 @@ Gem::Specification.new do |spec|
|
|
|
38
38
|
spec.add_runtime_dependency "dry-system", "~> 1.1"
|
|
39
39
|
spec.add_runtime_dependency "dry-logger", "~> 1.2", "< 2"
|
|
40
40
|
spec.add_runtime_dependency "hanami-cli", ">= 2.3.1"
|
|
41
|
-
spec.add_runtime_dependency "hanami-utils", "
|
|
41
|
+
spec.add_runtime_dependency "hanami-utils", "~> 3.0.0"
|
|
42
42
|
spec.add_runtime_dependency "json", ">= 2.7.2"
|
|
43
43
|
spec.add_runtime_dependency "zeitwerk", "~> 2.6"
|
|
44
44
|
spec.add_runtime_dependency "rack-session"
|
data/lib/hanami/app.rb
CHANGED
|
@@ -151,12 +151,22 @@ module Hanami
|
|
|
151
151
|
require_relative "providers/inflector"
|
|
152
152
|
register_provider(:inflector, source: Hanami::Providers::Inflector)
|
|
153
153
|
|
|
154
|
-
# Allow logger to be replaced by users with a manual provider, for advanced cases
|
|
154
|
+
# Allow the logger to be replaced by users with a manual provider, for advanced cases.
|
|
155
|
+
#
|
|
156
|
+
# Require the logger provider source up front, to make `configure_provider(:logger)`
|
|
157
|
+
# possible.
|
|
158
|
+
require_relative "providers/logger"
|
|
155
159
|
unless container.providers[:logger]
|
|
156
|
-
require_relative "providers/logger"
|
|
157
160
|
register_provider(:logger, source: Hanami::Providers::Logger)
|
|
158
161
|
end
|
|
159
162
|
|
|
163
|
+
# Ensure the logger is wrapped by `Hanami::UniversalLogger`, even if manually registered in a
|
|
164
|
+
# user-defined provider, guaranteeing Hanami's structured and tagged logging interface across
|
|
165
|
+
# the framework.
|
|
166
|
+
container.providers[:logger].source.after(:start) do
|
|
167
|
+
container.decorate(:logger) { |logger| Hanami::UniversalLogger[logger] }
|
|
168
|
+
end
|
|
169
|
+
|
|
160
170
|
if Hanami.bundled?("rack")
|
|
161
171
|
require_relative "providers/rack"
|
|
162
172
|
register_provider(:rack, source: Hanami::Providers::Rack, namespace: true)
|
data/lib/hanami/config/i18n.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Hanami
|
|
|
8
8
|
# Hanami I18n config
|
|
9
9
|
#
|
|
10
10
|
# @api public
|
|
11
|
-
# @since
|
|
11
|
+
# @since 3.0.0
|
|
12
12
|
class I18n
|
|
13
13
|
include Dry::Configurable
|
|
14
14
|
|
|
@@ -23,7 +23,7 @@ module Hanami
|
|
|
23
23
|
# config.i18n.default_locale = :fr
|
|
24
24
|
#
|
|
25
25
|
# @api public
|
|
26
|
-
# @since
|
|
26
|
+
# @since 3.0.0
|
|
27
27
|
setting :default_locale, default: Providers::I18n::DEFAULT_LOCALE
|
|
28
28
|
|
|
29
29
|
# @!attribute [rw] available_locales
|
|
@@ -41,7 +41,7 @@ module Hanami
|
|
|
41
41
|
# config.i18n.available_locales = [:en, :fr, :de]
|
|
42
42
|
#
|
|
43
43
|
# @api public
|
|
44
|
-
# @since
|
|
44
|
+
# @since 3.0.0
|
|
45
45
|
setting :available_locales, default: Providers::I18n::DEFAULT_AVAILABLE_LOCALES
|
|
46
46
|
|
|
47
47
|
# @!attribute [rw] load_path
|
|
@@ -65,7 +65,7 @@ module Hanami
|
|
|
65
65
|
# config.i18n.load_path = ["translations/**/*.yml"]
|
|
66
66
|
#
|
|
67
67
|
# @api public
|
|
68
|
-
# @since
|
|
68
|
+
# @since 3.0.0
|
|
69
69
|
setting :load_path, default: Providers::I18n::DEFAULT_LOAD_PATH
|
|
70
70
|
|
|
71
71
|
# @!attribute [rw] shared_load_path
|
|
@@ -91,7 +91,7 @@ module Hanami
|
|
|
91
91
|
# config.i18n.shared_load_path += ["vendor/translations/**/*.yml"]
|
|
92
92
|
#
|
|
93
93
|
# @api public
|
|
94
|
-
# @since
|
|
94
|
+
# @since 3.0.0
|
|
95
95
|
setting :shared_load_path, default: Providers::I18n::DEFAULT_SHARED_LOAD_PATH
|
|
96
96
|
|
|
97
97
|
# @!attribute [rw] fallbacks
|
|
@@ -117,7 +117,7 @@ module Hanami
|
|
|
117
117
|
# config.i18n.fallbacks = [:en]
|
|
118
118
|
#
|
|
119
119
|
# @api public
|
|
120
|
-
# @since
|
|
120
|
+
# @since 3.0.0
|
|
121
121
|
setting :fallbacks
|
|
122
122
|
|
|
123
123
|
private
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hanami
|
|
4
|
+
module Extensions
|
|
5
|
+
module Action
|
|
6
|
+
# Action translation and localization helpers.
|
|
7
|
+
#
|
|
8
|
+
# These helpers are automatically available on `Hanami::Action` when the `i18n` gem is
|
|
9
|
+
# bundled.
|
|
10
|
+
#
|
|
11
|
+
# When relative translation keys (with a leading dot) are given, they are expanded against a
|
|
12
|
+
# the action's name. For example, `t(".not_found")` within `Main::Actions::Posts::Show`
|
|
13
|
+
# becomes `"posts.show.not_found"`.
|
|
14
|
+
#
|
|
15
|
+
# @example Basic translation in an action
|
|
16
|
+
# module Main
|
|
17
|
+
# module Actions
|
|
18
|
+
# module Posts
|
|
19
|
+
# class Create < Main::Action
|
|
20
|
+
# def handle(req, res)
|
|
21
|
+
# res.flash[:notice] = t("messages.post_created")
|
|
22
|
+
# res.redirect_to routes.path(:posts)
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# @example Relative key lookup
|
|
30
|
+
# # In Main::Actions::Posts::Show, this looks up "posts.show.not_found"
|
|
31
|
+
# t(".not_found")
|
|
32
|
+
#
|
|
33
|
+
# @see Hanami::Helpers::I18nHelper::Methods
|
|
34
|
+
#
|
|
35
|
+
# @api public
|
|
36
|
+
# @since 3.0.0
|
|
37
|
+
module I18nHelper
|
|
38
|
+
include Hanami::Helpers::I18nHelper::Methods
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def _i18n
|
|
43
|
+
i18n
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def _resolve_i18n_key(key)
|
|
47
|
+
return key unless key.to_s.start_with?(".")
|
|
48
|
+
|
|
49
|
+
key_base = self.class.config.i18n_key_base
|
|
50
|
+
|
|
51
|
+
unless key_base
|
|
52
|
+
raise(
|
|
53
|
+
::I18n::ArgumentError,
|
|
54
|
+
"Cannot use relative translation key #{key.inspect} outside of a slice-configured action. " \
|
|
55
|
+
"Use an absolute key (without a leading dot) instead."
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
"#{key_base}#{key}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hanami
|
|
4
|
+
module Extensions
|
|
5
|
+
module Action
|
|
6
|
+
# Infers an action's name (e.g. `posts.show`) from its class name relative to its slice
|
|
7
|
+
# namespace.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
class NameInferrer
|
|
11
|
+
class << self
|
|
12
|
+
# @example
|
|
13
|
+
# NameInferrer.call(action_class_name: "Main::Actions::Posts::Show", slice: Main::Slice)
|
|
14
|
+
# # => "posts.show"
|
|
15
|
+
#
|
|
16
|
+
# @param action_class_name [String, nil] the action class name
|
|
17
|
+
# @param slice [Hanami::Slice] the slice the action belongs to
|
|
18
|
+
#
|
|
19
|
+
# @return [String, nil] the inferred name, or nil if `action_class_name` is nil
|
|
20
|
+
def call(action_class_name:, slice:)
|
|
21
|
+
return nil unless action_class_name
|
|
22
|
+
|
|
23
|
+
slice
|
|
24
|
+
.inflector
|
|
25
|
+
.underscore(action_class_name)
|
|
26
|
+
.sub(%r{^#{slice.slice_name.path}#{PATH_DELIMITER}}, "")
|
|
27
|
+
.sub(%r{^#{slice.config.actions.name_inference_base}#{PATH_DELIMITER}}, "")
|
|
28
|
+
.gsub("/", CONTAINER_KEY_DELIMITER)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
module Hanami
|
|
4
4
|
module Extensions
|
|
5
5
|
module Action
|
|
6
|
+
require_relative "name_inferrer"
|
|
7
|
+
|
|
6
8
|
# Provides slice-specific configuration and behavior for any action class defined
|
|
7
9
|
# within a slice's module namespace.
|
|
8
10
|
#
|
|
9
11
|
# @api private
|
|
10
|
-
# @since 2.0.0
|
|
11
12
|
class SliceConfiguredAction < Module
|
|
12
13
|
attr_reader :slice
|
|
13
14
|
|
|
@@ -18,7 +19,9 @@ module Hanami
|
|
|
18
19
|
|
|
19
20
|
def extended(action_class)
|
|
20
21
|
configure_action(action_class)
|
|
22
|
+
configure_action_i18n(action_class)
|
|
21
23
|
extend_behavior(action_class)
|
|
24
|
+
define_inherited
|
|
22
25
|
define_new
|
|
23
26
|
end
|
|
24
27
|
|
|
@@ -34,6 +37,7 @@ module Hanami
|
|
|
34
37
|
view_context_class = method(:view_context_class)
|
|
35
38
|
resolve_routes = method(:resolve_routes)
|
|
36
39
|
resolve_rack_monitor = method(:resolve_rack_monitor)
|
|
40
|
+
resolve_i18n = method(:resolve_i18n)
|
|
37
41
|
|
|
38
42
|
define_method(:new) do |**kwargs|
|
|
39
43
|
super(
|
|
@@ -41,11 +45,24 @@ module Hanami
|
|
|
41
45
|
view_context_class: kwargs.fetch(:view_context_class) { view_context_class.() },
|
|
42
46
|
routes: kwargs.fetch(:routes) { resolve_routes.() },
|
|
43
47
|
rack_monitor: kwargs.fetch(:rack_monitor) { resolve_rack_monitor.() },
|
|
48
|
+
i18n: kwargs.fetch(:i18n) { resolve_i18n.() },
|
|
44
49
|
**kwargs,
|
|
45
50
|
)
|
|
46
51
|
end
|
|
47
52
|
end
|
|
48
53
|
|
|
54
|
+
# Defines an `inherited` hook on this module so each subclass of the slice-configured
|
|
55
|
+
# action gets its own `config.i18n_key_base`, inferred from its class name. Mirrors the
|
|
56
|
+
# approach used by `SliceConfiguredView` for `config.template`.
|
|
57
|
+
def define_inherited
|
|
58
|
+
configure_action_i18n = method(:configure_action_i18n)
|
|
59
|
+
|
|
60
|
+
define_method(:inherited) do |subclass|
|
|
61
|
+
super(subclass)
|
|
62
|
+
configure_action_i18n.call(subclass)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
49
66
|
def configure_action(action_class)
|
|
50
67
|
action_class.settings.each do |setting|
|
|
51
68
|
# Configure the action from config on the slice, _unless it has already been configured
|
|
@@ -98,6 +115,14 @@ module Hanami
|
|
|
98
115
|
end
|
|
99
116
|
end
|
|
100
117
|
|
|
118
|
+
def configure_action_i18n(action_class)
|
|
119
|
+
return unless action_class.config.respond_to?(:i18n_key_base=)
|
|
120
|
+
|
|
121
|
+
action_class.config.i18n_key_base = NameInferrer.call(
|
|
122
|
+
action_class_name: action_class.name, slice:
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
101
126
|
def extend_behavior(action_class)
|
|
102
127
|
if actions_config.sessions.enabled?
|
|
103
128
|
require "hanami/action/session"
|
|
@@ -152,6 +177,10 @@ module Hanami
|
|
|
152
177
|
slice.app["rack.monitor"] if slice.app.key?("rack.monitor")
|
|
153
178
|
end
|
|
154
179
|
|
|
180
|
+
def resolve_i18n
|
|
181
|
+
slice["i18n"] if slice.key?("i18n")
|
|
182
|
+
end
|
|
183
|
+
|
|
155
184
|
def actions_config
|
|
156
185
|
slice.config.actions
|
|
157
186
|
end
|
|
@@ -43,8 +43,7 @@ module Hanami
|
|
|
43
43
|
# @api private
|
|
44
44
|
attr_reader :view_context_class
|
|
45
45
|
|
|
46
|
-
# Returns the
|
|
47
|
-
# action instance methods.
|
|
46
|
+
# Returns the slice's {Hanami::Slice::RoutesHelper RoutesHelper}.
|
|
48
47
|
#
|
|
49
48
|
# @return [Hanami::Slice::RoutesHelper]
|
|
50
49
|
#
|
|
@@ -52,8 +51,7 @@ module Hanami
|
|
|
52
51
|
# @since 2.0.0
|
|
53
52
|
attr_reader :routes
|
|
54
53
|
|
|
55
|
-
# Returns the
|
|
56
|
-
# action instance methods.
|
|
54
|
+
# Returns the slice's `Dry::Monitor::Rack::Middleware`.
|
|
57
55
|
#
|
|
58
56
|
# @return [Dry::Monitor::Rack::Middleware]
|
|
59
57
|
#
|
|
@@ -61,6 +59,14 @@ module Hanami
|
|
|
61
59
|
# @since 2.0.0
|
|
62
60
|
attr_reader :rack_monitor
|
|
63
61
|
|
|
62
|
+
# Returns the slice's i18n backend.
|
|
63
|
+
#
|
|
64
|
+
# @return [Hanami::Providers::I18n::Backend]
|
|
65
|
+
#
|
|
66
|
+
# @api public
|
|
67
|
+
# @since x.x.x
|
|
68
|
+
attr_reader :i18n
|
|
69
|
+
|
|
64
70
|
# @overload def initialize(routes: nil, **kwargs)
|
|
65
71
|
# Returns a new `Hanami::Action` with app components injected as dependencies.
|
|
66
72
|
#
|
|
@@ -71,11 +77,12 @@ module Hanami
|
|
|
71
77
|
#
|
|
72
78
|
# @api public
|
|
73
79
|
# @since 2.0.0
|
|
74
|
-
def initialize(view: nil, view_context_class: nil, rack_monitor: nil, routes: nil, **kwargs)
|
|
80
|
+
def initialize(view: nil, view_context_class: nil, rack_monitor: nil, routes: nil, i18n: nil, **kwargs)
|
|
75
81
|
@view = view
|
|
76
82
|
@view_context_class = view_context_class
|
|
77
83
|
@routes = routes
|
|
78
84
|
@rack_monitor = rack_monitor
|
|
85
|
+
@i18n = i18n
|
|
79
86
|
|
|
80
87
|
super(**kwargs)
|
|
81
88
|
end
|
|
@@ -132,3 +139,9 @@ module Hanami
|
|
|
132
139
|
end
|
|
133
140
|
|
|
134
141
|
Hanami::Action.include(Hanami::Extensions::Action)
|
|
142
|
+
|
|
143
|
+
if Hanami.bundled?("i18n")
|
|
144
|
+
require_relative "action/i18n_helper"
|
|
145
|
+
Hanami::Action.setting(:i18n_key_base)
|
|
146
|
+
Hanami::Action.include(Hanami::Extensions::Action::I18nHelper)
|
|
147
|
+
end
|
|
@@ -25,6 +25,7 @@ module Hanami
|
|
|
25
25
|
def extended(mailer_class)
|
|
26
26
|
configure_mailer(mailer_class)
|
|
27
27
|
define_new
|
|
28
|
+
define_inherited
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
def inspect
|
|
@@ -44,6 +45,19 @@ module Hanami
|
|
|
44
45
|
end
|
|
45
46
|
end
|
|
46
47
|
|
|
48
|
+
# Reconfigures the template name for each subclass.
|
|
49
|
+
def define_inherited
|
|
50
|
+
template_name = method(:template_name)
|
|
51
|
+
|
|
52
|
+
define_method(:inherited) do |subclass|
|
|
53
|
+
super(subclass)
|
|
54
|
+
|
|
55
|
+
if (template = template_name.(subclass))
|
|
56
|
+
subclass.config.template = template
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
47
61
|
def resolve_delivery_method
|
|
48
62
|
slice["mailers.delivery_method"] if slice.key?("mailers.delivery_method")
|
|
49
63
|
end
|
|
@@ -203,7 +203,7 @@ module Hanami
|
|
|
203
203
|
# @raise [Hanami::ComponentLoadError] if the i18n gem is not bundled
|
|
204
204
|
#
|
|
205
205
|
# @api public
|
|
206
|
-
# @since
|
|
206
|
+
# @since 3.0.0
|
|
207
207
|
def i18n
|
|
208
208
|
unless @i18n
|
|
209
209
|
raise Hanami::ComponentLoadError, "the i18n gem is required to access translations"
|
|
@@ -1,140 +1,226 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "cgi/escape"
|
|
4
4
|
|
|
5
5
|
module Hanami
|
|
6
6
|
module Helpers
|
|
7
|
-
#
|
|
7
|
+
# View-layer translation and localization helpers.
|
|
8
8
|
#
|
|
9
|
-
# These helpers
|
|
10
|
-
# classes when the `i18n` gem is bundled.
|
|
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><script></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"
|
|
11
42
|
#
|
|
12
43
|
# @api public
|
|
13
|
-
# @since
|
|
44
|
+
# @since 3.0.0
|
|
14
45
|
module I18nHelper
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# HTML-escaped before substitution.
|
|
18
|
-
#
|
|
19
|
-
# @api private
|
|
20
|
-
HTML_SAFE_TRANSLATION_KEY = /(\b|_)html\z/
|
|
21
|
-
|
|
22
|
-
# Translates the given key using the slice's i18n backend.
|
|
23
|
-
#
|
|
24
|
-
# When the key's final segment is `html` or ends in `_html`, the result is marked HTML-safe
|
|
25
|
-
# and any string interpolation values are HTML-escaped first.
|
|
46
|
+
# Shared `translate` / `localize` (and shorthand) helper methods used by both view-layer
|
|
47
|
+
# consumers and action-layer consumers.
|
|
26
48
|
#
|
|
27
|
-
#
|
|
28
|
-
# `<span class="translation_missing">` element containing the missing key, useful for
|
|
29
|
-
# spotting missing translations during development.
|
|
49
|
+
# **This module is abstract.** Including modules must override two private hooks:
|
|
30
50
|
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
# `users/index.html.erb` resolves to `users.index.title`, and `translate(".label")`
|
|
35
|
-
# inside `users/_form.html.erb` resolves to `users._form.label`. Raises
|
|
36
|
-
# `I18n::ArgumentError` if called outside a template render.
|
|
37
|
-
#
|
|
38
|
-
# @param key [String, Symbol] the translation key to look up
|
|
39
|
-
# @param options [Hash] translation options forwarded to the backend (`:locale`, `:scope`,
|
|
40
|
-
# `:default`, `:count`, `:raise`, etc.), plus any interpolation values
|
|
41
|
-
#
|
|
42
|
-
# @return [String, Hanami::View::HTML::SafeString] the translated string
|
|
43
|
-
#
|
|
44
|
-
# @example Basic translation
|
|
45
|
-
# <%= translate("messages.welcome") %>
|
|
46
|
-
# # => "Welcome"
|
|
47
|
-
#
|
|
48
|
-
# @example HTML-safe translation
|
|
49
|
-
# # en.yml
|
|
50
|
-
# # greeting_html: "Hello, <strong>%{name}</strong>!"
|
|
51
|
-
# <%= translate("greeting_html", name: "<script>") %>
|
|
52
|
-
# # => "Hello, <strong><script></strong>!" (marked HTML-safe)
|
|
53
|
-
#
|
|
54
|
-
# @example Missing translation
|
|
55
|
-
# <%= translate("missing.key") %>
|
|
56
|
-
# # => '<span class="translation_missing" title="...">missing.key</span>'
|
|
57
|
-
#
|
|
58
|
-
# @example Relative key lookup
|
|
59
|
-
# # In app/templates/users/index.html.erb:
|
|
60
|
-
# <%= translate(".title") %>
|
|
61
|
-
# # Looks up "users.index.title"
|
|
62
|
-
#
|
|
63
|
-
# # In app/templates/users/_form.html.erb (a partial):
|
|
64
|
-
# <%= translate(".label") %>
|
|
65
|
-
# # Looks up "users._form.label"
|
|
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.
|
|
66
54
|
#
|
|
67
55
|
# @api public
|
|
68
|
-
# @since
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
71
103
|
|
|
72
|
-
|
|
104
|
+
html_safe ? _i18n_mark_html_safe(result.to_s) : result
|
|
105
|
+
end
|
|
73
106
|
|
|
74
|
-
|
|
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
|
|
75
125
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
86
143
|
|
|
87
|
-
|
|
88
|
-
|
|
144
|
+
# @api public
|
|
145
|
+
# @since 3.0.0
|
|
146
|
+
alias_method :l, :localize
|
|
89
147
|
|
|
90
|
-
|
|
91
|
-
# @since x.x.x
|
|
92
|
-
alias_method :t, :translate
|
|
148
|
+
private
|
|
93
149
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
# @example
|
|
103
|
-
# <%= translate!("messages.welcome") %>
|
|
104
|
-
#
|
|
105
|
-
# @api public
|
|
106
|
-
# @since x.x.x
|
|
107
|
-
def translate!(key, **options)
|
|
108
|
-
translate(key, **options, raise: true)
|
|
109
|
-
end
|
|
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
|
|
110
158
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
114
169
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
130
214
|
end
|
|
131
215
|
|
|
132
|
-
|
|
133
|
-
# @since x.x.x
|
|
134
|
-
alias_method :l, :localize
|
|
216
|
+
include Methods
|
|
135
217
|
|
|
136
218
|
private
|
|
137
219
|
|
|
220
|
+
def _i18n
|
|
221
|
+
_context.i18n
|
|
222
|
+
end
|
|
223
|
+
|
|
138
224
|
def _resolve_i18n_key(key)
|
|
139
225
|
return key unless key.to_s.start_with?(".")
|
|
140
226
|
|
|
@@ -150,27 +236,6 @@ module Hanami
|
|
|
150
236
|
|
|
151
237
|
"#{template_name.tr("/", ".")}#{key}"
|
|
152
238
|
end
|
|
153
|
-
|
|
154
|
-
def _html_safe_translation_key?(key)
|
|
155
|
-
HTML_SAFE_TRANSLATION_KEY.match?(key.to_s)
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def _escape_translation_options(options)
|
|
159
|
-
options.each_with_object({}) do |(key, value), result|
|
|
160
|
-
result[key] =
|
|
161
|
-
if ::I18n::RESERVED_KEYS.include?(key) || !value.is_a?(String) || value.html_safe?
|
|
162
|
-
value
|
|
163
|
-
else
|
|
164
|
-
escape_html(value)
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
def _missing_translation_markup(key, error)
|
|
170
|
-
title = escape_html(error.message)
|
|
171
|
-
body = escape_html(key.to_s)
|
|
172
|
-
%(<span class="translation_missing" title="#{title}">#{body}</span>).html_safe
|
|
173
|
-
end
|
|
174
239
|
end
|
|
175
240
|
end
|
|
176
241
|
end
|
|
@@ -26,9 +26,9 @@ module Hanami
|
|
|
26
26
|
# highlighted using Rouge's SQL lexer. This is a soft dependency; if Rouge is not installed,
|
|
27
27
|
# queries output as plain, unhighlighted text.
|
|
28
28
|
#
|
|
29
|
-
# The Rouge theme defaults to
|
|
30
|
-
# environment variable to any Rouge theme name
|
|
31
|
-
#
|
|
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
32
|
#
|
|
33
33
|
# @see Hanami::Logger::SQLLogger
|
|
34
34
|
#
|
|
@@ -67,8 +67,8 @@ module Hanami
|
|
|
67
67
|
return nil
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
theme_name = ENV.fetch("
|
|
71
|
-
theme_class = Rouge::Theme.find(theme_name) || Rouge::Themes::
|
|
70
|
+
theme_name = ENV.fetch("HANAMI_LOG_SYNTAX_THEME", "pastie")
|
|
71
|
+
theme_class = Rouge::Theme.find(theme_name) || Rouge::Themes::Pastie
|
|
72
72
|
formatter = Rouge::Formatters::Terminal256.new(theme_class.new)
|
|
73
73
|
|
|
74
74
|
lexer = Rouge::Lexers::SQL.new
|
|
@@ -10,7 +10,7 @@ module Hanami
|
|
|
10
10
|
# this wrapper maintains per-instance state for true isolation between slices.
|
|
11
11
|
#
|
|
12
12
|
# @api public
|
|
13
|
-
# @since
|
|
13
|
+
# @since 3.0.0
|
|
14
14
|
class Backend
|
|
15
15
|
attr_reader :backend
|
|
16
16
|
|
|
@@ -19,7 +19,7 @@ module Hanami
|
|
|
19
19
|
# @return [Symbol] the default locale
|
|
20
20
|
#
|
|
21
21
|
# @api public
|
|
22
|
-
# @since
|
|
22
|
+
# @since 3.0.0
|
|
23
23
|
attr_reader :default_locale
|
|
24
24
|
|
|
25
25
|
# Creates a new Backend instance.
|
|
@@ -31,7 +31,7 @@ module Hanami
|
|
|
31
31
|
# @param fallbacks [I18n::Locale::Fallbacks, nil] fallbacks configuration for missing translations
|
|
32
32
|
#
|
|
33
33
|
# @api private
|
|
34
|
-
# @since
|
|
34
|
+
# @since 3.0.0
|
|
35
35
|
def initialize(backend, locale: nil, default_locale: :en, available_locales: [], fallbacks: nil)
|
|
36
36
|
@backend = backend
|
|
37
37
|
@default_locale = default_locale.to_sym
|
|
@@ -69,7 +69,7 @@ module Hanami
|
|
|
69
69
|
# translate("hello", locale: :fr) # => "Bonjour"
|
|
70
70
|
#
|
|
71
71
|
# @api public
|
|
72
|
-
# @since
|
|
72
|
+
# @since 3.0.0
|
|
73
73
|
def translate(key, **options)
|
|
74
74
|
locale = options[:locale] || self.locale
|
|
75
75
|
|
|
@@ -105,7 +105,7 @@ module Hanami
|
|
|
105
105
|
# rubocop:enable Metrics/PerceivedComplexity
|
|
106
106
|
|
|
107
107
|
# @api public
|
|
108
|
-
# @since
|
|
108
|
+
# @since 3.0.0
|
|
109
109
|
alias_method :t, :translate
|
|
110
110
|
|
|
111
111
|
# Translates the given key, raising an exception if translation is missing.
|
|
@@ -118,15 +118,19 @@ module Hanami
|
|
|
118
118
|
# @raise [I18n::MissingTranslationData] if translation is missing
|
|
119
119
|
#
|
|
120
120
|
# @example
|
|
121
|
-
#
|
|
122
|
-
#
|
|
121
|
+
# translate!("hello") # => "Hello"
|
|
122
|
+
# translate!("missing.key") # raises I18n::MissingTranslationData
|
|
123
123
|
#
|
|
124
124
|
# @api public
|
|
125
|
-
# @since
|
|
126
|
-
def
|
|
125
|
+
# @since 3.0.0
|
|
126
|
+
def translate!(key, **options)
|
|
127
127
|
translate(key, **options.merge(raise: true))
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
+
# @api public
|
|
131
|
+
# @since 3.0.0
|
|
132
|
+
alias_method :t!, :translate!
|
|
133
|
+
|
|
130
134
|
# Localizes the given date or time.
|
|
131
135
|
#
|
|
132
136
|
# Resolves symbol formats through this instance's translations (e.g. `:short` becomes
|
|
@@ -147,7 +151,7 @@ module Hanami
|
|
|
147
151
|
# localize(Date.today, locale: :fr, format: :long) # => "19 janvier 2026"
|
|
148
152
|
#
|
|
149
153
|
# @api public
|
|
150
|
-
# @since
|
|
154
|
+
# @since 3.0.0
|
|
151
155
|
def localize(object, locale: nil, format: :default, **options)
|
|
152
156
|
locale ||= self.locale
|
|
153
157
|
|
|
@@ -172,7 +176,7 @@ module Hanami
|
|
|
172
176
|
end
|
|
173
177
|
|
|
174
178
|
# @api public
|
|
175
|
-
# @since
|
|
179
|
+
# @since 3.0.0
|
|
176
180
|
alias_method :l, :localize
|
|
177
181
|
|
|
178
182
|
# Returns true if a translation exists for the given key.
|
|
@@ -188,7 +192,7 @@ module Hanami
|
|
|
188
192
|
# exists?("missing.key") # => false
|
|
189
193
|
#
|
|
190
194
|
# @api public
|
|
191
|
-
# @since
|
|
195
|
+
# @since 3.0.0
|
|
192
196
|
def exists?(key, locale: nil, **options)
|
|
193
197
|
locale ||= self.locale
|
|
194
198
|
@backend.exists?(locale, key, options)
|
|
@@ -207,7 +211,7 @@ module Hanami
|
|
|
207
211
|
# transliterate("Ærøskøbing") # => "AEroskobing"
|
|
208
212
|
#
|
|
209
213
|
# @api public
|
|
210
|
-
# @since
|
|
214
|
+
# @since 3.0.0
|
|
211
215
|
def transliterate(key, locale: nil, replacement: nil, **options)
|
|
212
216
|
locale ||= self.locale
|
|
213
217
|
@backend.transliterate(locale, key, replacement)
|
|
@@ -219,7 +223,7 @@ module Hanami
|
|
|
219
223
|
# Otherwise, returns all locales detected from loaded translation files.
|
|
220
224
|
#
|
|
221
225
|
# @api public
|
|
222
|
-
# @since
|
|
226
|
+
# @since 3.0.0
|
|
223
227
|
def available_locales
|
|
224
228
|
if @available_locales.any?
|
|
225
229
|
@available_locales
|
|
@@ -233,7 +237,7 @@ module Hanami
|
|
|
233
237
|
# @return [void]
|
|
234
238
|
#
|
|
235
239
|
# @api public
|
|
236
|
-
# @since
|
|
240
|
+
# @since 3.0.0
|
|
237
241
|
def reload!
|
|
238
242
|
@backend.reload!
|
|
239
243
|
end
|
|
@@ -243,7 +247,7 @@ module Hanami
|
|
|
243
247
|
# @return [void]
|
|
244
248
|
#
|
|
245
249
|
# @api public
|
|
246
|
-
# @since
|
|
250
|
+
# @since 3.0.0
|
|
247
251
|
def eager_load!
|
|
248
252
|
@backend.eager_load! if @backend.respond_to?(:eager_load!)
|
|
249
253
|
end
|
|
@@ -261,7 +265,7 @@ module Hanami
|
|
|
261
265
|
# locale # => :fr
|
|
262
266
|
#
|
|
263
267
|
# @api public
|
|
264
|
-
# @since
|
|
268
|
+
# @since 3.0.0
|
|
265
269
|
def locale
|
|
266
270
|
Thread.current[@storage_key] || default_locale
|
|
267
271
|
end
|
|
@@ -281,7 +285,7 @@ module Hanami
|
|
|
281
285
|
# self.locale = nil # resets to default_locale
|
|
282
286
|
#
|
|
283
287
|
# @api public
|
|
284
|
-
# @since
|
|
288
|
+
# @since 3.0.0
|
|
285
289
|
def locale=(value)
|
|
286
290
|
Thread.current[@storage_key] = value&.to_sym
|
|
287
291
|
end
|
|
@@ -312,7 +316,7 @@ module Hanami
|
|
|
312
316
|
# end
|
|
313
317
|
#
|
|
314
318
|
# @api public
|
|
315
|
-
# @since
|
|
319
|
+
# @since 3.0.0
|
|
316
320
|
def with_locale(tmp_locale)
|
|
317
321
|
previous_locale = Thread.current[@storage_key]
|
|
318
322
|
Thread.current[@storage_key] = tmp_locale&.to_sym
|
|
@@ -12,8 +12,30 @@ module Hanami
|
|
|
12
12
|
class Logger < Hanami::Provider::Source
|
|
13
13
|
# @api private
|
|
14
14
|
def start
|
|
15
|
-
register :logger,
|
|
15
|
+
register :logger, logger
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Returns the logger instance that will be registered as the slice's `"logger"` component.
|
|
19
|
+
#
|
|
20
|
+
# This is memoized, so lifecycle callbacks added when extending the provider via
|
|
21
|
+
# {Hanami::Slice::ClassMethods#configure_provider} (such as a `before :start` or `after :start`
|
|
22
|
+
# hook) can access and customize the very same logger that `start` registers — for example, to
|
|
23
|
+
# add a logging backend.
|
|
24
|
+
#
|
|
25
|
+
# @return [Dry::Logger::Dispatcher] the default logger, or the logger instance configured via
|
|
26
|
+
# {Hanami::Config#logger=}
|
|
27
|
+
#
|
|
28
|
+
# @api public
|
|
29
|
+
# @since 3.0.0
|
|
30
|
+
def logger
|
|
31
|
+
@logger ||= Hanami.app.config.logger_instance
|
|
16
32
|
end
|
|
17
33
|
end
|
|
34
|
+
|
|
35
|
+
Dry::System.register_provider_source(
|
|
36
|
+
:logger,
|
|
37
|
+
source: Logger,
|
|
38
|
+
group: :hanami
|
|
39
|
+
)
|
|
18
40
|
end
|
|
19
41
|
end
|
|
@@ -17,7 +17,7 @@ module Hanami
|
|
|
17
17
|
# )
|
|
18
18
|
#
|
|
19
19
|
# @api public
|
|
20
|
-
# @since
|
|
20
|
+
# @since 3.0.0
|
|
21
21
|
class CompositeStore
|
|
22
22
|
# @api private
|
|
23
23
|
Undefined = Dry::Core::Constants::Undefined
|
|
@@ -36,7 +36,7 @@ module Hanami
|
|
|
36
36
|
# @raise [KeyError] if no store has the key and no default is given
|
|
37
37
|
#
|
|
38
38
|
# @api public
|
|
39
|
-
# @since
|
|
39
|
+
# @since 3.0.0
|
|
40
40
|
def fetch(name, *args, &block)
|
|
41
41
|
@stores.each do |store|
|
|
42
42
|
value = store.fetch(name, Undefined)
|
data/lib/hanami/slice.rb
CHANGED
|
@@ -394,6 +394,26 @@ module Hanami
|
|
|
394
394
|
@slices ||= SliceRegistrar.new(self)
|
|
395
395
|
end
|
|
396
396
|
|
|
397
|
+
# Returns an array of this slice and all of its nested slices.
|
|
398
|
+
#
|
|
399
|
+
# The nested slices are returned first (with their more specific namespaces ahead of their
|
|
400
|
+
# parents'), and this slice is returned last. This ordering means a class can be matched to
|
|
401
|
+
# its most specific slice by detecting the first slice whose namespace it belongs to. It is
|
|
402
|
+
# useful when you need to operate across every container, such as in test support code.
|
|
403
|
+
#
|
|
404
|
+
# @example
|
|
405
|
+
# Hanami.app.with_slices # => [Main::Nested::Slice, Main::Slice, Admin::Slice, MyApp::App]
|
|
406
|
+
#
|
|
407
|
+
# @return [Array<Hanami::Slice>] this slice and all of its (nested) slices, this slice last
|
|
408
|
+
#
|
|
409
|
+
# @see #slices
|
|
410
|
+
#
|
|
411
|
+
# @api public
|
|
412
|
+
# @since 3.0.0
|
|
413
|
+
def with_slices
|
|
414
|
+
slices.with_nested + [self]
|
|
415
|
+
end
|
|
416
|
+
|
|
397
417
|
# @overload register_slice(name, &block)
|
|
398
418
|
# Registers a nested slice with the given name.
|
|
399
419
|
#
|
|
@@ -1009,7 +1029,7 @@ module Hanami
|
|
|
1009
1029
|
require_relative "providers/mailers"
|
|
1010
1030
|
|
|
1011
1031
|
# Only register the provider if the user hasn't provided their own.
|
|
1012
|
-
|
|
1032
|
+
if register_mailers_provider? && !container.providers[:mailers]
|
|
1013
1033
|
register_provider(:mailers, namespace: true, source: Providers::Mailers)
|
|
1014
1034
|
end
|
|
1015
1035
|
end
|
|
@@ -1172,6 +1192,17 @@ module Hanami
|
|
|
1172
1192
|
!config.shared_app_component_keys.include?("i18n")
|
|
1173
1193
|
end
|
|
1174
1194
|
|
|
1195
|
+
# Ensures a mailers provider is available in every slice.
|
|
1196
|
+
#
|
|
1197
|
+
# For the app, this will always be a standalone provider. For slices, this will be a
|
|
1198
|
+
# standalone provider unless the slice is configured to share the app's
|
|
1199
|
+
# "mailers.delivery_method" component.
|
|
1200
|
+
def register_mailers_provider?
|
|
1201
|
+
return true if self == app
|
|
1202
|
+
|
|
1203
|
+
!config.shared_app_component_keys.include?("mailers.delivery_method")
|
|
1204
|
+
end
|
|
1205
|
+
|
|
1175
1206
|
def register_db_provider?
|
|
1176
1207
|
concrete_db_provider? ||
|
|
1177
1208
|
db_config_dir? ||
|
|
@@ -57,9 +57,7 @@ module Hanami
|
|
|
57
57
|
def slice_for(klass)
|
|
58
58
|
return unless klass.name
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
slices.detect { |slice| klass.name.start_with?("#{slice.namespace}#{MODULE_DELIMITER}") }
|
|
60
|
+
Hanami.app.with_slices.detect { |slice| klass.name.start_with?("#{slice.namespace}#{MODULE_DELIMITER}") }
|
|
63
61
|
end
|
|
64
62
|
end
|
|
65
63
|
|
|
@@ -23,7 +23,7 @@ module Hanami
|
|
|
23
23
|
# This adapter is used for all loggers configured in Hanami apps.
|
|
24
24
|
#
|
|
25
25
|
# @api public
|
|
26
|
-
# @since
|
|
26
|
+
# @since 3.0.0
|
|
27
27
|
class UniversalLogger
|
|
28
28
|
class << self
|
|
29
29
|
# Wrap a logger if needed, or return it as-is if fully compatible.
|
|
@@ -59,7 +59,7 @@ module Hanami
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
# @api public
|
|
62
|
-
# @since
|
|
62
|
+
# @since 3.0.0
|
|
63
63
|
attr_reader :logger
|
|
64
64
|
|
|
65
65
|
# @api private
|
|
@@ -82,7 +82,7 @@ module Hanami
|
|
|
82
82
|
# @return [void]
|
|
83
83
|
#
|
|
84
84
|
# @api public
|
|
85
|
-
# @since
|
|
85
|
+
# @since 3.0.0
|
|
86
86
|
|
|
87
87
|
# @!method info(message = nil, **payload, &blk)
|
|
88
88
|
# Logs an info message.
|
|
@@ -92,7 +92,7 @@ module Hanami
|
|
|
92
92
|
# @yieldreturn [Hash] additional payload data to merge
|
|
93
93
|
# @return [void]
|
|
94
94
|
# @api public
|
|
95
|
-
# @since
|
|
95
|
+
# @since 3.0.0
|
|
96
96
|
|
|
97
97
|
# @!method warn(message = nil, **payload, &blk)
|
|
98
98
|
# Logs a warning message.
|
|
@@ -103,7 +103,7 @@ module Hanami
|
|
|
103
103
|
# @return [void]
|
|
104
104
|
#
|
|
105
105
|
# @api public
|
|
106
|
-
# @since
|
|
106
|
+
# @since 3.0.0
|
|
107
107
|
|
|
108
108
|
# @!method error(message = nil, **payload, &blk)
|
|
109
109
|
# Logs an error message.
|
|
@@ -114,7 +114,7 @@ module Hanami
|
|
|
114
114
|
# @return [void]
|
|
115
115
|
#
|
|
116
116
|
# @api public
|
|
117
|
-
# @since
|
|
117
|
+
# @since 3.0.0
|
|
118
118
|
|
|
119
119
|
# @!method fatal(message = nil, **payload, &blk)
|
|
120
120
|
# Logs a fatal message.
|
|
@@ -125,7 +125,7 @@ module Hanami
|
|
|
125
125
|
# @return [void]
|
|
126
126
|
#
|
|
127
127
|
# @api public
|
|
128
|
-
# @since
|
|
128
|
+
# @since 3.0.0
|
|
129
129
|
|
|
130
130
|
# @!method unknown(message = nil, **payload, &blk)
|
|
131
131
|
# Logs a message with unknown severity.
|
|
@@ -136,7 +136,7 @@ module Hanami
|
|
|
136
136
|
# @return [void]
|
|
137
137
|
#
|
|
138
138
|
# @api public
|
|
139
|
-
# @since
|
|
139
|
+
# @since 3.0.0
|
|
140
140
|
|
|
141
141
|
LOG_LEVEL_METHODS.each do |level|
|
|
142
142
|
define_method(level) do |message = nil, **payload, &blk|
|
|
@@ -145,7 +145,7 @@ module Hanami
|
|
|
145
145
|
end
|
|
146
146
|
|
|
147
147
|
# @api public
|
|
148
|
-
# @since
|
|
148
|
+
# @since 3.0.0
|
|
149
149
|
def add(severity, message = nil, progname = nil, &blk)
|
|
150
150
|
# Convert severity to a level symbol if it's an integer (the standard Logger uses integers).
|
|
151
151
|
level = _severity_to_level(severity)
|
|
@@ -157,11 +157,11 @@ module Hanami
|
|
|
157
157
|
end
|
|
158
158
|
|
|
159
159
|
# @api public
|
|
160
|
-
# @since
|
|
160
|
+
# @since 3.0.0
|
|
161
161
|
alias_method :log, :add
|
|
162
162
|
|
|
163
163
|
# @api public
|
|
164
|
-
# @since
|
|
164
|
+
# @since 3.0.0
|
|
165
165
|
def tagged(*tags)
|
|
166
166
|
previous_tags = _current_tags
|
|
167
167
|
self._current_tags = tags
|
data/lib/hanami/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hanami
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.0.0
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hanakai team
|
|
@@ -173,16 +173,16 @@ dependencies:
|
|
|
173
173
|
name: hanami-utils
|
|
174
174
|
requirement: !ruby/object:Gem::Requirement
|
|
175
175
|
requirements:
|
|
176
|
-
- - "
|
|
176
|
+
- - "~>"
|
|
177
177
|
- !ruby/object:Gem::Version
|
|
178
|
-
version:
|
|
178
|
+
version: 3.0.0
|
|
179
179
|
type: :runtime
|
|
180
180
|
prerelease: false
|
|
181
181
|
version_requirements: !ruby/object:Gem::Requirement
|
|
182
182
|
requirements:
|
|
183
|
-
- - "
|
|
183
|
+
- - "~>"
|
|
184
184
|
- !ruby/object:Gem::Version
|
|
185
|
-
version:
|
|
185
|
+
version: 3.0.0
|
|
186
186
|
- !ruby/object:Gem::Dependency
|
|
187
187
|
name: json
|
|
188
188
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -302,6 +302,8 @@ files:
|
|
|
302
302
|
- lib/hanami/errors.rb
|
|
303
303
|
- lib/hanami/extensions.rb
|
|
304
304
|
- lib/hanami/extensions/action.rb
|
|
305
|
+
- lib/hanami/extensions/action/i18n_helper.rb
|
|
306
|
+
- lib/hanami/extensions/action/name_inferrer.rb
|
|
305
307
|
- lib/hanami/extensions/action/slice_configured_action.rb
|
|
306
308
|
- lib/hanami/extensions/db/repo.rb
|
|
307
309
|
- lib/hanami/extensions/mailer.rb
|