diff --git a/lib/Handler/Templates/footer.twig b/lib/Handler/Templates/footer.twig
index 7a0bf52722..7d7488e328 100644
--- a/lib/Handler/Templates/footer.twig
+++ b/lib/Handler/Templates/footer.twig
@@ -6,12 +6,12 @@
{% endif %}
- {{ signedBy }}
+ {{ signedBy|raw }}
{% if validateIn %}
- {{ validateIn|replace({'%s': validationSite}) }}
+ {{ validateIn|replace({'%s': validationSite})|raw }}
{% endif %}
|
diff --git a/lib/Service/SignatureTextService.php b/lib/Service/SignatureTextService.php
index e933715c05..e847c6d5a3 100644
--- a/lib/Service/SignatureTextService.php
+++ b/lib/Service/SignatureTextService.php
@@ -388,6 +388,21 @@ private function mbWordwrap(string $text, int $width, string $break = "\n", bool
public function getDefaultTemplate(): string {
$collectMetadata = $this->appConfig->getValueBool(Application::APP_ID, 'collect_metadata', false);
if ($collectMetadata) {
+ // TRANSLATORS Variables enclosed in double curly braces {{variableName}} are template placeholders.
+ //
+ // DO NOT translate or remove these variables:
+ // - {{SignerCommonName}}
+ // - {{IssuerCommonName}}
+ // - {{ServerSignatureDate}}
+ // - {{SignerIP}}
+ // - {{SignerUserAgent}}
+ //
+ // Only translate the text outside the curly braces, such as:
+ // - "Signed with LibreSign"
+ // - "Issuer:"
+ // - "Date:"
+ // - "IP:"
+ // - "User agent:"
return $this->l10n->t(
"Signed with LibreSign\n"
. "{{SignerCommonName}}\n"
@@ -397,6 +412,17 @@ public function getDefaultTemplate(): string {
. 'User agent: {{SignerUserAgent}}'
);
}
+ // TRANSLATORS Variables enclosed in double curly braces {{variableName}} are template placeholders.
+ //
+ // DO NOT translate or remove these variables:
+ // - {{SignerCommonName}}
+ // - {{IssuerCommonName}}
+ // - {{ServerSignatureDate}}
+ //
+ // Only translate the text outside the curly braces, such as:
+ // - "Signed with LibreSign"
+ // - "Issuer:"
+ // - "Date:"
return $this->l10n->t(
"Signed with LibreSign\n"
. "{{SignerCommonName}}\n"
diff --git a/tests/php/Unit/Handler/FooterHandlerTest.php b/tests/php/Unit/Handler/FooterHandlerTest.php
index fc9864e5da..014a07c21e 100644
--- a/tests/php/Unit/Handler/FooterHandlerTest.php
+++ b/tests/php/Unit/Handler/FooterHandlerTest.php
@@ -325,4 +325,128 @@ public function testGetTemplateVariablesMetadata(): void {
$this->assertSame('string', $metadata['uuid']['type']);
$this->assertArrayHasKey('default', $metadata['signedBy']);
}
+
+ #[DataProvider('dataAccentedCharactersInFooter')]
+ public function testAccentedCharactersInFooterVariablesAreRenderedCorrectly(
+ string $testName,
+ string $signedByText,
+ array $expectedSubstrings,
+ array $forbiddenSubstrings,
+ ): void {
+ $this->appConfig->setValueBool(Application::APP_ID, 'add_footer', true);
+ $this->appConfig->setValueBool(Application::APP_ID, 'write_qrcode_on_footer', false);
+ $this->appConfig->deleteKey(Application::APP_ID, 'footer_template');
+
+ $dimensions = [['w' => 595, 'h' => 100]];
+ $this->l10n = $this->l10nFactory->get(Application::APP_ID, 'en');
+
+ $pdf = $this->getClass()
+ ->setTemplateVar('uuid', 'test-uuid')
+ ->setTemplateVar('signedBy', $signedByText)
+ ->setTemplateVar('linkToSite', 'https://libresign.coop')
+ ->getFooter($dimensions);
+
+ $this->assertNotEmpty($pdf);
+
+ $parser = new \Smalot\PdfParser\Parser();
+ $pdfParsed = $parser->parseContent($pdf);
+ $text = $pdfParsed->getText();
+
+ foreach ($expectedSubstrings as $expected) {
+ $this->assertStringContainsString($expected, $text, "Expected to find '{$expected}' for test: {$testName}");
+ }
+
+ foreach ($forbiddenSubstrings as $forbidden) {
+ $this->assertStringNotContainsString($forbidden, $text, "Should not find '{$forbidden}' for test: {$testName}");
+ }
+ }
+
+ public static function dataAccentedCharactersInFooter(): array {
+ return [
+ 'French accents' => [
+ 'testName' => 'French accents',
+ 'signedByText' => 'Signé numériquement par LibreSign',
+ 'expectedSubstrings' => ['Signé', 'numériquement'],
+ 'forbiddenSubstrings' => ['é', '&', 'é'],
+ ],
+ 'Portuguese accents and cedilla' => [
+ 'testName' => 'Portuguese accents',
+ 'signedByText' => 'Assinado digitalmente por João da Silva',
+ 'expectedSubstrings' => ['João', 'Silva'],
+ 'forbiddenSubstrings' => ['ã', '&', 'ã'],
+ ],
+ 'Spanish ñ and accents' => [
+ 'testName' => 'Spanish characters',
+ 'signedByText' => 'Firmado digitalmente por José Muñoz',
+ 'expectedSubstrings' => ['José', 'Muñoz'],
+ 'forbiddenSubstrings' => ['ñ', 'é', '&'],
+ ],
+ 'German umlauts' => [
+ 'testName' => 'German umlauts',
+ 'signedByText' => 'Digital signiert von Müller & Söhne',
+ 'expectedSubstrings' => ['Müller', 'Söhne'],
+ 'forbiddenSubstrings' => ['ü', 'ö', '&'],
+ ],
+ 'Multiple special characters' => [
+ 'testName' => 'Multiple accents',
+ 'signedByText' => 'Signé par Renée & José',
+ 'expectedSubstrings' => ['Signé', 'Renée', 'José'],
+ 'forbiddenSubstrings' => ['é', '&', ''],
+ ],
+ 'Greek characters' => [
+ 'testName' => 'Greek characters',
+ 'signedByText' => 'Υπογραφή από Αθήνα',
+ 'expectedSubstrings' => ['Υπογραφή', 'Αθήνα'],
+ 'forbiddenSubstrings' => ['&', ''],
+ ],
+ 'Cyrillic characters' => [
+ 'testName' => 'Cyrillic characters',
+ 'signedByText' => 'Подписано Москва',
+ 'expectedSubstrings' => ['Подписано', 'Москва'],
+ 'forbiddenSubstrings' => ['&', ''],
+ ],
+ 'Arabic characters (RTL)' => [
+ 'testName' => 'Arabic (RTL)',
+ 'signedByText' => 'توقيع رقمي من القاهرة',
+ 'expectedSubstrings' => ['عيقوت', 'ةرهاقلا'], // RTL text appears reversed in extracted PDF
+ 'forbiddenSubstrings' => ['&', ''],
+ ],
+ 'Hebrew characters (RTL)' => [
+ 'testName' => 'Hebrew (RTL)',
+ 'signedByText' => 'חתום דיגיטלית מירושלים',
+ 'expectedSubstrings' => ['םותח', 'םילשורימ'], // RTL text appears reversed in extracted PDF
+ 'forbiddenSubstrings' => ['&', ''],
+ ],
+ 'Chinese characters' => [
+ 'testName' => 'Chinese (CJK)',
+ 'signedByText' => '数字签名 北京',
+ 'expectedSubstrings' => ['数字签名', '北京'],
+ 'forbiddenSubstrings' => ['&', ''],
+ ],
+ 'Japanese characters' => [
+ 'testName' => 'Japanese (CJK)',
+ 'signedByText' => 'デジタル署名 東京',
+ 'expectedSubstrings' => ['デジタル署名', '東京'],
+ 'forbiddenSubstrings' => ['&', ''],
+ ],
+ 'Korean characters' => [
+ 'testName' => 'Korean (CJK)',
+ 'signedByText' => '디지털 서명 서울',
+ 'expectedSubstrings' => ['디지털', '서울'],
+ 'forbiddenSubstrings' => ['&', ''],
+ ],
+ 'Emoji characters' => [
+ 'testName' => 'Emoji',
+ 'signedByText' => 'Signed ✍️ by LibreSign 🔒',
+ 'expectedSubstrings' => ['Signed', 'LibreSign'],
+ 'forbiddenSubstrings' => ['&', ''],
+ ],
+ 'Mixed emoji and accents' => [
+ 'testName' => 'Emoji with accents',
+ 'signedByText' => 'Signé 📝 par José 👤',
+ 'expectedSubstrings' => ['Signé', 'José'],
+ 'forbiddenSubstrings' => ['é', '&', ''],
+ ],
+ ];
+ }
}