spree_core 4.2.0.rc1 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/spree.js +16 -0
  3. data/app/controllers/spree/base_controller.rb +2 -3
  4. data/app/controllers/spree/errors_controller.rb +11 -0
  5. data/app/finders/spree/addresses/find.rb +1 -12
  6. data/app/finders/spree/base_finder.rb +14 -0
  7. data/app/finders/spree/countries/find.rb +11 -3
  8. data/app/finders/spree/credit_cards/find.rb +2 -2
  9. data/app/finders/spree/orders/find_current.rb +2 -2
  10. data/app/finders/spree/products/find.rb +14 -3
  11. data/app/helpers/spree/base_helper.rb +4 -9
  12. data/app/helpers/spree/currency_helper.rb +24 -0
  13. data/app/helpers/spree/locale_helper.rb +31 -0
  14. data/app/helpers/spree/products_helper.rb +37 -12
  15. data/app/mailers/spree/base_mailer.rb +4 -4
  16. data/app/mailers/spree/order_mailer.rb +3 -3
  17. data/app/mailers/spree/reimbursement_mailer.rb +1 -1
  18. data/app/mailers/spree/shipment_mailer.rb +1 -1
  19. data/app/models/concerns/spree/default_price.rb +1 -5
  20. data/app/models/concerns/spree/product_scopes.rb +1 -1
  21. data/app/models/concerns/spree/user_methods.rb +2 -2
  22. data/app/models/concerns/spree/user_payment_source.rb +1 -1
  23. data/app/models/spree/ability.rb +45 -34
  24. data/app/models/spree/address.rb +4 -0
  25. data/app/models/spree/adjustment.rb +1 -0
  26. data/app/models/spree/app_configuration.rb +2 -3
  27. data/app/models/spree/app_dependencies.rb +6 -2
  28. data/app/models/spree/base.rb +5 -0
  29. data/app/models/spree/credit_card.rb +4 -0
  30. data/app/models/spree/fulfilment_changer.rb +58 -16
  31. data/app/models/spree/image.rb +14 -14
  32. data/app/models/spree/inventory_unit.rb +2 -7
  33. data/app/models/spree/line_item.rb +7 -15
  34. data/app/models/spree/log_entry.rb +1 -1
  35. data/app/models/spree/option_type.rb +2 -0
  36. data/app/models/spree/order.rb +5 -4
  37. data/app/models/spree/order/address_book.rb +7 -20
  38. data/app/models/spree/payment.rb +18 -4
  39. data/app/models/spree/payment/processing.rb +2 -2
  40. data/app/models/spree/payment_method.rb +3 -3
  41. data/app/models/spree/price.rb +2 -7
  42. data/app/models/spree/product.rb +41 -17
  43. data/app/models/spree/promotion.rb +4 -15
  44. data/app/models/spree/promotion/rules/option_value.rb +1 -1
  45. data/app/models/spree/promotion/rules/product.rb +2 -1
  46. data/app/models/spree/promotion/rules/user.rb +2 -1
  47. data/app/models/spree/promotion_handler/coupon.rb +1 -2
  48. data/app/models/spree/refund.rb +2 -2
  49. data/app/models/spree/return_item/eligibility_validator/default.rb +0 -2
  50. data/app/models/spree/return_item/eligibility_validator/{r_m_a_required.rb → rma_required.rb} +0 -0
  51. data/app/models/spree/shipment.rb +1 -1
  52. data/app/models/spree/shipping_method.rb +1 -5
  53. data/app/models/spree/shipping_rate.rb +2 -11
  54. data/app/models/spree/stock/availability_validator.rb +3 -4
  55. data/app/models/spree/stock_item.rb +1 -5
  56. data/app/models/spree/store.rb +59 -1
  57. data/app/models/spree/store_credit.rb +1 -1
  58. data/app/models/spree/variant.rb +9 -16
  59. data/app/models/spree/zone.rb +17 -4
  60. data/app/paginators/spree/shared/paginate.rb +8 -1
  61. data/app/presenters/spree/variant_presenter.rb +2 -5
  62. data/app/services/spree/account/addresses/create.rb +6 -1
  63. data/app/services/spree/account/addresses/{base.rb → helper.rb} +1 -3
  64. data/app/services/spree/account/addresses/update.rb +6 -1
  65. data/app/services/spree/build_localized_redirect_url.rb +101 -0
  66. data/app/services/spree/cart/estimate_shipping_rates.rb +1 -1
  67. data/app/services/spree/compare_line_items.rb +4 -2
  68. data/app/sorters/spree/base_sorter.rb +35 -0
  69. data/app/sorters/spree/orders/sort.rb +1 -37
  70. data/app/sorters/spree/products/sort.rb +9 -32
  71. data/app/validators/email_validator.rb +1 -1
  72. data/app/views/spree/errors/forbidden.html.erb +0 -0
  73. data/app/views/spree/errors/unauthorized.html.erb +0 -0
  74. data/app/views/spree/shared/_base_mailer_stylesheets.html.erb +13 -2
  75. data/app/views/spree/shared/_purchased_items_table.html.erb +15 -6
  76. data/app/views/spree/shared/purchased_items_table/_adjustment.html.erb +2 -2
  77. data/app/views/spree/shared/purchased_items_table/_line_item.html.erb +2 -2
  78. data/config/initializers/inflections.rb +3 -0
  79. data/config/initializers/rails61_fixes.rb +3 -0
  80. data/config/locales/en.yml +36 -63
  81. data/config/routes.rb +2 -1
  82. data/db/default/spree/stores.rb +1 -0
  83. data/db/default/spree/zones.rb +4 -1
  84. data/db/migrate/20130326175857_add_stock_location_to_rma.rb +1 -1
  85. data/db/migrate/20191017121054_add_supported_currencies_to_store.rb +1 -0
  86. data/db/migrate/20201006110150_add_checkout_zone_field_to_store.rb +12 -0
  87. data/db/migrate/20201012091259_add_filterable_column_to_spree_option_types.rb +10 -0
  88. data/db/migrate/20201127084048_add_default_country_kind_to_spree_zones.rb +5 -0
  89. data/db/migrate/20210112193440_remove_contact_email_from_spree_stores.rb +5 -0
  90. data/db/migrate/20210114182625_create_spree_payment_methods_stores.rb +10 -0
  91. data/db/migrate/20210114220232_migrate_data_payment_methods_stores.rb +15 -0
  92. data/db/migrate/20210117112551_remove_store_id_from_spree_payment_methods.rb +5 -0
  93. data/db/migrate/20210120142527_ensure_default_locale_in_spree_stores.rb +5 -0
  94. data/db/migrate/20210205211040_add_supported_locales_to_spree_stores.rb +11 -0
  95. data/db/migrate/20210215202602_migrate_spree_i18n_globalize_config.rb +22 -0
  96. data/lib/generators/spree/install/install_generator.rb +9 -6
  97. data/lib/spree/core.rb +2 -1
  98. data/lib/spree/core/controller_helpers/auth.rb +3 -1
  99. data/lib/spree/core/controller_helpers/common.rb +6 -8
  100. data/lib/spree/core/controller_helpers/currency.rb +47 -0
  101. data/lib/spree/core/controller_helpers/locale.rb +58 -0
  102. data/lib/spree/core/controller_helpers/search.rb +1 -1
  103. data/lib/spree/core/controller_helpers/store.rb +4 -16
  104. data/lib/spree/core/importer/order.rb +9 -9
  105. data/lib/spree/core/product_filters.rb +3 -3
  106. data/lib/spree/core/version.rb +1 -1
  107. data/lib/spree/i18n.rb +17 -19
  108. data/lib/spree/permitted_attributes.rb +2 -2
  109. data/lib/spree/service_module.rb +8 -4
  110. data/lib/spree/testing_support/capybara_config.rb +1 -1
  111. data/lib/spree/testing_support/common_rake.rb +1 -1
  112. data/lib/spree/testing_support/controller_requests.rb +10 -10
  113. data/lib/spree/testing_support/factories/promotion_factory.rb +29 -17
  114. data/lib/spree/testing_support/factories/shipment_factory.rb +7 -9
  115. data/lib/spree/testing_support/factories/stock_location_factory.rb +2 -2
  116. data/lib/spree/testing_support/factories/store_factory.rb +1 -0
  117. data/lib/spree/testing_support/factories/zone_factory.rb +16 -13
  118. data/lib/spree/testing_support/flatpickr_capybara.rb +101 -0
  119. data/lib/spree/testing_support/i18n.rb +1 -1
  120. data/lib/spree/testing_support/locale_helpers.rb +71 -0
  121. data/lib/spree/testing_support/next_instance_of.rb +38 -0
  122. data/lib/spree/testing_support/order_walkthrough.rb +8 -3
  123. data/lib/spree/testing_support/rspec_retry_config.rb +10 -0
  124. data/spree_core.gemspec +6 -5
  125. metadata +74 -27
  126. data/lib/generators/spree/install/templates/config/initializers/spree_storefront.rb +0 -1
  127. data/lib/generators/spree/install/templates/config/spree_storefront.yml +0 -67
  128. data/lib/spree/core/controller_helpers/currency_helpers.rb +0 -15
  129. data/lib/spree/i18n/base.rb +0 -17
  130. data/lib/spree/i18n/initializer.rb +0 -1
@@ -1,9 +1,8 @@
1
1
  module Spree
2
2
  class ShippingRate < Spree::Base
3
3
  belongs_to :shipment, class_name: 'Spree::Shipment'
4
- belongs_to :tax_rate, class_name: 'Spree::TaxRate'
5
- belongs_to :shipping_method, class_name: 'Spree::ShippingMethod', inverse_of: :shipping_rates
6
-
4
+ belongs_to :tax_rate, -> { with_deleted }, class_name: 'Spree::TaxRate'
5
+ belongs_to :shipping_method, -> { with_deleted }, class_name: 'Spree::ShippingMethod', inverse_of: :shipping_rates
7
6
  extend Spree::DisplayMoney
8
7
 
9
8
  money_methods :base_price, :final_price, :tax_amount
@@ -32,14 +31,6 @@ module Spree
32
31
  @tax_amount ||= tax_rate&.calculator&.compute_shipping_rate(self) || BigDecimal(0)
33
32
  end
34
33
 
35
- def shipping_method
36
- Spree::ShippingMethod.unscoped { super }
37
- end
38
-
39
- def tax_rate
40
- Spree::TaxRate.unscoped { super }
41
- end
42
-
43
34
  # returns base price - any available discounts for this Shipment
44
35
  # useful when you want to present a list of available shipping rates
45
36
  def final_price
@@ -14,10 +14,9 @@ module Spree
14
14
  display_name = variant.name.to_s
15
15
  display_name += " (#{variant.options_text})" unless variant.options_text.blank?
16
16
 
17
- line_item.errors[:quantity] << Spree.t(
18
- :selected_quantity_not_available,
19
- item: display_name.inspect
20
- )
17
+ line_item.errors.add(:quantity,
18
+ :selected_quantity_not_available,
19
+ message: Spree.t(:selected_quantity_not_available, item: display_name.inspect))
21
20
  end
22
21
 
23
22
  private
@@ -4,7 +4,7 @@ module Spree
4
4
 
5
5
  with_options inverse_of: :stock_items do
6
6
  belongs_to :stock_location, class_name: 'Spree::StockLocation'
7
- belongs_to :variant, class_name: 'Spree::Variant'
7
+ belongs_to :variant, -> { with_deleted }, class_name: 'Spree::Variant'
8
8
  end
9
9
  has_many :stock_movements, inverse_of: :stock_item
10
10
 
@@ -56,10 +56,6 @@ module Spree
56
56
  in_stock? || backorderable?
57
57
  end
58
58
 
59
- def variant
60
- Spree::Variant.unscoped { super }
61
- end
62
-
63
59
  def reduce_count_on_hand_to_zero
64
60
  set_count_on_hand(0) if count_on_hand > 0
65
61
  end
@@ -3,6 +3,7 @@ module Spree
3
3
  has_many :orders, class_name: 'Spree::Order'
4
4
  has_many :payment_methods, class_name: 'Spree::PaymentMethod'
5
5
  belongs_to :default_country, class_name: 'Spree::Country'
6
+ belongs_to :checkout_zone, class_name: 'Spree::Zone'
6
7
 
7
8
  with_options presence: true do
8
9
  validates :name, :url, :mail_from_address, :default_currency, :code
@@ -23,12 +24,15 @@ module Spree
23
24
  validates :mailer_logo, content_type: ['image/png', 'image/jpg', 'image/jpeg']
24
25
 
25
26
  before_save :ensure_default_exists_and_is_unique
27
+ before_save :ensure_supported_currencies, :ensure_supported_locales
26
28
  before_destroy :validate_not_default
27
29
 
28
30
  scope :by_url, ->(url) { where('url like ?', "%#{url}%") }
29
31
 
30
32
  after_commit :clear_cache
31
33
 
34
+ alias_attribute :contact_email, :customer_support_email
35
+
32
36
  def self.current(domain = nil)
33
37
  current_store = domain ? Store.by_url(domain).first : nil
34
38
  current_store || Store.default
@@ -40,12 +44,49 @@ module Spree
40
44
  end
41
45
  end
42
46
 
47
+ def self.available_locales
48
+ Rails.cache.fetch('stores_available_locales') do
49
+ Spree::Store.all.map(&:supported_locales_list).flatten.uniq
50
+ end
51
+ end
52
+
43
53
  def supported_currencies_list
44
- (read_attribute(:supported_currencies).to_s.split(',') << default_currency).map(&:to_s).map do |code|
54
+ @supported_currencies_list ||= (read_attribute(:supported_currencies).to_s.split(',') << default_currency).sort.map(&:to_s).map do |code|
45
55
  ::Money::Currency.find(code.strip)
46
56
  end.uniq.compact
47
57
  end
48
58
 
59
+ def supported_locales_list
60
+ # TODO: add support of multiple supported languages to a single Store
61
+ @supported_locales_list ||= (read_attribute(:supported_locales).to_s.split(',') << default_locale).compact.uniq.sort
62
+ end
63
+
64
+ def unique_name
65
+ @unique_name ||= "#{name} (#{code})"
66
+ end
67
+
68
+ def formatted_url
69
+ return if url.blank?
70
+
71
+ @formatted_url ||= if url.match(/http:\/\/|https:\/\//)
72
+ url
73
+ else
74
+ Rails.env.development? ? "http://#{url}" : "https://#{url}"
75
+ end
76
+ end
77
+
78
+ def countries_available_for_checkout
79
+ @countries_available_for_checkout ||= checkout_zone_or_default.try(:country_list) || Spree::Country.all
80
+ end
81
+
82
+ def states_available_for_checkout(country)
83
+ checkout_zone_or_default.try(:state_list_for, country) || country.states
84
+ end
85
+
86
+ def checkout_zone_or_default
87
+ @checkout_zone_or_default ||= checkout_zone || Spree::Zone.default_checkout_zone
88
+ end
89
+
49
90
  private
50
91
 
51
92
  def ensure_default_exists_and_is_unique
@@ -56,6 +97,22 @@ module Spree
56
97
  end
57
98
  end
58
99
 
100
+ def ensure_supported_locales
101
+ return unless attributes.keys.include?('supported_locales')
102
+ return if supported_locales.present?
103
+ return if default_locale.blank?
104
+
105
+ self.supported_locales = default_locale
106
+ end
107
+
108
+ def ensure_supported_currencies
109
+ return unless attributes.keys.include?('supported_currencies')
110
+ return if supported_currencies.present?
111
+ return if default_currency.blank?
112
+
113
+ self.supported_currencies = default_currency
114
+ end
115
+
59
116
  def validate_not_default
60
117
  if default
61
118
  errors.add(:base, :cannot_destroy_default_store)
@@ -65,6 +122,7 @@ module Spree
65
122
 
66
123
  def clear_cache
67
124
  Rails.cache.delete('default_store')
125
+ Rails.cache.delete('stores_available_locales')
68
126
  end
69
127
  end
70
128
  end
@@ -68,7 +68,7 @@ module Spree
68
68
  end
69
69
 
70
70
  def validate_authorization(amount, order_currency)
71
- if amount_remaining.to_d < amount.to_d
71
+ if BigDecimal(amount_remaining, 3) < BigDecimal(amount, 3)
72
72
  errors.add(:base, Spree.t('store_credit_payment_method.insufficient_funds'))
73
73
  elsif currency != order_currency
74
74
  errors.add(:base, Spree.t('store_credit_payment_method.currency_mismatch'))
@@ -3,7 +3,7 @@ module Spree
3
3
  acts_as_paranoid
4
4
  acts_as_list scope: :product
5
5
 
6
- belongs_to :product, touch: true, class_name: 'Spree::Product', inverse_of: :variants
6
+ belongs_to :product, -> { with_deleted }, touch: true, class_name: 'Spree::Product', inverse_of: :variants
7
7
  belongs_to :tax_category, class_name: 'Spree::TaxCategory', optional: true
8
8
 
9
9
  delegate :name, :name=, :description, :slug, :available_on, :shipping_category_id,
@@ -123,15 +123,15 @@ module Spree
123
123
  end
124
124
 
125
125
  def tax_category
126
- if self[:tax_category_id].nil?
127
- product.tax_category
128
- else
129
- Spree::TaxCategory.find(self[:tax_category_id])
130
- end
126
+ @tax_category ||= if self[:tax_category_id].nil?
127
+ product.tax_category
128
+ else
129
+ Spree::TaxCategory.find(self[:tax_category_id])
130
+ end
131
131
  end
132
132
 
133
133
  def options_text
134
- Spree::Variants::OptionsPresenter.new(self).to_sentence
134
+ @options_text ||= Spree::Variants::OptionsPresenter.new(self).to_sentence
135
135
  end
136
136
 
137
137
  # Default to master name
@@ -150,13 +150,6 @@ module Spree
150
150
  !!deleted_at
151
151
  end
152
152
 
153
- # Product may be created with deleted_at already set,
154
- # which would make AR's default finder return nil.
155
- # This is a stopgap for that little problem.
156
- def product
157
- Spree::Product.unscoped { super }
158
- end
159
-
160
153
  def options=(options = {})
161
154
  options.each do |option|
162
155
  set_option_value(option[:name], option[:value])
@@ -199,7 +192,7 @@ module Spree
199
192
  end
200
193
 
201
194
  def price_in(currency)
202
- prices.detect { |price| price.currency == currency } || prices.build(currency: currency)
195
+ prices.detect { |price| price.currency == currency&.upcase } || prices.build(currency: currency&.upcase)
203
196
  end
204
197
 
205
198
  def amount_in(currency)
@@ -233,7 +226,7 @@ module Spree
233
226
  end
234
227
 
235
228
  def compare_at_price
236
- price_in(cost_currency).try(:compare_at_amount)
229
+ @compare_at_price ||= price_in(cost_currency).try(:compare_at_amount)
237
230
  end
238
231
 
239
232
  def name_and_sku
@@ -70,6 +70,10 @@ module Spree
70
70
  matches.first
71
71
  end
72
72
 
73
+ def self.default_checkout_zone
74
+ find_by(name: Spree::Config[:checkout_zone])
75
+ end
76
+
73
77
  def kind
74
78
  if self[:kind].present?
75
79
  self[:kind]
@@ -97,8 +101,6 @@ module Spree
97
101
  zone_member.zoneable_id == address.country_id
98
102
  when 'Spree::State'
99
103
  zone_member.zoneable_id == address.state_id
100
- else
101
- false
102
104
  end
103
105
  end
104
106
  end
@@ -110,8 +112,6 @@ module Spree
110
112
  zoneables
111
113
  when 'state' then
112
114
  zoneables.collect(&:country)
113
- else
114
- []
115
115
  end.flatten.compact.uniq
116
116
  end
117
117
 
@@ -167,6 +167,19 @@ module Spree
167
167
  true
168
168
  end
169
169
 
170
+ def state_list
171
+ case kind
172
+ when 'country'
173
+ zoneables.map(&:states)
174
+ when 'state'
175
+ zoneables
176
+ end.flatten.compact.uniq
177
+ end
178
+
179
+ def state_list_for(country)
180
+ state_list.select { |state| state.country == country }
181
+ end
182
+
170
183
  private
171
184
 
172
185
  def remove_defunct_members
@@ -4,7 +4,14 @@ module Spree
4
4
  def initialize(collection, params)
5
5
  @collection = collection
6
6
  @page = params[:page]
7
- @per_page = params[:per_page]
7
+
8
+ per_page_limit = Spree::Api::Config[:api_v2_per_page_limit]
9
+
10
+ @per_page = if params[:per_page].to_i.between?(1, per_page_limit)
11
+ params[:per_page]
12
+ else
13
+ Kaminari.config.default_per_page
14
+ end
8
15
  end
9
16
 
10
17
  def call
@@ -2,6 +2,7 @@ module Spree
2
2
  class VariantPresenter
3
3
  include Rails.application.routes.url_helpers
4
4
  include Spree::BaseHelper
5
+ include Spree::ProductsHelper
5
6
 
6
7
  attr_reader :current_currency, :current_price_options, :current_store
7
8
 
@@ -19,7 +20,7 @@ module Spree
19
20
  display_price: display_price(variant),
20
21
  price: variant.price_in(current_currency),
21
22
  display_compare_at_price: display_compare_at_price(variant),
22
- should_display_compare_at_price: should_display_compare_at_price(variant),
23
+ should_display_compare_at_price: should_display_compare_at_price?(variant),
23
24
  is_product_available_in_currency: @is_product_available_in_currency,
24
25
  backorderable: backorderable?(variant),
25
26
  in_stock: in_stock?(variant),
@@ -75,9 +76,5 @@ module Spree
75
76
  purchasable: variant.purchasable?
76
77
  }
77
78
  end
78
-
79
- def should_display_compare_at_price(variant)
80
- variant.compare_at_price.present? && variant.compare_at_price > variant.price
81
- end
82
79
  end
83
80
  end
@@ -1,7 +1,12 @@
1
1
  module Spree
2
2
  module Account
3
3
  module Addresses
4
- class Create < ::Spree::Account::Addresses::Base
4
+ class Create
5
+ prepend Spree::ServiceModule::Base
6
+ include Spree::Account::Addresses::Helper
7
+
8
+ attr_accessor :country
9
+
5
10
  def call(user:, address_params:)
6
11
  fill_country_and_state_ids(address_params)
7
12
 
@@ -1,9 +1,7 @@
1
1
  module Spree
2
2
  module Account
3
3
  module Addresses
4
- class Base
5
- prepend Spree::ServiceModule::Base
6
-
4
+ module Helper
7
5
  private
8
6
 
9
7
  attr_accessor :country
@@ -1,7 +1,12 @@
1
1
  module Spree
2
2
  module Account
3
3
  module Addresses
4
- class Update < ::Spree::Account::Addresses::Base
4
+ class Update
5
+ prepend Spree::ServiceModule::Base
6
+ include Spree::Account::Addresses::Helper
7
+
8
+ attr_accessor :country
9
+
5
10
  def call(address:, address_params:)
6
11
  address_params[:country_id] ||= address.country_id
7
12
  fill_country_and_state_ids(address_params)
@@ -0,0 +1,101 @@
1
+ require 'uri'
2
+
3
+ module Spree
4
+ class BuildLocalizedRedirectUrl
5
+ prepend Spree::ServiceModule::Base
6
+
7
+ LOCALE_REGEX = /^\/[A-Za-z]{2}\/|^\/[A-Za-z]{2}-[A-Za-z]{2}\/|^\/[A-Za-z]{2}$|^\/[A-Za-z]{2}-[A-Za-z]{2}$/.freeze
8
+
9
+ SUPPORTED_PATHS_REGEX = /\/(products|t\/|cart|checkout|addresses|content)/.freeze
10
+
11
+ # rubocop:disable Lint/UnusedMethodArgument
12
+ def call(url:, locale:, default_locale: nil)
13
+ run :initialize_url_object
14
+ run :generate_new_path
15
+ run :append_locale_param
16
+ run :build_url
17
+ end
18
+ # rubocop:enable Lint/UnusedMethodArgument
19
+
20
+ protected
21
+
22
+ def initialize_url_object(url:, locale:, default_locale:)
23
+ success(
24
+ url: URI(url),
25
+ locale: locale,
26
+ default_locale_supplied: default_locale_supplied?(locale, default_locale)
27
+ )
28
+ end
29
+
30
+ def generate_new_path(url:, locale:, default_locale_supplied:)
31
+ unless supported_path?(url.path)
32
+ return success(
33
+ url: url,
34
+ locale: locale,
35
+ path: cleanup_path(url.path),
36
+ default_locale_supplied: default_locale_supplied,
37
+ locale_added_to_path: false
38
+ )
39
+ end
40
+
41
+ new_path = if default_locale_supplied
42
+ maches_locale_regex?(url.path) ? url.path.gsub(LOCALE_REGEX, '/') : url.path
43
+ else
44
+ maches_locale_regex?(url.path) ? url.path.gsub(LOCALE_REGEX, "/#{locale}/") : "/#{locale}#{url.path}"
45
+ end
46
+
47
+ success(
48
+ url: url,
49
+ locale: locale,
50
+ path: cleanup_path(new_path),
51
+ default_locale_supplied: default_locale_supplied,
52
+ locale_added_to_path: true
53
+ )
54
+ end
55
+
56
+ def append_locale_param(url:, locale:, path:, default_locale_supplied:, locale_added_to_path:)
57
+ return success(url: url, path: path, query: url.query) if locale_added_to_path
58
+
59
+ query_params = Rack::Utils.parse_nested_query(url.query)
60
+
61
+ if default_locale_supplied
62
+ query_params.delete('locale')
63
+ else
64
+ query_params.merge!('locale' => locale)
65
+ end
66
+
67
+ query_string = query_params.any? ? query_params.to_query : nil
68
+
69
+ success(url: url, path: path, query: query_string)
70
+ end
71
+
72
+ def build_url(url:, path:, query:)
73
+ localized_url = builder_class(url).build(host: url.host, port: url.port, path: path, query: query).to_s
74
+ success(localized_url)
75
+ end
76
+
77
+ private
78
+
79
+ def supported_path?(path)
80
+ return true if path.blank? || path == '/' || maches_locale_regex?(path)
81
+
82
+ path.match(SUPPORTED_PATHS_REGEX)
83
+ end
84
+
85
+ def maches_locale_regex?(path)
86
+ path.match(LOCALE_REGEX)[0].gsub('/', '') if path.match(LOCALE_REGEX)
87
+ end
88
+
89
+ def default_locale_supplied?(locale, default_locale)
90
+ default_locale.present? && default_locale.to_s == locale.to_s
91
+ end
92
+
93
+ def cleanup_path(path)
94
+ path.chomp('/').gsub('//', '/')
95
+ end
96
+
97
+ def builder_class(url)
98
+ url.scheme == 'http' ? URI::HTTP : URI::HTTPS
99
+ end
100
+ end
101
+ end