eluvia-base 3.37.1 → 3.39.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: 6f3de7ff3e455a14c86e7c241ac9456ef39ca93ba40c32fd0a8dfb480986b644
4
- data.tar.gz: 3ee766edbedf1243cf4036ef90b6e84da1b81585dd9115ed387c802280269850
3
+ metadata.gz: 691157713c8474d969d76eaee1106d331696a45b9458ee64a563762391e43ed3
4
+ data.tar.gz: 280074f623f0c865a349edaf49c277875c6eeb28374ab8062b0f2aff5f061d67
5
5
  SHA512:
6
- metadata.gz: 96a8f3748400de99c05d3bc237f5d598de09c07b7366a60336a38051af342bf61a14bd35a96572a704f36f8395213b1288ebd5dd5b78099979d082a0d8016b6d
7
- data.tar.gz: 175ea58ec0bc03a3f7a3271ab5447a2676d892df10eee44825c0effe924fa2b3029ec7c8bfc3ef253247f984847485f42e9f4cf49ef8bee64fac2cb2683532aa
6
+ metadata.gz: 1e202828e32b3c665d109ec6584c68fe4ff047b46a9af05b3119c68118d692e306ab3deac2534bc46779dcfc3e5a51b5af057e21940f0aea56ee076f6df9c8c9
7
+ data.tar.gz: 6eff59b834d789c738f7499d14770bc89cd2793d38ac337c2c7baf158b166170c94a38f55e7d4a6cad31cee49ba1937f81c1f9e15a9acd6748d711b8eb02356b
data/CHANGELOG.md ADDED
@@ -0,0 +1,44 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [3.39.0] — 2026-09-18
9
+
10
+ ### Added
11
+ - This changelog, in the format the ci-toolbox release job expects: on release the unreleased
12
+ heading is rewritten to the released version and date, and the section becomes the description
13
+ of the GitLab release.
14
+ - `Eluvia::Base::Config.max_page_limit` — upper bound for the `limit` pagination param on
15
+ `set_pagination` (default `1000`, `nil` disables the check), overridable per controller via
16
+ `pagination_max_limit` or per call via the `max_limit:` keyword.
17
+ - `Eluvia::Base::Config.disable_pagination_errors` and the `disable_errors:` keyword on
18
+ `set_pagination`, to restore the previous silent-fallback behavior for an invalid `limit`
19
+ instead of raising.
20
+ - `allow_meta_only_limit:` keyword on `set_pagination` to reject `limit=0` like any other invalid
21
+ value, for resources that don't support a meta-only request.
22
+ - Translations (`lib/eluvia/locales/*.yml`) for all validation error messages raised by this gem
23
+ (ordering, filtering, fieldset, pagination), in the same languages supported by `habarico`
24
+ (`cs`, `de`, `en`, `es`, `fr`, `it`, `ja`, `nl`, `pl`, `pt`, `ro`, `sk`, `uk`).
25
+ - `Eluvia::PaginationHandler.order_by_param_name` — resolves the `order_by`/`orderBy` query
26
+ parameter name the same way `set_pagination` does, for callers outside this concern (e.g.
27
+ request-level validators run in a `before_action` ahead of it) that need to read the same
28
+ parameter without duplicating the `Config.api_case` check.
29
+
30
+ ### Security
31
+ - **BREAKING:** `set_pagination`'s `limit` param is now capped and validated (HAB-946, from the
32
+ HAB-894 audit). A non-numeric, negative, or above-`max_page_limit` `limit` is rejected with a
33
+ `422 Unprocessable Entity` (error detail on the `limit` field) instead of being silently
34
+ truncated or defaulted. `limit=0` no longer raises `ZeroDivisionError` — it's now accepted as a
35
+ deliberate meta-only request (see `allow_meta_only_limit:`), mirroring `ListPagination` in
36
+ `eluvia-base-django`. Only a missing or blank `limit` still falls back to `default_limit`,
37
+ unchanged.
38
+ - **BREAKING:** `set_pagination`'s `order_by` param is now strictly validated (HAB-915, from the
39
+ HAB-894 audit). Each `field:direction` entry must have exactly one `:` separator, a non-blank
40
+ field name, and a lower-case `asc`/`desc` direction; anything else is rejected with a
41
+ `422 Unprocessable Entity` (format errors on `order_by`, direction errors on
42
+ `order_by__<field>`) instead of being silently truncated to two segments or defaulted to `asc`.
43
+ A blank `order_by=` no longer raises `NoMethodError` — it's treated as "not supplied" and falls
44
+ back to `default_order_by`/`fallback_order_by`.
data/README.md CHANGED
@@ -82,6 +82,58 @@ class TestRecordsController < ApplicationController
82
82
  end
83
83
  ```
84
84
 
85
+ **BREAKING CHANGE:** the `limit` param is now capped. Requests with a `limit` above the configured ceiling are
86
+ rejected with a `422 Unprocessable Entity` (error detail on the `limit` field) instead of being silently accepted.
87
+ A negative or non-numeric `limit` no longer falls through to Kaminari — it now also raises the same `422`. Only a
88
+ missing or blank `limit` param still falls back to `default_limit`, unchanged. `limit=0` is treated as a deliberate
89
+ meta-only request (see below) rather than an error, mirroring `ListPagination` in eluvia-base-django.
90
+
91
+ The ceiling defaults to `Eluvia::Base::Config.max_page_limit` (`1000` by default, `nil` disables the check):
92
+
93
+ ```ruby
94
+ Eluvia::Base::Config.max_page_limit = 500
95
+ ```
96
+
97
+ It can be overridden per controller by overriding `pagination_max_limit`, or per call via the `max_limit:` keyword
98
+ (an explicit `max_limit: nil` removes the ceiling for that call only, regardless of `pagination_max_limit`):
99
+
100
+ ```ruby
101
+ class TestRecordsController < ApplicationController
102
+ def pagination_max_limit
103
+ 50
104
+ end
105
+ end
106
+ ```
107
+
108
+ ```ruby
109
+ before_action -> { set_pagination(default_limit: 20, default_offset: 0, max_limit: 50) }, only: [:index]
110
+ ```
111
+
112
+ To restore the pre-validation behavior (invalid or above-ceiling `limit` silently falls back to `default_limit`
113
+ instead of raising `422`), enable `disable_errors` — globally via `Eluvia::Base::Config.disable_pagination_errors =
114
+ true` (default: `false`), or per call via the `disable_errors:` keyword, which takes precedence over the global
115
+ setting:
116
+
117
+ ```ruby
118
+ before_action -> { set_pagination(default_limit: 20, default_offset: 0, disable_errors: true) }, only: [:index]
119
+ ```
120
+
121
+ `limit=0` is accepted by default as a way to request only pagination metadata without fetching any records — check
122
+ `@limit.zero?` in the action and skip the query:
123
+
124
+ ```ruby
125
+ def index
126
+ @test_records = @limit.zero? ? TestRecord.none : TestRecord.all.order(@order_by).page(@page).per(@limit).padding(@padding)
127
+ end
128
+ ```
129
+
130
+ Pass `allow_meta_only_limit: false` (e.g. for a search-backed resource, mirroring
131
+ `AnySearchCursorBasedPagination` in eluvia-base-django) to reject `limit=0` like any other invalid value instead:
132
+
133
+ ```ruby
134
+ before_action -> { set_pagination(default_limit: 20, default_offset: 0, allow_meta_only_limit: false) }, only: [:index]
135
+ ```
136
+
85
137
  ### 2.3. Params parser
86
138
 
87
139
  You can use `parse_json_param` helper to parse JSON without predefined structure. In case you know the input JSON
@@ -223,3 +275,30 @@ end
223
275
 
224
276
  If `upload_key` is present on the assigned `Eluvia::File` but no finalizer is registered, a
225
277
  `Eluvia::Errors::StandardError` is raised.
278
+
279
+ ### 2.7. Translations
280
+
281
+ All validation error messages raised by this gem (ordering, filtering, fieldset and pagination errors) go through
282
+ `I18n.t` with an English `default:`, so the gem works out of the box without any locale setup. In addition,
283
+ `lib/eluvia/locales/*.yml` ships ready-made translations — under the `eluvia.errors.*` key namespace — for the
284
+ same set of languages supported by `habarico`: `cs`, `de`, `en`, `es`, `fr`, `it`, `ja`, `nl`, `pl`, `pt`, `ro`,
285
+ `sk`, `uk`. These are appended to `I18n.load_path` automatically when the gem loads; a host application only
286
+ needs to add the relevant locale to its own `I18n.available_locales` and set `I18n.locale` as usual. A host app's
287
+ own `config/locales/*.yml` takes precedence over these if it defines the same key.
288
+
289
+ ## 3. Development
290
+
291
+ ### 3.1. Versioning and releases
292
+
293
+ The gem version lives in `version.json` and is read at load time by `Eluvia::Base.version`. Releases are
294
+ driven by the `ruby-library` component of the
295
+ [ci-toolbox](https://gitlab.eluvia.dev/gitlab-ci/ci-toolbox) — the release job bumps the version, tags it,
296
+ and the tag pipeline publishes the gem to [geminabox](https://geminabox.eluvia.dev) and to
297
+ [rubygems.org](https://rubygems.org/gems/eluvia-base), and creates a GitLab release.
298
+
299
+ ### 3.2. Changelog
300
+
301
+ Notable changes go to [CHANGELOG.md](CHANGELOG.md) under the `## [Unreleased]` heading, in the
302
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format. The release job rewrites that heading to
303
+ the released version and the release date, and uses the section as the description of the GitLab release —
304
+ so what you write there is what the Releases page shows.
@@ -365,19 +365,29 @@ module Eluvia
365
365
  return scope if search_term.blank?
366
366
  return scope if filters_model_class.nil?
367
367
 
368
- fields = filters_model_class.search_fields
369
- return scope if fields.empty?
368
+ search_attrs = filters_model_class.search_attrs
369
+ return scope if search_attrs.empty?
370
370
 
371
371
  search_term.split.each do |word|
372
- sql_parts = fields.map do |field|
373
- col = field.is_a?(Symbol) ? "#{self.table_name}.#{field}" : field.to_s
374
- "lower(unaccent(trim(#{col}))) LIKE ('%' || lower(unaccent(trim(?))) || '%')"
372
+ sql_parts = search_attrs.map do |attr_name|
373
+ col = attr_name.is_a?(Symbol) ? "#{self.table_name}.#{attr_name}" : attr_name.to_s
374
+ "lower(#{normalize_search_expression(col)}) LIKE ('%' || lower(#{normalize_search_expression('?')}) || '%')"
375
375
  end
376
- scope = scope.where(sql_parts.join(' OR '), *([word] * fields.size))
376
+ scope = scope.where(sql_parts.join(' OR '), *([word] * search_attrs.size))
377
377
  end
378
378
  scope
379
379
  end
380
380
 
381
+ # Wraps a search expression in `trim` and, when enabled via config, `unaccent`.
382
+ # `unaccent` requires the PostgreSQL `unaccent` extension to be enabled in the database.
383
+ def normalize_search_expression(expr)
384
+ if Eluvia::Base::Config.search_use_unaccent
385
+ "unaccent(trim(#{expr}))"
386
+ else
387
+ "trim(#{expr})"
388
+ end
389
+ end
390
+
381
391
  def apply_filters_for_filter_attrs(scope, params, parent: nil, current_user: nil)
382
392
  return scope unless is_filters_model?(filters_model_class)
383
393
  filters_model_class.filter_attrs.each do |attr_spec|
@@ -12,11 +12,20 @@ module Eluvia
12
12
 
13
13
  class_methods do
14
14
 
15
- def search_fields(*fields)
16
- if fields.empty?
17
- @_search_fields ||= []
15
+ # Declares (with arguments) or returns (without arguments) the attributes searched by the
16
+ # generic `search` param. Each attribute is compared with a case-insensitive substring match,
17
+ # OR-combined across all of them.
18
+ #
19
+ # @example
20
+ # class User::Filters
21
+ # include Eluvia::ActiveRecord::FiltersModel
22
+ # search_attrs :first_name, :last_name, :email
23
+ # end
24
+ def search_attrs(*attr_names)
25
+ if attr_names.empty?
26
+ @_search_attrs ||= []
18
27
  else
19
- @_search_fields = fields
28
+ @_search_attrs = attr_names.flatten
20
29
  end
21
30
  end
22
31
 
@@ -33,6 +33,26 @@ module Eluvia
33
33
  mattr_accessor :uuid_seed
34
34
  @@uuid_seed = '12345678-1234-1234-1234-1234567890ab'
35
35
 
36
+ # Whether the generic `search` param should wrap compared columns in the PostgreSQL `unaccent`
37
+ # function, making the search insensitive to diacritics. Requires the `unaccent` extension to be
38
+ # enabled in the database (`enable_extension 'unaccent'`). When false, search is only
39
+ # case-insensitive. (default: true)
40
+ mattr_accessor :search_use_unaccent
41
+ @@search_use_unaccent = true
42
+
43
+ # Upper bound for the `limit` pagination param on `set_pagination`. Requests with a
44
+ # higher `limit` are rejected with 422 instead of being silently truncated. `nil`
45
+ # disables the check. Mirrors `MAX_PAGE_LIMIT` in eluvia-base-django. (default: 1000)
46
+ mattr_accessor :max_page_limit
47
+ @@max_page_limit = 1000
48
+
49
+ # Whether an invalid `limit` param (non-numeric, negative, above `max_page_limit`, or zero
50
+ # without `allow_meta_only_limit:`) on `set_pagination` silently falls back to
51
+ # `default_limit` instead of being rejected with 422. Mirrors `disable_errors` on
52
+ # `LimitResolutionMixin` in eluvia-base-django. (default: false)
53
+ mattr_accessor :disable_pagination_errors
54
+ @@disable_pagination_errors = false
55
+
36
56
  end
37
57
  end
38
58
  end
@@ -2,23 +2,104 @@ module Eluvia
2
2
  module PaginationHandler
3
3
  extend ::ActiveSupport::Concern
4
4
 
5
- def set_pagination(default_limit: 20, default_offset: 0, default_order_by: 'created_at:asc', fallback_order_by: 'created_at:asc', force_order_by: false)
5
+ # Resolves the query parameter name carrying the `order_by` input according to the configured
6
+ # API case. Exposed so callers outside this concern (e.g. request-level validators run in a
7
+ # `before_action` ahead of `set_pagination`) parse the exact same parameter, instead of
8
+ # duplicating this ternary and risking it drifting out of sync with `Config.api_case`.
9
+ def self.order_by_param_name
10
+ Eluvia::Base::Config.api_case == 'camel_case' ? :orderBy : :order_by
11
+ end
12
+
13
+ # Sentinel distinguishing an unset `max_limit:` kwarg (defer to `pagination_max_limit`) from
14
+ # an explicit `max_limit: nil` (no ceiling for this call only, regardless of the config/method).
15
+ NOT_SET = Object.new.freeze
16
+
17
+ # Ceiling applied to `limit` for this controller. Override to customize (e.g. a
18
+ # per-resource value pulled from a resource definition). Defaults to the library-wide config.
19
+ def pagination_max_limit
20
+ Eluvia::Base::Config.max_page_limit
21
+ end
22
+
23
+ def set_pagination(default_limit: 20, default_offset: 0, default_order_by: 'created_at:asc',
24
+ fallback_order_by: 'created_at:asc', force_order_by: false, max_limit: NOT_SET,
25
+ disable_errors: nil, allow_meta_only_limit: true)
6
26
 
7
27
  # Pagination
8
- @limit = params[:limit] ? params[:limit].to_i : default_limit
28
+ @limit = resolve_pagination_limit(default_limit: default_limit, max_limit: max_limit,
29
+ disable_errors: disable_errors, allow_meta_only_limit: allow_meta_only_limit)
30
+
9
31
  @offset = params[:offset] ? params[:offset].to_i : default_offset
10
- @page = (@offset / @limit) + 1
11
- @padding = @offset % @limit
32
+ if @limit.zero?
33
+ # Meta-only request (see `allow_meta_only_limit:`) - no records to page through.
34
+ @page = 1
35
+ @padding = 0
36
+ else
37
+ @page = (@offset / @limit) + 1
38
+ @padding = @offset % @limit
39
+ end
12
40
 
13
41
  # Order by
42
+ order_by_param = Eluvia::PaginationHandler.order_by_param_name
43
+ user_order_by = params[order_by_param].to_s.strip
44
+ # A blank `order_by` is treated as "not supplied" (falls through to defaults/fallback)
45
+ # rather than as a literal empty ordering, since a Kaminari scope always needs some order.
46
+ order_by = !force_order_by && user_order_by.present? ? user_order_by : default_order_by
47
+ order_by = "#{order_by},#{fallback_order_by}"
48
+
14
49
  @order_by = {}
15
- order_by_param = Eluvia::Base::Config.api_case == 'camel_case' ? :orderBy : :order_by
16
- order_by = "#{!force_order_by && params.key?(order_by_param) ? params[order_by_param] : default_order_by},#{fallback_order_by}"
17
- order_by.to_s.split(',').each do |o|
18
- s = o.to_s.split(':')
19
- @order_by[s.first] = s.length >= 2 && s[1].downcase == 'desc' ? :desc : :asc
50
+ order_by.split(',').each do |entry|
51
+ entry = entry.strip
52
+ # `-1` keeps a trailing empty segment (e.g. `name:`) instead of Ruby's default of
53
+ # dropping it, so it is caught below as an invalid direction rather than as a bare field.
54
+ parts = entry.split(':', -1)
55
+ field_name, direction = parts
56
+
57
+ if parts.length != 2 || field_name.blank?
58
+ raise Eluvia::Errors::UnprocessableEntity.new({
59
+ 'order_by' => I18n.t('eluvia.errors.ordering.invalid_format',
60
+ default: 'Invalid format, expected "field_name:asc" or "field_name:desc".')
61
+ })
62
+ end
63
+
64
+ unless %w[asc desc].include?(direction)
65
+ raise Eluvia::Errors::UnprocessableEntity.new({
66
+ "order_by__#{field_name}" => I18n.t('eluvia.errors.ordering.invalid_direction',
67
+ default: 'Invalid order direction. Allowed values are `asc` and `desc`.')
68
+ })
69
+ end
70
+
71
+ @order_by[field_name] = direction.to_sym
72
+ end
73
+
74
+ end
75
+
76
+ private
77
+
78
+ def resolve_pagination_limit(default_limit:, max_limit:, disable_errors:, allow_meta_only_limit:)
79
+ return default_limit unless params[:limit].present?
80
+
81
+ raw_limit = params[:limit].to_s
82
+ parsed_limit = raw_limit.match?(/\A-?\d+\z/) ? raw_limit.to_i : nil
83
+ errors_disabled = disable_errors.nil? ? Eluvia::Base::Config.disable_pagination_errors : disable_errors
84
+
85
+ if parsed_limit.nil? || parsed_limit.negative? || (parsed_limit.zero? && !allow_meta_only_limit)
86
+ return default_limit if errors_disabled
87
+
88
+ msg = I18n.t('eluvia.errors.pagination.invalid_limit',
89
+ default: 'Limit must be a positive integer, got %<limit>s.', limit: params[:limit])
90
+ raise Eluvia::Errors::UnprocessableEntity.new('limit' => msg)
91
+ end
92
+
93
+ ceiling = max_limit == NOT_SET ? pagination_max_limit : max_limit
94
+ if ceiling && parsed_limit > ceiling
95
+ return default_limit if errors_disabled
96
+
97
+ msg = I18n.t('eluvia.errors.pagination.max_limit_exceeded',
98
+ default: 'Limit %<limit>s exceeds the maximum allowed limit of %<max>s.', limit: parsed_limit, max: ceiling)
99
+ raise Eluvia::Errors::UnprocessableEntity.new('limit' => msg)
20
100
  end
21
101
 
102
+ parsed_limit
22
103
  end
23
104
 
24
105
  end
@@ -0,0 +1,22 @@
1
+ cs:
2
+ eluvia:
3
+ errors:
4
+ ordering:
5
+ invalid_format: 'Neplatný formát, očekává se "field_name:asc" nebo "field_name:desc".'
6
+ invalid_direction: "Neplatný směr řazení. Povolené hodnoty jsou `asc` a `desc`."
7
+ invalid_definition: "Neplatná definice atributu `order by`. Očekává se pouze jeden oddělovač `__`."
8
+ association_not_found: "Asociace nenalezena."
9
+ not_found_on_association: "Na asociaci neexistuje."
10
+ not_found: "Neexistuje."
11
+ filter:
12
+ unknown_param: "Neznámý parametr filtru."
13
+ invalid_term: "Neplatný filtrovací výraz."
14
+ invalid_value_format: "Neplatný formát hodnoty."
15
+ invalid_uuid_format: "Neplatný formát UUID."
16
+ invalid_integer_format: "Neplatný formát celého čísla."
17
+ invalid_date_format: "Neplatný formát data."
18
+ fieldset:
19
+ unknown_field: "Neznámé pole."
20
+ pagination:
21
+ invalid_limit: "Limit musí být kladné celé číslo, zadáno %<limit>s."
22
+ max_limit_exceeded: "Limit %<limit>s překračuje maximální povolený limit %<max>s."
@@ -0,0 +1,22 @@
1
+ de:
2
+ eluvia:
3
+ errors:
4
+ ordering:
5
+ invalid_format: 'Ungültiges Format, erwartet wird "field_name:asc" oder "field_name:desc".'
6
+ invalid_direction: "Ungültige Sortierrichtung. Erlaubte Werte sind `asc` und `desc`."
7
+ invalid_definition: "Ungültige Definition für ein `order by`-Attribut. Es wird genau ein `__`-Trennzeichen erwartet."
8
+ association_not_found: "Assoziation nicht gefunden."
9
+ not_found_on_association: "Existiert nicht in der Assoziation."
10
+ not_found: "Existiert nicht."
11
+ filter:
12
+ unknown_param: "Unbekannter Filterparameter."
13
+ invalid_term: "Ungültiger Filterbegriff."
14
+ invalid_value_format: "Ungültiges Wertformat."
15
+ invalid_uuid_format: "Ungültiges UUID-Format."
16
+ invalid_integer_format: "Ungültiges Ganzzahlformat."
17
+ invalid_date_format: "Ungültiges Datumsformat."
18
+ fieldset:
19
+ unknown_field: "Unbekanntes Feld."
20
+ pagination:
21
+ invalid_limit: "Limit muss eine positive Ganzzahl sein, erhalten: %<limit>s."
22
+ max_limit_exceeded: "Limit %<limit>s überschreitet das maximal zulässige Limit von %<max>s."
@@ -0,0 +1,25 @@
1
+ # Translations for the validation error messages raised by eluvia-base (ordering.rb, filtering.rb,
2
+ # fieldset_serializer.rb, pagination_handler.rb). Each key mirrors the `default:` string passed to
3
+ # `I18n.t` at the call site, so a host app without these locale files still gets the same English text.
4
+ en:
5
+ eluvia:
6
+ errors:
7
+ ordering:
8
+ invalid_format: 'Invalid format, expected "field_name:asc" or "field_name:desc".'
9
+ invalid_direction: "Invalid order direction. Allowed values are `asc` and `desc`."
10
+ invalid_definition: "Invalid definition for an `order by` attribute. Expecting only one `__` delimiter."
11
+ association_not_found: "Association not found."
12
+ not_found_on_association: "Does not exist on association."
13
+ not_found: "Does not exist."
14
+ filter:
15
+ unknown_param: "Unknown filter param."
16
+ invalid_term: "Invalid filter term."
17
+ invalid_value_format: "Invalid value format."
18
+ invalid_uuid_format: "Invalid UUID format."
19
+ invalid_integer_format: "Invalid integer format."
20
+ invalid_date_format: "Invalid date format."
21
+ fieldset:
22
+ unknown_field: "Unknown field."
23
+ pagination:
24
+ invalid_limit: "Limit must be a positive integer, got %<limit>s."
25
+ max_limit_exceeded: "Limit %<limit>s exceeds the maximum allowed limit of %<max>s."
@@ -0,0 +1,22 @@
1
+ es:
2
+ eluvia:
3
+ errors:
4
+ ordering:
5
+ invalid_format: 'Formato no válido, se espera "field_name:asc" o "field_name:desc".'
6
+ invalid_direction: "Dirección de ordenación no válida. Los valores permitidos son `asc` y `desc`."
7
+ invalid_definition: "Definición no válida para un atributo `order by`. Se espera solo un delimitador `__`."
8
+ association_not_found: "Asociación no encontrada."
9
+ not_found_on_association: "No existe en la asociación."
10
+ not_found: "No existe."
11
+ filter:
12
+ unknown_param: "Parámetro de filtro desconocido."
13
+ invalid_term: "Término de filtro no válido."
14
+ invalid_value_format: "Formato de valor no válido."
15
+ invalid_uuid_format: "Formato de UUID no válido."
16
+ invalid_integer_format: "Formato de número entero no válido."
17
+ invalid_date_format: "Formato de fecha no válido."
18
+ fieldset:
19
+ unknown_field: "Campo desconocido."
20
+ pagination:
21
+ invalid_limit: "El límite debe ser un número entero positivo, recibido %<limit>s."
22
+ max_limit_exceeded: "El límite %<limit>s supera el límite máximo permitido de %<max>s."
@@ -0,0 +1,22 @@
1
+ fr:
2
+ eluvia:
3
+ errors:
4
+ ordering:
5
+ invalid_format: 'Format invalide, attendu "field_name:asc" ou "field_name:desc".'
6
+ invalid_direction: "Direction de tri invalide. Les valeurs autorisées sont `asc` et `desc`."
7
+ invalid_definition: "Définition invalide pour un attribut `order by`. Un seul délimiteur `__` est attendu."
8
+ association_not_found: "Association introuvable."
9
+ not_found_on_association: "N'existe pas sur l'association."
10
+ not_found: "N'existe pas."
11
+ filter:
12
+ unknown_param: "Paramètre de filtre inconnu."
13
+ invalid_term: "Terme de filtre invalide."
14
+ invalid_value_format: "Format de valeur invalide."
15
+ invalid_uuid_format: "Format UUID invalide."
16
+ invalid_integer_format: "Format d'entier invalide."
17
+ invalid_date_format: "Format de date invalide."
18
+ fieldset:
19
+ unknown_field: "Champ inconnu."
20
+ pagination:
21
+ invalid_limit: "La limite doit être un entier positif, valeur reçue %<limit>s."
22
+ max_limit_exceeded: "La limite %<limit>s dépasse la limite maximale autorisée de %<max>s."
@@ -0,0 +1,22 @@
1
+ it:
2
+ eluvia:
3
+ errors:
4
+ ordering:
5
+ invalid_format: 'Formato non valido, previsto "field_name:asc" oppure "field_name:desc".'
6
+ invalid_direction: "Direzione di ordinamento non valida. I valori consentiti sono `asc` e `desc`."
7
+ invalid_definition: "Definizione non valida per un attributo `order by`. È previsto un solo delimitatore `__`."
8
+ association_not_found: "Associazione non trovata."
9
+ not_found_on_association: "Non esiste nell'associazione."
10
+ not_found: "Non esiste."
11
+ filter:
12
+ unknown_param: "Parametro di filtro sconosciuto."
13
+ invalid_term: "Termine di filtro non valido."
14
+ invalid_value_format: "Formato del valore non valido."
15
+ invalid_uuid_format: "Formato UUID non valido."
16
+ invalid_integer_format: "Formato numero intero non valido."
17
+ invalid_date_format: "Formato data non valido."
18
+ fieldset:
19
+ unknown_field: "Campo sconosciuto."
20
+ pagination:
21
+ invalid_limit: "Il limite deve essere un numero intero positivo, ricevuto %<limit>s."
22
+ max_limit_exceeded: "Il limite %<limit>s supera il limite massimo consentito di %<max>s."
@@ -0,0 +1,22 @@
1
+ ja:
2
+ eluvia:
3
+ errors:
4
+ ordering:
5
+ invalid_format: '形式が無効です。"field_name:asc" または "field_name:desc" を指定してください。'
6
+ invalid_direction: "並び順が無効です。使用できる値は `asc` と `desc` です。"
7
+ invalid_definition: "`order by` 属性の定義が無効です。`__` 区切り文字は1つだけ使用できます。"
8
+ association_not_found: "アソシエーションが見つかりません。"
9
+ not_found_on_association: "アソシエーション上に存在しません。"
10
+ not_found: "存在しません。"
11
+ filter:
12
+ unknown_param: "不明なフィルターパラメータです。"
13
+ invalid_term: "フィルター条件が無効です。"
14
+ invalid_value_format: "値の形式が無効です。"
15
+ invalid_uuid_format: "UUID の形式が無効です。"
16
+ invalid_integer_format: "整数の形式が無効です。"
17
+ invalid_date_format: "日付の形式が無効です。"
18
+ fieldset:
19
+ unknown_field: "不明なフィールドです。"
20
+ pagination:
21
+ invalid_limit: "limit は正の整数である必要があります(指定値: %<limit>s)。"
22
+ max_limit_exceeded: "limit %<limit>s は許可されている最大値 %<max>s を超えています。"
@@ -0,0 +1,22 @@
1
+ nl:
2
+ eluvia:
3
+ errors:
4
+ ordering:
5
+ invalid_format: 'Ongeldig formaat, verwacht wordt "field_name:asc" of "field_name:desc".'
6
+ invalid_direction: "Ongeldige sorteerrichting. Toegestane waarden zijn `asc` en `desc`."
7
+ invalid_definition: "Ongeldige definitie voor een `order by`-attribuut. Er wordt precies één `__`-scheidingsteken verwacht."
8
+ association_not_found: "Associatie niet gevonden."
9
+ not_found_on_association: "Bestaat niet binnen de associatie."
10
+ not_found: "Bestaat niet."
11
+ filter:
12
+ unknown_param: "Onbekende filterparameter."
13
+ invalid_term: "Ongeldige filterterm."
14
+ invalid_value_format: "Ongeldig waardeformaat."
15
+ invalid_uuid_format: "Ongeldig UUID-formaat."
16
+ invalid_integer_format: "Ongeldig geheel-getalformaat."
17
+ invalid_date_format: "Ongeldig datumformaat."
18
+ fieldset:
19
+ unknown_field: "Onbekend veld."
20
+ pagination:
21
+ invalid_limit: "Limit moet een positief geheel getal zijn, ontvangen: %<limit>s."
22
+ max_limit_exceeded: "Limit %<limit>s overschrijdt de maximaal toegestane limiet van %<max>s."
@@ -0,0 +1,22 @@
1
+ pl:
2
+ eluvia:
3
+ errors:
4
+ ordering:
5
+ invalid_format: 'Nieprawidłowy format, oczekiwano "field_name:asc" lub "field_name:desc".'
6
+ invalid_direction: "Nieprawidłowy kierunek sortowania. Dozwolone wartości to `asc` i `desc`."
7
+ invalid_definition: "Nieprawidłowa definicja atrybutu `order by`. Oczekiwany jest dokładnie jeden separator `__`."
8
+ association_not_found: "Nie znaleziono powiązania."
9
+ not_found_on_association: "Nie istnieje w powiązaniu."
10
+ not_found: "Nie istnieje."
11
+ filter:
12
+ unknown_param: "Nieznany parametr filtra."
13
+ invalid_term: "Nieprawidłowy termin filtra."
14
+ invalid_value_format: "Nieprawidłowy format wartości."
15
+ invalid_uuid_format: "Nieprawidłowy format UUID."
16
+ invalid_integer_format: "Nieprawidłowy format liczby całkowitej."
17
+ invalid_date_format: "Nieprawidłowy format daty."
18
+ fieldset:
19
+ unknown_field: "Nieznane pole."
20
+ pagination:
21
+ invalid_limit: "Limit musi być dodatnią liczbą całkowitą, otrzymano %<limit>s."
22
+ max_limit_exceeded: "Limit %<limit>s przekracza maksymalny dozwolony limit %<max>s."
@@ -0,0 +1,22 @@
1
+ pt:
2
+ eluvia:
3
+ errors:
4
+ ordering:
5
+ invalid_format: 'Formato inválido, esperado "field_name:asc" ou "field_name:desc".'
6
+ invalid_direction: "Direção de ordenação inválida. Os valores permitidos são `asc` e `desc`."
7
+ invalid_definition: "Definição inválida para o atributo `order by`. É esperado apenas um delimitador `__`."
8
+ association_not_found: "Associação não encontrada."
9
+ not_found_on_association: "Não existe na associação."
10
+ not_found: "Não existe."
11
+ filter:
12
+ unknown_param: "Parâmetro de filtro desconhecido."
13
+ invalid_term: "Termo de filtro inválido."
14
+ invalid_value_format: "Formato de valor inválido."
15
+ invalid_uuid_format: "Formato de UUID inválido."
16
+ invalid_integer_format: "Formato de número inteiro inválido."
17
+ invalid_date_format: "Formato de data inválido."
18
+ fieldset:
19
+ unknown_field: "Campo desconhecido."
20
+ pagination:
21
+ invalid_limit: "O limite deve ser um número inteiro positivo, recebido %<limit>s."
22
+ max_limit_exceeded: "O limite %<limit>s excede o limite máximo permitido de %<max>s."
@@ -0,0 +1,22 @@
1
+ ro:
2
+ eluvia:
3
+ errors:
4
+ ordering:
5
+ invalid_format: 'Format invalid, se așteaptă "field_name:asc" sau "field_name:desc".'
6
+ invalid_direction: "Direcție de sortare invalidă. Valorile permise sunt `asc` și `desc`."
7
+ invalid_definition: "Definiție invalidă pentru atributul `order by`. Este permis un singur delimitator `__`."
8
+ association_not_found: "Asocierea nu a fost găsită."
9
+ not_found_on_association: "Nu există în asociere."
10
+ not_found: "Nu există."
11
+ filter:
12
+ unknown_param: "Parametru de filtrare necunoscut."
13
+ invalid_term: "Termen de filtrare invalid."
14
+ invalid_value_format: "Format de valoare invalid."
15
+ invalid_uuid_format: "Format UUID invalid."
16
+ invalid_integer_format: "Format de număr întreg invalid."
17
+ invalid_date_format: "Format de dată invalid."
18
+ fieldset:
19
+ unknown_field: "Câmp necunoscut."
20
+ pagination:
21
+ invalid_limit: "Limita trebuie să fie un număr întreg pozitiv, valoare primită %<limit>s."
22
+ max_limit_exceeded: "Limita %<limit>s depășește limita maximă permisă de %<max>s."
@@ -0,0 +1,22 @@
1
+ sk:
2
+ eluvia:
3
+ errors:
4
+ ordering:
5
+ invalid_format: 'Neplatný formát, očakáva sa "field_name:asc" alebo "field_name:desc".'
6
+ invalid_direction: "Neplatný smer radenia. Povolené hodnoty sú `asc` a `desc`."
7
+ invalid_definition: "Neplatná definícia atribútu `order by`. Očakáva sa iba jeden oddeľovač `__`."
8
+ association_not_found: "Asociácia nebola nájdená."
9
+ not_found_on_association: "Na asociácii neexistuje."
10
+ not_found: "Neexistuje."
11
+ filter:
12
+ unknown_param: "Neznámy parameter filtra."
13
+ invalid_term: "Neplatný filtrovací výraz."
14
+ invalid_value_format: "Neplatný formát hodnoty."
15
+ invalid_uuid_format: "Neplatný formát UUID."
16
+ invalid_integer_format: "Neplatný formát celého čísla."
17
+ invalid_date_format: "Neplatný formát dátumu."
18
+ fieldset:
19
+ unknown_field: "Neznáme pole."
20
+ pagination:
21
+ invalid_limit: "Limit musí byť kladné celé číslo, zadané %<limit>s."
22
+ max_limit_exceeded: "Limit %<limit>s prekračuje maximálny povolený limit %<max>s."
@@ -0,0 +1,22 @@
1
+ uk:
2
+ eluvia:
3
+ errors:
4
+ ordering:
5
+ invalid_format: 'Недійсний формат, очікується "field_name:asc" або "field_name:desc".'
6
+ invalid_direction: "Недійсний напрямок сортування. Дозволені значення: `asc` і `desc`."
7
+ invalid_definition: "Недійсне визначення атрибута `order by`. Очікується рівно один роздільник `__`."
8
+ association_not_found: "Асоціацію не знайдено."
9
+ not_found_on_association: "Не існує в асоціації."
10
+ not_found: "Не існує."
11
+ filter:
12
+ unknown_param: "Невідомий параметр фільтра."
13
+ invalid_term: "Недійсний термін фільтра."
14
+ invalid_value_format: "Недійсний формат значення."
15
+ invalid_uuid_format: "Недійсний формат UUID."
16
+ invalid_integer_format: "Недійсний формат цілого числа."
17
+ invalid_date_format: "Недійсний формат дати."
18
+ fieldset:
19
+ unknown_field: "Невідоме поле."
20
+ pagination:
21
+ invalid_limit: "Ліміт має бути додатним цілим числом, отримано %<limit>s."
22
+ max_limit_exceeded: "Ліміт %<limit>s перевищує максимально допустимий ліміт %<max>s."
data/lib/eluvia-base.rb CHANGED
@@ -4,6 +4,9 @@ require 'eluvia/base/config'
4
4
  # Version
5
5
  require 'eluvia/base/version'
6
6
 
7
+ # Locales (translations for the validation error messages raised across this gem)
8
+ I18n.load_path += Dir[File.expand_path('eluvia/locales/*.yml', __dir__)]
9
+
7
10
  # Uploads
8
11
  require 'eluvia/uploads'
9
12
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eluvia-base
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.37.1
4
+ version: 3.39.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matěj Outlý
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-26 00:00:00.000000000 Z
11
+ date: 2026-09-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rest-client
@@ -117,6 +117,7 @@ executables: []
117
117
  extensions: []
118
118
  extra_rdoc_files: []
119
119
  files:
120
+ - CHANGELOG.md
120
121
  - LICENSE
121
122
  - README.md
122
123
  - lib/eluvia-base.rb
@@ -149,6 +150,19 @@ files:
149
150
  - lib/eluvia/handlers/params_handler.rb
150
151
  - lib/eluvia/helpers/attachment_helper.rb
151
152
  - lib/eluvia/integrations/eluvia_integration.rb
153
+ - lib/eluvia/locales/cs.yml
154
+ - lib/eluvia/locales/de.yml
155
+ - lib/eluvia/locales/en.yml
156
+ - lib/eluvia/locales/es.yml
157
+ - lib/eluvia/locales/fr.yml
158
+ - lib/eluvia/locales/it.yml
159
+ - lib/eluvia/locales/ja.yml
160
+ - lib/eluvia/locales/nl.yml
161
+ - lib/eluvia/locales/pl.yml
162
+ - lib/eluvia/locales/pt.yml
163
+ - lib/eluvia/locales/ro.yml
164
+ - lib/eluvia/locales/sk.yml
165
+ - lib/eluvia/locales/uk.yml
152
166
  - lib/eluvia/models/file.rb
153
167
  - lib/eluvia/models/file_wrapper.rb
154
168
  - lib/eluvia/models/image_wrapper.rb