@@ -33,6 +33,60 @@ abstract class Translator {
3333 private trimForLogs ( text : string ) {
3434 return `${ text . slice ( 0 , 80 ) } ${ text . length > 80 ? '...' : '' } ` ;
3535 }
36+
37+ /**
38+ * Validate that placeholders are all present and unchanged before and after translation.
39+ */
40+ public validateTemplatePlaceholders ( original : string , translated : string ) : boolean {
41+ const extractPlaceholders = ( s : string ) =>
42+ Array . from ( s . matchAll ( / \$ \{ [ ^ } ] * } / g) )
43+ . map ( ( m ) => m [ 0 ] )
44+ . sort ( ) ;
45+ const originalSpans = extractPlaceholders ( original ) ;
46+ const translatedSpans = extractPlaceholders ( translated ) ;
47+ return JSON . stringify ( originalSpans ) === JSON . stringify ( translatedSpans ) ;
48+ }
49+
50+ /**
51+ * Validate that the HTML structure is the same before and after translation.
52+ */
53+ public validateTemplateHTML ( original : string , translated : string ) : boolean {
54+ // Attributes that are allowed to be translated
55+ const TRANSLATABLE_ATTRIBUTES = [ 'alt' , 'title' , 'placeholder' , 'aria-label' , 'aria-describedby' , 'aria-labelledby' , 'value' ] ;
56+
57+ const parseHTMLStructure = ( s : string ) => {
58+ const tags = Array . from ( s . matchAll ( / < ( [ ^ > ] + ) > / g) ) ;
59+ return tags . map ( ( match ) => {
60+ const tagContent = match [ 1 ] ;
61+ const tagName = tagContent . split ( / \s / ) . at ( 0 ) ?. toLowerCase ( ) ;
62+
63+ // Extract attributes, excluding translatable ones
64+ const attributes : string [ ] = [ ] ;
65+ const attrMatches = Array . from ( tagContent . matchAll ( / ( \w + ) (?: = [ " ' ] ( [ ^ " ' ] * ) [ " ' ] ) ? / g) ) ;
66+
67+ for ( const attrMatch of attrMatches ) {
68+ const attrName = attrMatch [ 1 ] . toLowerCase ( ) ;
69+ const attrValue = attrMatch [ 2 ] || '' ;
70+
71+ // Only include non-translatable attributes in comparison
72+ if ( ! TRANSLATABLE_ATTRIBUTES . includes ( attrName ) ) {
73+ attributes . push ( `${ attrName } ="${ attrValue } "` ) ;
74+ }
75+ }
76+
77+ return {
78+ tagName,
79+ attributes : attributes . sort ( ) ,
80+ } ;
81+ } ) ;
82+ } ;
83+
84+ const originalStructure = parseHTMLStructure ( original ) ;
85+ const translatedStructure = parseHTMLStructure ( translated ) ;
86+
87+ // Compare structures (tag names and non-translatable attributes)
88+ return JSON . stringify ( originalStructure ) === JSON . stringify ( translatedStructure ) ;
89+ }
3690}
3791
3892export default Translator ;
0 commit comments