Skip to content

Commit

Permalink
feat!: custom translations (#15)
Browse files Browse the repository at this point in the history
- add support for custom translations through i18n
- adjust default validation error messages to be more ambiguous

Closes #12
BREAKING CHANGE: reword default validation error messages
  • Loading branch information
benmelz authored Jul 30, 2023
1 parent 085448d commit bacc785
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 18 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,22 @@ class MyModel < ApplicationRecord
end
```

### Translations

`Limitable` ships with i18n support for its validation error messages. Each column type has its own translation key,
outlined alongside their default values in `lib/limitable/locale/en.yml`.

Each validator will pass a `limit` parameter (min/max integer for integer columns, bytes for string/text/binary),
which can be used to make the messages less ambiguous if desired.

e.g.

```yaml
en:
limitable:
string_limit_exceeded: "may not exceed %{limit} characters"
```
### SQL Adapters
`Limitable` is designed to be SQL adapter agnostic, however different adapters have different default behaviors that
Expand Down
32 changes: 24 additions & 8 deletions lib/limitable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'active_record'
require 'i18n'
require_relative 'limitable/base'
require_relative 'limitable/locale'
require_relative 'limitable/version'

# == Limitable
Expand Down Expand Up @@ -32,10 +33,8 @@ def attach_limit_validator_if_needed(klass, column_name)
return if limit.blank?

case column.type
when :integer
klass.validate(&build_integer_limit_validator(column_name, limit))
when :binary, :string, :text
klass.validate(&build_string_limit_validator(column_name, limit))
when :integer, :string, :text, :binary
klass.validate(&send(:"build_#{column.type}_limit_validator", column_name, limit))
end
end

Expand All @@ -49,18 +48,35 @@ def build_integer_limit_validator(column_name, limit)
end
next unless value.is_a? Integer

errors.add column_name, I18n.t('errors.messages.greater_than_or_equal_to', count: min) if value < min
errors.add column_name, I18n.t('errors.messages.less_than_or_equal_to', count: max) if value > max
errors.add column_name, I18n.t('limitable.integer_limit_exceeded.lower', limit: min) if value < min
errors.add column_name, I18n.t('limitable.integer_limit_exceeded.upper', limit: max) if value > max
end
end

def build_string_limit_validator(column_name, limit)
lambda do
value = self.class.type_for_attribute(column_name).serialize self[column_name]
value = value.to_s if value.is_a? ActiveModel::Type::Binary::Data
next unless value.is_a?(String) && value.bytesize > limit

errors.add column_name, I18n.t('errors.messages.too_long.other', count: limit)
errors.add column_name, I18n.t('limitable.string_limit_exceeded', limit: limit)
end
end

def build_text_limit_validator(column_name, limit)
lambda do
value = self.class.type_for_attribute(column_name).serialize self[column_name]
next unless value.is_a?(String) && value.bytesize > limit

errors.add column_name, I18n.t('limitable.text_limit_exceeded', limit: limit)
end
end

def build_binary_limit_validator(column_name, limit)
lambda do
value = self.class.type_for_attribute(column_name).serialize self[column_name]
next unless value.is_a?(ActiveModel::Type::Binary::Data) && value.to_s.bytesize > limit

errors.add column_name, I18n.t('limitable.binary_limit_exceeded', limit: limit)
end
end

Expand Down
13 changes: 13 additions & 0 deletions lib/limitable/locale.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

require 'i18n'

module Limitable
# == Limitable::Locale
#
# Loads the default limitable custom translations into i18n.
#
module Locale
I18n.load_path << File.expand_path('locale/en.yml', __dir__)
end
end
8 changes: 8 additions & 0 deletions lib/limitable/locale/en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
en:
limitable:
integer_limit_exceeded:
upper: "is too large"
lower: "is too small"
string_limit_exceeded: "is too long"
text_limit_exceeded: "is too long"
binary_limit_exceeded: "is too large"
26 changes: 26 additions & 0 deletions spec/lib/limitable/locale_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

require 'i18n'
require 'limitable/locale'

RSpec.describe Limitable::Locale do
it 'defines a translation for integers that exceed an upper limit' do
expect { I18n.t! 'limitable.integer_limit_exceeded.upper' }.not_to raise_error
end

it 'defines a translation for integers that exceed a lower limit' do
expect { I18n.t! 'limitable.integer_limit_exceeded.lower' }.not_to raise_error
end

it 'defines a translation for strings that exceed a length limit' do
expect { I18n.t! 'limitable.string_limit_exceeded' }.not_to raise_error
end

it 'defines a translation for text that exceeds a length limit' do
expect { I18n.t! 'limitable.text_limit_exceeded' }.not_to raise_error
end

it 'defines a translation for binary data that exceeds a size limit' do
expect { I18n.t! 'limitable.binary_limit_exceeded' }.not_to raise_error
end
end
54 changes: 48 additions & 6 deletions spec/lib/limitable_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# frozen_string_literal: true

require 'limitable'
require 'i18n'
require 'support/active_record_shared_examples'
require 'support/locale'

RSpec.describe Limitable do
it 'has a version number' do
Expand Down Expand Up @@ -37,13 +39,29 @@
it 'sets a locale error message when the upper limit is violated' do
instance = model.new(limited_integer_column: 32_768).tap(&:validate)
error_messages = instance.errors.messages[:limited_integer_column]
expect(error_messages).to include(I18n.t('errors.messages.less_than_or_equal_to', count: 32_767))
expect(error_messages).to include(I18n.t('limitable.integer_limit_exceeded.upper'))
end

it 'allows customization of the upper limit locale error message' do
I18n.with_locale :test do
instance = model.new(limited_integer_column: 32_768).tap(&:validate)
error_messages = instance.errors.messages[:limited_integer_column]
expect(error_messages).to include('max integer is 32767')
end
end

it 'sets a locale error message when the lower limit is violated' do
instance = model.new(limited_integer_column: -32_768).tap(&:validate)
error_messages = instance.errors.messages[:limited_integer_column]
expect(error_messages).to include(I18n.t('errors.messages.greater_than_or_equal_to', count: -32_767))
expect(error_messages).to include(I18n.t('limitable.integer_limit_exceeded.lower'))
end

it 'allows customization of the lower limit locale error message' do
I18n.with_locale :test do
instance = model.new(limited_integer_column: -32_768).tap(&:validate)
error_messages = instance.errors.messages[:limited_integer_column]
expect(error_messages).to include('min integer is -32767')
end
end

it 'does not affect values just below the upper limit' do
Expand Down Expand Up @@ -79,7 +97,15 @@
it 'sets a locale error message when the limit is violated' do
instance = model.new(limited_string_column: 'abcd🖕').tap(&:validate)
error_messages = instance.errors.messages[:limited_string_column]
expect(error_messages).to include(I18n.t('errors.messages.too_long.other', count: 5))
expect(error_messages).to include(I18n.t('limitable.string_limit_exceeded'))
end

it 'allows customization of the locale error message' do
I18n.with_locale :test do
instance = model.new(limited_string_column: 'abcd🖕').tap(&:validate)
error_messages = instance.errors.messages[:limited_string_column]
expect(error_messages).to include('max string is 5')
end
end

it 'does not affect values within the limit' do
Expand Down Expand Up @@ -111,7 +137,15 @@
it 'sets a locale error message when the limit is violated' do
instance = model.new(limited_text_column: 'abcd🖕').tap(&:validate)
error_messages = instance.errors.messages[:limited_text_column]
expect(error_messages).to include(I18n.t('errors.messages.too_long.other', count: 5))
expect(error_messages).to include(I18n.t('limitable.text_limit_exceeded'))
end

it 'allows customization of the locale error message' do
I18n.with_locale :test do
instance = model.new(limited_text_column: 'abcd🖕').tap(&:validate)
error_messages = instance.errors.messages[:limited_text_column]
expect(error_messages).to include('max text is 5')
end
end

it 'does not affect values within the limit' do
Expand Down Expand Up @@ -143,7 +177,15 @@
it 'sets a locale error message when the limit is violated' do
instance = model.new(limited_binary_column: 'abcd🖕').tap(&:validate)
error_messages = instance.errors.messages[:limited_binary_column]
expect(error_messages).to include(I18n.t('errors.messages.too_long.other', count: 5))
expect(error_messages).to include(I18n.t('limitable.binary_limit_exceeded'))
end

it 'allows customization of the locale error message' do
I18n.with_locale :test do
instance = model.new(limited_binary_column: 'abcd🖕').tap(&:validate)
error_messages = instance.errors.messages[:limited_binary_column]
expect(error_messages).to include('max binary is 5')
end
end

it 'does not affect values within the limit' do
Expand Down Expand Up @@ -179,7 +221,7 @@
it 'sets a locale error message when the limit is violated' do
instance = model.new(limited_enum_column: 'bad_value').tap(&:validate)
error_messages = instance.errors.messages[:limited_enum_column]
expect(error_messages).to include(I18n.t('errors.messages.less_than_or_equal_to', count: 32_767))
expect(error_messages).to include(I18n.t('limitable.integer_limit_exceeded.upper'))
end

it 'does not affect values within the limit' do
Expand Down
4 changes: 0 additions & 4 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# frozen_string_literal: true

require 'active_record'
require 'i18n'
require 'simplecov'

SimpleCov.start do
Expand All @@ -15,9 +14,6 @@ class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end

I18n.available_locales = [:en]
I18n.backend.load_translations

RSpec.configure do |config|
config.example_status_persistence_file_path = '.rspec_status'
config.disable_monkey_patching!
Expand Down
5 changes: 5 additions & 0 deletions spec/support/locale.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

require 'i18n'

I18n.load_path << File.expand_path('locale/test.yml', __dir__)
8 changes: 8 additions & 0 deletions spec/support/locale/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
test:
limitable:
integer_limit_exceeded:
upper: "max integer is %{limit}"
lower: "min integer is %{limit}"
string_limit_exceeded: "max string is %{limit}"
text_limit_exceeded: "max text is %{limit}"
binary_limit_exceeded: "max binary is %{limit}"

0 comments on commit bacc785

Please sign in to comment.