dugway 1.0.14 → 1.2.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.bundle/config +2 -0
  3. data/.github/workflows/main.yml +1 -1
  4. data/.gitignore +1 -0
  5. data/README.md +9 -0
  6. data/lib/dugway/application.rb +5 -3
  7. data/lib/dugway/assets/big_cartel_logo.svg +4 -0
  8. data/lib/dugway/cli/build.rb +18 -1
  9. data/lib/dugway/cli/server.rb +2 -2
  10. data/lib/dugway/cli/templates/source/settings.json +8 -0
  11. data/lib/dugway/cli/validate.rb +20 -2
  12. data/lib/dugway/controller.rb +5 -1
  13. data/lib/dugway/liquid/drops/account_drop.rb +4 -0
  14. data/lib/dugway/liquid/drops/features_drop.rb +144 -0
  15. data/lib/dugway/liquid/drops/product_drop.rb +8 -0
  16. data/lib/dugway/liquid/drops/products_drop.rb +1 -1
  17. data/lib/dugway/liquid/drops/related_products_drop.rb +88 -0
  18. data/lib/dugway/liquid/drops/theme_drop.rb +23 -0
  19. data/lib/dugway/liquid/drops/translations_drop.rb +122 -0
  20. data/lib/dugway/liquifier.rb +44 -8
  21. data/lib/dugway/store.rb +7 -2
  22. data/lib/dugway/theme.rb +169 -3
  23. data/lib/dugway/version.rb +1 -1
  24. data/lib/dugway.rb +31 -1
  25. data/locales/storefront.de.yml +79 -0
  26. data/locales/storefront.en-CA.yml +79 -0
  27. data/locales/storefront.en-GB.yml +79 -0
  28. data/locales/storefront.en-US.yml +79 -0
  29. data/locales/storefront.es-ES.yml +79 -0
  30. data/locales/storefront.es-MX.yml +79 -0
  31. data/locales/storefront.fr-CA.yml +79 -0
  32. data/locales/storefront.fr-FR.yml +79 -0
  33. data/locales/storefront.id.yml +79 -0
  34. data/locales/storefront.it.yml +79 -0
  35. data/locales/storefront.ja.yml +79 -0
  36. data/locales/storefront.ko.yml +79 -0
  37. data/locales/storefront.nl.yml +79 -0
  38. data/locales/storefront.pl.yml +79 -0
  39. data/locales/storefront.pt-BR.yml +79 -0
  40. data/locales/storefront.pt-PT.yml +79 -0
  41. data/locales/storefront.ro.yml +79 -0
  42. data/locales/storefront.sv.yml +79 -0
  43. data/locales/storefront.tr.yml +79 -0
  44. data/locales/storefront.zh-CN.yml +79 -0
  45. data/locales/storefront.zh-TW.yml +79 -0
  46. data/log/dugway.log +1 -0
  47. data/spec/features/page_rendering_spec.rb +4 -4
  48. data/spec/fixtures/theme/layout.html +2 -0
  49. data/spec/fixtures/theme/settings.json +6 -0
  50. data/spec/spec_helper.rb +4 -0
  51. data/spec/units/dugway/liquid/drops/features_drop_spec.rb +182 -0
  52. data/spec/units/dugway/liquid/drops/product_drop_spec.rb +36 -0
  53. data/spec/units/dugway/liquid/drops/related_products_drop_spec.rb +80 -0
  54. data/spec/units/dugway/liquid/drops/theme_drop_spec.rb +45 -0
  55. data/spec/units/dugway/liquid/drops/translations_drop_spec.rb +292 -0
  56. data/spec/units/dugway/store_spec.rb +37 -0
  57. data/spec/units/dugway/theme_spec.rb +456 -0
  58. metadata +35 -2
@@ -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
@@ -104,6 +104,14 @@ module Dugway
104
104
  end
105
105
  end
106
106
 
107
+ def related_products
108
+ @related_products ||= begin
109
+ drop = RelatedProductsDrop.new(source)
110
+ drop.context = @context if @context
111
+ drop.products
112
+ end
113
+ end
114
+
107
115
  private
108
116
 
109
117
  def price_min_max
@@ -33,7 +33,7 @@ module Dugway
33
33
  when 'newest', 'date'
34
34
  'date'
35
35
  # We don't pass these in the API, so fake it
36
- when 'sales', 'sells', 'views'
36
+ when 'sales', 'sells', 'top-selling', 'views'
37
37
  'shuffle'
38
38
  else
39
39
  'position'
@@ -0,0 +1,88 @@
1
+ module Dugway
2
+ module Drops
3
+ class RelatedProductsDrop < BaseDrop
4
+ def initialize(product)
5
+ super()
6
+ @product = product
7
+ end
8
+
9
+ def products
10
+ fetch_related_products
11
+ end
12
+
13
+ private
14
+
15
+ def settings
16
+ @settings ||= theme.customization
17
+ end
18
+
19
+ def limit
20
+ @limit ||= begin
21
+ if settings
22
+ limit = settings['similar_products'] ||
23
+ settings['related_items'] ||
24
+ settings['related_products'] ||
25
+ settings['number_related_products'] ||
26
+ 4
27
+ limit
28
+ else
29
+ 4
30
+ end
31
+ end
32
+ end
33
+
34
+ def sort_order
35
+ @sort_order ||= begin
36
+ if settings
37
+ order = settings['related_products_order'] ||
38
+ settings['similar_products_order'] ||
39
+ "position"
40
+ order
41
+ else
42
+ "position"
43
+ end
44
+ end
45
+ end
46
+
47
+ def fetch_related_products
48
+ return [] unless @product
49
+
50
+ category_products = sort_products(fetch_category_products).take(limit)
51
+ return category_products if category_products.size >= limit
52
+
53
+ remaining_limit = limit - category_products.size
54
+ fallback_products = sort_products(fetch_fallback_products(category_products, remaining_limit)).take(remaining_limit)
55
+
56
+ category_products + fallback_products
57
+ end
58
+
59
+ def fetch_category_products
60
+ # Filter Dugway's product data to match the categories of the current product
61
+ Dugway.store.products.select do |product|
62
+ product_cats = product['category_ids'] || []
63
+ current_cats = @product['category_ids'] || []
64
+ (product_cats & current_cats).any? && product['id'] != @product['id']
65
+ end.map { |p| ProductDrop.new(p) }
66
+ end
67
+
68
+ def fetch_fallback_products(category_products, limit)
69
+ # Get additional products excluding already included ones
70
+ excluded_ids = category_products.map { |p| p['id'] } + [@product['id']]
71
+ Dugway.store.products
72
+ .reject { |product| excluded_ids.include?(product['id']) }
73
+ .map { |p| ProductDrop.new(p) }
74
+ end
75
+
76
+ def sort_products(products)
77
+ case sort_order
78
+ when 'date', 'newest'
79
+ products.sort { |a,b| b.source['id'] <=> a.source['id'] }
80
+ when 'sales', 'sells', 'top-selling', 'views'
81
+ products.shuffle
82
+ else
83
+ products.sort_by { |p| p.source['position'] }
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -1,6 +1,13 @@
1
1
  module Dugway
2
2
  module Drops
3
3
  class ThemeDrop < BaseDrop
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
10
+
4
11
  def before_method(method_or_key)
5
12
  # We should try to get away from this api and use the newer one below
6
13
  if source.respond_to?('has_key?') && source.has_key?(method_or_key) && settings_images.find { |image| image['variable'] == method_or_key.to_s }
@@ -20,6 +27,22 @@ module Dugway
20
27
  Drops::ThemeImageSetsDrop.new(source)
21
28
  end
22
29
 
30
+ def features
31
+ # Fetch features config from the top-level Dugway options hash,
32
+ # which should contain the parsed store configuration (e.g., dugway.json).
33
+ # This allows 'features' to be a peer to 'store' and 'customization'.
34
+ feature_data = Dugway.options&.fetch('features', {}) || {}
35
+ Drops::FeaturesDrop.new(feature_data)
36
+ end
37
+
38
+ # Provides access to theme translations via theme.translations
39
+ def translations
40
+ # Instantiate and return the TranslationsDrop, passing settings.
41
+ # Memoize the instance for efficiency within a single render cycle.
42
+ # Pass both customization (source) and definitions to TranslationsDrop.
43
+ @translations_drop ||= Drops::TranslationsDrop.new(source, @definitions)
44
+ end
45
+
23
46
  private
24
47
  def settings_images
25
48
  @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
@@ -16,6 +16,7 @@ module Dugway
16
16
  class Liquifier
17
17
  def initialize(request)
18
18
  @request = request
19
+ load_theme_locales
19
20
  end
20
21
 
21
22
  def render(content, variables={})
@@ -41,7 +42,7 @@ module Dugway
41
42
 
42
43
  def self.render_styles(css)
43
44
  Liquid::Template.parse(css).render!(
44
- { 'theme' => Drops::ThemeDrop.new(Dugway.theme.customization) },
45
+ { 'theme' => Drops::ThemeDrop.new(Dugway.theme.customization, Dugway.theme.settings) },
45
46
  :registers => { :settings => Dugway.theme.settings }
46
47
  )
47
48
  end
@@ -68,7 +69,8 @@ module Dugway
68
69
  {
69
70
  'store' => Drops::AccountDrop.new(store.account),
70
71
  'cart' => Drops::CartDrop.new(cart),
71
- 'theme' => Drops::ThemeDrop.new(theme.customization),
72
+ # Pass both customization and definitions to ThemeDrop
73
+ 'theme' => Drops::ThemeDrop.new(theme.customization, theme.settings),
72
74
  'pages' => Drops::PagesDrop.new(store.pages.map { |p| Drops::PageDrop.new(p) }),
73
75
  'categories' => Drops::CategoriesDrop.new(store.categories.map { |c| Drops::CategoryDrop.new(c) }),
74
76
  'artists' => Drops::ArtistsDrop.new(store.artists.map { |a| Drops::ArtistDrop.new(a) }),
@@ -77,6 +79,10 @@ module Dugway
77
79
  'head_content' => [window_bigcartel_script, head_content].join,
78
80
  'bigcartel_credit' => bigcartel_credit,
79
81
  'powered_by_big_cartel' => powered_by_big_cartel,
82
+ 'big_cartel_credit_logo' => big_cartel_credit_logo,
83
+ # Pass both customization and definitions to TranslationsDrop
84
+ 'translations' => Drops::TranslationsDrop.new(theme.customization, theme.settings),
85
+ 't' => Drops::TranslationsDrop.new(theme.customization, theme.settings),
80
86
  }
81
87
  end
82
88
 
@@ -109,17 +115,47 @@ module Dugway
109
115
  "<script>#{script}</script>"
110
116
  end
111
117
 
118
+ def read_svg_asset(filename)
119
+ File.read(File.join(File.dirname(__FILE__), 'assets', filename))
120
+ end
121
+
112
122
  def bigcartel_credit
113
123
  '<a href="http://bigcartel.com/" title="Start your own store at Big Cartel now">Online Store by Big Cartel</a>'
114
124
  end
115
125
 
116
126
  def powered_by_big_cartel
117
- %(<a class="bigcartel-credit" href="https://www.bigcartel.com/?utm_source=bigcartel&utm_medium=storefront&utm_campaign=123" title="Powered by Big Cartel">
118
- <span class="bigcartel-credit__text" aria-hidden="true">Powered by</span>
119
- <svg aria-hidden="true" class="bigcartel-credit__lockup" xmlns="http://www.w3.org/2000/svg" viewBox="1.99 2.05 124.96 24.95">
120
- <path d="M46.18 4A1.91 1.91 0 1 1 50 4a1.91 1.91 0 1 1-3.81 0Zm78.76 14.35a.81.81 0 0 1-.25-.69V2.23h-4.52v2.68h1.58V18c0 2.14 1.11 3.28 3.2 3.28a6.56 6.56 0 0 0 2-.42v-2.78c-1.28.51-1.8.38-2.01.23Zm-75.27.05h1.43V21h-5.79v-2.64h1.43v-8h-1.61V7.7h4.54Zm-11.09-11a5.81 5.81 0 0 0-4.36 1.87V2.23H29.7v2.68h1.62v12.71a.81.81 0 0 1-.25.69c-.43.33-1.5 0-2.06-.23v2.76a6.59 6.59 0 0 0 2.05.42 2.92 2.92 0 0 0 2.74-1.32 6.86 6.86 0 0 0 4.27 1.34 6.66 6.66 0 0 0 6.86-7c-.01-4.06-2.68-6.97-6.35-6.97ZM38 18.57c-2.15 0-3.72-1.75-3.72-4.17s1.55-4.32 3.78-4.32a3.75 3.75 0 0 1 3.75 4.1c.02 2.55-1.58 4.39-3.81 4.39Zm68.86-.49v2.76a7.52 7.52 0 0 1-2 .42c-2.09 0-3.2-1.14-3.2-3.28V5.36l2.93-1.92V7.7h3.81l-1.91 2.68h-1.9v7.24a.77.77 0 0 0 .26.69c.53.4 2.03-.23 2.03-.23ZM58 7.31c-3.88 0-6.69 3-6.69 7.11s2.66 6.87 6.33 6.87A6.14 6.14 0 0 0 62 19.45v1.42c0 2.72-2.4 3.45-3.83 3.45a5.22 5.22 0 0 1-3.12-1.06l-2.36 1.83A7.78 7.78 0 0 0 58 27c3.21 0 6.95-1.63 6.95-6.21v-6.38A6.84 6.84 0 0 0 58 7.31Zm.12 11.29c-2.19 0-3.72-1.74-3.72-4.23s1.6-4.21 3.8-4.21a3.94 3.94 0 0 1 3.8 4.21 4 4 0 0 1-3.85 4.23ZM120.6 15c0-5.05-3.45-7.69-6.85-7.69a6.76 6.76 0 0 0-6.58 7.06 7.13 7.13 0 0 0 12.69 4.39l-2.22-1.71a4.24 4.24 0 0 1-3.44 1.69 3.86 3.86 0 0 1-3.94-3.11h10.28a3.09 3.09 0 0 0 .06-.63Zm-10.35-2.08a3.65 3.65 0 0 1 3.56-3.11 3.77 3.77 0 0 1 3.77 3.11ZM94.92 10V7.7h-4v2.68h1.62v8h-2.35a.83.83 0 0 1-.61-.19.91.91 0 0 1-.19-.64v-5.77c0-1.31-.65-4.47-5.52-4.47a7.85 7.85 0 0 0-4.14 1.18l1.17 2.23a5 5 0 0 1 3-.78 3.26 3.26 0 0 1 1.76.49 2.08 2.08 0 0 1 .81 1.78v1.25a6.58 6.58 0 0 0-3.21-.92c-2.58 0-3.91 1.62-5.19 3.2s-2.51 3-5 2.92c-2.27-.11-3.63-1.86-3.63-4.43 0-2.39 1.45-4 3.54-4a3.75 3.75 0 0 1 3.7 3.18l2.45-1.9a6.3 6.3 0 0 0-6.3-4.18 6.72 6.72 0 0 0-6.48 7c0 3.43 2.1 7.16 6.62 7.16a7.45 7.45 0 0 0 5.87-2.73 4.38 4.38 0 0 0 4.08 2.57 5.91 5.91 0 0 0 3.93-1.66 2.87 2.87 0 0 0 2.8 1.33h7.42v-2.64h-1.61v-3.21c0-3.3 1.09-4.77 3.56-4.77a3.68 3.68 0 0 1 1.45.31V8a4.81 4.81 0 0 0-1.74-.25A4.21 4.21 0 0 0 94.92 10Zm-8.47 7.48a4.93 4.93 0 0 1-3.16 1.41 1.9 1.9 0 0 1-2.05-1.91 2 2 0 0 1 2.18-2 5 5 0 0 1 3 1.18ZM11 14.52v-.89a1.78 1.78 0 0 1 .83-1.51l7.35-4.7A1.79 1.79 0 0 0 20 5.91V2.05L11 7.8 2 2.05V14.2a8.69 8.69 0 0 0 3.88 7.58L11 25.05l5.12-3.27A8.69 8.69 0 0 0 20 14.2V8.77Z" class="a"></path>
121
- </svg>
122
- </a>)
127
+ build_powered_by_link(include_text: true)
128
+ end
129
+
130
+ def big_cartel_credit_logo
131
+ build_powered_by_link(include_text: false)
132
+ end
133
+
134
+ def build_powered_by_link(include_text: true)
135
+ svg_content = read_svg_asset('big_cartel_logo.svg')
136
+
137
+ text_span = include_text ? ' <span class="bigcartel-credit__text" aria-hidden="true">Powered by</span>' : ''
138
+ <<~HTML
139
+ <a class="bigcartel-credit" href="https://www.bigcartel.com/?utm_source=bigcartel&utm_medium=storefront&utm_campaign=123}" title="Powered by Big Cartel" data-bc-hook="attribution">
140
+ #{text_span}
141
+ #{svg_content}
142
+ </a>
143
+ HTML
144
+ end
145
+
146
+ # Load theme-specific override locales
147
+ def load_theme_locales
148
+ return unless Dugway.source_dir && Dir.exist?(File.dirname(Dugway.source_dir))
149
+
150
+ parent_dir = File.dirname(Dugway.source_dir)
151
+ theme_locales_dir = File.join(parent_dir, 'locales')
152
+
153
+ if Dir.exist?(theme_locales_dir)
154
+ theme_locales_path = File.expand_path(theme_locales_dir)
155
+ # Ensure theme locales are loaded *after* defaults to allow overrides
156
+ I18n.load_path |= Dir[File.join(theme_locales_path, 'storefront.*.yml')]
157
+ I18n.backend.reload!
158
+ end
123
159
  end
124
160
  end
125
161
  end
data/lib/dugway/store.rb CHANGED
@@ -8,8 +8,9 @@ module Dugway
8
8
  default_timeout 5
9
9
  headers 'User-Agent' => "Dugway #{ Dugway::VERSION }"
10
10
 
11
- def initialize(subdomain)
11
+ def initialize(subdomain, store_options = {})
12
12
  self.class.base_uri "https://api.bigcartel.com/#{ subdomain }"
13
+ @store_options = store_options || {}
13
14
  end
14
15
 
15
16
  def account
@@ -124,7 +125,11 @@ module Dugway
124
125
  end
125
126
 
126
127
  def locale
127
- currency['locale']
128
+ @store_options[:locale] || currency['locale']
129
+ end
130
+
131
+ def website
132
+ @store_options[:website] || account['website']
128
133
  end
129
134
 
130
135
  def instant_checkout?