ransack 6.0.1 → 6.1.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +43 -68
  3. data/lib/ransack/active_record/context.rb +207 -75
  4. data/lib/ransack/active_record/join_association.rb +8 -17
  5. data/lib/ransack/active_record/join_dependency.rb +163 -9
  6. data/lib/ransack/configuration.rb +25 -2
  7. data/lib/ransack/context.rb +55 -7
  8. data/lib/ransack/errors.rb +13 -0
  9. data/lib/ransack/invalid_search_error.rb +3 -3
  10. data/lib/ransack/locale/ar.yml +35 -0
  11. data/lib/ransack/locale/az.yml +36 -1
  12. data/lib/ransack/locale/bg.yml +35 -0
  13. data/lib/ransack/locale/ca.yml +36 -1
  14. data/lib/ransack/locale/cs.yml +35 -0
  15. data/lib/ransack/locale/da.yml +35 -0
  16. data/lib/ransack/locale/de.yml +36 -1
  17. data/lib/ransack/locale/el.yml +36 -1
  18. data/lib/ransack/locale/en.yml +35 -0
  19. data/lib/ransack/locale/es.yml +36 -1
  20. data/lib/ransack/locale/fa.yml +35 -0
  21. data/lib/ransack/locale/fi.yml +35 -0
  22. data/lib/ransack/locale/fr.yml +35 -0
  23. data/lib/ransack/locale/hu.yml +35 -0
  24. data/lib/ransack/locale/id.yml +35 -0
  25. data/lib/ransack/locale/it.yml +35 -0
  26. data/lib/ransack/locale/ja.yml +35 -0
  27. data/lib/ransack/locale/ko.yml +35 -0
  28. data/lib/ransack/locale/nl.yml +35 -0
  29. data/lib/ransack/locale/pt-BR.yml +35 -0
  30. data/lib/ransack/locale/ro.yml +35 -0
  31. data/lib/ransack/locale/ru.yml +35 -0
  32. data/lib/ransack/locale/sk.yml +35 -0
  33. data/lib/ransack/locale/sv.yml +35 -0
  34. data/lib/ransack/locale/tr.yml +36 -1
  35. data/lib/ransack/locale/uk.yml +35 -0
  36. data/lib/ransack/locale/zh-CN.yml +35 -0
  37. data/lib/ransack/locale/zh-TW.yml +35 -0
  38. data/lib/ransack/nodes/condition.rb +168 -42
  39. data/lib/ransack/nodes/grouping.rb +8 -17
  40. data/lib/ransack/nodes/node.rb +23 -1
  41. data/lib/ransack/nodes/sort.rb +7 -4
  42. data/lib/ransack/nodes/value.rb +6 -0
  43. data/lib/ransack/predicate.rb +19 -6
  44. data/lib/ransack/search.rb +21 -10
  45. data/lib/ransack/translate.rb +43 -35
  46. data/lib/ransack/version.rb +1 -1
  47. data/lib/ransack.rb +2 -1
  48. metadata +7 -6
@@ -6,6 +6,16 @@ module Ransack
6
6
  # and exposes the table aliasing Ransack needs to add one association at a
7
7
  # time to a relation that may already carry joins of its own.
8
8
  module JoinDependencyExtensions
9
+ # The table aliases a search counted on for one association it added,
10
+ # by position in its reflection chain (nil where the name could not be
11
+ # kept), and the join that ended up with them; see #pin_search_tables.
12
+ SearchPin = Struct.new(:name, :tables, :top_level, :joined_by, :joined_as)
13
+
14
+ # Set on a dependency Ransack stashes on a relation for an association
15
+ # a search added, and whether the search counted the relation's eager
16
+ # load first.
17
+ attr_accessor :added_by_search, :after_eager_load
18
+
9
19
  # Replaces ActiveRecord::Associations::JoinDependency#build
10
20
  def build(associations, base_klass)
11
21
  associations.map do |name, right|
@@ -42,10 +52,14 @@ module Ransack
42
52
  @references[table_name.to_sym] = table_name if table_name.is_a?(String)
43
53
  end
44
54
 
55
+ pin_search_tables(joins_to_add)
56
+
45
57
  joins = make_join_constraints(join_root, join_type)
46
58
 
47
- joins.concat joins_to_add.flat_map { |oj|
48
- if join_root.match?(oj.join_root) && join_root.table.name == oj.join_root.table.name
59
+ joins.concat eager_load_first(joins_to_add.to_a).flat_map { |oj|
60
+ if oj.added_by_search
61
+ search_join_constraints(oj)
62
+ elsif top_level?(oj)
49
63
  walk join_root, oj.join_root, oj.join_type
50
64
  else
51
65
  make_join_constraints(oj.join_root, oj.join_type)
@@ -53,6 +67,41 @@ module Ransack
53
67
  }
54
68
  end
55
69
 
70
+ # A search binds its conditions to the table aliases its joins are
71
+ # going to have, and roots a nested join at its parent's alias, all
72
+ # worked out from the relation it searched. A join added to the result
73
+ # afterwards (`result.joins(:target_person)`) renders first, and would
74
+ # take a name one of the search's joins was counting on, moving its
75
+ # conditions and nested joins onto the new join (#1776).
76
+ #
77
+ # So each association a search added keeps its aliases, those of a
78
+ # through association's intermediate tables included: the names are
79
+ # reserved before anything is joined, and a join of the same
80
+ # association added afterwards takes them over and stands in for the
81
+ # search's join; any other join is aliased around them. A name that a
82
+ # string or Arel join already claims cannot be kept; the joins nested
83
+ # under that association then follow the alias it does get.
84
+ def pin_search_tables(joins)
85
+ @search_pins = {}.compare_by_identity
86
+ aliases = alias_tracker.aliases
87
+ joins.each do |jd|
88
+ next unless jd.added_by_search
89
+
90
+ top_level = top_level?(jd)
91
+ jd.join_root.children.each do |node|
92
+ next if node.own_tables.blank?
93
+
94
+ tables = node.own_tables.map do |table|
95
+ next unless aliases[table.name].zero?
96
+
97
+ aliases[table.name] = 1
98
+ table
99
+ end
100
+ @search_pins[node] = SearchPin.new(node.own_tables.first.name, tables, top_level)
101
+ end
102
+ end
103
+ end
104
+
56
105
  def construct_tables_for_association!(join_root, association, join_type = self.join_type)
57
106
  tables = table_aliases_for(join_root, association, join_type)
58
107
  association.table = tables.first
@@ -61,6 +110,85 @@ module Ransack
61
110
 
62
111
  private
63
112
 
113
+ # Active Record renders the eager load last, after every
114
+ # `left_outer_joins` dependency. A search of a relation that eager
115
+ # loads counted the eager load's aliases before those of the joins it
116
+ # added, and may have rooted one of them at an eager-loaded join
117
+ # (#1767), so the eager load goes back in front of the first of them.
118
+ def eager_load_first(joins)
119
+ index = joins.index(&:after_eager_load)
120
+ joins.insert(index, joins.pop) if index && !joins.last.added_by_search
121
+ joins
122
+ end
123
+
124
+ def top_level?(jd)
125
+ join_root.match?(jd.join_root) && join_root.table.name == jd.join_root.table.name
126
+ end
127
+
128
+ # The pin for a node about to be joined: the search's own node, or a
129
+ # join of the same association under the root, which stands in for it.
130
+ # Each pin goes to whichever of them is joined first.
131
+ def take_search_pin(parent, node)
132
+ return unless @search_pins
133
+
134
+ pin = @search_pins[node]
135
+ if pin.nil? && parent.equal?(join_root)
136
+ pin = @search_pins.find { |search_node, p| p.top_level && !p.joined_by && search_node.match?(node) }&.last
137
+ end
138
+ return if pin.nil? || pin.joined_by
139
+
140
+ pin.joined_by = node
141
+ pin
142
+ end
143
+
144
+ # A search's join: left out when a join added afterwards stands in for
145
+ # it, and a nested one hung off the alias its parent actually got.
146
+ def search_join_constraints(jd)
147
+ root = jd.join_root
148
+ if top_level?(jd)
149
+ root.children.flat_map do |node|
150
+ joined_by = @search_pins[node]&.joined_by
151
+ if (joined_by && !joined_by.equal?(node)) || join_root.children.any? { |child| node.match?(child) }
152
+ []
153
+ else
154
+ make_constraints(join_root, node, jd.join_type)
155
+ end
156
+ end
157
+ else
158
+ parent = @search_pins.each_value.find { |pin| pin.name == root.table.name && pin.joined_as }
159
+ if parent && parent.joined_as.name != root.table.name
160
+ root = JoinDependency::JoinBase.new(root.base_klass, parent.joined_as, root.children)
161
+ end
162
+ make_join_constraints(root, jd.join_type)
163
+ end
164
+ end
165
+
166
+ # Replaces ActiveRecord::Associations::JoinDependency#make_constraints,
167
+ # to join an association a search added with the alias it pinned.
168
+ def make_constraints(parent, child, join_type)
169
+ foreign_table = parent.table
170
+ foreign_klass = parent.base_klass
171
+ pin = nil
172
+ child.join_constraints(foreign_table, foreign_klass, join_type, alias_tracker) do |reflection, remaining_reflection_chain|
173
+ table, terminated = @joined_tables[remaining_reflection_chain]
174
+ root = reflection == child.reflection
175
+ index = child.reflection.chain.size - remaining_reflection_chain.size
176
+ pin = take_search_pin(parent, child) if root
177
+
178
+ if table && (!root || !terminated) && reusable?(table, pin, index)
179
+ @joined_tables[remaining_reflection_chain] = [table, root] if root
180
+ pin.joined_as = table if pin && root
181
+ next table, true
182
+ end
183
+
184
+ table = aliased_table_for(parent, reflection, index, pin)
185
+ pin.joined_as = table if pin && root
186
+
187
+ @joined_tables[remaining_reflection_chain] ||= [table, root] if join_type == Arel::Nodes::OuterJoin
188
+ table
189
+ end.concat child.children.flat_map { |c| make_constraints(child, c, join_type) }
190
+ end
191
+
64
192
  # The tables for a node's reflection chain, assigned the way Active
65
193
  # Record's JoinDependency#make_constraints assigns them, so that the
66
194
  # aliases Ransack binds conditions to are the ones the query will have.
@@ -71,28 +199,54 @@ module Ransack
71
199
  @joined_tables ||= {}
72
200
  chain = node.reflection.chain
73
201
  terminated_at = nil
202
+ pin = nil
74
203
 
75
204
  chain.each_with_index.map { |reflection, index|
76
205
  remaining = chain[index..]
77
206
  table, terminated = @joined_tables[remaining]
78
207
  root = reflection == node.reflection
208
+ pin = take_search_pin(parent, node) if root && !terminated_at
79
209
 
80
210
  if terminated_at
81
211
  table || reflection.klass.arel_table
82
- elsif table && (!root || !terminated)
212
+ elsif table && (!root || !terminated) && reusable?(table, pin, index)
83
213
  @joined_tables[remaining] = [table, root] if root
214
+ pin.joined_as = table if pin && root
84
215
  terminated_at = index
85
216
  table
86
217
  else
87
- table_name = @references && @references[reflection.name.to_sym]&.to_s
88
- table = alias_tracker.aliased_table_for(reflection.klass.arel_table, table_name) do
89
- name = reflection.alias_candidate(parent.table_name)
90
- root ? name : "#{name}_join"
91
- end
218
+ table = aliased_table_for(parent, reflection, index, pin)
219
+ pin.joined_as = table if pin && root
92
220
  @joined_tables[remaining] ||= [table, root] if join_type == Arel::Nodes::OuterJoin
93
221
  table
94
222
  end
95
- }
223
+ }.tap do |tables|
224
+ # Kept from the search that built the node. A later search of the
225
+ # same result shares the node and may give it other aliases, but
226
+ # the first result's conditions still name these.
227
+ node.own_tables ||= tables.first(terminated_at || tables.size)
228
+ end
229
+ end
230
+
231
+ # Whether a table an earlier join already provides can stand in for
232
+ # one position of a node's reflection chain. Not when a search pinned
233
+ # another alias there: the search's conditions name that one, so the
234
+ # node is joined on its own (#1776).
235
+ def reusable?(table, pin, index)
236
+ pinned = pin&.tables&.at(index)
237
+ pinned.nil? || pinned.name == table.name
238
+ end
239
+
240
+ # A new table for one position of a node's reflection chain: the one a
241
+ # search pinned for it, or the next alias Active Record would give.
242
+ def aliased_table_for(parent, reflection, index, pin)
243
+ return pin.tables[index] if pin&.tables&.at(index)
244
+
245
+ table_name = @references && @references[reflection.name.to_sym]&.to_s
246
+ alias_tracker.aliased_table_for(reflection.klass.arel_table, table_name) do
247
+ name = reflection.alias_candidate(parent.table_name)
248
+ index.zero? ? name : "#{name}_join"
249
+ end
96
250
  end
97
251
 
98
252
  module ClassMethods
@@ -7,11 +7,12 @@ module Ransack
7
7
  mattr_accessor :predicates, :options
8
8
 
9
9
  class PredicateCollection
10
- attr_reader :sorted_names_with_underscores
10
+ attr_reader :sorted_names_with_underscores, :max_name_length
11
11
 
12
12
  def initialize
13
13
  @collection = {}
14
14
  @sorted_names_with_underscores = []
15
+ @max_name_length = 0
15
16
  end
16
17
 
17
18
  delegate :[], :keys, :has_key?, to: :@collection
@@ -20,6 +21,7 @@ module Ransack
20
21
  @sorted_names_with_underscores << [key, '_' + key]
21
22
  @sorted_names_with_underscores.sort! { |(a, _), (b, _)| b.length <=> a.length }
22
23
 
24
+ @max_name_length = key.length if key.length > @max_name_length
23
25
  @collection[key] = value
24
26
  end
25
27
  end
@@ -48,9 +50,26 @@ module Ransack
48
50
  def add_predicate(name, opts = {})
49
51
  name = name.to_s
50
52
  opts[:name] = name
53
+
54
+ if opts[:arel_predicate] && opts[:arel_node]
55
+ raise ArgumentError,
56
+ "add_predicate '#{name}' takes :arel_predicate or :arel_node, " \
57
+ "not both"
58
+ end
59
+
60
+ if opts[:arel_node] && !opts[:arel_node].respond_to?(:call)
61
+ raise ArgumentError,
62
+ "add_predicate '#{name}' needs :arel_node to be callable, " \
63
+ "got #{opts[:arel_node].class}"
64
+ end
65
+
51
66
  compounds = opts.delete(:compounds)
52
67
  compounds = true if compounds.nil?
53
68
  compounds = false if opts[:wants_array]
69
+ # _any/_all are built by appending a suffix to an Arel method name
70
+ # (see #arel_predicate_with_suffix below), which an arel_node builder
71
+ # has none of.
72
+ compounds = false if opts[:arel_node]
54
73
 
55
74
  self.predicates[name] = Predicate.new(opts)
56
75
 
@@ -279,7 +298,11 @@ module Ransack
279
298
  end
280
299
 
281
300
  def arel_predicate_with_suffix(arel_predicate, suffix)
282
- if arel_predicate === Proc
301
+ # `arel_predicate === Proc` reads as a class check, but `Proc#===` is
302
+ # an alias for `#call`: it invokes the proc, passing the `Proc`
303
+ # class itself as the argument, and branches on whatever comes
304
+ # back. `#respond_to?(:call)` is a real check.
305
+ if arel_predicate.respond_to?(:call)
283
306
  proc { |v| "#{arel_predicate.call(v)}#{suffix}" }
284
307
  else
285
308
  "#{arel_predicate}#{suffix}"
@@ -3,13 +3,25 @@ require 'ransack/visitor'
3
3
  module Ransack
4
4
  class Context
5
5
  attr_reader :search, :object, :klass, :base, :engine, :arel_visitor
6
- attr_accessor :auth_object, :search_key, :ignore_unknown_conditions
6
+ attr_accessor :search_key, :ignore_unknown_conditions
7
+ attr_reader :auth_object, :permitted
7
8
  attr_writer :null_sentinel
8
9
 
10
+ # The allowlists are read once per model and kept for the life of the
11
+ # context (see `allowlist_set`), so changing what they depend on drops
12
+ # the kept copies.
13
+ def auth_object=(value)
14
+ reset_name_caches
15
+ @auth_object = value
16
+ end
17
+
9
18
  # True when the search was built from `ActionController::Parameters` the
10
19
  # controller permitted. A model with no allowlist of its own then lets
11
20
  # that permit list be the boundary (see `allowlist` below).
12
- attr_accessor :permitted
21
+ def permitted=(value)
22
+ reset_name_caches
23
+ @permitted = value
24
+ end
13
25
 
14
26
  class << self
15
27
 
@@ -122,7 +134,9 @@ module Ransack
122
134
  # For scopes with arity 1, pass the array as a single argument instead of splatting
123
135
  @object.public_send(scope, args)
124
136
  else
125
- @object.public_send(scope, *args)
137
+ # Only an Array is spread into several arguments. A bare
138
+ # splat would call to_a, turning a Time into ten (#1769).
139
+ @object.public_send(scope, *Array.wrap(args))
126
140
  end
127
141
  end
128
142
 
@@ -202,7 +216,7 @@ module Ransack
202
216
  end
203
217
 
204
218
  def unpolymorphize_association(str)
205
- if (match = str.match(/_of_([^_]+?)_type$/)) &&
219
+ if (match = str.match(/_of_([^_]+?)_type\z/)) &&
206
220
  (klass = polymorphic_class(match.captures.first))
207
221
  [match.pre_match, klass]
208
222
  else
@@ -240,9 +254,19 @@ module Ransack
240
254
  # `term_or_daddy` with `term => name_or_email` becomes
241
255
  # `name_or_email_or_parent_name`. Returns the name unchanged when it
242
256
  # holds no alias.
257
+ #
258
+ # A key is resolved several times while a search is built (to check it,
259
+ # to split it into attributes, to bind them), so the result is kept for
260
+ # the life of the context, frozen, and dropped with the allowlists it
261
+ # depends on.
243
262
  def resolve_aliases(str)
244
263
  return str.to_s unless key_within_depth_limit?(str)
245
264
 
265
+ key = str.to_s
266
+ (@resolved_aliases ||= {})[key] ||= -resolve_aliases_uncached(key)
267
+ end
268
+
269
+ def resolve_aliases_uncached(str)
246
270
  whole = resolve_alias_segment(str)
247
271
  return whole if whole != str
248
272
  # A name that is an attribute in its own right is never split, even if
@@ -256,17 +280,18 @@ module Ransack
256
280
  combinator = str[/_(or|and)_/, 1] || Constants::OR
257
281
  resolved.join("_#{combinator}_")
258
282
  end
283
+ private :resolve_aliases_uncached
259
284
 
260
285
  def ransackable_attribute?(str, klass)
261
- attributes_for(klass).any? { |s| s.to_sym == str.to_sym }
286
+ allowlist_set(klass, :attributes_for).include?(str.to_s)
262
287
  end
263
288
 
264
289
  def ransortable_attribute?(str, klass)
265
- sortable_attributes_for(klass).any? { |s| s.to_sym == str.to_sym }
290
+ allowlist_set(klass, :sortable_attributes_for).include?(str.to_s)
266
291
  end
267
292
 
268
293
  def ransackable_association?(str, klass)
269
- associations_for(klass).any? { |s| s.to_sym == str.to_sym }
294
+ allowlist_set(klass, :associations_for).include?(str.to_s)
270
295
  end
271
296
 
272
297
  def ransackable_scope?(str, klass)
@@ -289,8 +314,31 @@ module Ransack
289
314
  associations_for(traverse(str))
290
315
  end
291
316
 
317
+ # Hooks a condition calls to record the joins it reads. The Active
318
+ # Record context overrides them; a context registered for another ORM
319
+ # need not, and a search through it still builds.
320
+ def lock_association(_association); end
321
+
322
+ def use_association(_association); end
323
+
324
+ def use_ancestors_of(_association); end
325
+
292
326
  private
293
327
 
328
+ # Every name in a search is checked against the allowlists several times
329
+ # over, and each check used to call the model's `ransackable_*` method
330
+ # and scan the array it returned. They are asked once per model per
331
+ # context instead, and what is cached here is dropped whenever the auth
332
+ # object or the permitted flag changes.
333
+ def reset_name_caches
334
+ @allowlist_sets = @resolved_aliases = @attribute_methods = nil
335
+ end
336
+
337
+ def allowlist_set(klass, kind)
338
+ ((@allowlist_sets ||= {})[kind] ||= {})[klass] ||=
339
+ send(kind, klass).to_set(&:to_s).freeze
340
+ end
341
+
294
342
  def attributes_for(klass)
295
343
  allowlist(klass, :ransackable_attributes) { klass.authorizable_ransackable_attributes }
296
344
  end
@@ -0,0 +1,13 @@
1
+ module Ransack
2
+ # Raised for a search that cannot be built from its params: an unknown
3
+ # attribute, predicate, combinator or sort under a strict search, or params
4
+ # of the wrong shape (a string where a hash of conditions belongs) under any
5
+ # search. Search params usually come from the request, so this is the error
6
+ # to rescue to turn a bad search into a 400 rather than a 500. It is an
7
+ # ArgumentError, so code that rescues ArgumentError still catches it.
8
+ class InvalidSearchError < ArgumentError; end
9
+
10
+ # Raised internally when a name does not resolve to an association. The
11
+ # form builder rescues it; a search never lets it escape.
12
+ class UntraversableAssociationError < StandardError; end
13
+ end
@@ -1,3 +1,3 @@
1
- module Ransack
2
- class InvalidSearchError < ArgumentError; end
3
- end
1
+ # Kept so that `require 'ransack/invalid_search_error'` still works; the error
2
+ # classes live in ransack/errors.
3
+ require 'ransack/errors'
@@ -50,21 +50,56 @@ ar:
50
50
  not_cont: "غير محتو"
51
51
  not_cont_any: "غير محتو لواحد على اﻷقل"
52
52
  not_cont_all: "غير محتو للجميع"
53
+ i_cont: "محتو مع تجاهل حالة الأحرف"
54
+ i_cont_any: "محتو لواحد على اﻷقل مع تجاهل حالة الأحرف"
55
+ i_cont_all: "محتو لجميع مع تجاهل حالة الأحرف"
56
+ not_i_cont: "غير محتو مع تجاهل حالة الأحرف"
57
+ not_i_cont_any: "غير محتو لواحد على اﻷقل مع تجاهل حالة الأحرف"
58
+ not_i_cont_all: "غير محتو للجميع مع تجاهل حالة الأحرف"
53
59
  start: "يبدأ بـ"
54
60
  start_any: "يبدأ بواحد على اﻷقل"
55
61
  start_all: "يبدأ بالجميع"
56
62
  not_start: "لا يبدأ بـ"
57
63
  not_start_any: "لا يبدأ بواحد على اﻷقل"
58
64
  not_start_all: "لا يبدأ بالجميع"
65
+ i_start: "يبدأ بـ مع تجاهل حالة الأحرف"
66
+ i_start_any: "يبدأ بواحد على اﻷقل مع تجاهل حالة الأحرف"
67
+ i_start_all: "يبدأ بالجميع مع تجاهل حالة الأحرف"
68
+ not_i_start: "لا يبدأ بـ مع تجاهل حالة الأحرف"
69
+ not_i_start_any: "لا يبدأ بواحد على اﻷقل مع تجاهل حالة الأحرف"
70
+ not_i_start_all: "لا يبدأ بالجميع مع تجاهل حالة الأحرف"
59
71
  end: "ينتهي بـ"
60
72
  end_any: "ينتهي بواحد على اﻷقل"
61
73
  end_all: "ينتهي بالجميع"
62
74
  not_end: "لا ينتهي بـ"
63
75
  not_end_any: "لا ينتهي بواحد على اﻷقل"
64
76
  not_end_all: "لا ينتهي بالجميع"
77
+ i_end: "ينتهي بـ مع تجاهل حالة الأحرف"
78
+ i_end_any: "ينتهي بواحد على اﻷقل مع تجاهل حالة الأحرف"
79
+ i_end_all: "ينتهي بالجميع مع تجاهل حالة الأحرف"
80
+ not_i_end: "لا ينتهي بـ مع تجاهل حالة الأحرف"
81
+ not_i_end_any: "لا ينتهي بواحد على اﻷقل مع تجاهل حالة الأحرف"
82
+ not_i_end_all: "لا ينتهي بالجميع مع تجاهل حالة الأحرف"
65
83
  'true': "صحيح"
84
+ not_true: "غير صحيح"
66
85
  'false': "خطأ"
86
+ not_false: "غير خطأ"
67
87
  present: "مستقبل"
68
88
  blank: "فراغ"
69
89
  'null': "عدم"
70
90
  not_null: "غير مساو لقيمة عدم"
91
+ length_eq: "الطول معادل"
92
+ length_eq_any: "الطول معادل على اﻷقل لواحد"
93
+ length_eq_all: "الطول معادل للجميع"
94
+ length_lt: "الطول أصغر من"
95
+ length_lt_any: "الطول أصغر لواحد على اﻷقل"
96
+ length_lt_all: "الطول أصغر من الجميع"
97
+ length_lteq: "الطول أصغر أو مساو لـ"
98
+ length_lteq_any: "الطول أصغر أو مساو لواحد على اﻷقل"
99
+ length_lteq_all: "الطول أصغر أو مساو للجميع"
100
+ length_gt: "الطول أكبر من"
101
+ length_gt_any: "الطول أكبر من واحد على اﻷقل"
102
+ length_gt_all: "الطول أكبر من الجميع"
103
+ length_gteq: "الطول أكبر أو مساو لـ"
104
+ length_gteq_any: "الطول أكبر أو مساو لواحد على اﻷقل"
105
+ length_gteq_all: "الطول أكبر أو مساو للجميع"
@@ -32,7 +32,7 @@ az:
32
32
  lteq: "daha kiçik və ya bərabər"
33
33
  lteq_any: "daha kiçik və ya hər hansı birinə bərabər"
34
34
  lteq_all: "daha kiçik və ya hamısına bərabər"
35
- gt: "daha böyük "
35
+ gt: "daha böyük"
36
36
  gt_any: "hər hansı birindən daha böyük"
37
37
  gt_all: "hamısından daha böyük"
38
38
  gteq: "daha böyük və ya bərabər"
@@ -50,21 +50,56 @@ az:
50
50
  not_cont: "ehtiva etməyən"
51
51
  not_cont_any: "hər hansı birini ehtiva etməyən"
52
52
  not_cont_all: "heç birini birini ehtiva etməyən"
53
+ i_cont: "ehtiva edən, hərf halına baxmayaraq"
54
+ i_cont_any: "hər hansı birini ehtiva edən, hərf halına baxmayaraq"
55
+ i_cont_all: "hamısını ehtiva edən, hərf halına baxmayaraq"
56
+ not_i_cont: "ehtiva etməyən, hərf halına baxmayaraq"
57
+ not_i_cont_any: "hər hansı birini ehtiva etməyən, hərf halına baxmayaraq"
58
+ not_i_cont_all: "heç birini birini ehtiva etməyən, hərf halına baxmayaraq"
53
59
  start: "ilə başlayan"
54
60
  start_any: "hər hansı biri ilə başlayan"
55
61
  start_all: "hamısı ilə başlayan"
56
62
  not_start: "ilə başlamayan"
57
63
  not_start_any: "hər hansı biri ilə başlamayan"
58
64
  not_start_all: "hiçbiri ilə başlamayan"
65
+ i_start: "ilə başlayan, hərf halına baxmayaraq"
66
+ i_start_any: "hər hansı biri ilə başlayan, hərf halına baxmayaraq"
67
+ i_start_all: "hamısı ilə başlayan, hərf halına baxmayaraq"
68
+ not_i_start: "ilə başlamayan, hərf halına baxmayaraq"
69
+ not_i_start_any: "hər hansı biri ilə başlamayan, hərf halına baxmayaraq"
70
+ not_i_start_all: "hiçbiri ilə başlamayan, hərf halına baxmayaraq"
59
71
  end: "ilə bitən"
60
72
  end_any: "hər hansı biri ilə bitən"
61
73
  end_all: "hamısı ilə bitən"
62
74
  not_end: "ilə bitməyən"
63
75
  not_end_any: "hər hansı biri ilə bitməyən"
64
76
  not_end_all: "heç biri ilə bitməyən"
77
+ i_end: "ilə bitən, hərf halına baxmayaraq"
78
+ i_end_any: "hər hansı biri ilə bitən, hərf halına baxmayaraq"
79
+ i_end_all: "hamısı ilə bitən, hərf halına baxmayaraq"
80
+ not_i_end: "ilə bitməyən, hərf halına baxmayaraq"
81
+ not_i_end_any: "hər hansı biri ilə bitməyən, hərf halına baxmayaraq"
82
+ not_i_end_all: "heç biri ilə bitməyən, hərf halına baxmayaraq"
65
83
  'true': "doğru"
84
+ not_true: "doğru deyil"
66
85
  'false': "yanlış"
86
+ not_false: "yanlış deyil"
67
87
  present: "mövcüd"
68
88
  blank: "boş"
69
89
  'null': "keçərsiz"
70
90
  not_null: "keçərli"
91
+ length_eq: "uzunluğu bərabər"
92
+ length_eq_any: "uzunluğu hər hansı birinə bərabər"
93
+ length_eq_all: "uzunluğu hamısına bərabər"
94
+ length_lt: "uzunluğu daha kiçik"
95
+ length_lt_any: "uzunluğu hər hansı birindən kiçik"
96
+ length_lt_all: "uzunluğu hamısından kiçik"
97
+ length_lteq: "uzunluğu daha kiçik və ya bərabər"
98
+ length_lteq_any: "uzunluğu daha kiçik və ya hər hansı birinə bərabər"
99
+ length_lteq_all: "uzunluğu daha kiçik və ya hamısına bərabər"
100
+ length_gt: "uzunluğu daha böyük"
101
+ length_gt_any: "uzunluğu hər hansı birindən daha böyük"
102
+ length_gt_all: "uzunluğu hamısından daha böyük"
103
+ length_gteq: "uzunluğu daha böyük və ya bərabər"
104
+ length_gteq_any: "uzunluğu daha böyük və ya hər hansı birine bərabər"
105
+ length_gteq_all: "uzunluğu daha böyük və ya hamısıne bərabər"
@@ -50,21 +50,56 @@ bg:
50
50
  not_cont: "не съдържа"
51
51
  not_cont_any: "не съдържа никое"
52
52
  not_cont_all: "не съдържа всички"
53
+ i_cont: "съдържа, без оглед на регистъра"
54
+ i_cont_any: "съдържа всякое, без оглед на регистъра"
55
+ i_cont_all: "съдържа всички, без оглед на регистъра"
56
+ not_i_cont: "не съдържа, без оглед на регистъра"
57
+ not_i_cont_any: "не съдържа никое, без оглед на регистъра"
58
+ not_i_cont_all: "не съдържа всички, без оглед на регистъра"
53
59
  start: "започва с"
54
60
  start_any: "започва с всякое"
55
61
  start_all: "започва с всички"
56
62
  not_start: "не започва с"
57
63
  not_start_any: "не започва с никое"
58
64
  not_start_all: "не започва с всички"
65
+ i_start: "започва с, без оглед на регистъра"
66
+ i_start_any: "започва с всякое, без оглед на регистъра"
67
+ i_start_all: "започва с всички, без оглед на регистъра"
68
+ not_i_start: "не започва с, без оглед на регистъра"
69
+ not_i_start_any: "не започва с никое, без оглед на регистъра"
70
+ not_i_start_all: "не започва с всички, без оглед на регистъра"
59
71
  end: "завършва с"
60
72
  end_any: "завършва с всякое"
61
73
  end_all: "завършва с всички"
62
74
  not_end: "не завършва с"
63
75
  not_end_any: "не завършва с никое"
64
76
  not_end_all: "не завършва с всички"
77
+ i_end: "завършва с, без оглед на регистъра"
78
+ i_end_any: "завършва с всякое, без оглед на регистъра"
79
+ i_end_all: "завършва с всички, без оглед на регистъра"
80
+ not_i_end: "не завършва с, без оглед на регистъра"
81
+ not_i_end_any: "не завършва с никое, без оглед на регистъра"
82
+ not_i_end_all: "не завършва с всички, без оглед на регистъра"
65
83
  'true': "истина"
84
+ not_true: "не е истина"
66
85
  'false': "неистина"
86
+ not_false: "не е неистина"
67
87
  present: "присъства"
68
88
  blank: "е празно"
69
89
  'null': "е нула"
70
90
  not_null: "не е нула"
91
+ length_eq: "дължината се равнява на"
92
+ length_eq_any: "дължината се равнява на всяко"
93
+ length_eq_all: "дължината се равнява на всички"
94
+ length_lt: "дължината по-малко от"
95
+ length_lt_any: "дължината по-малко от някое"
96
+ length_lt_all: "дължината по-малко от всички"
97
+ length_lteq: "дължината по-малко или равно на"
98
+ length_lteq_any: "дължината по-малко или равно на някое"
99
+ length_lteq_all: "дължината по-малко или равно на всички"
100
+ length_gt: "дължината по-голямо от"
101
+ length_gt_any: "дължината по-голямо от всякое"
102
+ length_gt_all: "дължината по-голямо от всички"
103
+ length_gteq: "дължината по-голямо или равно на"
104
+ length_gteq_any: "дължината по-голямо или равно на всякое"
105
+ length_gteq_all: "дължината по-голямо или равно на всички"