dugway 1.1.0 → 1.3.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 +4 -4
- data/.bundle/config +2 -0
- data/.github/workflows/main.yml +1 -1
- data/.gitignore +1 -0
- data/README.md +32 -7
- data/dugway.gemspec +11 -9
- data/lib/dugway/application.rb +5 -3
- data/lib/dugway/assets/big_cartel_logo.svg +4 -0
- data/lib/dugway/cli/build.rb +7 -1
- data/lib/dugway/cli/server.rb +69 -9
- data/lib/dugway/cli/templates/source/settings.json +8 -0
- data/lib/dugway/cli/validate.rb +9 -2
- data/lib/dugway/controller.rb +5 -1
- data/lib/dugway/liquid/drops/account_drop.rb +4 -0
- data/lib/dugway/liquid/drops/artists_drop.rb +12 -0
- data/lib/dugway/liquid/drops/base_drop.rb +27 -2
- data/lib/dugway/liquid/drops/categories_drop.rb +12 -0
- data/lib/dugway/liquid/drops/features_drop.rb +144 -0
- data/lib/dugway/liquid/drops/pages_drop.rb +30 -2
- data/lib/dugway/liquid/drops/product_drop.rb +11 -0
- data/lib/dugway/liquid/drops/products_drop.rb +39 -0
- data/lib/dugway/liquid/drops/theme_drop.rb +52 -6
- data/lib/dugway/liquid/drops/translations_drop.rb +122 -0
- data/lib/dugway/liquid/filters/core_filters.rb +169 -7
- data/lib/dugway/liquid/filters/font_filters.rb +1 -0
- data/lib/dugway/liquid/filters/util_filters.rb +1 -10
- data/lib/dugway/liquid/tags/get.rb +6 -6
- data/lib/dugway/liquid/tags/paginate.rb +61 -11
- data/lib/dugway/liquifier.rb +44 -8
- data/lib/dugway/store.rb +46 -3
- data/lib/dugway/theme.rb +151 -15
- data/lib/dugway/version.rb +1 -1
- data/lib/dugway.rb +55 -2
- data/locales/storefront.de.yml +81 -0
- data/locales/storefront.en-CA.yml +81 -0
- data/locales/storefront.en-GB.yml +81 -0
- data/locales/storefront.en-US.yml +81 -0
- data/locales/storefront.es-ES.yml +81 -0
- data/locales/storefront.es-MX.yml +81 -0
- data/locales/storefront.fr-CA.yml +81 -0
- data/locales/storefront.fr-FR.yml +81 -0
- data/locales/storefront.id.yml +81 -0
- data/locales/storefront.it.yml +81 -0
- data/locales/storefront.ja.yml +81 -0
- data/locales/storefront.ko.yml +81 -0
- data/locales/storefront.nl.yml +81 -0
- data/locales/storefront.pl.yml +81 -0
- data/locales/storefront.pt-BR.yml +81 -0
- data/locales/storefront.pt-PT.yml +81 -0
- data/locales/storefront.ro.yml +81 -0
- data/locales/storefront.sv.yml +81 -0
- data/locales/storefront.tr.yml +81 -0
- data/locales/storefront.zh-CN.yml +81 -0
- data/locales/storefront.zh-TW.yml +81 -0
- data/log/dugway.log +1 -0
- data/mise.toml +2 -0
- data/spec/features/page_rendering_spec.rb +4 -4
- data/spec/fixtures/theme/layout.html +2 -0
- data/spec/fixtures/theme/settings.json +6 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/units/dugway/liquid/drops/features_drop_spec.rb +182 -0
- data/spec/units/dugway/liquid/drops/pages_drop_spec.rb +186 -7
- data/spec/units/dugway/liquid/drops/product_drop_spec.rb +17 -0
- data/spec/units/dugway/liquid/drops/theme_drop_spec.rb +45 -0
- data/spec/units/dugway/liquid/drops/translations_drop_spec.rb +292 -0
- data/spec/units/dugway/liquid/filters/core_filters_spec.rb +301 -3
- data/spec/units/dugway/store_spec.rb +55 -0
- data/spec/units/dugway/theme_spec.rb +543 -1
- metadata +84 -25
data/lib/dugway/controller.rb
CHANGED
@@ -95,7 +95,11 @@ module Dugway
|
|
95
95
|
end
|
96
96
|
|
97
97
|
def render_page(variables={})
|
98
|
-
|
98
|
+
# Prioritize page object passed in variables, otherwise fetch default using Controller#page
|
99
|
+
current_page = variables[:page] || page
|
100
|
+
render_variables = variables.merge({ :page => current_page })
|
101
|
+
# Use the permalink from the determined page object
|
102
|
+
render_file("#{ current_page['permalink'] }.html", render_variables)
|
99
103
|
end
|
100
104
|
|
101
105
|
def render_json(object)
|
@@ -1,6 +1,18 @@
|
|
1
1
|
module Dugway
|
2
2
|
module Drops
|
3
3
|
class ArtistsDrop < BaseDrop
|
4
|
+
# Liquid 5.x compatibility: implement liquid_method_missing for method access
|
5
|
+
def liquid_method_missing(method)
|
6
|
+
# Try to find artist by permalink
|
7
|
+
result = before_method(method.to_s)
|
8
|
+
# Wrap hash results in ArtistDrop if it looks like an artist
|
9
|
+
if result.is_a?(Hash) && result['permalink']
|
10
|
+
ArtistDrop.new(result)
|
11
|
+
else
|
12
|
+
result
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
4
16
|
def all
|
5
17
|
@all ||= source
|
6
18
|
end
|
@@ -34,9 +34,14 @@ module Dugway
|
|
34
34
|
return source.send(method_or_key)
|
35
35
|
elsif source.respond_to?('has_key?') && source.has_key?(method_or_key)
|
36
36
|
return source[method_or_key]
|
37
|
-
elsif source.is_a?(Array) && source.first
|
37
|
+
elsif source.is_a?(Array) && source.first
|
38
|
+
# Handle both hash items and drop items in array
|
38
39
|
for item in source
|
39
|
-
|
40
|
+
if item.is_a?(Hash) && item['permalink'] == method_or_key.to_s
|
41
|
+
return item
|
42
|
+
elsif item.respond_to?(:source) && item.source.is_a?(Hash) && item.source['permalink'] == method_or_key.to_s
|
43
|
+
return item
|
44
|
+
end
|
40
45
|
end
|
41
46
|
end
|
42
47
|
|
@@ -47,6 +52,11 @@ module Dugway
|
|
47
52
|
before_method(method.to_s)
|
48
53
|
end
|
49
54
|
|
55
|
+
# Liquid 5.x compatibility: implement liquid_method_missing for property access
|
56
|
+
def liquid_method_missing(method_name)
|
57
|
+
before_method(method_name.to_s)
|
58
|
+
end
|
59
|
+
|
50
60
|
def errors
|
51
61
|
@context['errors']
|
52
62
|
end
|
@@ -54,6 +64,21 @@ module Dugway
|
|
54
64
|
def error(msg)
|
55
65
|
errors << msg
|
56
66
|
end
|
67
|
+
|
68
|
+
# Liquid 5.x compatibility: make drops JSON-serializable
|
69
|
+
# Only return source for JSON serialization, not for filter processing
|
70
|
+
def to_liquid
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def as_json(*args)
|
75
|
+
if source.is_a?(Hash)
|
76
|
+
# Convert Ruby hash to proper JSON-compatible format
|
77
|
+
source.stringify_keys
|
78
|
+
else
|
79
|
+
{}
|
80
|
+
end
|
81
|
+
end
|
57
82
|
end
|
58
83
|
end
|
59
84
|
end
|
@@ -1,6 +1,18 @@
|
|
1
1
|
module Dugway
|
2
2
|
module Drops
|
3
3
|
class CategoriesDrop < BaseDrop
|
4
|
+
# Liquid 5.x compatibility: implement liquid_method_missing for method access
|
5
|
+
def liquid_method_missing(method)
|
6
|
+
# Try to find category by permalink
|
7
|
+
result = before_method(method.to_s)
|
8
|
+
# Wrap hash results in CategoryDrop if it looks like a category
|
9
|
+
if result.is_a?(Hash) && result['permalink']
|
10
|
+
CategoryDrop.new(result)
|
11
|
+
else
|
12
|
+
result
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
4
16
|
def all
|
5
17
|
@all ||= source
|
6
18
|
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Dugway
|
4
|
+
module Drops
|
5
|
+
# Provides access to specific account feature flags within Liquid templates,
|
6
|
+
# mirroring the behavior of the storefront's FeaturesDrop but reading
|
7
|
+
# configuration from the Dugway store configuration (e.g., dugway.json).
|
8
|
+
#
|
9
|
+
# Feature flags can have a default status (enabled or disabled). This status,
|
10
|
+
# along with the list of exposed features, opt-ins, and opt-outs, should be
|
11
|
+
# defined under a top-level "features" key in the store configuration file,
|
12
|
+
# making it a peer to "store" and "customization".
|
13
|
+
#
|
14
|
+
# Example structure expected within the store configuration (e.g., dugway.json):
|
15
|
+
# {
|
16
|
+
# "store": { ... },
|
17
|
+
# "customization": { ... },
|
18
|
+
# "features": {
|
19
|
+
# "definitions": {
|
20
|
+
# "theme_bnpl_messaging": "enabled_by_default",
|
21
|
+
# "theme_category_collages": "disabled_by_default"
|
22
|
+
# },
|
23
|
+
# "opt_ins": ["some_opted_in_feature"],
|
24
|
+
# "opt_outs": ["some_opted_out_feature"]
|
25
|
+
# }
|
26
|
+
# }
|
27
|
+
# The `FeaturesDrop` receives the top-level `features` hash as its `source`
|
28
|
+
# via the ThemeDrop.
|
29
|
+
#
|
30
|
+
# Usage in Liquid:
|
31
|
+
# {% if features.has_theme_bnpl_messaging %} ... {% endif %}
|
32
|
+
# Note: Liquid might require the trailing '?' depending on context,
|
33
|
+
# but this implementation handles calls without it.
|
34
|
+
#
|
35
|
+
class FeaturesDrop < BaseDrop
|
36
|
+
# Memoized hash of feature definitions (name => default_status_string)
|
37
|
+
# read from the source (store configuration data).
|
38
|
+
# @return [Hash<String, String>]
|
39
|
+
def feature_definitions
|
40
|
+
@feature_definitions ||= source&.fetch('definitions', {}) || {}
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns a memoized list of features this account has explicitly opted *into*,
|
44
|
+
# filtered to only include features defined in the definitions.
|
45
|
+
# @return [Array<String>] List of opted-in feature names.
|
46
|
+
def opt_ins
|
47
|
+
return @opt_ins if defined?(@opt_ins)
|
48
|
+
|
49
|
+
source_opt_ins = source&.fetch('opt_ins', []) || []
|
50
|
+
# Ensure source data is treated as a Set for efficient intersection
|
51
|
+
source_set = source_opt_ins.respond_to?(:to_set) ? source_opt_ins.to_set : Set.new(Array(source_opt_ins))
|
52
|
+
|
53
|
+
# Only keep opt-ins that correspond to defined features
|
54
|
+
@opt_ins = (Set.new(feature_definitions.keys) & source_set).to_a
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns a memoized list of features this account has explicitly opted *out of*,
|
58
|
+
# filtered to only include features defined in the definitions.
|
59
|
+
# @return [Array<String>] List of opted-out feature names.
|
60
|
+
def opt_outs
|
61
|
+
return @opt_outs if defined?(@opt_outs)
|
62
|
+
|
63
|
+
source_opt_outs = source&.fetch('opt_outs', []) || []
|
64
|
+
# Ensure source data is treated as a Set for efficient intersection
|
65
|
+
source_set = source_opt_outs.respond_to?(:to_set) ? source_opt_outs.to_set : Set.new(Array(source_opt_outs))
|
66
|
+
|
67
|
+
# Only keep opt-outs that correspond to defined features
|
68
|
+
@opt_outs = (Set.new(feature_definitions.keys) & source_set).to_a
|
69
|
+
end
|
70
|
+
|
71
|
+
# Override BaseDrop's `before_method` to intercept `has_...` calls directly.
|
72
|
+
# This is called by BaseDrop's `method_missing` and allows us to intercept
|
73
|
+
# `has_...` calls before BaseDrop tries to resolve them against the source hash.
|
74
|
+
def before_method(method_or_key)
|
75
|
+
method_str = method_or_key.to_s
|
76
|
+
|
77
|
+
if method_str.start_with?('has_')
|
78
|
+
# Extract feature name, removing 'has_' prefix and optional trailing '?'
|
79
|
+
# to handle calls like `features.has_feature_name` or `features.has_feature_name?`
|
80
|
+
feature_name = method_str.sub(/^has_/, '').chomp('?')
|
81
|
+
|
82
|
+
# Check if this feature is defined
|
83
|
+
# If the extracted name doesn't match a defined feature, it's not available.
|
84
|
+
unless feature_definitions.key?(feature_name)
|
85
|
+
return false
|
86
|
+
end
|
87
|
+
|
88
|
+
# If defined, calculate availability based on defaults and overrides.
|
89
|
+
is_feature_available?(feature_name)
|
90
|
+
else
|
91
|
+
# If it's not a 'has_' method, delegate to BaseDrop's original logic.
|
92
|
+
super(method_or_key)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Keep liquid_method_missing for compatibility or explicit Liquid contexts,
|
97
|
+
# We keep liquid_method_missing defined as it's the official Liquid hook,
|
98
|
+
# although our `before_method` override likely handles most cases in Liquid 3.
|
99
|
+
# This provides potential compatibility if Liquid internals change or call this directly.
|
100
|
+
def liquid_method_missing(method_name)
|
101
|
+
method_str = method_name.to_s
|
102
|
+
|
103
|
+
if method_str.start_with?('has_')
|
104
|
+
feature_name = method_str.sub(/^has_/, '').chomp('?')
|
105
|
+
# Check definition and calculate availability, returning false if undefined.
|
106
|
+
feature_definitions.key?(feature_name) ? is_feature_available?(feature_name) : false
|
107
|
+
else
|
108
|
+
# Fallback for non-'has_' methods.
|
109
|
+
super
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
# Determines if a specific feature is available for the account based on its
|
115
|
+
# default status (from store customization) and the account's explicit opt-ins/outs
|
116
|
+
# (also from store customization).
|
117
|
+
#
|
118
|
+
# @param feature_name [String] The name of the feature to check.
|
119
|
+
# @return [Boolean] True if the feature is available, false otherwise.
|
120
|
+
def is_feature_available?(feature_name)
|
121
|
+
default_behavior_str = feature_definitions[feature_name]
|
122
|
+
# Should not happen if called via before_method/liquid_method_missing,
|
123
|
+
# but return false if the definition was somehow missing.
|
124
|
+
return false unless default_behavior_str
|
125
|
+
|
126
|
+
is_opted_in = opt_ins.include?(feature_name)
|
127
|
+
is_opted_out = opt_outs.include?(feature_name)
|
128
|
+
|
129
|
+
# Determine final availability:
|
130
|
+
# - If explicitly opted in, it's available.
|
131
|
+
# - If explicitly opted out, it's unavailable.
|
132
|
+
# - Otherwise, availability depends on the defined default behavior string.
|
133
|
+
# Precedence: Opt-out > Opt-in > Default
|
134
|
+
if is_opted_out
|
135
|
+
false
|
136
|
+
elsif is_opted_in
|
137
|
+
true
|
138
|
+
else
|
139
|
+
default_behavior_str == 'enabled_by_default'
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -1,12 +1,40 @@
|
|
1
1
|
module Dugway
|
2
2
|
module Drops
|
3
3
|
class PagesDrop < BaseDrop
|
4
|
+
# Liquid 5.x compatibility: implement liquid_method_missing for method access
|
5
|
+
def liquid_method_missing(method)
|
6
|
+
# Try to find page by permalink
|
7
|
+
result = before_method(method.to_s)
|
8
|
+
# Wrap hash results in PageDrop if it looks like a page
|
9
|
+
if result.is_a?(Hash) && result['permalink']
|
10
|
+
PageDrop.new(result)
|
11
|
+
else
|
12
|
+
result
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
4
16
|
def all
|
5
|
-
@all ||= source.select { |page| page['category'] == 'custom' }
|
17
|
+
@all ||= source.select { |page| (page.source['category'] == 'custom' || page.source['category'] == 'external') && !page.source['required'] }
|
6
18
|
end
|
7
19
|
|
8
20
|
def cart
|
9
|
-
@cart ||= source.find { |page| page['permalink'] == 'cart' }
|
21
|
+
@cart ||= source.find { |page| page.source['permalink'] == 'cart' }
|
22
|
+
end
|
23
|
+
|
24
|
+
def subscribe_page
|
25
|
+
@subscribe_page ||= store.subscribe_url ? source.find { |page| page.source['url'] == store.subscribe_url } : nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def custom_pages
|
29
|
+
@custom_pages ||= source.select { |page| page.source['category'] == 'custom' && !page.source['required'] }
|
30
|
+
end
|
31
|
+
|
32
|
+
def external_pages
|
33
|
+
@external_pages ||= source.select { |page| page.source['category'] == 'external' && !page.source['required'] }
|
34
|
+
end
|
35
|
+
|
36
|
+
def required_pages
|
37
|
+
@required_pages ||= source.select { |page| page.source['required'] == true }
|
10
38
|
end
|
11
39
|
end
|
12
40
|
end
|
@@ -1,6 +1,11 @@
|
|
1
1
|
module Dugway
|
2
2
|
module Drops
|
3
3
|
class ProductDrop < BaseDrop
|
4
|
+
|
5
|
+
# Liquid 5.x compatibility: ensure proper string representation
|
6
|
+
def to_s
|
7
|
+
name || "Product ##{id}"
|
8
|
+
end
|
4
9
|
def created_at
|
5
10
|
Time.parse(source['created_at'])
|
6
11
|
end
|
@@ -112,6 +117,12 @@ module Dugway
|
|
112
117
|
end
|
113
118
|
end
|
114
119
|
|
120
|
+
# Used to simulate price suffix behavior
|
121
|
+
# In prod this is a per-product setting but for dugway it needs to be global
|
122
|
+
def price_suffix
|
123
|
+
store.price_suffix
|
124
|
+
end
|
125
|
+
|
115
126
|
private
|
116
127
|
|
117
128
|
def price_min_max
|
@@ -3,6 +3,45 @@ require 'will_paginate/array'
|
|
3
3
|
module Dugway
|
4
4
|
module Drops
|
5
5
|
class ProductsDrop < BaseDrop
|
6
|
+
|
7
|
+
# Liquid 5.x compatibility: implement liquid_method_missing for method access
|
8
|
+
def liquid_method_missing(method)
|
9
|
+
method_str = method.to_s
|
10
|
+
|
11
|
+
# Handle known methods
|
12
|
+
case method_str
|
13
|
+
when 'all'
|
14
|
+
all
|
15
|
+
when 'current'
|
16
|
+
current
|
17
|
+
when 'on_sale'
|
18
|
+
on_sale
|
19
|
+
else
|
20
|
+
# Try to find product by permalink
|
21
|
+
result = before_method(method_str)
|
22
|
+
# Wrap hash results in ProductDrop if it looks like a product
|
23
|
+
if result.is_a?(Hash) && result['permalink']
|
24
|
+
ProductDrop.new(result)
|
25
|
+
else
|
26
|
+
result
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Also ensure before_method works as fallback
|
32
|
+
def before_method(method_or_key)
|
33
|
+
case method_or_key.to_s
|
34
|
+
when 'all'
|
35
|
+
all
|
36
|
+
when 'current'
|
37
|
+
current
|
38
|
+
when 'on_sale'
|
39
|
+
on_sale
|
40
|
+
else
|
41
|
+
super
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
6
45
|
def all
|
7
46
|
sort_and_paginate source
|
8
47
|
end
|
@@ -1,13 +1,43 @@
|
|
1
1
|
module Dugway
|
2
2
|
module Drops
|
3
3
|
class ThemeDrop < BaseDrop
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
# @param customization [Hash] Theme customization values.
|
5
|
+
# @param definitions [Hash] Theme setting definitions (from settings.json).
|
6
|
+
def initialize(customization, definitions = {})
|
7
|
+
super(customization)
|
8
|
+
@definitions = definitions || {}
|
9
|
+
end
|
9
10
|
|
10
|
-
|
11
|
+
# Liquid 5.x compatibility: implement liquid_method_missing for property access
|
12
|
+
def liquid_method_missing(method)
|
13
|
+
method_str = method.to_s
|
14
|
+
method_sym = method.to_sym
|
15
|
+
|
16
|
+
# Try different key formats - source might have string or symbol keys
|
17
|
+
# Use has_key? to handle false/nil values properly
|
18
|
+
if source.has_key?(method_str)
|
19
|
+
value = source[method_str]
|
20
|
+
elsif source.has_key?(method_sym)
|
21
|
+
value = source[method_sym]
|
22
|
+
elsif source.has_key?(method_str.to_sym)
|
23
|
+
value = source[method_str.to_sym]
|
24
|
+
elsif source.has_key?(method_sym.to_s)
|
25
|
+
value = source[method_sym.to_s]
|
26
|
+
else
|
27
|
+
value = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
# Check for image settings - need to wrap in ImageDrop
|
31
|
+
if !value.nil? && settings_images.find { |image| image['variable'] == method_str }
|
32
|
+
return ImageDrop.new(value.is_a?(Hash) ? value.stringify_keys : value)
|
33
|
+
end
|
34
|
+
|
35
|
+
value
|
36
|
+
end
|
37
|
+
|
38
|
+
def before_method(method_or_key)
|
39
|
+
# Fallback for older Liquid versions
|
40
|
+
liquid_method_missing(method_or_key)
|
11
41
|
end
|
12
42
|
|
13
43
|
# Newer API for theme images.
|
@@ -20,6 +50,22 @@ module Dugway
|
|
20
50
|
Drops::ThemeImageSetsDrop.new(source)
|
21
51
|
end
|
22
52
|
|
53
|
+
def features
|
54
|
+
# Fetch features config from the top-level Dugway options hash,
|
55
|
+
# which should contain the parsed store configuration (e.g., dugway.json).
|
56
|
+
# This allows 'features' to be a peer to 'store' and 'customization'.
|
57
|
+
feature_data = Dugway.options&.fetch('features', {}) || {}
|
58
|
+
Drops::FeaturesDrop.new(feature_data)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Provides access to theme translations via theme.translations
|
62
|
+
def translations
|
63
|
+
# Instantiate and return the TranslationsDrop, passing settings.
|
64
|
+
# Memoize the instance for efficiency within a single render cycle.
|
65
|
+
# Pass both customization (source) and definitions to TranslationsDrop.
|
66
|
+
@translations_drop ||= Drops::TranslationsDrop.new(source, @definitions)
|
67
|
+
end
|
68
|
+
|
23
69
|
private
|
24
70
|
def settings_images
|
25
71
|
@settings_images ||= settings.has_key?('images') ? settings['images'] : []
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'i18n'
|
2
|
+
|
3
|
+
module Dugway
|
4
|
+
module Drops
|
5
|
+
class TranslationsDrop < BaseDrop
|
6
|
+
class MissingTranslationError < StandardError; end
|
7
|
+
|
8
|
+
# @param settings [Hash] Theme customization values (resolved settings).
|
9
|
+
# @param definitions [Hash] Raw theme setting definitions (from settings.json).
|
10
|
+
def initialize(settings, definitions = {})
|
11
|
+
@settings = settings || {}
|
12
|
+
@definitions = definitions || {} # Store definitions
|
13
|
+
# Read settings using both symbol and string keys for flexibility.
|
14
|
+
@translation_mode = (@settings[:translation_mode] || @settings['translation_mode'])&.to_s
|
15
|
+
# Use explicit theme setting for locale if present, otherwise default to current I18n.locale
|
16
|
+
@translation_locale = (@settings[:translation_locale] || @settings['translation_locale'])&.to_s.presence || I18n.locale.to_s
|
17
|
+
super()
|
18
|
+
end
|
19
|
+
|
20
|
+
# Liquid access: `{{ translations['key.name'] }}`
|
21
|
+
# Returns the translation string (which could be an empty string),
|
22
|
+
# or a "[MISSING TRANSLATION: ...]" string if the key is not found or its value is nil.
|
23
|
+
#
|
24
|
+
# @param key [String, Symbol] The translation key.
|
25
|
+
# @return [String, nil] The translation or missing translation indicator.
|
26
|
+
def [](key)
|
27
|
+
key_str = key.to_s
|
28
|
+
translation = find_translation(key_str)
|
29
|
+
|
30
|
+
if translation.nil?
|
31
|
+
# Key is missing, or explicitly nil in manual mode - treat both as missing.
|
32
|
+
log_missing_translation(key_str)
|
33
|
+
return missing_translation_message(key_str)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Key found, return its value.
|
37
|
+
translation
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def manual_mode?
|
43
|
+
@translation_mode == 'manual'
|
44
|
+
end
|
45
|
+
|
46
|
+
# Looks up translation using I18n.
|
47
|
+
# Returns nil if the key is not found or the locale is invalid.
|
48
|
+
def lookup_automatic_translation(key)
|
49
|
+
locale_to_use = @translation_locale
|
50
|
+
# `default: nil` prevents I18n from raising an error on missing keys.
|
51
|
+
I18n.t("storefront.#{key}", locale: locale_to_use, default: nil)
|
52
|
+
rescue I18n::InvalidLocale
|
53
|
+
nil # Treat invalid locale as a missing translation.
|
54
|
+
end
|
55
|
+
|
56
|
+
# Converts a canonical key ('a.b.c') to the theme setting key format (:a_b_c_tr_text).
|
57
|
+
def derive_manual_setting_key(key)
|
58
|
+
"#{key.tr('.', '_')}_tr_text".to_sym
|
59
|
+
end
|
60
|
+
|
61
|
+
# Finds the translation based on the current mode.
|
62
|
+
# In manual mode, respects the 'allow_blank' setting definition.
|
63
|
+
# Returns the translation string, or nil if not found or disallowed blank.
|
64
|
+
def find_translation(key_str)
|
65
|
+
if manual_mode?
|
66
|
+
manual_setting_key_sym = derive_manual_setting_key(key_str)
|
67
|
+
manual_setting_key_str = manual_setting_key_sym.to_s
|
68
|
+
custom_value = nil
|
69
|
+
setting_exists = false
|
70
|
+
|
71
|
+
# Check both symbol and string keys for manual overrides.
|
72
|
+
if @settings.key?(manual_setting_key_sym)
|
73
|
+
custom_value = @settings[manual_setting_key_sym]
|
74
|
+
setting_exists = true
|
75
|
+
elsif @settings.key?(manual_setting_key_str)
|
76
|
+
custom_value = @settings[manual_setting_key_str]
|
77
|
+
setting_exists = true
|
78
|
+
end
|
79
|
+
|
80
|
+
if setting_exists
|
81
|
+
# Find the corresponding definition to check allow_blank
|
82
|
+
# Assuming manual translations are defined similarly to 'options'
|
83
|
+
definition = find_setting_definition(manual_setting_key_str) || {}
|
84
|
+
allow_blank = definition['allow_blank'].to_s == 'true'
|
85
|
+
|
86
|
+
# Return custom value if it's present, or if it's blank but allowed
|
87
|
+
return custom_value if custom_value.present? || (allow_blank && custom_value == '')
|
88
|
+
|
89
|
+
# If we reach here, the value is blank but blank is not allowed.
|
90
|
+
# Fall through to automatic lookup.
|
91
|
+
end
|
92
|
+
|
93
|
+
# If setting key didn't exist, fall through to automatic lookup.
|
94
|
+
|
95
|
+
end # End manual_mode? check
|
96
|
+
|
97
|
+
# Automatic mode or fallback from manual mode
|
98
|
+
lookup_automatic_translation(key_str)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Helper to find a setting definition by its variable name across common sections.
|
102
|
+
def find_setting_definition(variable_name)
|
103
|
+
# Look primarily in 'options' as manual translations often mimic them.
|
104
|
+
# Add other potential sections if needed in the future.
|
105
|
+
(@definitions['options'] || []).find { |d| d['variable'] == variable_name }
|
106
|
+
end
|
107
|
+
|
108
|
+
# Logs a warning message for a missing translation.
|
109
|
+
def log_missing_translation(key_str)
|
110
|
+
mode = manual_mode? ? 'manual' : 'automatic'
|
111
|
+
locale_info = manual_mode? ? '' : " locale='#{@translation_locale}'"
|
112
|
+
message = "Missing translation: key='#{key_str}'#{locale_info} mode='#{mode}'"
|
113
|
+
Dugway.logger.warn(message)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns the standard string indicating a missing translation.
|
117
|
+
def missing_translation_message(key_str)
|
118
|
+
"[MISSING TRANSLATION: #{key_str}]"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|