From 98c91275e223e0dc4e53b437318480cbdaedfeb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 30 Mar 2026 08:34:49 +0200 Subject: [PATCH] Use scan on raw translate for link_translate building We changed the way we output translation text in the link_translate function. By using a SafeBuffer, the original text was already escaped before it got handled by the link helper. Instead, we can pass the raw link part of the translation string to the link helper, allowing it to handle escaping, and output the rest of the translation manually to the SafeBuffer. This way, the entire string is subjected to escaping still, but will allow entities to not be escaped https://community.openproject.org/work_packages/73513 --- lib_static/redmine/i18n.rb | 15 +++++++++++---- spec/lib/redmine/i18n_spec.rb | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/lib_static/redmine/i18n.rb b/lib_static/redmine/i18n.rb index e93a9de16c5..c844ec1cc6b 100644 --- a/lib_static/redmine/i18n.rb +++ b/lib_static/redmine/i18n.rb @@ -116,13 +116,20 @@ module Redmine # @param links [Hash] Link names mapped to URLs. # @param external [Boolean] Whether the links should be opened as external links, i.e. in a new tab (default: true) # @param underline [Boolean] Whether to underline links inserted into the text (default: true) - def link_translate(i18n_key, i18n_args: {}, links: {}, external: true, underline: true, **) + def link_translate(i18n_key, i18n_args: {}, links: {}, external: true, underline: true, **) # rubocop:disable Metrics/AbcSize + translation = ApplicationController.helpers.t(i18n_key.to_s, **i18n_args) output = ActiveSupport::SafeBuffer.new - output << ApplicationController.helpers.t(i18n_key.to_s, **i18n_args) + last_end = 0 - output.html_safe_gsub(link_regex) do - create_link_content($3, $2, external:, links:, underline:, **) + translation.scan(link_regex) do + match = Regexp.last_match + output << translation[last_end...match.begin(0)] + output << create_link_content(match[3], match[2], external:, links:, underline:, **) + last_end = match.end(0) end + output << translation[last_end..] + + output end ## diff --git a/spec/lib/redmine/i18n_spec.rb b/spec/lib/redmine/i18n_spec.rb index ac7ade0d0ce..db8157fb9a8 100644 --- a/spec/lib/redmine/i18n_spec.rb +++ b/spec/lib/redmine/i18n_spec.rb @@ -211,6 +211,25 @@ module OpenProject expect(links[1][:href]).to eq("/baz") end + context "when the link text contains an apostrophe" do + before do + allow(::I18n) + .to receive(:translate) + .with("translation_with_apostrophe", *any_args) + .and_return("Here's [what's new](url) to see.") + end + + it "does not double-escape the apostrophe in the link text" do + translated = link_translate :translation_with_apostrophe, + links: { url: "https://example.com" }, + external: false + fragment = Capybara.string(translated) + + link = fragment.find("a") + expect(link.text).to eq("what's new") + end + end + context "when passing URLs as a list of symbols" do let(:urls) do { url_1: [:a, :b], url_2: [:a, :c] }