diff --git a/bridgetown-core/lib/bridgetown-core/helpers.rb b/bridgetown-core/lib/bridgetown-core/helpers.rb
index 9f181dad8..4d9be29e2 100644
--- a/bridgetown-core/lib/bridgetown-core/helpers.rb
+++ b/bridgetown-core/lib/bridgetown-core/helpers.rb
@@ -155,10 +155,17 @@ def translate(key, **options) # rubocop:disable Metrics/CyclomaticComplexity, Me
key = "#{view_path.tr("/", ".")}#{key}" if view_path.present?
end
- ActiveSupport::HtmlSafeTranslation.translate(key, **options)
+ return I18n.translate(key, **options) unless %r{(?:_|\b)html\z}.match?(key)
+
+ translate_with_html(key, **options)
end
alias_method :t, :translate
+ def translate_with_html(key, **options)
+ escaper = ->(input) { input.to_s.encode(xml: :attr).gsub(%r{\A"|"\Z}, "") }
+ Bridgetown::Foundation::SafeTranslations.translate(key, escaper, **options)
+ end
+
# Delegates to I18n.localize with no additional functionality.
#
# @return [String] the localized string
diff --git a/bridgetown-core/lib/bridgetown-core/rack/boot.rb b/bridgetown-core/lib/bridgetown-core/rack/boot.rb
index f99ad186b..126acd124 100644
--- a/bridgetown-core/lib/bridgetown-core/rack/boot.rb
+++ b/bridgetown-core/lib/bridgetown-core/rack/boot.rb
@@ -65,7 +65,6 @@ def self.autoload_server_folder( # rubocop:todo Metrics
loader.reload
loader.eager_load
- Bridgetown::Rack::Routes.reload_subclasses
rescue SyntaxError => e
Bridgetown::Errors.print_build_error(e)
end.start
@@ -78,7 +77,6 @@ def self.autoload_server_folder( # rubocop:todo Metrics
next unless load_path == server_folder
loader.eager_load
- Bridgetown::Rack::Routes.reload_subclasses
end
loaders_manager.setup_loaders([server_folder])
diff --git a/bridgetown-core/lib/bridgetown-core/rack/routes.rb b/bridgetown-core/lib/bridgetown-core/rack/routes.rb
index 8002248ef..d656d4ea0 100644
--- a/bridgetown-core/lib/bridgetown-core/rack/routes.rb
+++ b/bridgetown-core/lib/bridgetown-core/rack/routes.rb
@@ -45,9 +45,6 @@ def print_routes
end
# rubocop:enable Bridgetown/NoPutsAllowed, Metrics/MethodLength
- # @return [Hash]
- attr_accessor :tracked_subclasses
-
# @return [Proc]
attr_accessor :router_block
@@ -59,30 +56,9 @@ def <=>(other)
"#{priorities[priority]}#{self}" <=> "#{priorities[other.priority]}#{other}"
end
- # @param base [Class(Routes)]
- def inherited(base)
- Bridgetown::Rack::Routes.track_subclass base
- super
- end
-
- # @param klass [Class(Routes)]
- def track_subclass(klass)
- Bridgetown::Rack::Routes.tracked_subclasses ||= {}
- Bridgetown::Rack::Routes.tracked_subclasses[klass.name] = klass
- end
-
# @return [Array]
def sorted_subclasses
- Bridgetown::Rack::Routes.tracked_subclasses&.values&.sort
- end
-
- # @return [void]
- def reload_subclasses
- Bridgetown::Rack::Routes.tracked_subclasses&.each_key do |klassname|
- Kernel.const_get(klassname)
- rescue NameError
- Bridgetown::Rack::Routes.tracked_subclasses.delete klassname
- end
+ Bridgetown::Rack::Routes.descendants.sort
end
# Add a router block via the current Routes class
diff --git a/bridgetown-core/test/source/src/_locales/en.yml b/bridgetown-core/test/source/src/_locales/en.yml
index 9b00003ec..2cc483fe1 100644
--- a/bridgetown-core/test/source/src/_locales/en.yml
+++ b/bridgetown-core/test/source/src/_locales/en.yml
@@ -3,6 +3,7 @@ en:
foo: foo
bar: bar
foo_html: foo
+ dangerous_html:
contacts:
bar:
foo: foo
diff --git a/bridgetown-core/test/test_ruby_helpers.rb b/bridgetown-core/test/test_ruby_helpers.rb
index 395178b12..88505158c 100644
--- a/bridgetown-core/test/test_ruby_helpers.rb
+++ b/bridgetown-core/test/test_ruby_helpers.rb
@@ -97,6 +97,11 @@ def setup
assert @helpers.translate("about.foo_html").html_safe?
end
+ should "return escaped interpolated values within html safe translation" do
+ assert_equal "",
+ @helpers.translate("about.dangerous_html", me: "Me")
+ end
+
should "not return html safe string when key does not end with _html" do
refute @helpers.translate("about.foo").html_safe?
end
diff --git a/bridgetown-foundation/.rubocop.yml b/bridgetown-foundation/.rubocop.yml
index 7e1ca7a38..862e28a88 100644
--- a/bridgetown-foundation/.rubocop.yml
+++ b/bridgetown-foundation/.rubocop.yml
@@ -4,3 +4,4 @@ inherit_from: ../.rubocop.yml
AllCops:
Exclude:
- "*.gemspec"
+ - "script/console"
diff --git a/bridgetown-foundation/lib/bridgetown/foundation/core_ext/module.rb b/bridgetown-foundation/lib/bridgetown/foundation/core_ext/module.rb
new file mode 100644
index 000000000..76f025b8b
--- /dev/null
+++ b/bridgetown-foundation/lib/bridgetown/foundation/core_ext/module.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Bridgetown::Foundation
+ module CoreExt
+ module Module
+ module Nested
+ def nested_within?(other)
+ other.nested_parents.within?(nested_parents[1..])
+ end
+
+ def nested_parents
+ return [] unless name
+
+ nesting_segments = name.split("::")[...-1]
+ nesting_segments.map.each_with_index do |_nesting_name, index|
+ Kernel.const_get(nesting_segments[..-(index + 1)].join("::"))
+ end
+ end
+
+ def nested_parent
+ nested_parents.first
+ end
+
+ def nested_name
+ name&.split("::")&.last
+ end
+ end
+
+ ::Module.include Nested
+ end
+ end
+end
diff --git a/bridgetown-foundation/lib/bridgetown/foundation/safe_translations.rb b/bridgetown-foundation/lib/bridgetown/foundation/safe_translations.rb
new file mode 100644
index 000000000..99aeec911
--- /dev/null
+++ b/bridgetown-foundation/lib/bridgetown/foundation/safe_translations.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Bridgetown
+ module Foundation
+ # NOTE: this is tested by `test/test_ruby_helpers.rb` in bridgetown-core
+ #
+ # This is loosely based on the HtmlSafeTranslation module from ActiveSupport, but you can
+ # actually use it for any kind of safety use case in a translation setting because its
+ # decoupled from any specific escaping or safety mechanisms.
+ module SafeTranslations
+ def self.translate(key, escaper, safety_method = :html_safe, **options)
+ safe_options = escape_translation_options(options, escaper)
+
+ i18n_error = false
+
+ exception_handler = ->(*args) do
+ i18n_error = true
+ I18n.exception_handler.(*args)
+ end
+
+ I18n.translate(key, **safe_options, exception_handler:).then do |translation|
+ i18n_error ? translation : safe_translation(translation, safety_method)
+ end
+ end
+
+ def self.escape_translation_options(options, escaper)
+ @reserved_i18n_keys ||= I18n::RESERVED_KEYS.to_set
+
+ options.to_h do |name, value|
+ unless @reserved_i18n_keys.include?(name) || (name == :count && value.is_a?(Numeric))
+ next [name, escaper.(value)]
+ end
+
+ [name, value]
+ end
+ end
+
+ def self.safe_translation(translation, safety_method)
+ @safe_value ||= -> { _1.respond_to?(safety_method) ? _1.send(safety_method) : _1 }
+
+ return translation.map { @safe_value.(_1) } if translation.respond_to?(:map)
+
+ @safe_value.(translation)
+ end
+ end
+ end
+end
diff --git a/bridgetown-foundation/script/console b/bridgetown-foundation/script/console
new file mode 100755
index 000000000..9d1cba79b
--- /dev/null
+++ b/bridgetown-foundation/script/console
@@ -0,0 +1,12 @@
+#!/usr/bin/env ruby
+
+require "bundler"
+Bundler.setup
+
+require "bridgetown-foundation"
+
+module Bridgetown
+ module Foundation
+ binding.irb
+ end
+end
diff --git a/bridgetown-website/Gemfile b/bridgetown-website/Gemfile
index bf8a0d76e..1a97aee70 100644
--- a/bridgetown-website/Gemfile
+++ b/bridgetown-website/Gemfile
@@ -6,6 +6,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem "bridgetown", path: "../bridgetown"
gem "bridgetown-builder", path: "../bridgetown-builder"
gem "bridgetown-core", path: "../bridgetown-core"
+gem "bridgetown-foundation", path: "../bridgetown-foundation"
gem "bridgetown-paginate", path: "../bridgetown-paginate"
gem "puma", "< 7"
diff --git a/bridgetown-website/Gemfile.lock b/bridgetown-website/Gemfile.lock
index 94e21fda7..0a68ee808 100644
--- a/bridgetown-website/Gemfile.lock
+++ b/bridgetown-website/Gemfile.lock
@@ -12,6 +12,7 @@ PATH
activesupport (>= 6.0, < 8.0)
addressable (~> 2.4)
amazing_print (~> 1.2)
+ bridgetown-foundation (= 1.3.4)
colorator (~> 1.0)
csv (~> 3.2)
erubi (~> 1.9)
@@ -34,6 +35,12 @@ PATH
tilt (~> 2.0)
zeitwerk (~> 2.5)
+PATH
+ remote: ../bridgetown-foundation
+ specs:
+ bridgetown-foundation (1.3.4)
+ zeitwerk (~> 2.5)
+
PATH
remote: ../bridgetown-paginate
specs:
@@ -147,6 +154,7 @@ DEPENDENCIES
bridgetown-builder!
bridgetown-core!
bridgetown-feed (~> 3)
+ bridgetown-foundation!
bridgetown-paginate!
bridgetown-quick-search (~> 2.0)
bridgetown-seo-tag (~> 6.0)