cmdx 1.1.0 → 1.1.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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/docs.md +9 -0
  3. data/.cursor/prompts/rspec.md +13 -12
  4. data/.cursor/prompts/yardoc.md +11 -6
  5. data/CHANGELOG.md +13 -2
  6. data/README.md +1 -0
  7. data/docs/ai_prompts.md +269 -195
  8. data/docs/basics/call.md +124 -58
  9. data/docs/basics/chain.md +190 -160
  10. data/docs/basics/context.md +242 -154
  11. data/docs/basics/setup.md +302 -32
  12. data/docs/callbacks.md +390 -94
  13. data/docs/configuration.md +181 -65
  14. data/docs/deprecation.md +245 -0
  15. data/docs/getting_started.md +161 -39
  16. data/docs/internationalization.md +590 -70
  17. data/docs/interruptions/exceptions.md +135 -118
  18. data/docs/interruptions/faults.md +150 -125
  19. data/docs/interruptions/halt.md +134 -80
  20. data/docs/logging.md +181 -118
  21. data/docs/middlewares.md +150 -377
  22. data/docs/outcomes/result.md +140 -112
  23. data/docs/outcomes/states.md +134 -99
  24. data/docs/outcomes/statuses.md +204 -146
  25. data/docs/parameters/coercions.md +232 -281
  26. data/docs/parameters/defaults.md +224 -169
  27. data/docs/parameters/definitions.md +289 -141
  28. data/docs/parameters/namespacing.md +250 -161
  29. data/docs/parameters/validations.md +260 -133
  30. data/docs/testing.md +191 -197
  31. data/docs/workflows.md +143 -98
  32. data/lib/cmdx/callback.rb +23 -19
  33. data/lib/cmdx/callback_registry.rb +1 -3
  34. data/lib/cmdx/chain_inspector.rb +23 -23
  35. data/lib/cmdx/chain_serializer.rb +38 -19
  36. data/lib/cmdx/coercion.rb +20 -12
  37. data/lib/cmdx/coercion_registry.rb +51 -32
  38. data/lib/cmdx/configuration.rb +84 -31
  39. data/lib/cmdx/context.rb +32 -21
  40. data/lib/cmdx/core_ext/hash.rb +13 -13
  41. data/lib/cmdx/core_ext/module.rb +1 -1
  42. data/lib/cmdx/core_ext/object.rb +12 -12
  43. data/lib/cmdx/correlator.rb +60 -39
  44. data/lib/cmdx/errors.rb +105 -131
  45. data/lib/cmdx/fault.rb +66 -45
  46. data/lib/cmdx/immutator.rb +20 -21
  47. data/lib/cmdx/lazy_struct.rb +78 -70
  48. data/lib/cmdx/log_formatters/json.rb +1 -1
  49. data/lib/cmdx/log_formatters/key_value.rb +1 -1
  50. data/lib/cmdx/log_formatters/line.rb +1 -1
  51. data/lib/cmdx/log_formatters/logstash.rb +1 -1
  52. data/lib/cmdx/log_formatters/pretty_json.rb +1 -1
  53. data/lib/cmdx/log_formatters/pretty_key_value.rb +1 -1
  54. data/lib/cmdx/log_formatters/pretty_line.rb +1 -1
  55. data/lib/cmdx/log_formatters/raw.rb +2 -2
  56. data/lib/cmdx/logger.rb +19 -14
  57. data/lib/cmdx/logger_ansi.rb +33 -17
  58. data/lib/cmdx/logger_serializer.rb +85 -24
  59. data/lib/cmdx/middleware.rb +39 -21
  60. data/lib/cmdx/middleware_registry.rb +4 -3
  61. data/lib/cmdx/parameter.rb +151 -89
  62. data/lib/cmdx/parameter_inspector.rb +34 -21
  63. data/lib/cmdx/parameter_registry.rb +36 -30
  64. data/lib/cmdx/parameter_serializer.rb +21 -14
  65. data/lib/cmdx/result.rb +136 -135
  66. data/lib/cmdx/result_ansi.rb +31 -17
  67. data/lib/cmdx/result_inspector.rb +32 -27
  68. data/lib/cmdx/result_logger.rb +23 -14
  69. data/lib/cmdx/result_serializer.rb +65 -27
  70. data/lib/cmdx/task.rb +234 -113
  71. data/lib/cmdx/task_deprecator.rb +22 -25
  72. data/lib/cmdx/task_processor.rb +89 -88
  73. data/lib/cmdx/task_serializer.rb +27 -14
  74. data/lib/cmdx/utils/monotonic_runtime.rb +2 -4
  75. data/lib/cmdx/validator.rb +25 -16
  76. data/lib/cmdx/validator_registry.rb +53 -31
  77. data/lib/cmdx/validators/exclusion.rb +1 -1
  78. data/lib/cmdx/validators/format.rb +2 -2
  79. data/lib/cmdx/validators/inclusion.rb +2 -2
  80. data/lib/cmdx/validators/length.rb +2 -2
  81. data/lib/cmdx/validators/numeric.rb +3 -3
  82. data/lib/cmdx/validators/presence.rb +2 -2
  83. data/lib/cmdx/version.rb +1 -1
  84. data/lib/cmdx/workflow.rb +54 -33
  85. data/lib/generators/cmdx/task_generator.rb +6 -6
  86. data/lib/generators/cmdx/workflow_generator.rb +6 -6
  87. metadata +3 -1
@@ -1,145 +1,665 @@
1
1
  # Internationalization (i18n)
2
2
 
3
- CMDx provides comprehensive internationalization support for all error messages, including parameter coercion errors, validation failures, and fault messages. All error text is automatically localized based on the current `I18n.locale`.
3
+ CMDx provides comprehensive internationalization support for all error messages, parameter validation failures, coercion errors, and fault messages. All user-facing text is automatically localized based on the current `I18n.locale`, ensuring your applications can serve global audiences with native-language error reporting.
4
4
 
5
5
  ## Table of Contents
6
6
 
7
7
  - [TLDR](#tldr)
8
8
  - [Available Locales](#available-locales)
9
+ - [Configuration](#configuration)
9
10
  - [Fault Messages](#fault-messages)
10
11
  - [Parameter Messages](#parameter-messages)
11
12
  - [Coercion Messages](#coercion-messages)
12
13
  - [Validation Messages](#validation-messages)
14
+ - [Custom Message Overrides](#custom-message-overrides)
15
+ - [Error Handling and Debugging](#error-handling-and-debugging)
13
16
 
14
17
  ## TLDR
15
18
 
16
- - **24 languages** - Built-in translations for major world languages
17
- - **Automatic localization** - Based on `I18n.locale` setting
18
- - **Complete coverage** - Coercion errors, validation failures, and fault messages
19
- - **Custom overrides** - Parameter-specific messages override locale defaults
19
+ ```ruby
20
+ # Automatic localization based on I18n.locale
21
+ I18n.locale = :es
22
+ result = CreateUserTask.call(email: "invalid", age: "too-young")
23
+ result.metadata[:messages][:email] #=> ["formato inválido"]
24
+
25
+ # 24 built-in languages with complete coverage
26
+ # Parameter-specific overrides available
27
+ # Covers coercion, validation, and fault messages
28
+ ```
29
+
30
+ > [!NOTE]
31
+ > CMDx automatically localizes all error messages based on your application's `I18n.locale` setting. No additional configuration is required for basic usage.
20
32
 
21
33
  ## Available Locales
22
34
 
23
- CMDx includes built-in translations for 24 languages:
24
-
25
- | Language | Locale | Language | Locale |
26
- |----------|--------|----------|--------|
27
- | English | `:en` | Russian | `:ru` |
28
- | Spanish | `:es` | Arabic | `:ar` |
29
- | French | `:fr` | Korean | `:ko` |
30
- | German | `:de` | Dutch | `:nl` |
31
- | Portuguese | `:pt` | Swedish | `:sv` |
32
- | Italian | `:it` | Hindi | `:hi` |
33
- | Japanese | `:ja` | Polish | `:pl` |
34
- | Chinese | `:zh` | Turkish | `:tr` |
35
- | Norwegian | `:no` | Danish | `:da` |
36
- | Finnish | `:fi` | Greek | `:el` |
37
- | Hebrew | `:he` | Thai | `:th` |
38
- | Vietnamese | `:vi` | Czech | `:cs` |
35
+ CMDx includes built-in translations for 24 major world languages, covering both Western and Eastern language families:
36
+
37
+ | Language | Locale | Language | Locale | Language | Locale |
38
+ |----------|--------|----------|--------|----------|--------|
39
+ | English | `:en` | Russian | `:ru` | Arabic | `:ar` |
40
+ | Spanish | `:es` | Korean | `:ko` | Dutch | `:nl` |
41
+ | French | `:fr` | Hindi | `:hi` | Swedish | `:sv` |
42
+ | German | `:de` | Polish | `:pl` | Norwegian | `:no` |
43
+ | Portuguese | `:pt` | Turkish | `:tr` | Finnish | `:fi` |
44
+ | Italian | `:it` | Danish | `:da` | Greek | `:el` |
45
+ | Japanese | `:ja` | Czech | `:cs` | Hebrew | `:he` |
46
+ | Chinese | `:zh` | Thai | `:th` | Vietnamese | `:vi` |
47
+
48
+ > [!TIP]
49
+ > All locales provide complete coverage for every error message type, including complex nested parameter validation errors and multi-type coercion failures.
50
+
51
+ ## Configuration
52
+
53
+ ### Basic Setup
54
+
55
+ ```ruby
56
+ # In Rails applications (config/application.rb)
57
+ config.i18n.default_locale = :en
58
+ config.i18n.available_locales = [:en, :es, :fr, :de]
59
+
60
+ # Runtime locale switching
61
+ class ApiController < ApplicationController
62
+ before_action :set_locale
63
+
64
+ private
65
+
66
+ def set_locale
67
+ I18n.locale = params[:locale] || request.headers['Accept-Language']&.scan(/^[a-z]{2}/)&.first || :en
68
+ end
69
+ end
70
+ ```
71
+
72
+ ### Per-Request Localization
73
+
74
+ ```ruby
75
+ class ProcessOrderTask < CMDx::Task
76
+ required :amount, type: :float
77
+ required :customer_email, format: { with: /@/ }
78
+
79
+ def call
80
+ # Task logic runs with current I18n.locale
81
+ ChargeCustomer.call(amount: amount, email: customer_email)
82
+ end
83
+ end
84
+
85
+ # Different locales produce localized errors
86
+ I18n.with_locale(:fr) do
87
+ result = ProcessOrderTask.call(amount: "invalid", customer_email: "bad-email")
88
+ result.metadata[:messages][:amount] #=> ["impossible de contraindre en float"]
89
+ end
90
+ ```
39
91
 
40
92
  ## Fault Messages
41
93
 
42
- Default fault messages from `skip!` and `fail!` methods are localized:
94
+ > [!IMPORTANT]
95
+ > Fault messages from `fail!` and `skip!` methods are automatically localized when no explicit reason is provided.
96
+
97
+ ### Default Fault Localization
43
98
 
44
99
  ```ruby
45
100
  class ProcessPaymentTask < CMDx::Task
101
+ required :payment_method, inclusion: { in: %w[card paypal bank] }
102
+ required :amount, type: :float
103
+
46
104
  def call
47
- # When no reason is provided, uses localized default
48
- fail! if payment_declined?
105
+ if payment_declined?
106
+ fail! # Uses localized default message
107
+ end
108
+
109
+ if amount < minimum_charge
110
+ skip! # Uses localized default message
111
+ end
112
+
113
+ charge_payment
114
+ end
115
+
116
+ private
117
+
118
+ def payment_declined?
119
+ # Payment gateway logic
120
+ rand > 0.8
121
+ end
122
+
123
+ def minimum_charge
124
+ 5.00
49
125
  end
50
126
  end
51
127
 
52
128
  # English
53
129
  I18n.locale = :en
54
- result = ProcessPaymentTask.call(payment_id: 123)
130
+ result = ProcessPaymentTask.call(payment_method: "card", amount: 99.99)
55
131
  result.metadata[:reason] #=> "no reason given"
56
132
 
57
- # Chinese
58
- I18n.locale = :zh
59
- result = ProcessPaymentTask.call(payment_id: 123)
60
- result.metadata[:reason] #=> "未提供原因"
133
+ # Spanish
134
+ I18n.locale = :es
135
+ result = ProcessPaymentTask.call(payment_method: "card", amount: 99.99)
136
+ result.metadata[:reason] #=> "no se proporcionó razón"
137
+
138
+ # Japanese
139
+ I18n.locale = :ja
140
+ result = ProcessPaymentTask.call(payment_method: "card", amount: 99.99)
141
+ result.metadata[:reason] #=> "理由が提供されませんでした"
142
+ ```
143
+
144
+ ### Custom Fault Messages
145
+
146
+ ```ruby
147
+ class ProcessRefundTask < CMDx::Task
148
+ required :order_id, type: :integer
149
+ required :reason, presence: true
150
+
151
+ def call
152
+ order = find_order(order_id)
153
+
154
+ # Custom messages override locale defaults
155
+ fail!("Payment gateway unavailable") if gateway_down?
156
+ skip!("Refund already processed") if order.refunded?
157
+
158
+ process_refund(order)
159
+ end
160
+ end
61
161
  ```
62
162
 
63
163
  ## Parameter Messages
64
164
 
65
- Parameter required or undefined source errors are automatically localized:
165
+ > [!WARNING]
166
+ > Parameter errors include both missing required parameters and undefined source method delegates, with full internationalization support.
167
+
168
+ ### Required Parameter Errors
66
169
 
67
170
  ```ruby
68
- class ProcessOrderTask < CMDx::Task
69
- required :order_id, type: :integer
70
- optional :user_name, source: :nonexistent_method
171
+ class CreateUserAccountTask < CMDx::Task
172
+ required :email, format: { with: /@/ }
173
+ required :password, length: { min: 8 }
174
+ required :age, type: :integer, numeric: { min: 18 }
175
+
176
+ optional :profile_image, source: :nonexistent_upload_method
177
+ optional :referral_code, source: :missing_referral_source
71
178
 
72
179
  def call
73
- # Task implementation
180
+ User.create!(email: email, password: password, age: age)
74
181
  end
75
182
  end
76
183
 
77
- # English locale
184
+ # Missing required parameters
78
185
  I18n.locale = :en
79
- result = ProcessOrderTask.call({}) # Missing required parameter
80
- result.metadata[:messages][:order_id] #=> ["is a required parameter"]
81
-
82
- result = ProcessOrderTask.call(order_id: 123) # Undefined source method
83
- result.metadata[:messages][:user_name] #=> ["delegates to undefined method nonexistent_method"]
186
+ result = CreateUserAccountTask.call({})
187
+ result.metadata[:messages]
188
+ # {
189
+ # email: ["is a required parameter"],
190
+ # password: ["is a required parameter"],
191
+ # age: ["is a required parameter"]
192
+ # }
193
+
194
+ # German localization
195
+ I18n.locale = :de
196
+ result = CreateUserAccountTask.call({})
197
+ result.metadata[:messages]
198
+ # {
199
+ # email: ["ist ein erforderlicher Parameter"],
200
+ # password: ["ist ein erforderlicher Parameter"],
201
+ # age: ["ist ein erforderlicher Parameter"]
202
+ # }
203
+ ```
84
204
 
85
- # Spanish locale
86
- I18n.locale = :es
87
- result = ProcessOrderTask.call({}) # Missing required parameter
88
- result.metadata[:messages][:order_id] #=> ["es un parámetro requerido"]
205
+ ### Source Method Errors
89
206
 
90
- result = ProcessOrderTask.call(order_id: 123) # Undefined source method
91
- result.metadata[:messages][:user_name] #=> ["delegado al método indefinido nonexistent_method"]
207
+ ```ruby
208
+ # Undefined source method delegation
209
+ I18n.locale = :en
210
+ result = CreateUserAccountTask.call(
211
+ email: "user@example.com",
212
+ password: "securepass",
213
+ age: 25
214
+ )
215
+ result.metadata[:messages]
216
+ # {
217
+ # profile_image: ["delegates to undefined method nonexistent_upload_method"],
218
+ # referral_code: ["delegates to undefined method missing_referral_source"]
219
+ # }
220
+
221
+ # French localization
222
+ I18n.locale = :fr
223
+ result = CreateUserAccountTask.call(
224
+ email: "user@example.com",
225
+ password: "securepass",
226
+ age: 25
227
+ )
228
+ result.metadata[:messages]
229
+ # {
230
+ # profile_image: ["délègue à la méthode non définie nonexistent_upload_method"],
231
+ # referral_code: ["délègue à la méthode non définie missing_referral_source"]
232
+ # }
92
233
  ```
93
234
 
94
235
  ## Coercion Messages
95
236
 
96
- Type conversion errors are automatically localized:
237
+ > [!NOTE]
238
+ > Type conversion failures provide detailed, localized error messages that specify the attempted type(s) and input value context.
239
+
240
+ ### Single Type Coercion Errors
97
241
 
98
242
  ```ruby
99
- class ProcessOrderTask < CMDx::Task
100
- required :order_id, type: :integer
101
- required :amount, type: :float
243
+ class ProcessInventoryTask < CMDx::Task
244
+ required :product_id, type: :integer
245
+ required :price, type: :float
246
+ required :in_stock, type: :boolean
247
+ required :categories, type: :array
248
+ required :metadata, type: :hash
102
249
 
103
250
  def call
104
251
  # Task implementation
105
252
  end
106
253
  end
107
254
 
108
- # English
255
+ # English coercion errors
109
256
  I18n.locale = :en
110
- result = ProcessOrderTask.call(order_id: "invalid", amount: "bad")
111
- result.metadata[:messages][:order_id] #=> ["could not coerce into an integer"]
112
-
113
- # Spanish
257
+ result = ProcessInventoryTask.call(
258
+ product_id: "not-a-number",
259
+ price: "invalid-price",
260
+ in_stock: "maybe",
261
+ categories: "[invalid json",
262
+ metadata: "not-a-hash"
263
+ )
264
+
265
+ result.metadata[:messages]
266
+ # {
267
+ # product_id: ["could not coerce into an integer"],
268
+ # price: ["could not coerce into a float"],
269
+ # in_stock: ["could not coerce into a boolean"],
270
+ # categories: ["could not coerce into an array"],
271
+ # metadata: ["could not coerce into a hash"]
272
+ # }
273
+
274
+ # Spanish coercion errors
114
275
  I18n.locale = :es
115
- result = ProcessOrderTask.call(order_id: "invalid", amount: "bad")
116
- result.metadata[:messages][:order_id] #=> ["no podía coacciona el valor a un integer"]
276
+ result = ProcessInventoryTask.call(
277
+ product_id: "not-a-number",
278
+ price: "invalid-price"
279
+ )
280
+
281
+ result.metadata[:messages]
282
+ # {
283
+ # product_id: ["no se pudo coaccionar a un integer"],
284
+ # price: ["no se pudo coaccionar a un float"]
285
+ # }
117
286
  ```
118
287
 
119
- ## Validation Messages
120
-
121
- All validator error messages support internationalization:
288
+ ### Multiple Type Coercion Errors
122
289
 
123
290
  ```ruby
124
- class RegisterUserTask < CMDx::Task
125
- required :email, format: { with: /@/ }
126
- required :age, numeric: { min: 18 }
127
- required :status, inclusion: { in: %w[active inactive] }
291
+ class ProcessFlexibleDataTask < CMDx::Task
292
+ required :amount, type: [:float, :big_decimal, :integer]
293
+ required :identifier, type: [:integer, :string]
294
+ required :timestamp, type: [:datetime, :date, :time]
128
295
 
129
296
  def call
130
297
  # Task implementation
131
298
  end
132
299
  end
133
300
 
134
- # English
301
+ # Multiple type failure messages
135
302
  I18n.locale = :en
136
- result = RegisterUserTask.call(email: "invalid", age: 16, status: "unknown")
137
- result.metadata[:messages][:email] #=> ["is an invalid format"]
303
+ result = ProcessFlexibleDataTask.call(
304
+ amount: "definitely-not-numeric",
305
+ identifier: nil,
306
+ timestamp: "not-a-date"
307
+ )
308
+
309
+ result.metadata[:messages]
310
+ # {
311
+ # amount: ["could not coerce into one of: float, big_decimal, integer"],
312
+ # identifier: ["could not coerce into one of: integer, string"],
313
+ # timestamp: ["could not coerce into one of: datetime, date, time"]
314
+ # }
315
+
316
+ # Chinese localization
317
+ I18n.locale = :zh
318
+ result = ProcessFlexibleDataTask.call(amount: "invalid")
319
+ result.metadata[:messages][:amount] #=> ["无法强制转换为以下类型之一:float、big_decimal、integer"]
320
+ ```
138
321
 
139
- # Japanese
322
+ ### Nested Parameter Coercion
323
+
324
+ ```ruby
325
+ class ProcessOrderTask < CMDx::Task
326
+ required :order, type: :hash do
327
+ required :id, type: :integer
328
+ required :total, type: :float
329
+
330
+ required :customer, type: :hash do
331
+ required :id, type: :integer
332
+ required :active, type: :boolean
333
+ end
334
+ end
335
+
336
+ def call
337
+ # Task implementation
338
+ end
339
+ end
340
+
341
+ # Nested coercion errors with full path context
342
+ result = ProcessOrderTask.call(
343
+ order: {
344
+ id: "not-a-number",
345
+ total: "invalid-amount",
346
+ customer: {
347
+ id: "bad-id",
348
+ active: "maybe"
349
+ }
350
+ }
351
+ )
352
+
353
+ result.metadata[:messages]
354
+ # {
355
+ # "order.id": ["could not coerce into an integer"],
356
+ # "order.total": ["could not coerce into a float"],
357
+ # "order.customer.id": ["could not coerce into an integer"],
358
+ # "order.customer.active": ["could not coerce into a boolean"]
359
+ # }
360
+ ```
361
+
362
+ ## Validation Messages
363
+
364
+ > [!TIP]
365
+ > All built-in validators provide comprehensive internationalization support, including contextual information for complex validation rules.
366
+
367
+ ### Format Validation
368
+
369
+ ```ruby
370
+ class CreateUserTask < CMDx::Task
371
+ required :email, format: { with: /@/, message: nil } # Use default i18n
372
+ required :phone, format: { with: /\A\+?[\d\s-()]+\z/ }
373
+ required :username, format: { with: /\A[a-zA-Z0-9_]+\z/ }
374
+
375
+ def call
376
+ User.create!(email: email, phone: phone, username: username)
377
+ end
378
+ end
379
+
380
+ # English format errors
381
+ I18n.locale = :en
382
+ result = CreateUserTask.call(
383
+ email: "not-an-email",
384
+ phone: "invalid!phone",
385
+ username: "bad@username"
386
+ )
387
+
388
+ result.metadata[:messages]
389
+ # {
390
+ # email: ["is an invalid format"],
391
+ # phone: ["is an invalid format"],
392
+ # username: ["is an invalid format"]
393
+ # }
394
+
395
+ # Japanese format errors
140
396
  I18n.locale = :ja
141
- result = RegisterUserTask.call(email: "invalid", age: 16, status: "unknown")
142
- result.metadata[:messages][:email] #=> ["無効な形式です"]
397
+ result = CreateUserTask.call(email: "invalid", phone: "bad")
398
+ result.metadata[:messages]
399
+ # {
400
+ # email: ["無効な形式です"],
401
+ # phone: ["無効な形式です"]
402
+ # }
403
+ ```
404
+
405
+ ### Numeric Validation
406
+
407
+ ```ruby
408
+ class ConfigureServiceTask < CMDx::Task
409
+ required :port, numeric: { min: 1024, max: 65535 }
410
+ required :timeout, numeric: { greater_than: 0, less_than: 300 }
411
+ required :retry_count, numeric: { min: 1, max: 10 }
412
+
413
+ def call
414
+ # Service configuration
415
+ end
416
+ end
417
+
418
+ # English numeric errors
419
+ I18n.locale = :en
420
+ result = ConfigureServiceTask.call(
421
+ port: 80, # Below minimum
422
+ timeout: 500, # Above maximum
423
+ retry_count: 0 # Below minimum
424
+ )
425
+
426
+ result.metadata[:messages]
427
+ # {
428
+ # port: ["must be greater than or equal to 1024"],
429
+ # timeout: ["must be less than 300"],
430
+ # retry_count: ["must be greater than or equal to 1"]
431
+ # }
432
+
433
+ # German numeric errors
434
+ I18n.locale = :de
435
+ result = ConfigureServiceTask.call(port: 80, timeout: 500)
436
+ result.metadata[:messages]
437
+ # {
438
+ # port: ["muss größer oder gleich 1024 sein"],
439
+ # timeout: ["muss kleiner als 300 sein"]
440
+ # }
441
+ ```
442
+
443
+ ### Inclusion and Exclusion
444
+
445
+ ```ruby
446
+ class ProcessSubscriptionTask < CMDx::Task
447
+ required :plan, inclusion: { in: %w[basic premium enterprise] }
448
+ required :billing_cycle, inclusion: { in: %w[monthly yearly] }
449
+ required :username, exclusion: { from: %w[admin root system] }
450
+
451
+ def call
452
+ # Subscription processing
453
+ end
454
+ end
455
+
456
+ # English inclusion/exclusion errors
457
+ I18n.locale = :en
458
+ result = ProcessSubscriptionTask.call(
459
+ plan: "invalid-plan",
460
+ billing_cycle: "weekly",
461
+ username: "admin"
462
+ )
463
+
464
+ result.metadata[:messages]
465
+ # {
466
+ # plan: ["is not included in the list"],
467
+ # billing_cycle: ["is not included in the list"],
468
+ # username: ["is reserved"]
469
+ # }
470
+
471
+ # French inclusion/exclusion errors
472
+ I18n.locale = :fr
473
+ result = ProcessSubscriptionTask.call(plan: "invalid", username: "root")
474
+ result.metadata[:messages]
475
+ # {
476
+ # plan: ["n'est pas inclus dans la liste"],
477
+ # username: ["est réservé"]
478
+ # }
479
+ ```
480
+
481
+ ### Length Validation
482
+
483
+ ```ruby
484
+ class CreatePostTask < CMDx::Task
485
+ required :title, length: { min: 5, max: 100 }
486
+ required :content, length: { min: 50 }
487
+ required :tags, length: { max: 10 }
488
+
489
+ def call
490
+ Post.create!(title: title, content: content, tags: tags)
491
+ end
492
+ end
493
+
494
+ # English length errors
495
+ I18n.locale = :en
496
+ result = CreatePostTask.call(
497
+ title: "Hi", # Too short
498
+ content: "Brief content", # Too short
499
+ tags: (1..15).to_a # Too many
500
+ )
501
+
502
+ result.metadata[:messages]
503
+ # {
504
+ # title: ["is too short (minimum is 5 characters)"],
505
+ # content: ["is too short (minimum is 50 characters)"],
506
+ # tags: ["is too long (maximum is 10 characters)"]
507
+ # }
508
+
509
+ # Russian length errors
510
+ I18n.locale = :ru
511
+ result = CreatePostTask.call(title: "Hi", content: "Short")
512
+ result.metadata[:messages]
513
+ # {
514
+ # title: ["слишком короткий (минимум 5 символов)"],
515
+ # content: ["слишком короткий (минимум 50 символов)"]
516
+ # }
517
+ ```
518
+
519
+ ## Custom Message Overrides
520
+
521
+ > [!IMPORTANT]
522
+ > Parameter-specific custom messages always take precedence over locale defaults, allowing fine-grained control while maintaining i18n support.
523
+
524
+ ### Override Examples
525
+
526
+ ```ruby
527
+ class RegisterAccountTask < CMDx::Task
528
+ required :email,
529
+ format: { with: /@/, message: "Please provide a valid email address" }
530
+
531
+ required :password,
532
+ length: { min: 8, message: "Password must be at least 8 characters" }
533
+
534
+ required :age,
535
+ numeric: { min: 18, message: "You must be 18 or older to register" }
536
+
537
+ def call
538
+ # Custom messages override i18n, regardless of locale
539
+ end
540
+ end
541
+
542
+ # Custom messages ignore locale settings
543
+ I18n.locale = :es
544
+ result = RegisterAccountTask.call(
545
+ email: "invalid",
546
+ password: "short",
547
+ age: 16
548
+ )
549
+
550
+ result.metadata[:messages]
551
+ # {
552
+ # email: ["Please provide a valid email address"], # Custom override
553
+ # password: ["Password must be at least 8 characters"], # Custom override
554
+ # age: ["You must be 18 or older to register"] # Custom override
555
+ # }
556
+ ```
557
+
558
+ ### Conditional Overrides
559
+
560
+ ```ruby
561
+ class ProcessPaymentTask < CMDx::Task
562
+ required :amount, type: :float, numeric: { min: 0.01 }
563
+
564
+ # Conditional message based on context
565
+ def validate_amount
566
+ if context[:currency] == "USD" && amount < 0.50
567
+ add_error(:amount, "USD payments must be at least $0.50")
568
+ elsif context[:currency] == "EUR" && amount < 0.01
569
+ add_error(:amount, "EUR payments must be at least €0.01")
570
+ end
571
+ end
572
+
573
+ def call
574
+ validate_amount
575
+ # Payment processing
576
+ end
577
+ end
578
+ ```
579
+
580
+ ## Error Handling and Debugging
581
+
582
+ > [!WARNING]
583
+ > When debugging i18n issues, check locale availability, fallback behavior, and message key resolution to identify configuration problems.
584
+
585
+ ### Debugging Missing Translations
586
+
587
+ ```ruby
588
+ # Enable translation debugging
589
+ I18n.exception_handler = lambda do |exception, locale, key, options|
590
+ Rails.logger.warn "Missing translation: #{locale}.#{key}"
591
+ "translation missing: #{locale}.#{key}"
592
+ end
593
+
594
+ class DebuggingTask < CMDx::Task
595
+ required :test_param, type: :integer
596
+
597
+ def call
598
+ # Intentionally trigger coercion error for debugging
599
+ end
600
+ end
601
+
602
+ # Test with unsupported locale
603
+ I18n.locale = :unsupported_locale
604
+ result = DebuggingTask.call(test_param: "invalid")
605
+ # Logs: "Missing translation: unsupported_locale.cmdx.errors.coercion.integer"
606
+ ```
607
+
608
+ ### Fallback Configuration
609
+
610
+ ```ruby
611
+ # Configure fallback behavior in Rails
612
+ I18n.fallbacks = { es: [:es, :en], fr: [:fr, :en] }
613
+
614
+ class TestLocalizationTask < CMDx::Task
615
+ required :value, type: :integer
616
+
617
+ def call
618
+ # Task logic
619
+ end
620
+ end
621
+
622
+ # Test fallback behavior
623
+ I18n.locale = :es # Falls back to :en if Spanish translation missing
624
+ result = TestLocalizationTask.call(value: "invalid")
625
+ # Uses English if Spanish translation unavailable
626
+ ```
627
+
628
+ ### Error Message Analysis
629
+
630
+ ```ruby
631
+ class AnalyzeErrorsTask < CMDx::Task
632
+ required :data, type: :hash do
633
+ required :id, type: :integer
634
+ required :nested, type: :hash do
635
+ required :value, type: :float
636
+ end
637
+ end
638
+
639
+ def call
640
+ # Complex nested structure for testing
641
+ end
642
+ end
643
+
644
+ # Comprehensive error analysis
645
+ result = AnalyzeErrorsTask.call(
646
+ data: {
647
+ id: "not-integer",
648
+ nested: {
649
+ value: "not-float"
650
+ }
651
+ }
652
+ )
653
+
654
+ # Analyze error structure
655
+ puts "Failed: #{result.failed?}"
656
+ puts "Error count: #{result.metadata[:messages].count}"
657
+ puts "Nested errors present: #{result.metadata[:messages].keys.any? { |k| k.include?('.') }}"
658
+
659
+ result.metadata[:messages].each do |param, errors|
660
+ puts "#{param}: #{errors.join(', ')}"
661
+ end
662
+ # Output shows full parameter path context for nested errors
143
663
  ```
144
664
 
145
665
  ---