-
Notifications
You must be signed in to change notification settings - Fork 0
Automated Test: feature-html-sanitizer-implementation #316
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -72,6 +72,17 @@ | |
| <version>2.2</version> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>com.googlecode.owasp-java-html-sanitizer</groupId> | ||
| <artifactId>owasp-java-html-sanitizer</artifactId> | ||
| <version>20240325.1</version> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.apache.commons</groupId> | ||
| <artifactId>commons-text</artifactId> | ||
| <version>1.13.0</version> | ||
| <scope>compile</scope> | ||
| </dependency> | ||
|
Comment on lines
+75
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Vulnerability: CVE-2025-66021 (GitHub advisory GHSA-g9gq-3pfx-2gw2) affects OWASP Java HTML Sanitizer (Maven What it is: A cross-site scripting (XSS) issue that can occur only in a specific configuration: when Severity: CVSS v4 base score 8.6 (High) as reported by the CNA (GitHub). [2] Fix / patched version: Upgrade to 20260101.1, which GitHub lists as the patched release for this advisory. [1] Mitigation (if you can’t upgrade immediately):
Sources: GitHub Advisory Database [1], NVD [2]. 🌐 Web query:
💡 Result: For Apache Commons Text 1.13.0, the well-known Commons Text RCE issue (“Text4Shell”, CVE-2022-42889) does not apply, because the affected range is Commons Text 1.5–1.9 (i.e., versions prior to 1.10.0) and the mitigation was released in 1.10.0 (safer defaults for interpolation lookups) [1][2][3]. What to do
Sources 🏁 Script executed: #!/bin/bash
# Check Maven Central for these dependencies and their latest versions
echo "=== Checking owasp-java-html-sanitizer versions ==="
curl -s "https://repo1.maven.org/maven2/com/googlecode/owasp-java-html-sanitizer/owasp-java-html-sanitizer/" | grep -oP '(20[0-9]{6}\.[0-9]+|jar|pom)' | sort -u | head -20
echo ""
echo "=== Checking commons-text versions ==="
curl -s "https://repo1.maven.org/maven2/org/apache/commons/commons-text/" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+' | sort -V | tail -10Repository: admin-coderabbit/coderabbit-keycloak-mirror Length of output: 468 Upgrade OWASP HTML Sanitizer to patch critical XSS vulnerability. The dependency The 🤖 Prompt for AI Agents |
||
| </dependencies> | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,15 +17,22 @@ | |
| package org.keycloak.themeverifier; | ||
|
|
||
| import org.apache.maven.plugin.MojoExecutionException; | ||
| import org.owasp.html.PolicyFactory; | ||
|
|
||
| import java.io.BufferedReader; | ||
| import java.io.File; | ||
| import java.io.FileInputStream; | ||
| import java.io.IOException; | ||
| import java.io.StringReader; | ||
| import java.nio.file.Files; | ||
| import java.util.ArrayList; | ||
| import java.util.HashSet; | ||
| import java.util.List; | ||
| import java.util.MissingResourceException; | ||
| import java.util.Objects; | ||
| import java.util.PropertyResourceBundle; | ||
| import java.util.regex.Matcher; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| public class VerifyMessageProperties { | ||
|
|
||
|
|
@@ -41,12 +48,129 @@ public List<String> verify() throws MojoExecutionException { | |
| try { | ||
| String contents = Files.readString(file.toPath()); | ||
| verifyNoDuplicateKeys(contents); | ||
| verifySafeHtml(); | ||
| } catch (IOException e) { | ||
| throw new MojoExecutionException("Can not read file " + file, e); | ||
| } | ||
| return messages; | ||
| } | ||
|
|
||
| PolicyFactory POLICY_SOME_HTML = new org.owasp.html.HtmlPolicyBuilder() | ||
| .allowElements( | ||
| "br", "p", "strong", "b" | ||
| ).toFactory(); | ||
|
|
||
| PolicyFactory POLICY_NO_HTML = new org.owasp.html.HtmlPolicyBuilder().toFactory(); | ||
|
|
||
| private void verifySafeHtml() { | ||
| PropertyResourceBundle bundle; | ||
| try (FileInputStream fis = new FileInputStream(file)) { | ||
| bundle = new PropertyResourceBundle(fis); | ||
| } catch (IOException e) { | ||
| throw new RuntimeException("unable to read file " + file, e); | ||
| } | ||
|
|
||
| PropertyResourceBundle bundleEnglish; | ||
| String englishFile = file.getAbsolutePath().replaceAll("resources-community", "resources") | ||
| .replaceAll("_[a-zA-Z-_]*\\.properties", "_en.properties"); | ||
| try (FileInputStream fis = new FileInputStream(englishFile)) { | ||
| bundleEnglish = new PropertyResourceBundle(fis); | ||
| } catch (IOException e) { | ||
| throw new RuntimeException("unable to read file " + englishFile, e); | ||
| } | ||
|
|
||
| bundle.getKeys().asIterator().forEachRemaining(key -> { | ||
| String value = bundle.getString(key); | ||
| value = normalizeValue(key, value); | ||
| String englishValue = getEnglishValue(key, bundleEnglish); | ||
| englishValue = normalizeValue(key, englishValue); | ||
|
|
||
| value = santizeAnchors(key, value, englishValue); | ||
|
|
||
| // Only if the English source string contains HTML we also allow HTML in the translation | ||
| PolicyFactory policy = containsHtml(englishValue) ? POLICY_SOME_HTML : POLICY_NO_HTML; | ||
| String sanitized = policy.sanitize(value); | ||
|
|
||
| // Sanitizer will escape HTML entities for quotes and also for numberic tags like '<1>' | ||
| sanitized = org.apache.commons.text.StringEscapeUtils.unescapeHtml4(sanitized); | ||
| // Sanitizer will add them when there are double curly braces | ||
| sanitized = sanitized.replace("<!-- -->", ""); | ||
|
|
||
| if (!Objects.equals(sanitized, value)) { | ||
|
|
||
| // Strip identical characters from the beginning and the end to show where the difference is | ||
| int start = 0; | ||
| while (start < sanitized.length() && start < value.length() && value.charAt(start) == sanitized.charAt(start)) { | ||
| start++; | ||
| } | ||
| int end = 0; | ||
| while (end < sanitized.length() && end < value.length() && value.charAt(value.length() - end - 1) == sanitized.charAt(sanitized.length() - end - 1)) { | ||
| end++; | ||
| } | ||
|
|
||
| messages.add("Illegal HTML in key " + key + " for file " + file + ": '" + value.substring(start, value.length() - end) + "' vs. '" + sanitized.substring(start, sanitized.length() - end) + "'"); | ||
|
Comment on lines
+99
to
+111
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential If 🐛 Proposed fix to handle overlap if (!Objects.equals(sanitized, value)) {
// Strip identical characters from the beginning and the end to show where the difference is
int start = 0;
while (start < sanitized.length() && start < value.length() && value.charAt(start) == sanitized.charAt(start)) {
start++;
}
int end = 0;
- while (end < sanitized.length() && end < value.length() && value.charAt(value.length() - end - 1) == sanitized.charAt(sanitized.length() - end - 1)) {
+ while (end < sanitized.length() - start && end < value.length() - start && value.charAt(value.length() - end - 1) == sanitized.charAt(sanitized.length() - end - 1)) {
end++;
}
messages.add("Illegal HTML in key " + key + " for file " + file + ": '" + value.substring(start, value.length() - end) + "' vs. '" + sanitized.substring(start, sanitized.length() - end) + "'");
}🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| }); | ||
| } | ||
|
|
||
| private String normalizeValue(String key, String value) { | ||
| if (key.equals("templateHelp")) { | ||
| // Allow "CLAIM.<NAME>" here | ||
| value = value.replaceAll("CLAIM\\.<[A-Z]*>", ""); | ||
| } else if (key.equals("optimizeLookupHelp")) { | ||
| // Allow "<Extensions>" here | ||
| value = value.replaceAll("<Extensions>", ""); | ||
| } else if (key.startsWith("linkExpirationFormatter.timePeriodUnit") || key.equals("error-invalid-multivalued-size")) { | ||
| // The problem is the "<" that appears in the choice | ||
| value = value.replaceAll("\\{[0-9]+,choice,[^}]*}", "..."); | ||
| } | ||
|
|
||
| // Unescape HTML entities, as we later also unescape HTML entities in the sanitized value | ||
| value = org.apache.commons.text.StringEscapeUtils.unescapeHtml4(value); | ||
|
|
||
| if (file.getAbsolutePath().contains("email")) { | ||
| // TODO: move the RTL information for emails | ||
| value = value.replaceAll(Pattern.quote(" style=\"direction: rtl;\""), ""); | ||
| } | ||
| return value; | ||
| } | ||
|
|
||
| Pattern HTML_TAGS = Pattern.compile("<[a-z]+[^>]*>"); | ||
|
|
||
| private boolean containsHtml(String englishValue) { | ||
| return HTML_TAGS.matcher(englishValue).find(); | ||
| } | ||
|
|
||
| private static final Pattern ANCHOR_PATTERN = Pattern.compile("</?a[^>]*>"); | ||
|
|
||
| /** | ||
| * Allow only those anchor tags from the source key to also appear in the target key. | ||
| */ | ||
| private String santizeAnchors(String key, String value, String englishValue) { | ||
| Matcher matcher = ANCHOR_PATTERN.matcher(value); | ||
| Matcher englishMatcher = ANCHOR_PATTERN.matcher(englishValue); | ||
| while (matcher.find()) { | ||
| if (englishMatcher.find() && Objects.equals(matcher.group(), englishMatcher.group())) { | ||
| value = value.replaceFirst(Pattern.quote(englishMatcher.group()), ""); | ||
| } else { | ||
| messages.add("Didn't find anchor tag " + matcher.group() + " in original string"); | ||
| break; | ||
| } | ||
| } | ||
| return value; | ||
| } | ||
|
|
||
| private static String getEnglishValue(String key, PropertyResourceBundle bundleEnglish) { | ||
| String englishValue; | ||
| try { | ||
| englishValue = bundleEnglish.getString(key); | ||
| } catch (MissingResourceException ex) { | ||
| englishValue = ""; | ||
| } | ||
| return englishValue; | ||
| } | ||
|
|
||
| private void verifyNoDuplicateKeys(String contents) throws IOException { | ||
| BufferedReader bufferedReader = new BufferedReader(new StringReader(contents)); | ||
| String line; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # | ||
| # Copyright 2025 Red Hat, Inc. and/or its affiliates | ||
| # and other contributors as indicated by the @author tags. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| # | ||
| key=Some <a href="http://malicious.com">link</a> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # | ||
| # Copyright 2025 Red Hat, Inc. and/or its affiliates | ||
| # and other contributors as indicated by the @author tags. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| # | ||
| key=Some <a href="http://example.com">link</a> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # | ||
| # Copyright 2025 Red Hat, Inc. and/or its affiliates | ||
| # and other contributors as indicated by the @author tags. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| # | ||
| key=Some <div>tag</div |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # | ||
| # Copyright 2025 Red Hat, Inc. and/or its affiliates | ||
| # and other contributors as indicated by the @author tags. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| # | ||
| key=Some <b>HTML</b> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # | ||
| # Copyright 2025 Red Hat, Inc. and/or its affiliates | ||
| # and other contributors as indicated by the @author tags. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| # | ||
| key=No HTML |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -138,7 +138,7 @@ revoke=Kumoa oikeudet | |||||
|
|
||||||
| configureAuthenticators=Konfiguroitu kaksivaiheinen kirjautuminen | ||||||
| mobile=Mobiili | ||||||
| totpStep1=Asenna <a href="https://freeotp.github.io/" target="_blank">FreeOTP</a> tai Google Authenticator ohjelma laiteellesi. Kummatkin sovellukset ovat saatavilla <a href="https://play.google.com">Google Play</a> ja Apple App Store kaupoissa. | ||||||
| totpStep1=Asenna jokin seuraavista sovelluksista matkapuhelimeesi: | ||||||
| totpStep2=Avaa sovellus ja skannaa QR-koodi tai kirjoita avain. | ||||||
| totpStep3=Täytä saamasi kertaluontoinen koodisi allaolevaan kenttään ja paina Tallenna. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix Finnish typo in instruction text. “allaolevaan” should be “alla olevaan”. 🔧 Proposed fix-totpStep3=Täytä saamasi kertaluontoinen koodisi allaolevaan kenttään ja paina Tallenna.
+totpStep3=Täytä saamasi kertaluontoinen koodisi alla olevaan kenttään ja paina Tallenna.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| totpStep3DeviceName=Anna laitteelle nimi, jotta voit hallinnoida OTP-laitteitasi. | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -98,7 +98,7 @@ revoke=Atšaukti įgaliojimą | |||||
|
|
||||||
| configureAuthenticators=Sukonfigūruotas autentifikatorius | ||||||
| mobile=Mobilus | ||||||
| totpStep1=Įdiekite <a href="https://freeotp.github.io/" target="_blank">FreeOTP</a> arba Google Authenticator savo įrenginyje. Programėlės prieinamos <a href="https://play.google.com">Google Play</a> ir Apple App Store. | ||||||
| totpStep1=Installa una delle seguenti applicazioni sul tuo cellulare: | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, verify the file exists and check its structure
find . -name "messages_lt.properties" -type fRepository: admin-coderabbit/coderabbit-keycloak-mirror Length of output: 642 🏁 Script executed: # Check the content around line 101
if [ -f "themes/src/main/resources-community/theme/base/account/messages/messages_lt.properties" ]; then
echo "=== Lines 99-105 of messages_lt.properties ==="
sed -n '99,105p' "themes/src/main/resources-community/theme/base/account/messages/messages_lt.properties"
echo ""
echo "=== File statistics ==="
wc -l "themes/src/main/resources-community/theme/base/account/messages/messages_lt.properties"
fiRepository: admin-coderabbit/coderabbit-keycloak-mirror Length of output: 642 🏁 Script executed: # Search for the Italian text pattern mentioned in the review across all locale files
echo "=== Searching for Italian text in non-Italian locale files ==="
rg -n "Installa una delle|sul tuo cellulare|seguenti applicazioni" \
--glob '**/messages_*.properties' \
--glob '!**/messages_it.properties' \
-A 0 -B 0Repository: admin-coderabbit/coderabbit-keycloak-mirror Length of output: 473 🏁 Script executed: # Also check if there's an Italian locale file to compare
echo "=== Checking for Italian locale file ==="
find . -name "messages_it.properties" -type fRepository: admin-coderabbit/coderabbit-keycloak-mirror Length of output: 582 🏁 Script executed: # If Italian locale file exists, check line 101 there for comparison
if [ -f "themes/src/main/resources-community/theme/base/account/messages/messages_it.properties" ]; then
echo "=== Lines 99-105 of messages_it.properties (for comparison) ==="
sed -n '99,105p' "themes/src/main/resources-community/theme/base/account/messages/messages_it.properties"
fiRepository: admin-coderabbit/coderabbit-keycloak-mirror Length of output: 297 Critical localization error: Italian text in Lithuanian locale files. The value for The same Italian text also appears in 🌍 Proposed fix with Lithuanian translation-totpStep1=Installa una delle seguenti applicazioni sul tuo cellulare:
+totpStep1=Įdiekite vieną iš šių programėlių į savo mobilųjį telefoną:Apply the same fix to 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| totpStep2=Atidarykite programėlę ir nuskenuokite barkodą arba įveskite kodą. | ||||||
| totpStep3=Įveskite programėlėje sugeneruotą vieną kartą galiojantį kodą ir paspauskite Saugoti norėdami prisijungti. | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -93,7 +93,7 @@ revoke=Upphäv rättighet | |||||||||||||
|
|
||||||||||||||
| configureAuthenticators=Konfigurerade autentiserare | ||||||||||||||
| mobile=Mobil | ||||||||||||||
| totpStep1=Installera <a href="https://freeotp.github.io/" target="_blank">FreeOTP</a> eller Google Authenticator på din enhet. Båda applikationerna finns tillgängliga på <a href="https://play.google.com">Google Play</a> och Apple App Store. | ||||||||||||||
| totpStep1=Installera en av följande applikationer på din mobil: | ||||||||||||||
| totpStep2=Öppna applikationen och skanna streckkoden eller skriv i nyckeln. | ||||||||||||||
| totpStep3=Fyll i engångskoden som tillhandahålls av applikationen och klicka på Spara för att avsluta inställningarna. | ||||||||||||||
|
Comment on lines
+96
to
98
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor Swedish grammar correction needed in totpStep2. The phrase "skriv i nyckeln" in line 97 is grammatically awkward in Swedish. The preposition "i" suggests writing "inside" something, but for entering or typing a key, Swedish uses "skriv in" (write/enter) or "ange" (enter/specify). 📝 Suggested correction totpStep1=Installera en av följande applikationer på din mobil:
-totpStep2=Öppna applikationen och skanna streckkoden eller skriv i nyckeln.
+totpStep2=Öppna applikationen och skanna streckkoden eller skriv in nyckeln.
totpStep3=Fyll i engångskoden som tillhandahålls av applikationen och klicka på Spara för att avsluta inställningarna.Alternatively, you could use "ange nyckeln" (enter the key) for a more formal tone: -totpStep2=Öppna applikationen och skanna streckkoden eller skriv i nyckeln.
+totpStep2=Öppna applikationen och skanna streckkoden eller ange nyckeln.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -109,7 +109,7 @@ revoke=收回授权 | |||||
|
|
||||||
| configureAuthenticators=配置的认证者 | ||||||
| mobile=手机 | ||||||
| totpStep1=在你的设备上安装 <a href="https://fedorahosted.org/freeotp/" target="_blank">FreeOTP</a> 或者 Google Authenticator.两个应用可以从 <a href="https://play.google.com">Google Play</a> 和 Apple App Store下载。 | ||||||
| totpStep1=在您的手機上安裝以下應用程式之一: | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use Simplified Chinese wording for zh_CN. The updated text uses Traditional characters (“手機”, “應用程式”) while this file is zh_CN and surrounding strings are Simplified. Consider using “手机 / 应用程序” to keep locale consistency. ✏️ Suggested fix-totpStep1=在您的手機上安裝以下應用程式之一:
+totpStep1=在您的手机上安装以下应用程序之一:📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| totpStep2=打开应用扫描二维码输入验证码 | ||||||
| totpStep3=输入应用提供的一次性验证码单击保存 | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -6,7 +6,7 @@ emailTestBody=Dette er en test besked | |||||
| emailTestBodyHtml=<p>Dette er en test besked</p> | ||||||
| identityProviderLinkSubject=Link {0} | ||||||
| identityProviderLinkBody=Nogen vil forbinde din "{1}" konto med "{0}" kontoen som er tilknyttet brugeren {2}. Hvis dette var dig, bedes du klikke på forbindet herunder for at forbinde de to konti\n\n{3}\n\nDette link vil udløbe efter {5}.\n\nHvis du ikke vil forbinde disse konti, kan du bare ignore denne besked. Hvis du vælger at forbinde de to konti, kan du logge ind som {1} via {0}. | ||||||
| identityProviderLinkBodyHtml=<p>Nogen vil forbinde din <b>{1}</b> konto med <b>{0}</b> kontoen som er tilknyttet brugeren {2}. Hvis dette var dig, bedes du klikke på forbindet herunder for at forbinde de to konti</p><p><a href="{3}">Bekræft</a></p><p>Dette link vil udløbe efter {5}.</p><p>nHvis du ikke vil forbinde disse konti, kan du bare ignore denne besked. Hvis du vælger at forbinde de to konti, kan du logge ind som {1} via {0}. | ||||||
| identityProviderLinkBodyHtml=<p>Nogen vil forbinde din <b>{1}</b> konto med <b>{0}</b> kontoen som er tilknyttet brugeren {2}. Hvis dette var dig, bedes du klikke på forbindet herunder for at forbinde de to konti</p><p><a href="{3}">Bekræft</a></p><p>Dette link vil udløbe efter {5}.</p><p>nHvis du ikke vil forbinde disse konti, kan du bare ignore denne besked. Hvis du vælger at forbinde de to konti, kan du logge ind som {1} via {0}.</p> | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor typo: "nHvis" should be "Hvis" The HTML structure improvements look good with proper closing tags and punctuation. However, there's a typo in the last paragraph where "nHvis" should be "Hvis" (without the leading "n"). 📝 Proposed fix for the typo-identityProviderLinkBodyHtml=<p>Nogen vil forbinde din <b>{1}</b> konto med <b>{0}</b> kontoen som er tilknyttet brugeren {2}. Hvis dette var dig, bedes du klikke på forbindet herunder for at forbinde de to konti</p><p><a href="{3}">Bekræft</a></p><p>Dette link vil udløbe efter {5}.</p><p>nHvis du ikke vil forbinde disse konti, kan du bare ignore denne besked. Hvis du vælger at forbinde de to konti, kan du logge ind som {1} via {0}.</p>
+identityProviderLinkBodyHtml=<p>Nogen vil forbinde din <b>{1}</b> konto med <b>{0}</b> kontoen som er tilknyttet brugeren {2}. Hvis dette var dig, bedes du klikke på forbindet herunder for at forbinde de to konti</p><p><a href="{3}">Bekræft</a></p><p>Dette link vil udløbe efter {5}.</p><p>Hvis du ikke vil forbinde disse konti, kan du bare ignore denne besked. Hvis du vælger at forbinde de to konti, kan du logge ind som {1} via {0}.</p>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| passwordResetSubject=Gendan adgangskode | ||||||
| passwordResetBody=Nogen har forsøgt at nulstille adgangskoden til {2}. Hvis dette var dig, bedes du klikke på linket herunder for at nulstille adgangskoden.\n\n{0}\n\nDette link og kode vil udløbe efter {3}.\n\nHvis du ikke ønsker at nulstille din adgangskode, kan du se bort fra denne besked. | ||||||
| passwordResetBodyHtml=<p>Nogen har forsøgt at nulstille adgangskoden til {2}. Hvis dette var dig, bedes du klikke på linket herunder for at nulstille adgangskoden.</p><p><a href="{0}">Nulstil adgangskode</a></p><p>Dette link og kode vil udløbe efter {3}.</p><p>Hvis du ikke ønsker at nulstille din adgangskode, kan du se bort fra denne besked.</p> | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 1542
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 114
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 211
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 69
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 1464
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 3071
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 69
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 3970
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 284
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 1538
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 770
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 278
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 25013
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 743
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 1043
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 419
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 132
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 1575
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 69
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 985
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 149
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 4134
🏁 Script executed:
Repository: admin-coderabbit/coderabbit-keycloak-mirror
Length of output: 972
🌐 Web query:
i18next ICU MessageFormat choice syntax support💡 Result:
i18next can use ICU MessageFormat only via a plugin such as
i18next-icu, which swaps i18next’s default interpolation/plural system for FormatJSintl-messageformatparsing/formatting. [1]What ICU “choice” syntax support means in this setup
i18next-icusupports the ICU MessageFormat arguments thatintl-messageformatsupports:plural,select, andselectordinal(plus number/date/time formatting). [2]choiceformatting. In ICU/MessageFormat history, “choice” exists as a format type alongside plural/select in ICU implementations, but FormatJS’sintl-messageformatfeature set is centered onplural/select/selectordinalrather than “choice”. [2][3]What to use instead of
choiceplural(with exact matches like=0,=1, etc.). [2]select(must includeother). [2][3]Example
"{count, plural, =0 {No items} one {One item} other {# items}}" "{gender, select, male {He} female {She} other {They}}"Sources
[1] i18next-icu README (uses FormatJS
intl-messageformat)[2] FormatJS
intl-messageformatdocs (supported: plural/select/selectordinal)[3] ICU docs noting “choice” as a distinct format type in ICU MessageFormat implementations
Replace ICU choice syntax with i18next-compatible plural syntax or add i18next-icu support.
The admin-ui formatter uses i18next without the
i18next-icuplugin, so the ICU choice pattern{2,choice,0#values|1#value|1<values}will render literally in the UI instead of being formatted. Either install and configurei18next-icuto enable ICU MessageFormat support, or rewrite the message using i18next's native plural syntax:{2, plural, =0 {values} one {value} other {values}}.🤖 Prompt for AI Agents