hanami 2.3.2 → 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 +103 -19
- data/LICENSE +20 -0
- data/README.md +18 -35
- data/hanami.gemspec +36 -37
- data/lib/hanami/app.rb +12 -2
- data/lib/hanami/config/db.rb +2 -0
- data/lib/hanami/config/i18n.rb +138 -0
- data/lib/hanami/config/logger.rb +15 -7
- data/lib/hanami/config/null_config.rb +1 -1
- data/lib/hanami/config/views.rb +17 -0
- data/lib/hanami/config.rb +66 -22
- data/lib/hanami/errors.rb +6 -0
- 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 +31 -2
- data/lib/hanami/extensions/action.rb +20 -7
- data/lib/hanami/extensions/mailer/slice_configured_mailer.rb +134 -0
- data/lib/hanami/extensions/mailer.rb +28 -0
- data/lib/hanami/extensions/operation/slice_configured_db_operation.rb +2 -0
- data/lib/hanami/extensions/view/context.rb +26 -4
- data/lib/hanami/extensions/view/part.rb +2 -0
- data/lib/hanami/extensions/view/slice_configured_context.rb +7 -0
- data/lib/hanami/extensions/view/slice_configured_part.rb +2 -0
- data/lib/hanami/extensions/view/slice_configured_view.rb +8 -8
- data/lib/hanami/extensions/view/standard_helpers.rb +4 -0
- data/lib/hanami/extensions.rb +6 -1
- data/lib/hanami/helpers/assets_helper.rb +0 -4
- data/lib/hanami/helpers/form_helper.rb +1 -1
- data/lib/hanami/helpers/i18n_helper.rb +241 -0
- data/lib/hanami/logger/rack_formatter.rb +73 -0
- data/lib/hanami/logger/sql_formatter.rb +80 -0
- data/lib/hanami/logger/sql_logger.rb +48 -0
- data/lib/hanami/middleware/render_errors.rb +2 -2
- data/lib/hanami/providers/db.rb +7 -2
- data/lib/hanami/providers/db_logging.rb +4 -7
- data/lib/hanami/providers/i18n/backend.rb +373 -0
- data/lib/hanami/providers/i18n/locale/en.yml +57 -0
- data/lib/hanami/providers/i18n.rb +114 -0
- data/lib/hanami/providers/logger.rb +23 -1
- data/lib/hanami/providers/mailers.rb +101 -0
- data/lib/hanami/routes.rb +1 -0
- data/lib/hanami/settings/composite_store.rb +53 -0
- data/lib/hanami/settings.rb +4 -4
- data/lib/hanami/slice/router.rb +15 -10
- data/lib/hanami/slice.rb +102 -11
- data/lib/hanami/slice_configurable.rb +1 -3
- data/lib/hanami/slice_registrar.rb +2 -2
- data/lib/hanami/universal_logger.rb +250 -0
- data/lib/hanami/version.rb +1 -1
- data/lib/hanami/web/rack_logger.rb +2 -80
- data/lib/hanami/web/welcome.html.erb +443 -58
- data/lib/hanami.rb +4 -2
- metadata +34 -280
- data/CODE_OF_CONDUCT.md +0 -74
- data/FEATURES.md +0 -269
- data/LICENSE.md +0 -22
- data/spec/integration/action/cookies_spec.rb +0 -58
- data/spec/integration/action/csrf_protection_spec.rb +0 -54
- data/spec/integration/action/format_config_spec.rb +0 -129
- data/spec/integration/action/routes_spec.rb +0 -71
- data/spec/integration/action/sessions_spec.rb +0 -50
- data/spec/integration/action/slice_configuration_spec.rb +0 -284
- data/spec/integration/action/view_rendering/automatic_rendering_spec.rb +0 -247
- data/spec/integration/action/view_rendering/paired_view_inference_spec.rb +0 -115
- data/spec/integration/action/view_rendering/view_context_spec.rb +0 -221
- data/spec/integration/action/view_rendering_spec.rb +0 -89
- data/spec/integration/assets/assets_spec.rb +0 -155
- data/spec/integration/assets/cross_slice_assets_helpers_spec.rb +0 -129
- data/spec/integration/assets/serve_static_assets_spec.rb +0 -152
- data/spec/integration/code_loading/loading_from_app_spec.rb +0 -152
- data/spec/integration/code_loading/loading_from_lib_spec.rb +0 -242
- data/spec/integration/code_loading/loading_from_slice_spec.rb +0 -165
- data/spec/integration/container/application_routes_helper_spec.rb +0 -48
- data/spec/integration/container/auto_injection_spec.rb +0 -53
- data/spec/integration/container/auto_registration_spec.rb +0 -86
- data/spec/integration/container/autoloader_spec.rb +0 -82
- data/spec/integration/container/imports_spec.rb +0 -253
- data/spec/integration/container/prepare_container_spec.rb +0 -125
- data/spec/integration/container/provider_environment_spec.rb +0 -52
- data/spec/integration/container/provider_lifecycle_spec.rb +0 -61
- data/spec/integration/container/shutdown_spec.rb +0 -91
- data/spec/integration/container/standard_providers/rack_provider_spec.rb +0 -44
- data/spec/integration/container/standard_providers_spec.rb +0 -124
- data/spec/integration/db/auto_registration_spec.rb +0 -39
- data/spec/integration/db/commands_spec.rb +0 -80
- data/spec/integration/db/db_inflector_spec.rb +0 -57
- data/spec/integration/db/db_slices_spec.rb +0 -398
- data/spec/integration/db/db_spec.rb +0 -245
- data/spec/integration/db/gateways_spec.rb +0 -361
- data/spec/integration/db/logging_spec.rb +0 -301
- data/spec/integration/db/mappers_spec.rb +0 -84
- data/spec/integration/db/provider_config_spec.rb +0 -88
- data/spec/integration/db/provider_spec.rb +0 -35
- data/spec/integration/db/relations_spec.rb +0 -60
- data/spec/integration/db/repo_spec.rb +0 -300
- data/spec/integration/db/slices_importing_from_parent.rb +0 -130
- data/spec/integration/dotenv_loading_spec.rb +0 -138
- data/spec/integration/logging/exception_logging_spec.rb +0 -120
- data/spec/integration/logging/notifications_spec.rb +0 -68
- data/spec/integration/logging/request_logging_spec.rb +0 -202
- data/spec/integration/operations/extension_spec.rb +0 -122
- data/spec/integration/rack_app/body_parser_spec.rb +0 -108
- data/spec/integration/rack_app/method_override_spec.rb +0 -97
- data/spec/integration/rack_app/middleware_spec.rb +0 -720
- data/spec/integration/rack_app/non_booted_rack_app_spec.rb +0 -104
- data/spec/integration/rack_app/rack_app_spec.rb +0 -442
- data/spec/integration/rake_tasks_spec.rb +0 -107
- data/spec/integration/router/resource_routes_spec.rb +0 -281
- data/spec/integration/settings/access_in_slice_class_body_spec.rb +0 -83
- data/spec/integration/settings/access_to_constants_spec.rb +0 -46
- data/spec/integration/settings/loading_from_env_spec.rb +0 -188
- data/spec/integration/settings/settings_component_loading_spec.rb +0 -113
- data/spec/integration/settings/slice_registration_spec.rb +0 -145
- data/spec/integration/settings/using_types_spec.rb +0 -80
- data/spec/integration/setup_spec.rb +0 -165
- data/spec/integration/slices/external_slice_spec.rb +0 -91
- data/spec/integration/slices/slice_configuration_spec.rb +0 -42
- data/spec/integration/slices/slice_loading_spec.rb +0 -171
- data/spec/integration/slices/slice_registrations_spec.rb +0 -80
- data/spec/integration/slices/slice_routing_spec.rb +0 -219
- data/spec/integration/slices_spec.rb +0 -471
- data/spec/integration/view/config/default_context_spec.rb +0 -149
- data/spec/integration/view/config/inflector_spec.rb +0 -57
- data/spec/integration/view/config/part_class_spec.rb +0 -147
- data/spec/integration/view/config/part_namespace_spec.rb +0 -103
- data/spec/integration/view/config/paths_spec.rb +0 -119
- data/spec/integration/view/config/scope_class_spec.rb +0 -147
- data/spec/integration/view/config/scope_namespace_spec.rb +0 -103
- data/spec/integration/view/config/template_spec.rb +0 -38
- data/spec/integration/view/context/assets_spec.rb +0 -79
- data/spec/integration/view/context/inflector_spec.rb +0 -40
- data/spec/integration/view/context/request_spec.rb +0 -57
- data/spec/integration/view/context/routes_spec.rb +0 -84
- data/spec/integration/view/helpers/form_helper_spec.rb +0 -174
- data/spec/integration/view/helpers/part_helpers_spec.rb +0 -124
- data/spec/integration/view/helpers/scope_helpers_spec.rb +0 -84
- data/spec/integration/view/helpers/user_defined_helpers/part_helpers_spec.rb +0 -162
- data/spec/integration/view/helpers/user_defined_helpers/scope_helpers_spec.rb +0 -119
- data/spec/integration/view/parts/default_rendering_spec.rb +0 -138
- data/spec/integration/view/slice_configuration_spec.rb +0 -289
- data/spec/integration/view/views_spec.rb +0 -103
- data/spec/integration/web/content_security_policy_nonce_spec.rb +0 -251
- data/spec/integration/web/render_detailed_errors_spec.rb +0 -107
- data/spec/integration/web/render_errors_spec.rb +0 -242
- data/spec/integration/web/welcome_view_spec.rb +0 -84
- data/spec/spec_helper.rb +0 -28
- data/spec/support/app_integration.rb +0 -157
- data/spec/support/coverage.rb +0 -1
- data/spec/support/matchers.rb +0 -32
- data/spec/support/rspec.rb +0 -27
- data/spec/unit/hanami/config/actions/content_security_policy_spec.rb +0 -96
- data/spec/unit/hanami/config/actions/cookies_spec.rb +0 -46
- data/spec/unit/hanami/config/actions/csrf_protection_spec.rb +0 -58
- data/spec/unit/hanami/config/actions/default_values_spec.rb +0 -43
- data/spec/unit/hanami/config/actions/sessions_spec.rb +0 -48
- data/spec/unit/hanami/config/actions_spec.rb +0 -52
- data/spec/unit/hanami/config/base_url_spec.rb +0 -25
- data/spec/unit/hanami/config/console_spec.rb +0 -22
- data/spec/unit/hanami/config/db_spec.rb +0 -38
- data/spec/unit/hanami/config/inflector_spec.rb +0 -35
- data/spec/unit/hanami/config/logger_spec.rb +0 -195
- data/spec/unit/hanami/config/render_detailed_errors_spec.rb +0 -25
- data/spec/unit/hanami/config/render_errors_spec.rb +0 -25
- data/spec/unit/hanami/config/router_spec.rb +0 -44
- data/spec/unit/hanami/config/slices_spec.rb +0 -34
- data/spec/unit/hanami/config/views_spec.rb +0 -80
- data/spec/unit/hanami/env_spec.rb +0 -37
- data/spec/unit/hanami/extensions/view/context_spec.rb +0 -59
- data/spec/unit/hanami/helpers/assets_helper/asset_url_spec.rb +0 -120
- data/spec/unit/hanami/helpers/assets_helper/audio_tag_spec.rb +0 -132
- data/spec/unit/hanami/helpers/assets_helper/favicon_tag_spec.rb +0 -87
- data/spec/unit/hanami/helpers/assets_helper/image_tag_spec.rb +0 -92
- data/spec/unit/hanami/helpers/assets_helper/javascript_tag_spec.rb +0 -143
- data/spec/unit/hanami/helpers/assets_helper/stylesheet_tag_spec.rb +0 -126
- data/spec/unit/hanami/helpers/assets_helper/video_tag_spec.rb +0 -136
- data/spec/unit/hanami/helpers/form_helper_spec.rb +0 -2857
- data/spec/unit/hanami/port_spec.rb +0 -117
- data/spec/unit/hanami/providers/db/config/default_config_spec.rb +0 -100
- data/spec/unit/hanami/providers/db/config/gateway_spec.rb +0 -73
- data/spec/unit/hanami/providers/db/config_spec.rb +0 -143
- data/spec/unit/hanami/router/errors/not_allowed_error_spec.rb +0 -27
- data/spec/unit/hanami/router/errors/not_found_error_spec.rb +0 -22
- data/spec/unit/hanami/settings/env_store_spec.rb +0 -52
- data/spec/unit/hanami/settings_spec.rb +0 -111
- data/spec/unit/hanami/slice_configurable_spec.rb +0 -141
- data/spec/unit/hanami/slice_name_spec.rb +0 -47
- data/spec/unit/hanami/slice_spec.rb +0 -99
- data/spec/unit/hanami/web/rack_logger_spec.rb +0 -99
data/lib/hanami/providers/db.rb
CHANGED
|
@@ -7,6 +7,8 @@ require_relative "../constants"
|
|
|
7
7
|
|
|
8
8
|
module Hanami
|
|
9
9
|
module Providers
|
|
10
|
+
# rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
11
|
+
|
|
10
12
|
# @api private
|
|
11
13
|
# @since 2.2.0
|
|
12
14
|
class DB < Hanami::Provider::Source
|
|
@@ -250,7 +252,7 @@ module Hanami
|
|
|
250
252
|
end
|
|
251
253
|
|
|
252
254
|
# Set the default gateway from ENV var without suffix
|
|
253
|
-
|
|
255
|
+
unless database_urls.key?(:default)
|
|
254
256
|
fallback_vars = ["#{env_var_prefix}DATABASE_URL", "DATABASE_URL"].uniq
|
|
255
257
|
|
|
256
258
|
fallback_vars.each do |var|
|
|
@@ -290,7 +292,9 @@ module Hanami
|
|
|
290
292
|
|
|
291
293
|
return if Hanami.bundled?(database_gem)
|
|
292
294
|
|
|
293
|
-
raise Hanami::ComponentLoadError,
|
|
295
|
+
raise Hanami::ComponentLoadError, <<~STR
|
|
296
|
+
The "#{database_gem}" gem is required to connect to #{database_url}. Please add it to your Gemfile.
|
|
297
|
+
STR
|
|
294
298
|
end
|
|
295
299
|
|
|
296
300
|
def register_rom_components(component_type, path)
|
|
@@ -309,6 +313,7 @@ module Hanami
|
|
|
309
313
|
end
|
|
310
314
|
end
|
|
311
315
|
end
|
|
316
|
+
# rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
312
317
|
|
|
313
318
|
Dry::System.register_provider_source(
|
|
314
319
|
:db,
|
|
@@ -3,19 +3,16 @@
|
|
|
3
3
|
module Hanami
|
|
4
4
|
module Providers
|
|
5
5
|
# @api private
|
|
6
|
-
# @since 2.2.0
|
|
7
6
|
class DBLogging < Hanami::Provider::Source
|
|
8
|
-
# @api private
|
|
9
|
-
# @since 2.2.0
|
|
10
7
|
def prepare
|
|
11
|
-
require "dry/monitor/sql/logger"
|
|
12
8
|
slice["notifications"].register_event :sql
|
|
13
9
|
end
|
|
14
10
|
|
|
15
|
-
# @api private
|
|
16
|
-
# @since 2.2.0
|
|
17
11
|
def start
|
|
18
|
-
|
|
12
|
+
require "hanami/logger/sql_logger"
|
|
13
|
+
Hanami::Logger::SQLLogger
|
|
14
|
+
.new(slice["logger"], level: slice.config.db.log_level)
|
|
15
|
+
.subscribe(slice["notifications"])
|
|
19
16
|
end
|
|
20
17
|
end
|
|
21
18
|
end
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hanami
|
|
4
|
+
module Providers
|
|
5
|
+
class I18n < Hanami::Provider::Source
|
|
6
|
+
# A wrapper that provides a full `I18n`-like interface for an individual I18n backend. This
|
|
7
|
+
# allows each Hanami slice to have its own isolated I18n instance.
|
|
8
|
+
#
|
|
9
|
+
# Unlike the global I18n module which uses class variables for configuration,
|
|
10
|
+
# this wrapper maintains per-instance state for true isolation between slices.
|
|
11
|
+
#
|
|
12
|
+
# @api public
|
|
13
|
+
# @since 3.0.0
|
|
14
|
+
class Backend
|
|
15
|
+
attr_reader :backend
|
|
16
|
+
|
|
17
|
+
# Returns the default locale.
|
|
18
|
+
#
|
|
19
|
+
# @return [Symbol] the default locale
|
|
20
|
+
#
|
|
21
|
+
# @api public
|
|
22
|
+
# @since 3.0.0
|
|
23
|
+
attr_reader :default_locale
|
|
24
|
+
|
|
25
|
+
# Creates a new Backend instance.
|
|
26
|
+
#
|
|
27
|
+
# @param backend [I18n::Backend::Base] the underlying I18n backend
|
|
28
|
+
# @param locale [Symbol, String, nil] initial locale to set in thread-local storage
|
|
29
|
+
# @param default_locale [Symbol, String] the default locale to use when no locale is set
|
|
30
|
+
# @param available_locales [Array<Symbol, String>] list of available locales
|
|
31
|
+
# @param fallbacks [I18n::Locale::Fallbacks, nil] fallbacks configuration for missing translations
|
|
32
|
+
#
|
|
33
|
+
# @api private
|
|
34
|
+
# @since 3.0.0
|
|
35
|
+
def initialize(backend, locale: nil, default_locale: :en, available_locales: [], fallbacks: nil)
|
|
36
|
+
@backend = backend
|
|
37
|
+
@default_locale = default_locale.to_sym
|
|
38
|
+
@available_locales = Array(available_locales).map(&:to_sym)
|
|
39
|
+
@fallbacks = fallbacks
|
|
40
|
+
|
|
41
|
+
# Set initial locale (if provided) in thread-local storage.
|
|
42
|
+
@storage_key = :"hanami_i18n_#{object_id}"
|
|
43
|
+
self.locale = locale if locale
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
47
|
+
|
|
48
|
+
# Translates the given key.
|
|
49
|
+
#
|
|
50
|
+
# @param key [String, Symbol] the translation key to look up
|
|
51
|
+
# @param options [Hash] translation options
|
|
52
|
+
# @option options [Symbol, String] :locale the locale to use (defaults to current locale)
|
|
53
|
+
# @option options [String, Proc, Array] :default default value if translation is missing
|
|
54
|
+
# @option options [Hash] :scope additional scope for the key
|
|
55
|
+
# @option options [Integer] :count for pluralization
|
|
56
|
+
# @option options [Boolean] :raise whether to raise an exception for missing translations
|
|
57
|
+
#
|
|
58
|
+
# @return [String, Object] the translated string or default value
|
|
59
|
+
#
|
|
60
|
+
# @raise [I18n::MissingTranslationData] if translation is missing and :raise option is true
|
|
61
|
+
#
|
|
62
|
+
# @example Basic translation
|
|
63
|
+
# translate("hello") # => "Hello"
|
|
64
|
+
#
|
|
65
|
+
# @example Translation with interpolation
|
|
66
|
+
# translate("greeting", name: "Alice") # => "Hello, Alice"
|
|
67
|
+
#
|
|
68
|
+
# @example With explicit locale
|
|
69
|
+
# translate("hello", locale: :fr) # => "Bonjour"
|
|
70
|
+
#
|
|
71
|
+
# @api public
|
|
72
|
+
# @since 3.0.0
|
|
73
|
+
def translate(key, **options)
|
|
74
|
+
locale = options[:locale] || self.locale
|
|
75
|
+
|
|
76
|
+
result = catch(:exception) do
|
|
77
|
+
@backend.translate(locale, key, options)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# If translation is missing and fallbacks are configured, try fallback locales
|
|
81
|
+
if result.is_a?(::I18n::MissingTranslation) && @fallbacks && !options[:fallback]
|
|
82
|
+
fallback_locales = @fallbacks[locale] - [locale]
|
|
83
|
+
fallback_locales.each do |fallback_locale|
|
|
84
|
+
fallback_result = catch(:exception) do
|
|
85
|
+
@backend.translate(fallback_locale, key, options.merge(fallback: true))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
unless fallback_result.is_a?(::I18n::MissingTranslation)
|
|
89
|
+
return fallback_result
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
if result.is_a?(::I18n::MissingTranslation)
|
|
95
|
+
if options[:raise]
|
|
96
|
+
raise ::I18n::MissingTranslationData.new(locale, key, options)
|
|
97
|
+
else
|
|
98
|
+
handle_missing_translation(result, options)
|
|
99
|
+
end
|
|
100
|
+
else
|
|
101
|
+
result
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
106
|
+
|
|
107
|
+
# @api public
|
|
108
|
+
# @since 3.0.0
|
|
109
|
+
alias_method :t, :translate
|
|
110
|
+
|
|
111
|
+
# Translates the given key, raising an exception if translation is missing.
|
|
112
|
+
#
|
|
113
|
+
# @param key [String, Symbol] the translation key to look up
|
|
114
|
+
# @param options [Hash] translation options (see {#translate})
|
|
115
|
+
#
|
|
116
|
+
# @return [String, Object] the translated string
|
|
117
|
+
#
|
|
118
|
+
# @raise [I18n::MissingTranslationData] if translation is missing
|
|
119
|
+
#
|
|
120
|
+
# @example
|
|
121
|
+
# translate!("hello") # => "Hello"
|
|
122
|
+
# translate!("missing.key") # raises I18n::MissingTranslationData
|
|
123
|
+
#
|
|
124
|
+
# @api public
|
|
125
|
+
# @since 3.0.0
|
|
126
|
+
def translate!(key, **options)
|
|
127
|
+
translate(key, **options.merge(raise: true))
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @api public
|
|
131
|
+
# @since 3.0.0
|
|
132
|
+
alias_method :t!, :translate!
|
|
133
|
+
|
|
134
|
+
# Localizes the given date or time.
|
|
135
|
+
#
|
|
136
|
+
# Resolves symbol formats through this instance's translations (e.g. `:short` becomes
|
|
137
|
+
# `date.formats.short` or `time.formats.short`). Also resolves locale-dependent strftime
|
|
138
|
+
# codes (e.g. `%a`, `%A`, `%b`, `%B`, `%p`, `%P`).
|
|
139
|
+
#
|
|
140
|
+
# @param object [Date, Time, DateTime] the object to localize
|
|
141
|
+
# @param locale [Symbol, String, nil] the locale to use (defaults to current locale)
|
|
142
|
+
# @param format [Symbol, String] the format to use for localization
|
|
143
|
+
# @param options [Hash] additional localization options
|
|
144
|
+
#
|
|
145
|
+
# @return [String] the localized string representation
|
|
146
|
+
#
|
|
147
|
+
# @example Localize a date
|
|
148
|
+
# localize(Date.today, format: :long) # => "January 19, 2026"
|
|
149
|
+
#
|
|
150
|
+
# @example Localize with specific locale
|
|
151
|
+
# localize(Date.today, locale: :fr, format: :long) # => "19 janvier 2026"
|
|
152
|
+
#
|
|
153
|
+
# @api public
|
|
154
|
+
# @since 3.0.0
|
|
155
|
+
def localize(object, locale: nil, format: :default, **options)
|
|
156
|
+
locale ||= self.locale
|
|
157
|
+
|
|
158
|
+
return options[:default] if object.nil? && options.key?(:default)
|
|
159
|
+
|
|
160
|
+
unless object.respond_to?(:strftime)
|
|
161
|
+
raise ArgumentError, <<~MSG
|
|
162
|
+
Object must be a Date, DateTime or Time object. #{object.inspect} given.
|
|
163
|
+
MSG
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
if format.is_a?(Symbol)
|
|
167
|
+
type = object.respond_to?(:sec) ? "time" : "date"
|
|
168
|
+
format = translate(
|
|
169
|
+
:"#{type}.formats.#{format}",
|
|
170
|
+
**options, locale:, object:, raise: true
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
format = expand_localization_format(locale, object, format)
|
|
175
|
+
object.strftime(format)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# @api public
|
|
179
|
+
# @since 3.0.0
|
|
180
|
+
alias_method :l, :localize
|
|
181
|
+
|
|
182
|
+
# Returns true if a translation exists for the given key.
|
|
183
|
+
#
|
|
184
|
+
# @param key [String, Symbol] the translation key to check
|
|
185
|
+
# @param locale [Symbol, String, nil] the locale to check (defaults to current locale)
|
|
186
|
+
# @param options [Hash] additional options
|
|
187
|
+
#
|
|
188
|
+
# @return [Boolean] true if the translation exists, false otherwise
|
|
189
|
+
#
|
|
190
|
+
# @example
|
|
191
|
+
# exists?("hello") # => true
|
|
192
|
+
# exists?("missing.key") # => false
|
|
193
|
+
#
|
|
194
|
+
# @api public
|
|
195
|
+
# @since 3.0.0
|
|
196
|
+
def exists?(key, locale: nil, **options)
|
|
197
|
+
locale ||= self.locale
|
|
198
|
+
@backend.exists?(locale, key, options)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Transliterates the given string.
|
|
202
|
+
#
|
|
203
|
+
# @param key [String] the string to transliterate
|
|
204
|
+
# @param locale [Symbol, String, nil] the locale to use (defaults to current locale)
|
|
205
|
+
# @param replacement [String, nil] replacement string for non-transliteratable characters
|
|
206
|
+
# @param options [Hash] additional transliteration options
|
|
207
|
+
#
|
|
208
|
+
# @return [String] the transliterated string
|
|
209
|
+
#
|
|
210
|
+
# @example
|
|
211
|
+
# transliterate("Ærøskøbing") # => "AEroskobing"
|
|
212
|
+
#
|
|
213
|
+
# @api public
|
|
214
|
+
# @since 3.0.0
|
|
215
|
+
def transliterate(key, locale: nil, replacement: nil, **options)
|
|
216
|
+
locale ||= self.locale
|
|
217
|
+
@backend.transliterate(locale, key, replacement)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Returns available locales.
|
|
221
|
+
#
|
|
222
|
+
# If configured via `config.i18n.available_locales`, returns the configured locales.
|
|
223
|
+
# Otherwise, returns all locales detected from loaded translation files.
|
|
224
|
+
#
|
|
225
|
+
# @api public
|
|
226
|
+
# @since 3.0.0
|
|
227
|
+
def available_locales
|
|
228
|
+
if @available_locales.any?
|
|
229
|
+
@available_locales
|
|
230
|
+
else
|
|
231
|
+
@backend.available_locales
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Reloads translations from translation files.
|
|
236
|
+
#
|
|
237
|
+
# @return [void]
|
|
238
|
+
#
|
|
239
|
+
# @api public
|
|
240
|
+
# @since 3.0.0
|
|
241
|
+
def reload!
|
|
242
|
+
@backend.reload!
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Eager loads translations if the backend supports it.
|
|
246
|
+
#
|
|
247
|
+
# @return [void]
|
|
248
|
+
#
|
|
249
|
+
# @api public
|
|
250
|
+
# @since 3.0.0
|
|
251
|
+
def eager_load!
|
|
252
|
+
@backend.eager_load! if @backend.respond_to?(:eager_load!)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Returns the current locale from fiber/thread-local storage, or default_locale.
|
|
256
|
+
#
|
|
257
|
+
# This is thread-safe and works correctly with concurrent requests.
|
|
258
|
+
# Each backend instance maintains its own locale in thread storage.
|
|
259
|
+
#
|
|
260
|
+
# @return [Symbol] the current locale or default locale if none is set
|
|
261
|
+
#
|
|
262
|
+
# @example
|
|
263
|
+
# locale # => :en
|
|
264
|
+
# self.locale = :fr
|
|
265
|
+
# locale # => :fr
|
|
266
|
+
#
|
|
267
|
+
# @api public
|
|
268
|
+
# @since 3.0.0
|
|
269
|
+
def locale
|
|
270
|
+
Thread.current[@storage_key] || default_locale
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Sets the current locale in fiber/thread-local storage.
|
|
274
|
+
#
|
|
275
|
+
# This is thread-safe and works correctly with concurrent requests.
|
|
276
|
+
# Each backend instance maintains its own locale in thread storage.
|
|
277
|
+
#
|
|
278
|
+
# @param value [Symbol, String, nil] the locale to set (converted to symbol)
|
|
279
|
+
#
|
|
280
|
+
# @return [Symbol, nil] the locale value that was set
|
|
281
|
+
#
|
|
282
|
+
# @example
|
|
283
|
+
# self.locale = :fr
|
|
284
|
+
# self.locale = "de"
|
|
285
|
+
# self.locale = nil # resets to default_locale
|
|
286
|
+
#
|
|
287
|
+
# @api public
|
|
288
|
+
# @since 3.0.0
|
|
289
|
+
def locale=(value)
|
|
290
|
+
Thread.current[@storage_key] = value&.to_sym
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Executes a block with a temporary locale.
|
|
294
|
+
#
|
|
295
|
+
# This is useful for executing code with a specific locale without affecting
|
|
296
|
+
# other concurrent requests. The previous locale is restored even if the block raises.
|
|
297
|
+
#
|
|
298
|
+
# @param tmp_locale [Symbol, String, nil] the temporary locale to use
|
|
299
|
+
#
|
|
300
|
+
# @yieldreturn [Object] the return value of the block
|
|
301
|
+
#
|
|
302
|
+
# @return [Object] the return value of the block
|
|
303
|
+
#
|
|
304
|
+
# @example
|
|
305
|
+
# with_locale(:fr) do
|
|
306
|
+
# t("greeting") # Uses French locale
|
|
307
|
+
# end
|
|
308
|
+
# # locale is restored to previous value
|
|
309
|
+
#
|
|
310
|
+
# @example Nested usage
|
|
311
|
+
# with_locale(:fr) do
|
|
312
|
+
# with_locale(:de) do
|
|
313
|
+
# t("hello") # Uses German
|
|
314
|
+
# end
|
|
315
|
+
# t("hello") # Uses French
|
|
316
|
+
# end
|
|
317
|
+
#
|
|
318
|
+
# @api public
|
|
319
|
+
# @since 3.0.0
|
|
320
|
+
def with_locale(tmp_locale)
|
|
321
|
+
previous_locale = Thread.current[@storage_key]
|
|
322
|
+
Thread.current[@storage_key] = tmp_locale&.to_sym
|
|
323
|
+
yield
|
|
324
|
+
ensure
|
|
325
|
+
Thread.current[@storage_key] = previous_locale
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
private
|
|
329
|
+
|
|
330
|
+
def handle_missing_translation(missing_translation, options)
|
|
331
|
+
default = options[:default]
|
|
332
|
+
return default if default.is_a?(String)
|
|
333
|
+
|
|
334
|
+
key = missing_translation.key
|
|
335
|
+
key.to_s
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# rubocop:disable Layout/LineLength, Style/NestedTernaryOperator, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
339
|
+
|
|
340
|
+
# Expands locale-dependent strftime codes (`%a`, `%A`, `%b`, `%B`, `%p`, `%P` and their
|
|
341
|
+
# uppercase `^` variants) by looking up day/month names and meridiem markers through this
|
|
342
|
+
# backend.
|
|
343
|
+
#
|
|
344
|
+
# This is a deliberate port of `I18n::Backend::Base#translate_localization_format`. We can't
|
|
345
|
+
# call this upstream method directly because it's private and also hardcodes `I18n.t!` (the
|
|
346
|
+
# global module) for every lookup, which defeats our per-slice isolated backends.
|
|
347
|
+
#
|
|
348
|
+
# This set of locale-dependent strftime codes is fixed by the C `strftime` spec and has not
|
|
349
|
+
# changed in the upstream gem since at least 2010, so this table is effectively frozen.
|
|
350
|
+
def expand_localization_format(locale, object, format)
|
|
351
|
+
format.to_s.gsub(/%(|\^)[aAbBpP]/) do |match|
|
|
352
|
+
case match
|
|
353
|
+
when "%a" then t!(:"date.abbr_day_names", locale:, format:)[object.wday]
|
|
354
|
+
when "%^a" then t!(:"date.abbr_day_names", locale:, format:)[object.wday].upcase
|
|
355
|
+
when "%A" then t!(:"date.day_names", locale:, format:)[object.wday]
|
|
356
|
+
when "%^A" then t!(:"date.day_names", locale:, format:)[object.wday].upcase
|
|
357
|
+
when "%b" then t!(:"date.abbr_month_names", locale:, format:)[object.mon]
|
|
358
|
+
when "%^b" then t!(:"date.abbr_month_names", locale:, format:)[object.mon].upcase
|
|
359
|
+
when "%B" then t!(:"date.month_names", locale:, format:)[object.mon]
|
|
360
|
+
when "%^B" then t!(:"date.month_names", locale:, format:)[object.mon].upcase
|
|
361
|
+
when "%p" then t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", locale:, format:).upcase
|
|
362
|
+
when "%P" then t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", locale:, format:).downcase
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
rescue ::I18n::MissingTranslationData => exception
|
|
366
|
+
exception.message
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# rubocop:enable Layout/LineLength, Style/NestedTernaryOperator, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
en:
|
|
2
|
+
date:
|
|
3
|
+
formats:
|
|
4
|
+
default: "%a, %-d %b %Y"
|
|
5
|
+
short: "%-d %b"
|
|
6
|
+
long: "%-d %B %Y"
|
|
7
|
+
day_names:
|
|
8
|
+
- Sunday
|
|
9
|
+
- Monday
|
|
10
|
+
- Tuesday
|
|
11
|
+
- Wednesday
|
|
12
|
+
- Thursday
|
|
13
|
+
- Friday
|
|
14
|
+
- Saturday
|
|
15
|
+
abbr_day_names:
|
|
16
|
+
- Sun
|
|
17
|
+
- Mon
|
|
18
|
+
- Tue
|
|
19
|
+
- Wed
|
|
20
|
+
- Thu
|
|
21
|
+
- Fri
|
|
22
|
+
- Sat
|
|
23
|
+
month_names:
|
|
24
|
+
- ~
|
|
25
|
+
- January
|
|
26
|
+
- February
|
|
27
|
+
- March
|
|
28
|
+
- April
|
|
29
|
+
- May
|
|
30
|
+
- June
|
|
31
|
+
- July
|
|
32
|
+
- August
|
|
33
|
+
- September
|
|
34
|
+
- October
|
|
35
|
+
- November
|
|
36
|
+
- December
|
|
37
|
+
abbr_month_names:
|
|
38
|
+
- ~
|
|
39
|
+
- Jan
|
|
40
|
+
- Feb
|
|
41
|
+
- Mar
|
|
42
|
+
- Apr
|
|
43
|
+
- May
|
|
44
|
+
- Jun
|
|
45
|
+
- Jul
|
|
46
|
+
- Aug
|
|
47
|
+
- Sep
|
|
48
|
+
- Oct
|
|
49
|
+
- Nov
|
|
50
|
+
- Dec
|
|
51
|
+
time:
|
|
52
|
+
formats:
|
|
53
|
+
default: "%a, %-d %b %Y %H:%M:%S %:z"
|
|
54
|
+
short: "%-d %b %-I:%M %P"
|
|
55
|
+
long: "%-d %B %Y %-I:%M %P"
|
|
56
|
+
am: "am"
|
|
57
|
+
pm: "pm"
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hanami
|
|
4
|
+
module Providers
|
|
5
|
+
# @api private
|
|
6
|
+
class I18n < Hanami::Provider::Source
|
|
7
|
+
SLICE_CONFIGURED_SETTINGS = %i[
|
|
8
|
+
default_locale available_locales load_path shared_load_path fallbacks
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
11
|
+
DEFAULT_LOCALE = :en
|
|
12
|
+
DEFAULT_AVAILABLE_LOCALES = [].freeze
|
|
13
|
+
DEFAULT_LOAD_PATH = ["config/i18n/**/*.{yml,yaml,json,rb}"].freeze
|
|
14
|
+
DEFAULT_SHARED_LOAD_PATH = ["config/i18n/shared/**/*.{yml,yaml,json,rb}"].freeze
|
|
15
|
+
|
|
16
|
+
# Built-in English baseline for date/time localization. Loaded into every slice before any
|
|
17
|
+
# user translation files, so users can supply overrides if desired.
|
|
18
|
+
BUNDLED_DEFAULTS_PATH = File.expand_path("i18n/locale/en.yml", __dir__).freeze
|
|
19
|
+
|
|
20
|
+
setting :default_locale, default: DEFAULT_LOCALE
|
|
21
|
+
setting :available_locales, default: DEFAULT_AVAILABLE_LOCALES
|
|
22
|
+
setting :backend
|
|
23
|
+
setting :load_path, default: DEFAULT_LOAD_PATH
|
|
24
|
+
setting :shared_load_path, default: DEFAULT_SHARED_LOAD_PATH
|
|
25
|
+
setting :fallbacks
|
|
26
|
+
|
|
27
|
+
def prepare
|
|
28
|
+
require "i18n"
|
|
29
|
+
|
|
30
|
+
SLICE_CONFIGURED_SETTINGS.each do |setting|
|
|
31
|
+
next if config.configured?(setting)
|
|
32
|
+
|
|
33
|
+
config.public_send(:"#{setting}=", slice.config.i18n.public_send(setting))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# rubocop:disable Metrics/AbcSize
|
|
38
|
+
def start
|
|
39
|
+
backend = config.backend || ::I18n::Backend::Simple.new
|
|
40
|
+
|
|
41
|
+
# Configure fallbacks if enabled (only for default backend, not custom backends)
|
|
42
|
+
fallbacks_config = nil
|
|
43
|
+
if config.fallbacks && !config.backend
|
|
44
|
+
fallbacks_config =
|
|
45
|
+
case config.fallbacks
|
|
46
|
+
when true
|
|
47
|
+
# Use default_locale as the only fallback
|
|
48
|
+
fallbacks = ::I18n::Locale::Fallbacks.new
|
|
49
|
+
fallbacks.defaults = [config.default_locale]
|
|
50
|
+
fallbacks
|
|
51
|
+
when Hash
|
|
52
|
+
# Use explicit per-locale fallback mapping
|
|
53
|
+
::I18n::Locale::Fallbacks.new(config.fallbacks)
|
|
54
|
+
when Array
|
|
55
|
+
# Use the given fallbacks for all locales
|
|
56
|
+
fallbacks = ::I18n::Locale::Fallbacks.new
|
|
57
|
+
fallbacks.defaults = config.fallbacks
|
|
58
|
+
fallbacks
|
|
59
|
+
else
|
|
60
|
+
::I18n::Locale::Fallbacks.new(config.fallbacks)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Only load translation files if using the default backend. Custom backends are expected to
|
|
65
|
+
# handle their own initialization.
|
|
66
|
+
unless config.backend
|
|
67
|
+
translation_files = [
|
|
68
|
+
BUNDLED_DEFAULTS_PATH,
|
|
69
|
+
*resolve_load_paths(Array(config.shared_load_path), root: slice.app.root),
|
|
70
|
+
*resolve_load_paths(Array(config.load_path), root: slice.root)
|
|
71
|
+
].uniq
|
|
72
|
+
|
|
73
|
+
backend.load_translations(*translation_files)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
register "i18n", Backend.new(
|
|
77
|
+
backend,
|
|
78
|
+
locale: config.default_locale,
|
|
79
|
+
default_locale: config.default_locale,
|
|
80
|
+
available_locales: config.available_locales,
|
|
81
|
+
fallbacks: fallbacks_config
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
# rubocop:enable Metrics/AbcSize
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
# Resolves load path patterns to actual file paths. Relative patterns are resolved against
|
|
89
|
+
# the given `root`. Absolute paths are used as-is.
|
|
90
|
+
def resolve_load_paths(patterns, root:)
|
|
91
|
+
patterns.flat_map do |pattern|
|
|
92
|
+
if absolute_path?(pattern)
|
|
93
|
+
# Absolute path or already-globbed absolute paths
|
|
94
|
+
File.exist?(pattern) ? [pattern] : Dir.glob(pattern)
|
|
95
|
+
else
|
|
96
|
+
Dir.glob(root.join(pattern))
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def absolute_path?(path)
|
|
102
|
+
Pathname.new(path).absolute?
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
Dry::System.register_provider_source(
|
|
107
|
+
:i18n,
|
|
108
|
+
source: I18n,
|
|
109
|
+
group: :hanami
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
require_relative "i18n/backend"
|
|
@@ -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
|