structured_params 0.9.2 → 0.9.3

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: 99bd6bd346b7e55ee390e747bb9ff232aa84af52e0198b48d162b42e7caed5f8
4
- data.tar.gz: b4746d75c8201e0751e1d413155697a2f23da1d0da667491749838db0a6c0c19
3
+ metadata.gz: b571c05cd70b8d5f5d3cbf40de3d06158b8b1ddfa9d0a7bd1edb4a158cd05813
4
+ data.tar.gz: 4eeb5375b4e0d1a80feb1c0185084858e2932591c61cef9fef5ee10a1acbad42
5
5
  SHA512:
6
- metadata.gz: 2aef79c11cbb97312962a39869726aa7ce95ef1e1b3f29974d3d8bc1d1637487ef7cb85dbe47742866db1db9572e83999bb52095fe634247c1170cafd0ff5433
7
- data.tar.gz: b2a6bce45a08f4521979084f2ecd6b20385b7aeec3794ae2cfb8bd57d6888e87ffdae4dae10a4887b07e47417afc80d256235e25bc8ce5c9c96a3316391f81ab
6
+ metadata.gz: fe8a1dee53c763112c935211fd941d746871cf28ef2a16499cabc71625e9a8219279fb354887d9d7f8bc80d13537cd123eb19a29feb6424351ffa720207b36b0
7
+ data.tar.gz: b03d0e6cd37482e1ddc578788e3d46e191a73a53e5e091a34ee80ccf88bd1258aa2f29f3b5544fa9252309042f6060c9f630ab820087f29b29d9a7d786dca1fa
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.3] - 2026-04-09
4
+
5
+ ## What's Changed
6
+ * Enhance i18n support for nested attributes and update documentation by @Syati in https://github.com/Syati/structured_params/pull/15
7
+
8
+
9
+ **Full Changelog**: https://github.com/Syati/structured_params/compare/v0.9.2...v0.9.3
10
+
3
11
  ## [0.9.2] - 2026-04-02
4
12
 
5
13
  ## What's Changed
@@ -0,0 +1,132 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module StructuredParams
5
+ # Provides i18n-aware human_attribute_name resolution for nested dot-notation
6
+ # attributes (e.g. "hobbies.0.name").
7
+ #
8
+ # When included in a Params subclass, overrides +human_attribute_name+ so that
9
+ # each segment of the path is resolved by the corresponding nested model class,
10
+ # ensuring that child-model translations are respected instead of falling back
11
+ # to the parent model's i18n context.
12
+ #
13
+ # == i18n keys
14
+ #
15
+ # You can customize how array indices and object nesting are rendered by
16
+ # defining the following keys in your locale file:
17
+ #
18
+ # ja:
19
+ # activemodel:
20
+ # errors:
21
+ # nested_attribute:
22
+ # array: "%{parent} %{index} 番目の%{child}"
23
+ # object: "%{parent}の%{child}"
24
+ #
25
+ # Without these keys the defaults are:
26
+ # array → "<parent> <index> <child>" (e.g. "Hobbies 0 Name")
27
+ # object → "<parent> <child>" (e.g. "Address Postal code")
28
+ module I18n
29
+ extend ActiveSupport::Concern
30
+
31
+ class_methods do # rubocop:disable Metrics/BlockLength
32
+ # Override human_attribute_name to resolve nested dot-notation paths.
33
+ #
34
+ # Flat attributes (no dot) are delegated to the default ActiveModel
35
+ # behaviour unchanged.
36
+ #
37
+ # Example (en default):
38
+ # human_attribute_name(:'hobbies.0.name') # => "Hobbies 0 Name"
39
+ #
40
+ # Example with i18n (ja):
41
+ # human_attribute_name(:'hobbies.0.name') # => "趣味 0 番目の名前"
42
+ #
43
+ #: (Symbol | String, ?Hash[untyped, untyped]) -> String
44
+ def human_attribute_name(attribute, options = {})
45
+ parts = attribute.to_s.split('.')
46
+ return super if parts.length == 1
47
+ return super unless structured_attributes.key?(parts.first)
48
+
49
+ resolve_nested_human_attribute_name(parts, options)
50
+ end
51
+
52
+ private
53
+
54
+ # Walk +parts+ (e.g. ["hobbies", "0", "name"]) and build a human-readable
55
+ # label by delegating each segment to the appropriate nested class.
56
+ #
57
+ # Only +:locale+ is forwarded to inner +human_attribute_name+ calls.
58
+ # Options such as +:default+ are specific to the outer call (e.g. from
59
+ # +full_messages+) and must not bleed into individual segment lookups,
60
+ # where they would replace the segment's own translation fallback.
61
+ #
62
+ #: (Array[String], Hash[untyped, untyped]) -> String
63
+ def resolve_nested_human_attribute_name(parts, options)
64
+ label = nil
65
+ klass = self
66
+ inner_opts = options.slice(:locale)
67
+
68
+ attr_segments(parts).each do |index, attr|
69
+ human = klass&.human_attribute_name(attr, inner_opts) || attr.humanize
70
+ label = build_nested_label(label, index, human, options)
71
+ klass &&= klass.structured_attributes[attr]
72
+ end
73
+
74
+ label || parts.last.humanize
75
+ end
76
+
77
+ # Convert a parts array into (index_or_nil, attr) pairs.
78
+ #
79
+ # attr_segments(["hobbies", "0", "name"]) #=> [[nil, "hobbies"], ["0", "name"]]
80
+ # attr_segments(["address", "postal_code"]) #=> [[nil, "address"], [nil, "postal_code"]]
81
+ #
82
+ #: (Array[String]) -> Array[[String?, String]]
83
+ def attr_segments(parts)
84
+ index = nil
85
+ parts.each_with_object([]) do |part, segments|
86
+ if part.match?(/\A\d+\z/)
87
+ index = part
88
+ else
89
+ segments << [index, part]
90
+ index = nil
91
+ end
92
+ end
93
+ end
94
+
95
+ # Combine +result+ (accumulated label so far), an optional array +index+,
96
+ # and the new +attr_human+ into a single label string.
97
+ #
98
+ # Uses the i18n keys:
99
+ # activemodel.errors.nested_attribute.array (parent, index, child)
100
+ # activemodel.errors.nested_attribute.object (parent, child)
101
+ #
102
+ # The +locale:+ key from +options+ is forwarded to ::I18n.t so that an
103
+ # explicit locale passed to human_attribute_name is honoured.
104
+ #
105
+ #: (String?, String?, String, Hash[untyped, untyped]) -> String
106
+ def build_nested_label(result, index, attr_human, options)
107
+ return attr_human if result.nil?
108
+
109
+ i18n_opts = options.slice(:locale)
110
+
111
+ if index
112
+ ::I18n.t(
113
+ 'activemodel.errors.nested_attribute.array',
114
+ parent: result,
115
+ index: index,
116
+ child: attr_human,
117
+ default: "#{result} #{index} #{attr_human}",
118
+ **i18n_opts
119
+ )
120
+ else
121
+ ::I18n.t(
122
+ 'activemodel.errors.nested_attribute.object',
123
+ parent: result,
124
+ child: attr_human,
125
+ default: "#{result} #{attr_human}",
126
+ **i18n_opts
127
+ )
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -55,6 +55,7 @@ module StructuredParams
55
55
  include ActiveModel::Attributes
56
56
  include AttributeMethods
57
57
  include Validations
58
+ include I18n
58
59
 
59
60
  # @rbs @errors: ::StructuredParams::Errors?
60
61
 
@@ -290,9 +291,11 @@ module StructuredParams
290
291
  #: (untyped, String) -> void
291
292
  def import_structured_errors(structured_errors, prefix)
292
293
  structured_errors.each do |error|
293
- # Create dotted attribute path and import normally
294
+ # Create dotted attribute path and import with the message already resolved
295
+ # in the child model's i18n context, so the parent model's locale does not
296
+ # override the child's translations.
294
297
  error_attribute = "#{prefix}.#{error.attribute}"
295
- errors.import(error, attribute: error_attribute.to_sym)
298
+ errors.import(error, attribute: error_attribute.to_sym, message: error.message)
296
299
  end
297
300
  end
298
301
  end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module StructuredParams
5
- VERSION = '0.9.2' #: string
5
+ VERSION = '0.9.3' #: string
6
6
  end
@@ -12,6 +12,7 @@ require_relative 'structured_params/version'
12
12
  require_relative 'structured_params/errors'
13
13
  require_relative 'structured_params/attribute_methods'
14
14
  require_relative 'structured_params/validations'
15
+ require_relative 'structured_params/i18n'
15
16
 
16
17
  # types (load first for module definition)
17
18
  require_relative 'structured_params/type/object'
@@ -0,0 +1,78 @@
1
+ # Generated from lib/structured_params/i18n.rb with RBS::Inline
2
+
3
+ module StructuredParams
4
+ # Provides i18n-aware human_attribute_name resolution for nested dot-notation
5
+ # attributes (e.g. "hobbies.0.name").
6
+ #
7
+ # When included in a Params subclass, overrides +human_attribute_name+ so that
8
+ # each segment of the path is resolved by the corresponding nested model class,
9
+ # ensuring that child-model translations are respected instead of falling back
10
+ # to the parent model's i18n context.
11
+ #
12
+ # == i18n keys
13
+ #
14
+ # You can customize how array indices and object nesting are rendered by
15
+ # defining the following keys in your locale file:
16
+ #
17
+ # ja:
18
+ # activemodel:
19
+ # errors:
20
+ # nested_attribute:
21
+ # array: "%{parent} %{index} 番目の%{child}"
22
+ # object: "%{parent}の%{child}"
23
+ #
24
+ # Without these keys the defaults are:
25
+ # array → "<parent> <index> <child>" (e.g. "Hobbies 0 Name")
26
+ # object → "<parent> <child>" (e.g. "Address Postal code")
27
+ module I18n
28
+ extend ActiveSupport::Concern
29
+
30
+ # Override human_attribute_name to resolve nested dot-notation paths.
31
+ #
32
+ # Flat attributes (no dot) are delegated to the default ActiveModel
33
+ # behaviour unchanged.
34
+ #
35
+ # Example (en default):
36
+ # human_attribute_name(:'hobbies.0.name') # => "Hobbies 0 Name"
37
+ #
38
+ # Example with i18n (ja):
39
+ # human_attribute_name(:'hobbies.0.name') # => "趣味 0 番目の名前"
40
+ #
41
+ # : (Symbol | String, ?Hash[untyped, untyped]) -> String
42
+ def human_attribute_name: (Symbol | String, ?Hash[untyped, untyped]) -> String
43
+
44
+ private
45
+
46
+ # Walk +parts+ (e.g. ["hobbies", "0", "name"]) and build a human-readable
47
+ # label by delegating each segment to the appropriate nested class.
48
+ #
49
+ # Only +:locale+ is forwarded to inner +human_attribute_name+ calls.
50
+ # Options such as +:default+ are specific to the outer call (e.g. from
51
+ # +full_messages+) and must not bleed into individual segment lookups,
52
+ # where they would replace the segment's own translation fallback.
53
+ #
54
+ # : (Array[String], Hash[untyped, untyped]) -> String
55
+ def resolve_nested_human_attribute_name: (Array[String], Hash[untyped, untyped]) -> String
56
+
57
+ # Convert a parts array into (index_or_nil, attr) pairs.
58
+ #
59
+ # attr_segments(["hobbies", "0", "name"]) #=> [[nil, "hobbies"], ["0", "name"]]
60
+ # attr_segments(["address", "postal_code"]) #=> [[nil, "address"], [nil, "postal_code"]]
61
+ #
62
+ # : (Array[String]) -> Array[[String?, String]]
63
+ def attr_segments: (Array[String]) -> Array[[ String?, String ]]
64
+
65
+ # Combine +result+ (accumulated label so far), an optional array +index+,
66
+ # and the new +attr_human+ into a single label string.
67
+ #
68
+ # Uses the i18n keys:
69
+ # activemodel.errors.nested_attribute.array (parent, index, child)
70
+ # activemodel.errors.nested_attribute.object (parent, child)
71
+ #
72
+ # The +locale:+ key from +options+ is forwarded to ::I18n.t so that an
73
+ # explicit locale passed to human_attribute_name is honoured.
74
+ #
75
+ # : (String?, String?, String, Hash[untyped, untyped]) -> String
76
+ def build_nested_label: (String?, String?, String, Hash[untyped, untyped]) -> String
77
+ end
78
+ end
@@ -58,6 +58,8 @@ module StructuredParams
58
58
 
59
59
  include Validations
60
60
 
61
+ include I18n
62
+
61
63
  @errors: ::StructuredParams::Errors?
62
64
 
63
65
  self.@structured_attributes: Hash[Symbol, singleton(::StructuredParams::Params)]?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: structured_params
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.2
4
+ version: 0.9.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mizuki Yamamoto
@@ -61,6 +61,7 @@ files:
61
61
  - lib/structured_params.rb
62
62
  - lib/structured_params/attribute_methods.rb
63
63
  - lib/structured_params/errors.rb
64
+ - lib/structured_params/i18n.rb
64
65
  - lib/structured_params/params.rb
65
66
  - lib/structured_params/type/array.rb
66
67
  - lib/structured_params/type/object.rb
@@ -69,6 +70,7 @@ files:
69
70
  - sig/structured_params.rbs
70
71
  - sig/structured_params/attribute_methods.rbs
71
72
  - sig/structured_params/errors.rbs
73
+ - sig/structured_params/i18n.rbs
72
74
  - sig/structured_params/params.rbs
73
75
  - sig/structured_params/type/array.rbs
74
76
  - sig/structured_params/type/object.rbs