diff --git a/README.md b/README.md index 889a9e1..7cdef0f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/limitable.rb b/lib/limitable.rb index 805d0e3..981ac1c 100644 --- a/lib/limitable.rb +++ b/lib/limitable.rb @@ -3,6 +3,7 @@ require 'active_record' require 'i18n' require_relative 'limitable/base' +require_relative 'limitable/locale' require_relative 'limitable/version' # == Limitable @@ -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 @@ -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 diff --git a/lib/limitable/locale.rb b/lib/limitable/locale.rb new file mode 100644 index 0000000..3c01fb5 --- /dev/null +++ b/lib/limitable/locale.rb @@ -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 diff --git a/lib/limitable/locale/en.yml b/lib/limitable/locale/en.yml new file mode 100644 index 0000000..99e3169 --- /dev/null +++ b/lib/limitable/locale/en.yml @@ -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" diff --git a/spec/lib/limitable/locale_spec.rb b/spec/lib/limitable/locale_spec.rb new file mode 100644 index 0000000..618f683 --- /dev/null +++ b/spec/lib/limitable/locale_spec.rb @@ -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 diff --git a/spec/lib/limitable_spec.rb b/spec/lib/limitable_spec.rb index a1c59e0..a348696 100644 --- a/spec/lib/limitable_spec.rb +++ b/spec/lib/limitable_spec.rb @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ad7f263..7f84ae2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'active_record' -require 'i18n' require 'simplecov' SimpleCov.start do @@ -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! diff --git a/spec/support/locale.rb b/spec/support/locale.rb new file mode 100644 index 0000000..1c15416 --- /dev/null +++ b/spec/support/locale.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require 'i18n' + +I18n.load_path << File.expand_path('locale/test.yml', __dir__) diff --git a/spec/support/locale/test.yml b/spec/support/locale/test.yml new file mode 100644 index 0000000..c5754ae --- /dev/null +++ b/spec/support/locale/test.yml @@ -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}"