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 +4 -4
- data/CHANGELOG.md +8 -0
- data/lib/structured_params/i18n.rb +132 -0
- data/lib/structured_params/params.rb +5 -2
- data/lib/structured_params/version.rb +1 -1
- data/lib/structured_params.rb +1 -0
- data/sig/structured_params/i18n.rbs +78 -0
- data/sig/structured_params/params.rbs +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b571c05cd70b8d5f5d3cbf40de3d06158b8b1ddfa9d0a7bd1edb4a158cd05813
|
|
4
|
+
data.tar.gz: 4eeb5375b4e0d1a80feb1c0185084858e2932591c61cef9fef5ee10a1acbad42
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
data/lib/structured_params.rb
CHANGED
|
@@ -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
|
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.
|
|
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
|