Skip to content

Commit 1783e34

Browse files
scruffianmikachan
andauthored
i18n: Makes it possible to translate text strings in HTML comments (#788)
* i18n: Makes it possible to translate text strings in HTML comments * add tests * escape attributes * text fix * simplify code * Apply suggestion from @mikachan Co-authored-by: Sarah Norris <1645628+mikachan@users.noreply.github.com> * add test for not changing attributes --------- Co-authored-by: Sarah Norris <1645628+mikachan@users.noreply.github.com>
1 parent 0d47eed commit 1783e34

File tree

3 files changed

+192
-0
lines changed

3 files changed

+192
-0
lines changed

includes/create-theme/theme-locale.php

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,30 @@ private static function get_text_replacement_patterns_for_html( $block_name ) {
130130
}
131131
}
132132

133+
/**
134+
* Get the list of block attributes that should be localized.
135+
*
136+
* @param string $block_name The block name.
137+
* @return array|null The array of attribute names to localize.
138+
* Returns null if the block does not have localizable attributes.
139+
*/
140+
private static function get_localizable_block_attributes( $block_name ) {
141+
switch ( $block_name ) {
142+
case 'core/search':
143+
return array( 'label', 'placeholder', 'buttonText' );
144+
case 'core/query-pagination-previous':
145+
case 'core/query-pagination-next':
146+
case 'core/comments-pagination-previous':
147+
case 'core/comments-pagination-next':
148+
case 'core/post-navigation-link':
149+
return array( 'label' );
150+
case 'core/post-excerpt':
151+
return array( 'moreText' );
152+
default:
153+
return null;
154+
}
155+
}
156+
133157
/*
134158
* Localize text in text blocks.
135159
*
@@ -210,6 +234,74 @@ function( $content ) use ( $replace_content_callback, $pattern ) {
210234
}
211235
}
212236
}
237+
213238
return $blocks;
214239
}
240+
241+
/**
242+
* Escape block attribute strings for localization in serialized block markup.
243+
*
244+
* This method processes the serialized block markup string to add localization
245+
* to attribute values. It must be called AFTER serialize_blocks() because
246+
* PHP tags in attributes would be JSON-encoded during serialization.
247+
*
248+
* @param string $content The serialized block markup string.
249+
* @return string The content with localized attribute values.
250+
*/
251+
public static function escape_block_attribute_strings( $content ) {
252+
// Pattern to match block comments with JSON attributes.
253+
// This captures: <!-- wp:block/name {...attributes...} --> or <!-- wp:block/name {...attributes...} /-->
254+
// Using .*? to lazily match everything until we hit the closing -->
255+
$pattern = '/<!--\s+wp:([a-z0-9\/-]+)\s+(\{.*?\})\s*(\/)?-->/s';
256+
257+
return preg_replace_callback(
258+
$pattern,
259+
function ( $matches ) {
260+
$block_name = $matches[1];
261+
$attrs_json = $matches[2];
262+
$self_closer = isset( $matches[3] ) ? $matches[3] : '';
263+
264+
// Get localizable attributes for this block.
265+
$localizable_attrs = self::get_localizable_block_attributes( 'core/' . $block_name );
266+
267+
// If no localizable attributes for this block, return unchanged.
268+
if ( ! $localizable_attrs ) {
269+
return $matches[0];
270+
}
271+
272+
// Decode the JSON attributes.
273+
$attrs = json_decode( $attrs_json, true );
274+
275+
// If JSON decode failed, return unchanged.
276+
if ( ! is_array( $attrs ) ) {
277+
return $matches[0];
278+
}
279+
280+
// Process each localizable attribute.
281+
$modified = false;
282+
foreach ( $localizable_attrs as $attr_name ) {
283+
if ( isset( $attrs[ $attr_name ] ) && is_string( $attrs[ $attr_name ] ) ) {
284+
// Skip if already escaped.
285+
if ( str_starts_with( $attrs[ $attr_name ], '<?php' ) ) {
286+
continue;
287+
}
288+
289+
// Escape the attribute value.
290+
$attrs[ $attr_name ] = self::escape_attribute( $attrs[ $attr_name ] );
291+
$modified = true;
292+
}
293+
}
294+
295+
// If we modified any attributes, re-encode to JSON.
296+
if ( $modified ) {
297+
$new_attrs_json = wp_json_encode( $attrs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
298+
return '<!-- wp:' . $block_name . ' ' . $new_attrs_json . ' ' . $self_closer . '-->';
299+
}
300+
301+
// Return original if nothing was modified.
302+
return $matches[0];
303+
},
304+
$content
305+
);
306+
}
215307
}

includes/create-theme/theme-templates.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,8 @@ public static function escape_text_in_template( $template ) {
286286
$template_blocks = parse_blocks( $template->content );
287287
$localized_blocks = CBT_Theme_Locale::escape_text_content_of_blocks( $template_blocks );
288288
$updated_template_content = serialize_blocks( $localized_blocks );
289+
// Process block attributes after serialization to avoid JSON encoding of PHP tags
290+
$updated_template_content = CBT_Theme_Locale::escape_block_attribute_strings( $updated_template_content );
289291
$template->content = $updated_template_content;
290292
return $template;
291293
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
require_once __DIR__ . '/base.php';
4+
5+
/**
6+
* Tests for the CBT_Theme_Locale::escape_text_content_of_blocks method with block attributes.
7+
*
8+
* @package Create_Block_Theme
9+
* @covers CBT_Theme_Locale::escape_text_content_of_blocks
10+
* @covers CBT_Theme_Locale::escape_block_attribute_strings
11+
* @group locale
12+
*/
13+
class CBT_Theme_Locale_EscapeBlockAttributes extends CBT_Theme_Locale_UnitTestCase {
14+
15+
/**
16+
* @dataProvider data_test_escape_block_attributes
17+
*/
18+
public function test_escape_block_attributes( $block_markup, $expected_markup ) {
19+
// Parse the block markup.
20+
$blocks = parse_blocks( $block_markup );
21+
// Escape the text content of the blocks.
22+
$escaped_blocks = CBT_Theme_Locale::escape_text_content_of_blocks( $blocks );
23+
// Serialize the blocks to get the markup.
24+
$escaped_markup = serialize_blocks( $escaped_blocks );
25+
// Process block attributes after serialization to avoid JSON encoding of PHP tags.
26+
$escaped_markup = CBT_Theme_Locale::escape_block_attribute_strings( $escaped_markup );
27+
28+
$this->assertEquals( $expected_markup, $escaped_markup, 'The markup result is not as the expected one.' );
29+
}
30+
31+
public function data_test_escape_block_attributes() {
32+
return array(
33+
34+
'search block with label, placeholder, and buttonText' => array(
35+
'block_markup' => '<!-- wp:search {"label":"Search","placeholder":"Type here...","buttonText":"Search"} /-->',
36+
'expected_markup' => '<!-- wp:search {"label":"<?php esc_attr_e(\'Search\', \'test-locale-theme\');?>","placeholder":"<?php esc_attr_e(\'Type here...\', \'test-locale-theme\');?>","buttonText":"<?php esc_attr_e(\'Search\', \'test-locale-theme\');?>"} /-->',
37+
),
38+
39+
'query-pagination-previous with label' => array(
40+
'block_markup' => '<!-- wp:query-pagination-previous {"label":"Previous"} /-->',
41+
'expected_markup' => '<!-- wp:query-pagination-previous {"label":"<?php esc_attr_e(\'Previous\', \'test-locale-theme\');?>"} /-->',
42+
),
43+
44+
'query-pagination-next with label' => array(
45+
'block_markup' => '<!-- wp:query-pagination-next {"label":"Next"} /-->',
46+
'expected_markup' => '<!-- wp:query-pagination-next {"label":"<?php esc_attr_e(\'Next\', \'test-locale-theme\');?>"} /-->',
47+
),
48+
49+
'comments-pagination-next with label' => array(
50+
'block_markup' => '<!-- wp:comments-pagination-next {"label":"⪻ newer"} /-->',
51+
'expected_markup' => '<!-- wp:comments-pagination-next {"label":"<?php esc_attr_e(\'⪻ newer\', \'test-locale-theme\');?>"} /-->',
52+
),
53+
54+
'comments-pagination-previous with label' => array(
55+
'block_markup' => '<!-- wp:comments-pagination-previous {"label":"older ⪼"} /-->',
56+
'expected_markup' => '<!-- wp:comments-pagination-previous {"label":"<?php esc_attr_e(\'older ⪼\', \'test-locale-theme\');?>"} /-->',
57+
),
58+
59+
'post-excerpt with moreText' => array(
60+
'block_markup' => '<!-- wp:post-excerpt {"moreText":"— read more","showMoreOnNewLine":false} /-->',
61+
'expected_markup' => '<!-- wp:post-excerpt {"moreText":"<?php esc_attr_e(\'— read more\', \'test-locale-theme\');?>","showMoreOnNewLine":false} /-->',
62+
),
63+
64+
'post-navigation-link with label' => array(
65+
'block_markup' => '<!-- wp:post-navigation-link {"label":"Custom Label"} /-->',
66+
'expected_markup' => '<!-- wp:post-navigation-link {"label":"<?php esc_attr_e(\'Custom Label\', \'test-locale-theme\');?>"} /-->',
67+
),
68+
69+
'search block with only some attributes' => array(
70+
'block_markup' => '<!-- wp:search {"placeholder":"Search..."} /-->',
71+
'expected_markup' => '<!-- wp:search {"placeholder":"<?php esc_attr_e(\'Search...\', \'test-locale-theme\');?>"} /-->',
72+
),
73+
74+
'query pagination blocks in context' => array(
75+
'block_markup' => '<!-- wp:query-pagination -->
76+
<!-- wp:query-pagination-previous {"label":"Previous"} /-->
77+
<!-- wp:query-pagination-numbers /-->
78+
<!-- wp:query-pagination-next {"label":"Next"} /-->
79+
<!-- /wp:query-pagination -->',
80+
'expected_markup' => '<!-- wp:query-pagination -->
81+
<!-- wp:query-pagination-previous {"label":"<?php esc_attr_e(\'Previous\', \'test-locale-theme\');?>"} /-->
82+
<!-- wp:query-pagination-numbers /-->
83+
<!-- wp:query-pagination-next {"label":"<?php esc_attr_e(\'Next\', \'test-locale-theme\');?>"} /-->
84+
<!-- /wp:query-pagination -->',
85+
),
86+
87+
'block without attributes should remain unchanged' => array(
88+
'block_markup' => '<!-- wp:search /-->',
89+
'expected_markup' => '<!-- wp:search /-->',
90+
),
91+
92+
'block with only non-translatable attributes should remain unchanged' => array(
93+
'block_markup' => '<!-- wp:search {"showLabel":false,"buttonPosition":"button-inside"} /-->',
94+
'expected_markup' => '<!-- wp:search {"showLabel":false,"buttonPosition":"button-inside"} /-->',
95+
),
96+
);
97+
}
98+
}

0 commit comments

Comments
 (0)