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,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"
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ # @api private
5
+ module Providers
6
+ # Provider source to register inflector component in Hanami slices.
7
+ #
8
+ # @api private
9
+ # @since 2.0.0
10
+ class Inflector < Hanami::Provider::Source
11
+ # @api private
12
+ def start
13
+ register :inflector, Hanami.app.inflector
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ # @api private
5
+ module Providers
6
+ # Provider source to register logger component in Hanami slices.
7
+ #
8
+ # @see Hanami::Config#logger
9
+ #
10
+ # @api private
11
+ # @since 2.0.0
12
+ class Logger < Hanami::Provider::Source
13
+ # @api private
14
+ def start
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
32
+ end
33
+ end
34
+
35
+ Dry::System.register_provider_source(
36
+ :logger,
37
+ source: Logger,
38
+ group: :hanami
39
+ )
40
+ end
41
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Providers
5
+ # Registers the `"mailers.delivery_method"` component. This is an SMTP delivery method built
6
+ # from SMTP environment variables when present, otherwise the test delivery method.
7
+ #
8
+ # SMTP env vars may take a per-slice prefix derived from the slice name (e.g. an "admin" slice
9
+ # reads `ADMIN__SMTP_ADDRESS`, falling back to `SMTP_ADDRESS`). Register your own `:mailers`
10
+ # provider if you need to use another delivery method or different setup logic.
11
+ #
12
+ # In the test env, environment variables are ignored, and the test delivery method is always
13
+ # used, so the test suite can never send real email.
14
+ #
15
+ # In the production env, warns noisily when there is no SMTP configuration, before falling back
16
+ # to the test delivery method. This ensures an app whose mail setup is a work in progress can
17
+ # still boot.
18
+ #
19
+ # @api private
20
+ class Mailers < Hanami::Provider::Source
21
+ # Maps SMTP environment variable names to their compatible delivery option keys.
22
+ #
23
+ # Example values:
24
+ #
25
+ # SMTP_ADDRESS=smtp.example.com
26
+ # SMTP_PORT=587
27
+ # SMTP_USERNAME=postmaster@example.com
28
+ # SMTP_PASSWORD=s3cr3t
29
+ # SMTP_AUTHENTICATION=plain
30
+ SMTP_ENV_VARS = {
31
+ "SMTP_ADDRESS" => :address,
32
+ "SMTP_PORT" => :port,
33
+ "SMTP_USERNAME" => :user_name,
34
+ "SMTP_PASSWORD" => :password,
35
+ "SMTP_AUTHENTICATION" => :authentication
36
+ }.freeze
37
+
38
+ # Coercions applied to SMTP env var values, keyed by option. Options not listed here are
39
+ # passed through unchanged (as strings).
40
+ SMTP_COERCIONS = Hash.new(:itself.to_proc).update(
41
+ port: ->(value) { Integer(value) },
42
+ authentication: ->(value) { value.to_sym }
43
+ ).freeze
44
+
45
+ def start
46
+ require "hanami/mailer"
47
+
48
+ register "delivery_method", build_delivery_method
49
+ end
50
+
51
+ private
52
+
53
+ def build_delivery_method
54
+ return Hanami::Mailer::Delivery::Test.new if Hanami.env?(:test)
55
+
56
+ smtp_options = smtp_options_from_env
57
+
58
+ return Hanami::Mailer::Delivery::SMTP.new(**smtp_options) if smtp_options.key?(:address)
59
+
60
+ warn_missing_smtp if Hanami.env?(:production)
61
+
62
+ Hanami::Mailer::Delivery::Test.new
63
+ end
64
+
65
+ def smtp_options_from_env
66
+ SMTP_ENV_VARS.each_with_object({}) do |(var, option), options|
67
+ value = env_value(var)
68
+ next if value.nil?
69
+
70
+ coercion = SMTP_COERCIONS[option]
71
+ options[option] = coercion ? coercion.call(value) : value
72
+ end
73
+ end
74
+
75
+ # Reads an SMTP env var, preferring a per-slice prefixed name (e.g. `ADMIN__SMTP_ADDRESS`)
76
+ # and falling back to the unprefixed name shared across slices.
77
+ def env_value(var)
78
+ return ENV[var] if slice.app?
79
+
80
+ slice_prefixed_var = "#{slice.slice_name.name.gsub("/", "__").upcase}__#{var}"
81
+ ENV[slice_prefixed_var] || ENV[var]
82
+ end
83
+
84
+ def warn_missing_smtp
85
+ message = \
86
+ "No SMTP configuration found for #{slice.slice_name.name} in production; " \
87
+ "falling back to the test delivery method — mail will NOT be sent. " \
88
+ "Set SMTP_ADDRESS (and related SMTP_* variables), or register a custom :mailers provider."
89
+
90
+ slice.app["logger"].warn(message)
91
+ end
92
+ end
93
+
94
+ Dry::System.register_provider_source(
95
+ :mailers,
96
+ source: Mailers,
97
+ group: :hanami,
98
+ provider_options: {namespace: true}
99
+ )
100
+ end
101
+ end