num2words 0.3.1 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 78f705a73f571176b4d920dfe220590d2bd217ea513fb817028a0ae2c22221cd
4
- data.tar.gz: 1e8726b93feee82a3b87143ae8a25edf388d726f65ecf424ec713fcc4323ea08
3
+ metadata.gz: 131bb24135ba3f8c1345bbd7ccf5c3b27d79b9c9776d7e0ec45b259960be1dbd
4
+ data.tar.gz: 27a943ef2bbaae410ae51d1aca3fe009375f68632e1928b4639a570919619c25
5
5
  SHA512:
6
- metadata.gz: d8a078fef78bc1e26fc28d90c6604e6a3a09aa05e3b716df2faea6d7423bb91a4de1c907eaa7d7e9c85b0d3019fa98257f6b499113f1987d646c902680bcdd66
7
- data.tar.gz: a87e663b01668bdf114cba5eb2c2f9f55b453b4aae314b5ceceac78047574e1fc5917a46d97fddaf60cea2a64333c2b1f0901694daed5aade3beff7d6682c1b9
6
+ metadata.gz: 3601c493e2cf40b2a908daca5980f79c95933316fa2a87393306c479919ec795c15af731e2eb075fd92b9aa61a97fd72badaa575723d8201e5093cecc89e6d88
7
+ data.tar.gz: 5000cdf5a8fcaa4cf2af57cb2ad7640e111f9f2a1eb7fdf94783ac19a76a2cd8aedd2ef44cbecd7eee9647d3bb1be064f2d70f711634aa3de679f83ede17a47d
data/CHANGELOG.md CHANGED
@@ -11,6 +11,38 @@
11
11
 
12
12
  ---
13
13
 
14
+ ## [0.5.0] - 2026-07-06
15
+
16
+ ### Added
17
+ - Опция `strict: true` для `to_currency`, которая вызывает `Num2words::UnsupportedCurrencyError` при недоступной валюте вместо fallback на валюту по умолчанию.
18
+ - Классы ошибок `Num2words::UnsupportedInputError`, `Num2words::UnsupportedOptionError` и `Num2words::UnsupportedCurrencyAmountError`, совместимые с `ArgumentError`.
19
+ - Rails/ActiveModel сообщения ошибок для валидаторов `num2words_currency` и `num2words_locale`.
20
+ - Публичный metadata API `Num2words.currency_options` для select-полей и настроек.
21
+ - Публичный metadata API `Num2words.locale_options` для select-полей и настроек.
22
+
23
+ ---
24
+
25
+ ## [0.4.0] - 2026-07-05
26
+
27
+ ### Added
28
+ - Опциональные Rails helpers и Railtie:
29
+ - `number_to_words`
30
+ - `currency_to_words`
31
+ - `date_to_words`
32
+ - `time_to_words`
33
+ - `datetime_to_words`
34
+ - Интеграционные specs для проверки Railtie в минимальном Rails-приложении.
35
+ - Подключение Rails helpers в controller helper context.
36
+ - Валидатор ActiveModel `num2words_currency` для проверки валюты по локали.
37
+ - Валидатор ActiveModel `num2words_locale` для проверки поддерживаемой локали.
38
+ - Публичный metadata API `Num2words.available_locales`.
39
+ - Публичный metadata API `Num2words.currency_available?`.
40
+ - Публичный metadata API `Num2words.currency_info`.
41
+ - Публичный metadata API `Num2words.default_currency_info`.
42
+ - Документация по Rails-интеграции: `docs/rails.md` и `docs/rails.ru.md`.
43
+
44
+ ---
45
+
14
46
  ## [0.3.1] - 2026-07-04
15
47
 
16
48
  ### Added
data/Gemfile CHANGED
@@ -9,4 +9,5 @@ gem "i18n", "~> 1.10"
9
9
 
10
10
  group :development, :test do
11
11
  gem "rspec", "~> 3.12"
12
+ gem "rails", "~> 7.1"
12
13
  end
data/README.md CHANGED
@@ -14,6 +14,9 @@ Ruby-гем для преобразования чисел, дробей, ден
14
14
  - Даты, время и дата-время.
15
15
  - Регистры вывода: `word_case: :upper | :downcase | :capitalize | :title`.
16
16
  - Удобные методы для `Integer`, `Float`, `String`, `Date`, `Time`, `DateTime`.
17
+ - Опциональные Rails helpers и Railtie.
18
+ - ActiveModel validators для локалей и валют.
19
+ - Metadata API для локалей и валют.
17
20
  - Интерактивная консоль `num2words-console`.
18
21
 
19
22
  ---
@@ -81,6 +84,9 @@ Num2words.to_currency(BigDecimal("21.05"), :ru)
81
84
  12.50.to_currency(:ru, code: :USD)
82
85
  # => "двенадцать долларов пятьдесят центов"
83
86
 
87
+ Num2words.to_currency(12.50, :ru, code: :XYZ, strict: true)
88
+ # raises Num2words::UnsupportedCurrencyError
89
+
84
90
  1.to_currency(:ru, minor: :nonzero)
85
91
  # => "один рубль"
86
92
 
@@ -142,6 +148,7 @@ Num2words.to_currency(BigDecimal("21.05"), :ru)
142
148
  - `word_case: :default | :upper | :downcase | :capitalize | :title` — регистр результата.
143
149
  - `minor: :always | :nonzero | :never` — вывод младшей денежной единицы.
144
150
  - `code: :USD` — выбор валюты для `to_currency`.
151
+ - `strict: true` — ошибка вместо fallback при недоступной валюте в `to_currency`.
145
152
 
146
153
  Полная справка по API:
147
154
 
@@ -153,6 +160,86 @@ Num2words.to_currency(BigDecimal("21.05"), :ru)
153
160
  - [English](docs/limits.md)
154
161
  - [Русский](docs/limits.ru.md)
155
162
 
163
+ Rails-интеграция:
164
+
165
+ - [English](docs/rails.md)
166
+ - [Русский](docs/rails.ru.md)
167
+
168
+ ---
169
+
170
+ ## Rails
171
+
172
+ Если Rails доступен, helpers подключаются автоматически через Railtie.
173
+
174
+ ```erb
175
+ <%= number_to_words(123, locale: :ru) %>
176
+ <%= currency_to_words(@payment.amount, locale: :ru, code: @payment.currency) %>
177
+ <%= date_to_words(Date.current, locale: I18n.locale) %>
178
+ ```
179
+
180
+ Также доступны ActiveModel validators:
181
+
182
+ ```ruby
183
+ class Payment < ApplicationRecord
184
+ validates :currency, num2words_currency: { locale: ->(payment) { payment.locale } }
185
+ validates :locale, num2words_locale: true
186
+ end
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Metadata API
192
+
193
+ ```ruby
194
+ Num2words.available_locales
195
+ # => [:ar, :be, :bg, :bn, :cs, ...]
196
+
197
+ Num2words.currency_available?(:ru, :RUB)
198
+ # => true
199
+
200
+ Num2words.currency_info(:ru, :RUB)
201
+ # => {
202
+ # code: :RUB,
203
+ # major_unit: ["рубль", "рубля", "рублей"],
204
+ # minor_unit: ["копейка", "копейки", "копеек"],
205
+ # symbol: "₽"
206
+ # }
207
+
208
+ Num2words.default_currency_info(:ru)
209
+ # => {
210
+ # code: :RUB,
211
+ # major_unit: ["рубль", "рубля", "рублей"],
212
+ # minor_unit: ["копейка", "копейки", "копеек"],
213
+ # symbol: "₽"
214
+ # }
215
+
216
+ Num2words.currency_options(:ru)
217
+ # => [["RUB — ₽", :RUB], ["USD — $", :USD], ...]
218
+
219
+ Num2words.locale_options
220
+ # => [["ar", :ar], ["be", :be], ["bg", :bg], ...]
221
+ ```
222
+
223
+ ---
224
+
225
+ ## Ошибки
226
+
227
+ ```ruby
228
+ Num2words.to_currency(10, :ru, code: :XYZ, strict: true)
229
+ # raises Num2words::UnsupportedCurrencyError
230
+
231
+ Num2words.to_words(Object.new, :ru)
232
+ # raises Num2words::UnsupportedInputError
233
+
234
+ Num2words.to_words(0.5, :ru, joiner: :plus)
235
+ # raises Num2words::UnsupportedOptionError
236
+
237
+ Num2words.to_currency("abc", :ru)
238
+ # raises Num2words::UnsupportedCurrencyAmountError
239
+ ```
240
+
241
+ Ошибки пользовательских аргументов наследуются от `ArgumentError`, поэтому существующий код с `rescue ArgumentError` продолжает работать.
242
+
156
243
  ---
157
244
 
158
245
  ## Консоль
@@ -184,3 +184,7 @@ en:
184
184
  major_unit: ["shilling", "shillings", "shillings"]
185
185
  minor_unit: ["cent", "cents", "cents"]
186
186
  symbol: "Sh"
187
+ errors:
188
+ messages:
189
+ unsupported_currency: "is not supported for this locale"
190
+ unsupported_locale: "is not supported"
@@ -204,3 +204,7 @@ ru:
204
204
  major_unit: ["шиллинг", "шиллинга", "шиллингов"]
205
205
  minor_unit: ["цент", "цента", "центов"]
206
206
  symbol: "Sh"
207
+ errors:
208
+ messages:
209
+ unsupported_currency: "не поддерживается для выбранной локали"
210
+ unsupported_locale: "не поддерживается"
data/docs/api.md CHANGED
@@ -318,6 +318,15 @@ Num2words.to_currency(12.50, :ru, code: :USD)
318
318
 
319
319
  If the currency is not available for the locale, `num2words` falls back to the locale default currency and may emit a warning depending on configuration.
320
320
 
321
+ ### `strict`
322
+
323
+ For `to_currency`, enables strict currency validation. When `code:` is unavailable for the locale, the method raises `Num2words::UnsupportedCurrencyError` instead of falling back to the default currency.
324
+
325
+ ```ruby
326
+ Num2words.to_currency(12.50, :ru, code: :XYZ, strict: true)
327
+ # raises Num2words::UnsupportedCurrencyError
328
+ ```
329
+
321
330
  ### `minor`
322
331
 
323
332
  Controls minor currency unit output.
@@ -364,6 +373,99 @@ Num2words.available_currencies(:ru)
364
373
  # => [:RUB, :USD, :EUR, ...]
365
374
  ```
366
375
 
376
+ ### `Num2words.currency_available?`
377
+
378
+ Checks whether a currency is available for a locale.
379
+
380
+ ```ruby
381
+ Num2words.currency_available?(:ru, :RUB)
382
+ # => true
383
+
384
+ Num2words.currency_available?(:ru, "usd")
385
+ # => true
386
+
387
+ Num2words.currency_available?(:ru, :XYZ)
388
+ # => false
389
+ ```
390
+
391
+ ### `Num2words.currency_info`
392
+
393
+ Returns currency metadata for a locale or `nil` when the currency is unavailable.
394
+
395
+ ```ruby
396
+ Num2words.currency_info(:ru, :RUB)
397
+ # => {
398
+ # code: :RUB,
399
+ # major_unit: ["рубль", "рубля", "рублей"],
400
+ # minor_unit: ["копейка", "копейки", "копеек"],
401
+ # symbol: "₽"
402
+ # }
403
+
404
+ Num2words.currency_info(:ru, :XYZ)
405
+ # => nil
406
+ ```
407
+
408
+ ### `Num2words.default_currency_info`
409
+
410
+ Returns metadata for the locale default currency.
411
+
412
+ ```ruby
413
+ Num2words.default_currency_info(:ru)
414
+ # => {
415
+ # code: :RUB,
416
+ # major_unit: ["рубль", "рубля", "рублей"],
417
+ # minor_unit: ["копейка", "копейки", "копеек"],
418
+ # symbol: "₽"
419
+ # }
420
+ ```
421
+
422
+ ### `Num2words.currency_options`
423
+
424
+ Returns an array for select inputs and settings screens.
425
+
426
+ ```ruby
427
+ Num2words.currency_options(:ru)
428
+ # => [["RUB — ₽", :RUB], ["USD — $", :USD], ...]
429
+
430
+ Num2words.currency_options(:ru, format: :code)
431
+ # => [["RUB", :RUB], ["USD", :USD], ...]
432
+
433
+ Num2words.currency_options(:ru, format: :name)
434
+ # => [["рубль", :RUB], ["доллар", :USD], ...]
435
+ ```
436
+
437
+ Supported `format:` values:
438
+
439
+ - `:symbol` - currency code and symbol, default.
440
+ - `:code` - currency code only.
441
+ - `:name` - localized major currency unit name.
442
+
443
+ ### `Num2words.available_locales`
444
+
445
+ Returns supported locale codes.
446
+
447
+ ```ruby
448
+ Num2words.available_locales
449
+ # => [:ar, :be, :bg, :bn, :cs, ...]
450
+ ```
451
+
452
+ ### `Num2words.locale_options`
453
+
454
+ Returns an array of locales for select inputs and settings screens.
455
+
456
+ ```ruby
457
+ Num2words.locale_options
458
+ # => [["ar", :ar], ["be", :be], ["bg", :bg], ...]
459
+
460
+ Num2words.locale_options(format: :upcase)
461
+ # => [["AR", :ar], ["BE", :be], ["BG", :bg], ...]
462
+ ```
463
+
464
+ Supported `format:` values:
465
+
466
+ - `:code` - locale code, default.
467
+ - `:upcase` - upper-case locale code.
468
+
367
469
  ### `Num2words.currency_warnings`
368
470
 
369
471
  Controls warnings when requested currency is unavailable.
@@ -391,32 +493,42 @@ KZT MYR NOK PKR PLN RON RSD RUB SAR SEK THB TRY UAH USD VND
391
493
 
392
494
  ## Errors
393
495
 
496
+ User argument errors inherit from `ArgumentError`, so existing code using `rescue ArgumentError` continues to work.
497
+
394
498
  Unsupported input type:
395
499
 
396
500
  ```ruby
397
501
  Num2words.to_words(Object.new, :en)
398
- # raises ArgumentError: Unsupported input type: ...
502
+ # raises Num2words::UnsupportedInputError
399
503
  ```
400
504
 
401
505
  Unsupported options:
402
506
 
403
507
  ```ruby
404
508
  Num2words.to_words(0.5, :ru, joiner: :plus)
405
- # raises ArgumentError: Unsupported joiner option: :plus
509
+ # raises Num2words::UnsupportedOptionError
406
510
 
407
511
  Num2words.to_currency(12, :ru, minor: :sometimes)
408
- # raises ArgumentError: Unsupported minor option: :sometimes
512
+ # raises Num2words::UnsupportedOptionError
409
513
  ```
410
514
 
411
515
  Unsupported currency amount:
412
516
 
413
517
  ```ruby
414
518
  Num2words.to_currency("abc", :en)
415
- # raises ArgumentError: Unsupported currency amount: "abc"
519
+ # raises Num2words::UnsupportedCurrencyAmountError
520
+ ```
521
+
522
+ Unsupported currency in strict mode:
523
+
524
+ ```ruby
525
+ Num2words.to_currency(10, :ru, code: :XYZ, strict: true)
526
+ # raises Num2words::UnsupportedCurrencyError
416
527
  ```
417
528
 
418
529
  ## Development References
419
530
 
420
531
  - [Numeric limits](limits.md)
532
+ - [Rails integration](rails.md)
421
533
  - [Locale development guide](locale_development.md)
422
534
  - [Russian locale development guide](locale_development.ru.md)
data/docs/api.ru.md CHANGED
@@ -318,6 +318,15 @@ Num2words.to_currency(12.50, :ru, code: :USD)
318
318
 
319
319
  Если валюта недоступна для локали, `num2words` использует валюту локали по умолчанию и может вывести warning в зависимости от настроек.
320
320
 
321
+ ### `strict`
322
+
323
+ Для `to_currency` включает строгую проверку валюты. Если `code:` недоступен для локали, метод вызывает `Num2words::UnsupportedCurrencyError` вместо fallback на валюту по умолчанию.
324
+
325
+ ```ruby
326
+ Num2words.to_currency(12.50, :ru, code: :XYZ, strict: true)
327
+ # raises Num2words::UnsupportedCurrencyError
328
+ ```
329
+
321
330
  ### `minor`
322
331
 
323
332
  Управляет выводом младшей денежной единицы.
@@ -364,6 +373,99 @@ Num2words.available_currencies(:ru)
364
373
  # => [:RUB, :USD, :EUR, ...]
365
374
  ```
366
375
 
376
+ ### `Num2words.currency_available?`
377
+
378
+ Проверяет, доступна ли валюта для локали.
379
+
380
+ ```ruby
381
+ Num2words.currency_available?(:ru, :RUB)
382
+ # => true
383
+
384
+ Num2words.currency_available?(:ru, "usd")
385
+ # => true
386
+
387
+ Num2words.currency_available?(:ru, :XYZ)
388
+ # => false
389
+ ```
390
+
391
+ ### `Num2words.currency_info`
392
+
393
+ Возвращает данные валюты для локали или `nil`, если валюта недоступна.
394
+
395
+ ```ruby
396
+ Num2words.currency_info(:ru, :RUB)
397
+ # => {
398
+ # code: :RUB,
399
+ # major_unit: ["рубль", "рубля", "рублей"],
400
+ # minor_unit: ["копейка", "копейки", "копеек"],
401
+ # symbol: "₽"
402
+ # }
403
+
404
+ Num2words.currency_info(:ru, :XYZ)
405
+ # => nil
406
+ ```
407
+
408
+ ### `Num2words.default_currency_info`
409
+
410
+ Возвращает данные валюты по умолчанию для локали.
411
+
412
+ ```ruby
413
+ Num2words.default_currency_info(:ru)
414
+ # => {
415
+ # code: :RUB,
416
+ # major_unit: ["рубль", "рубля", "рублей"],
417
+ # minor_unit: ["копейка", "копейки", "копеек"],
418
+ # symbol: "₽"
419
+ # }
420
+ ```
421
+
422
+ ### `Num2words.currency_options`
423
+
424
+ Возвращает массив для select-полей и настроек.
425
+
426
+ ```ruby
427
+ Num2words.currency_options(:ru)
428
+ # => [["RUB — ₽", :RUB], ["USD — $", :USD], ...]
429
+
430
+ Num2words.currency_options(:ru, format: :code)
431
+ # => [["RUB", :RUB], ["USD", :USD], ...]
432
+
433
+ Num2words.currency_options(:ru, format: :name)
434
+ # => [["рубль", :RUB], ["доллар", :USD], ...]
435
+ ```
436
+
437
+ Поддерживаемые значения `format:`:
438
+
439
+ - `:symbol` - код и символ валюты, значение по умолчанию.
440
+ - `:code` - только код валюты.
441
+ - `:name` - локализованное название основной денежной единицы.
442
+
443
+ ### `Num2words.available_locales`
444
+
445
+ Возвращает список поддерживаемых локалей.
446
+
447
+ ```ruby
448
+ Num2words.available_locales
449
+ # => [:ar, :be, :bg, :bn, :cs, ...]
450
+ ```
451
+
452
+ ### `Num2words.locale_options`
453
+
454
+ Возвращает массив локалей для select-полей и настроек.
455
+
456
+ ```ruby
457
+ Num2words.locale_options
458
+ # => [["ar", :ar], ["be", :be], ["bg", :bg], ...]
459
+
460
+ Num2words.locale_options(format: :upcase)
461
+ # => [["AR", :ar], ["BE", :be], ["BG", :bg], ...]
462
+ ```
463
+
464
+ Поддерживаемые значения `format:`:
465
+
466
+ - `:code` - код локали, значение по умолчанию.
467
+ - `:upcase` - код локали в верхнем регистре.
468
+
367
469
  ### `Num2words.currency_warnings`
368
470
 
369
471
  Управляет warning при запросе валюты, недоступной для локали.
@@ -391,32 +493,42 @@ KZT MYR NOK PKR PLN RON RSD RUB SAR SEK THB TRY UAH USD VND
391
493
 
392
494
  ## Ошибки
393
495
 
496
+ Ошибки пользовательских аргументов наследуются от `ArgumentError`, поэтому существующий код с `rescue ArgumentError` продолжает работать.
497
+
394
498
  Неподдерживаемый тип входных данных:
395
499
 
396
500
  ```ruby
397
501
  Num2words.to_words(Object.new, :en)
398
- # raises ArgumentError: Unsupported input type: ...
502
+ # raises Num2words::UnsupportedInputError
399
503
  ```
400
504
 
401
505
  Неподдерживаемые опции:
402
506
 
403
507
  ```ruby
404
508
  Num2words.to_words(0.5, :ru, joiner: :plus)
405
- # raises ArgumentError: Unsupported joiner option: :plus
509
+ # raises Num2words::UnsupportedOptionError
406
510
 
407
511
  Num2words.to_currency(12, :ru, minor: :sometimes)
408
- # raises ArgumentError: Unsupported minor option: :sometimes
512
+ # raises Num2words::UnsupportedOptionError
409
513
  ```
410
514
 
411
515
  Некорректная денежная сумма:
412
516
 
413
517
  ```ruby
414
518
  Num2words.to_currency("abc", :en)
415
- # raises ArgumentError: Unsupported currency amount: "abc"
519
+ # raises Num2words::UnsupportedCurrencyAmountError
520
+ ```
521
+
522
+ Недоступная валюта в строгом режиме:
523
+
524
+ ```ruby
525
+ Num2words.to_currency(10, :ru, code: :XYZ, strict: true)
526
+ # raises Num2words::UnsupportedCurrencyError
416
527
  ```
417
528
 
418
529
  ## Документация для разработки
419
530
 
420
531
  - [Числовые ограничения](limits.ru.md)
532
+ - [Rails-интеграция](rails.ru.md)
421
533
  - [Locale development guide](locale_development.md)
422
534
  - [Русская инструкция по разработке локалей](locale_development.ru.md)
data/docs/rails.md ADDED
@@ -0,0 +1,141 @@
1
+ # Rails Integration
2
+
3
+ `num2words` provides optional Rails helpers without adding Rails as a runtime dependency.
4
+
5
+ If Rails is available, `Num2words::Railtie` automatically includes helpers into Action View. Outside Rails, the helper module can be included manually.
6
+
7
+ The integration is covered by specs with a minimal Rails application, but Rails remains a development/test dependency only.
8
+
9
+ ## Helpers
10
+
11
+ Available helpers:
12
+
13
+ - `number_to_words(number, locale: I18n.locale, **options)`
14
+ - `currency_to_words(amount, locale: I18n.locale, **options)`
15
+ - `date_to_words(date, locale: I18n.locale, **options)`
16
+ - `time_to_words(time, locale: I18n.locale, **options)`
17
+ - `datetime_to_words(datetime, locale: I18n.locale, **options)`
18
+
19
+ ## Rails Views
20
+
21
+ ```erb
22
+ <%= number_to_words(123, locale: :ru) %>
23
+ <%= currency_to_words(@invoice.total, locale: :ru, code: :RUB) %>
24
+ <%= date_to_words(Date.current, locale: I18n.locale) %>
25
+ <%= time_to_words(Time.current, locale: I18n.locale) %>
26
+ ```
27
+
28
+ When `locale:` is omitted, helpers use `I18n.locale`.
29
+
30
+ ```erb
31
+ <%= number_to_words(123) %>
32
+ ```
33
+
34
+ ## Controllers
35
+
36
+ Helpers are also exposed through the controller helper context:
37
+
38
+ ```ruby
39
+ class PaymentsController < ApplicationController
40
+ def show
41
+ @amount_in_words = helpers.currency_to_words(@payment.amount, locale: :ru, code: @payment.currency)
42
+ end
43
+ end
44
+ ```
45
+
46
+ ## ActiveModel Validations
47
+
48
+ `num2words` provides a currency validator for Rails and ActiveModel models:
49
+
50
+ ```ruby
51
+ class Payment < ApplicationRecord
52
+ validates :currency, num2words_currency: { locale: :ru }
53
+ end
54
+ ```
55
+
56
+ Valid values are checked against `Num2words.currency_available?(locale, currency)`.
57
+
58
+ ```ruby
59
+ Payment.new(currency: "RUB").valid?
60
+ # => true
61
+
62
+ Payment.new(currency: "XYZ").valid?
63
+ # => false
64
+ ```
65
+
66
+ The locale can also be dynamic:
67
+
68
+ ```ruby
69
+ class Payment < ApplicationRecord
70
+ validates :currency, num2words_currency: { locale: ->(payment) { payment.locale } }
71
+ end
72
+ ```
73
+
74
+ Locale validation is also available:
75
+
76
+ ```ruby
77
+ class Payment < ApplicationRecord
78
+ validates :locale, num2words_locale: true
79
+ end
80
+ ```
81
+
82
+ ```ruby
83
+ Payment.new(locale: "ru").valid?
84
+ # => true
85
+
86
+ Payment.new(locale: "xx").valid?
87
+ # => false
88
+ ```
89
+
90
+ Valid values are checked against `Num2words.available_locales`.
91
+
92
+ Error messages for `unsupported_currency` and `unsupported_locale` are included in the gem locale files. Rails apps can override them through the standard `errors.messages` keys.
93
+
94
+ ## Metadata API
95
+
96
+ For forms, select inputs and settings screens, use the public metadata API:
97
+
98
+ ```ruby
99
+ Num2words.available_locales
100
+ Num2words.available_currencies(:ru)
101
+ Num2words.currency_available?(:ru, :RUB)
102
+ Num2words.currency_info(:ru, :RUB)
103
+ Num2words.default_currency_info(:ru)
104
+ Num2words.currency_options(:ru)
105
+ Num2words.locale_options
106
+ ```
107
+
108
+ Rails form example:
109
+
110
+ ```erb
111
+ <%= form.select :currency, Num2words.currency_options(I18n.locale) %>
112
+ <%= form.select :locale, Num2words.locale_options %>
113
+ ```
114
+
115
+ ## Manual Usage
116
+
117
+ The helpers can be used without Rails:
118
+
119
+ ```ruby
120
+ helper = Object.new.extend(Num2words::Rails::Helpers)
121
+
122
+ helper.number_to_words(123, locale: :en)
123
+ # => "one hundred twenty three"
124
+
125
+ helper.currency_to_words("12.50", locale: :ru, code: :USD)
126
+ # => "двенадцать долларов пятьдесят центов"
127
+ ```
128
+
129
+ ## Options
130
+
131
+ Helpers forward all options to `Num2words.to_words` or `Num2words.to_currency`.
132
+
133
+ ```ruby
134
+ number_to_words(0.5, locale: :ru, joiner: :and)
135
+ # => "ноль и пять десятых"
136
+
137
+ currency_to_words(12, locale: :ru, minor: :nonzero)
138
+ # => "двенадцать рублей"
139
+ ```
140
+
141
+ See [API reference](api.md) for the full option list.
data/docs/rails.ru.md ADDED
@@ -0,0 +1,141 @@
1
+ # Rails-интеграция
2
+
3
+ `num2words` предоставляет optional Rails helpers без добавления Rails как runtime-зависимости.
4
+
5
+ Если Rails доступен, `Num2words::Railtie` автоматически подключает helpers в Action View. Вне Rails модуль helpers можно подключить вручную.
6
+
7
+ Интеграция покрыта specs с минимальным Rails-приложением, но Rails остается зависимостью только для development/test.
8
+
9
+ ## Helpers
10
+
11
+ Доступные helpers:
12
+
13
+ - `number_to_words(number, locale: I18n.locale, **options)`
14
+ - `currency_to_words(amount, locale: I18n.locale, **options)`
15
+ - `date_to_words(date, locale: I18n.locale, **options)`
16
+ - `time_to_words(time, locale: I18n.locale, **options)`
17
+ - `datetime_to_words(datetime, locale: I18n.locale, **options)`
18
+
19
+ ## Rails views
20
+
21
+ ```erb
22
+ <%= number_to_words(123, locale: :ru) %>
23
+ <%= currency_to_words(@invoice.total, locale: :ru, code: :RUB) %>
24
+ <%= date_to_words(Date.current, locale: I18n.locale) %>
25
+ <%= time_to_words(Time.current, locale: I18n.locale) %>
26
+ ```
27
+
28
+ Если `locale:` не передан, helpers используют `I18n.locale`.
29
+
30
+ ```erb
31
+ <%= number_to_words(123) %>
32
+ ```
33
+
34
+ ## Controllers
35
+
36
+ Helpers также доступны через controller helper context:
37
+
38
+ ```ruby
39
+ class PaymentsController < ApplicationController
40
+ def show
41
+ @amount_in_words = helpers.currency_to_words(@payment.amount, locale: :ru, code: @payment.currency)
42
+ end
43
+ end
44
+ ```
45
+
46
+ ## ActiveModel validations
47
+
48
+ `num2words` предоставляет валидатор валюты для Rails и ActiveModel моделей:
49
+
50
+ ```ruby
51
+ class Payment < ApplicationRecord
52
+ validates :currency, num2words_currency: { locale: :ru }
53
+ end
54
+ ```
55
+
56
+ Валидные значения проверяются через `Num2words.currency_available?(locale, currency)`.
57
+
58
+ ```ruby
59
+ Payment.new(currency: "RUB").valid?
60
+ # => true
61
+
62
+ Payment.new(currency: "XYZ").valid?
63
+ # => false
64
+ ```
65
+
66
+ Локаль может быть динамической:
67
+
68
+ ```ruby
69
+ class Payment < ApplicationRecord
70
+ validates :currency, num2words_currency: { locale: ->(payment) { payment.locale } }
71
+ end
72
+ ```
73
+
74
+ Также доступна проверка локали:
75
+
76
+ ```ruby
77
+ class Payment < ApplicationRecord
78
+ validates :locale, num2words_locale: true
79
+ end
80
+ ```
81
+
82
+ ```ruby
83
+ Payment.new(locale: "ru").valid?
84
+ # => true
85
+
86
+ Payment.new(locale: "xx").valid?
87
+ # => false
88
+ ```
89
+
90
+ Валидные значения проверяются через `Num2words.available_locales`.
91
+
92
+ Сообщения ошибок для `unsupported_currency` и `unsupported_locale` уже добавлены в locale files гема. Их можно переопределить в Rails-приложении стандартным способом через `errors.messages`.
93
+
94
+ ## Metadata API
95
+
96
+ Для форм, select-полей и настроек можно использовать публичные методы metadata API:
97
+
98
+ ```ruby
99
+ Num2words.available_locales
100
+ Num2words.available_currencies(:ru)
101
+ Num2words.currency_available?(:ru, :RUB)
102
+ Num2words.currency_info(:ru, :RUB)
103
+ Num2words.default_currency_info(:ru)
104
+ Num2words.currency_options(:ru)
105
+ Num2words.locale_options
106
+ ```
107
+
108
+ Пример для Rails form:
109
+
110
+ ```erb
111
+ <%= form.select :currency, Num2words.currency_options(I18n.locale) %>
112
+ <%= form.select :locale, Num2words.locale_options %>
113
+ ```
114
+
115
+ ## Ручное использование
116
+
117
+ Helpers можно использовать без Rails:
118
+
119
+ ```ruby
120
+ helper = Object.new.extend(Num2words::Rails::Helpers)
121
+
122
+ helper.number_to_words(123, locale: :en)
123
+ # => "one hundred twenty three"
124
+
125
+ helper.currency_to_words("12.50", locale: :ru, code: :USD)
126
+ # => "двенадцать долларов пятьдесят центов"
127
+ ```
128
+
129
+ ## Опции
130
+
131
+ Helpers передают все опции в `Num2words.to_words` или `Num2words.to_currency`.
132
+
133
+ ```ruby
134
+ number_to_words(0.5, locale: :ru, joiner: :and)
135
+ # => "ноль и пять десятых"
136
+
137
+ currency_to_words(12, locale: :ru, minor: :nonzero)
138
+ # => "двенадцать рублей"
139
+ ```
140
+
141
+ Полный список опций смотри в [справочнике API](api.ru.md).
@@ -27,6 +27,52 @@ module Num2words
27
27
  I18n.t("num2words.currencies", locale: locale).keys.map(&:to_sym)
28
28
  end
29
29
 
30
+ def currency_available?(locale, currency)
31
+ available_currencies(locale).include?(currency.to_s.upcase.to_sym)
32
+ end
33
+
34
+ def currency_info(locale, currency)
35
+ code = currency.to_s.upcase.to_sym
36
+ data = I18n.t("num2words.currencies", locale: locale)[code]
37
+
38
+ return unless data
39
+
40
+ {
41
+ code: code,
42
+ major_unit: data[:major_unit],
43
+ minor_unit: data[:minor_unit],
44
+ symbol: data[:symbol]
45
+ }
46
+ end
47
+
48
+ def default_currency_info(locale = I18n.locale)
49
+ currency_info(locale, default_currency(locale))
50
+ end
51
+
52
+ def currency_options(locale = I18n.locale, format: :symbol)
53
+ validate_currency_options_format!(format)
54
+
55
+ available_currencies(locale).map do |code|
56
+ info = currency_info(locale, code)
57
+
58
+ [currency_option_label(info, format), code]
59
+ end
60
+ end
61
+
62
+ def available_locales
63
+ @available_locales ||= Dir[File.expand_path("../../config/locales/*.yml", __dir__)]
64
+ .map { |file| File.basename(file, ".yml").to_sym }
65
+ .sort
66
+ end
67
+
68
+ def locale_options(format: :code)
69
+ validate_locale_options_format!(format)
70
+
71
+ available_locales.map do |locale|
72
+ [locale_option_label(locale, format), locale]
73
+ end
74
+ end
75
+
30
76
  def reset!(locale = nil)
31
77
  return @local_currency.delete(locale) if locale
32
78
 
@@ -34,6 +80,34 @@ module Num2words
34
80
  @local_currency = {}
35
81
  @currency_warnings = true
36
82
  end
83
+
84
+ private
85
+
86
+ def validate_currency_options_format!(format)
87
+ return if %i[symbol code name].include?(format)
88
+
89
+ raise UnsupportedOptionError.new(name: :format, value: format)
90
+ end
91
+
92
+ def validate_locale_options_format!(format)
93
+ return if %i[code upcase].include?(format)
94
+
95
+ raise UnsupportedOptionError.new(name: :format, value: format)
96
+ end
97
+
98
+ def currency_option_label(info, format)
99
+ case format
100
+ when :code then info[:code].to_s
101
+ when :name then info[:major_unit].first
102
+ else [info[:code], info[:symbol]].compact.join(" — ")
103
+ end
104
+ end
105
+
106
+ def locale_option_label(locale, format)
107
+ label = locale.to_s
108
+
109
+ format == :upcase ? label.upcase : label
110
+ end
37
111
  end
38
112
 
39
113
  def self.config
@@ -52,6 +126,30 @@ module Num2words
52
126
  config.available_currencies(locale)
53
127
  end
54
128
 
129
+ def self.currency_available?(locale, currency)
130
+ config.currency_available?(locale, currency)
131
+ end
132
+
133
+ def self.currency_info(locale, currency)
134
+ config.currency_info(locale, currency)
135
+ end
136
+
137
+ def self.default_currency_info(locale = I18n.locale)
138
+ config.default_currency_info(locale)
139
+ end
140
+
141
+ def self.currency_options(locale = I18n.locale, format: :symbol)
142
+ config.currency_options(locale, format: format)
143
+ end
144
+
145
+ def self.available_locales
146
+ config.available_locales
147
+ end
148
+
149
+ def self.locale_options(format: :code)
150
+ config.locale_options(format: format)
151
+ end
152
+
55
153
  def self.currency_warnings
56
154
  config.currency_warnings
57
155
  end
@@ -33,7 +33,7 @@ module Num2words
33
33
  else nil
34
34
  end
35
35
 
36
- raise ArgumentError, "Unsupported input type: #{number.inspect}" if result.nil?
36
+ raise UnsupportedInputError, number if result.nil?
37
37
 
38
38
  apply_case(result, word_case)
39
39
  end
@@ -43,12 +43,15 @@ module Num2words
43
43
  word_case = opts[:word_case] || :downcase
44
44
  currency = (opts[:code] || Num2words.default_currency(locale)).to_s.upcase.to_sym
45
45
  minor = opts[:minor] || :always
46
+ strict = opts[:strict] || false
46
47
 
47
48
  validate_option!(:minor, minor, %i[always nonzero never])
48
49
 
49
50
  unless Num2words.available_currencies(locale).include?(currency)
51
+ raise UnsupportedCurrencyError.new(locale: locale, currency: currency) if strict
52
+
50
53
  warn I18n.t("num2words.warnings.currency_not_available",
51
- currency: currency, locale: locale) if Num2words.currency_warnings
54
+ currency: currency, lang: locale, locale: locale) if Num2words.currency_warnings
52
55
  currency = Num2words.default_currency(locale)
53
56
  end
54
57
 
@@ -83,7 +86,7 @@ module Num2words
83
86
  def validate_option!(name, value, allowed_values)
84
87
  return if allowed_values.include?(value)
85
88
 
86
- raise ArgumentError, "Unsupported #{name} option: #{value.inspect}"
89
+ raise UnsupportedOptionError.new(name: name, value: value)
87
90
  end
88
91
 
89
92
  def decimal_currency_amount(amount)
@@ -91,8 +94,8 @@ module Num2words
91
94
 
92
95
  normalized_amount = amount.is_a?(String) ? amount.tr(",", ".") : amount.to_s
93
96
  BigDecimal(normalized_amount)
94
- rescue ArgumentError
95
- raise ArgumentError, "Unsupported currency amount: #{amount.inspect}"
97
+ rescue ::ArgumentError
98
+ raise UnsupportedCurrencyAmountError, amount
96
99
  end
97
100
 
98
101
  def pluralize(number, singular, few, plural)
@@ -383,7 +386,8 @@ module Num2words
383
386
  time_format = short && only == :time ? :short : :default
384
387
 
385
388
  date_part = to_words_date(datetime.to_date, locale, locale_data, format: date_format, date_case: date_case)
386
- time_part = to_words_time(datetime.to_time, locale, locale_data, format: time_format, short: short)
389
+ time_part = to_words_time(Time.new(datetime.year, datetime.month, datetime.day, datetime.hour, datetime.min, datetime.sec),
390
+ locale, locale_data, format: time_format, short: short)
387
391
 
388
392
  return date_part if only == :date
389
393
  return time_part if only == :time
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Num2words
4
+ class Error < StandardError; end
5
+
6
+ class ArgumentError < ::ArgumentError; end
7
+
8
+ class UnsupportedInputError < ArgumentError
9
+ attr_reader :input
10
+
11
+ def initialize(input)
12
+ @input = input
13
+
14
+ super("Unsupported input type: #{input.inspect}")
15
+ end
16
+ end
17
+
18
+ class UnsupportedOptionError < ArgumentError
19
+ attr_reader :name, :value
20
+
21
+ def initialize(name:, value:)
22
+ @name = name
23
+ @value = value
24
+
25
+ super("Unsupported #{name} option: #{value.inspect}")
26
+ end
27
+ end
28
+
29
+ class UnsupportedCurrencyError < Error
30
+ attr_reader :locale, :currency
31
+
32
+ def initialize(locale:, currency:)
33
+ @locale = locale
34
+ @currency = currency
35
+
36
+ super("Currency #{currency} is not supported for locale #{locale}")
37
+ end
38
+ end
39
+
40
+ class UnsupportedCurrencyAmountError < ArgumentError
41
+ attr_reader :amount
42
+
43
+ def initialize(amount)
44
+ @amount = amount
45
+
46
+ super("Unsupported currency amount: #{amount.inspect}")
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Num2words
4
+ module Rails
5
+ module Helpers
6
+ def number_to_words(number, locale: I18n.locale, **options)
7
+ Num2words.to_words(number, locale: locale, **options)
8
+ end
9
+
10
+ def currency_to_words(amount, locale: I18n.locale, **options)
11
+ Num2words.to_currency(amount, locale: locale, **options)
12
+ end
13
+
14
+ def date_to_words(date, locale: I18n.locale, **options)
15
+ Num2words.to_words(date, locale: locale, **options)
16
+ end
17
+
18
+ def time_to_words(time, locale: I18n.locale, **options)
19
+ Num2words.to_words(time, locale: locale, **options)
20
+ end
21
+
22
+ def datetime_to_words(datetime, locale: I18n.locale, **options)
23
+ Num2words.to_words(datetime, locale: locale, **options)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ module Num2words
6
+ module Rails
7
+ module Validators
8
+ class CurrencyValidator < ActiveModel::EachValidator
9
+ def validate_each(record, attribute, value)
10
+ locale = resolve_locale(record)
11
+ currency = value.to_s.upcase.to_sym
12
+
13
+ return if Num2words.currency_available?(locale, currency)
14
+
15
+ record.errors.add(attribute, :unsupported_currency, currency: currency, num2words_locale: locale)
16
+ end
17
+
18
+ private
19
+
20
+ def resolve_locale(record)
21
+ locale = options[:locale]
22
+ locale.respond_to?(:call) ? locale.call(record) : locale || I18n.locale
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ class Num2wordsCurrencyValidator < Num2words::Rails::Validators::CurrencyValidator
30
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ module Num2words
6
+ module Rails
7
+ module Validators
8
+ class LocaleValidator < ActiveModel::EachValidator
9
+ def validate_each(record, attribute, value)
10
+ locale = value.to_s.to_sym
11
+
12
+ return if Num2words.available_locales.include?(locale)
13
+
14
+ record.errors.add(attribute, :unsupported_locale, unsupported_locale: locale)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ class Num2wordsLocaleValidator < Num2words::Rails::Validators::LocaleValidator
22
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rails/helpers"
4
+
5
+ begin
6
+ require "rails/railtie"
7
+ require_relative "rails/validators/currency_validator"
8
+ require_relative "rails/validators/locale_validator"
9
+ rescue LoadError
10
+ nil
11
+ end
12
+
13
+ if defined?(::Rails::Railtie)
14
+ module Num2words
15
+ class Railtie < ::Rails::Railtie
16
+ initializer "num2words.helpers" do
17
+ ActiveSupport.on_load(:action_view) do
18
+ include Num2words::Rails::Helpers
19
+ end
20
+
21
+ ActiveSupport.on_load(:action_controller) do
22
+ helper Num2words::Rails::Helpers if respond_to?(:helper)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Num2words
4
- VERSION = "0.3.1"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/num2words.rb CHANGED
@@ -2,10 +2,13 @@
2
2
 
3
3
  require_relative "num2words/i18n"
4
4
  require_relative "num2words/version"
5
+ require_relative "num2words/errors"
5
6
  require_relative "num2words/converter"
6
7
  require_relative "num2words/core_ext"
7
8
  require_relative "num2words/locales"
8
9
  require_relative "num2words/config"
10
+ require_relative "num2words/rails/helpers"
11
+ require_relative "num2words/railtie"
9
12
 
10
13
  module Num2words
11
14
  def self.to_words(number, *args, **opts)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: num2words
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ruslan Fedotov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-04 00:00:00.000000000 Z
11
+ date: 2026-07-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: i18n
@@ -113,12 +113,15 @@ files:
113
113
  - docs/limits.ru.md
114
114
  - docs/locale_development.md
115
115
  - docs/locale_development.ru.md
116
+ - docs/rails.md
117
+ - docs/rails.ru.md
116
118
  - exe/num2words-console
117
119
  - lib/num2words.rb
118
120
  - lib/num2words/config.rb
119
121
  - lib/num2words/console.rb
120
122
  - lib/num2words/converter.rb
121
123
  - lib/num2words/core_ext.rb
124
+ - lib/num2words/errors.rb
122
125
  - lib/num2words/i18n.rb
123
126
  - lib/num2words/locales.rb
124
127
  - lib/num2words/locales/ar.rb
@@ -171,6 +174,10 @@ files:
171
174
  - lib/num2words/locales/ur.rb
172
175
  - lib/num2words/locales/vi.rb
173
176
  - lib/num2words/locales/zh.rb
177
+ - lib/num2words/rails/helpers.rb
178
+ - lib/num2words/rails/validators/currency_validator.rb
179
+ - lib/num2words/rails/validators/locale_validator.rb
180
+ - lib/num2words/railtie.rb
174
181
  - lib/num2words/version.rb
175
182
  - num2words.gemspec
176
183
  homepage: https://github.com/skyrusx/num2words