1212 */
1313final class StrictPoLoader extends Loader
1414{
15+ /** @var bool */
16+ public $ throwOnWarning = false ;
17+ /** @var bool */
18+ public $ displayErrorLine = false ;
19+
1520 /** @var Translations */
1621 private $ translations ;
1722 /** @var Translation */
@@ -26,58 +31,49 @@ final class StrictPoLoader extends Loader
2631 private $ pluralCount ;
2732 /** @var bool */
2833 private $ inPreviousPart ;
29- /** @var bool */
30- private $ throwOnWarning ;
3134 /** @var string[] */
3235 private $ warnings = [];
3336 /** @var bool */
3437 private $ isDisabled ;
38+ /** @var bool */
39+ private $ displayLineColumn ;
3540
3641 /**
3742 * Generates a Translations object from a .po based string
3843 */
3944 public function loadString (string $ data , Translations $ translations = null ): Translations
4045 {
41- return $ this ->loadStringExtended (...func_get_args ());
42- }
43-
44- /**
45- * Generates a Translations object from a .po based string with extra options
46- */
47- public function loadStringExtended (
48- string $ data ,
49- Translations $ translations = null ,
50- bool $ throwOnWarning = false
51- ): Translations {
5246 $ this ->data = $ data ;
5347 $ this ->position = 0 ;
5448 $ this ->translations = parent ::loadString ($ this ->data , $ translations );
5549 $ this ->header = $ this ->translations ->find (null , '' );
5650 $ this ->pluralCount = $ this ->translations ->getHeaders ()->getPluralForm ()[0 ] ?? null ;
57- $ this ->throwOnWarning = $ throwOnWarning ;
5851 $ this ->warnings = [];
5952 for ($ length = strlen ($ this ->data ); $ this ->newEntry (); $ this ->saveEntry ()) {
6053 for ($ hasComment = false ; $ this ->readComment (); $ hasComment = true );
6154 $ this ->readWhitespace ();
6255 // End of data
6356 if ($ this ->position >= $ length ) {
6457 if ($ hasComment ) {
65- $ this ->addWarning ("Comment ignored at the end of the string at byte {$ this ->position }" );
58+ $ this ->addWarning ("Comment ignored at the end of the string {$ this ->getErrorPosition () }" );
6659 }
6760 break ;
6861 }
6962 $ this ->readContext ();
7063 $ this ->readOriginal ();
64+ if ($ this ->translations ->has ($ this ->translation )) {
65+ throw new Exception ("Duplicated entry {$ this ->getErrorPosition ()}" );
66+ }
7167 if (!$ this ->readPlural ()) {
7268 $ this ->readTranslation ();
7369 continue ;
7470 }
7571 for ($ count = 0 ; $ this ->readPluralTranslation (!$ count ); ++$ count );
7672 $ count !== ($ this ->pluralCount ?? $ count ) && $ this ->addWarning ("The translation has {$ count } plural "
77- . "forms, while the header expects {$ this ->pluralCount } at byte {$ this ->position }" );
73+ . "forms, while the header expects {$ this ->pluralCount }{$ this ->getErrorPosition () }" );
7874 }
7975 if (!$ this ->header ) {
80- $ this ->addWarning ("The loaded string has no header translation at byte {$ this ->position }" );
76+ $ this ->addWarning ("The loaded string has no header translation {$ this ->getErrorPosition () }" );
8177 }
8278
8379 return $ this ->translations ;
@@ -112,9 +108,6 @@ private function saveEntry(): void
112108
113109 return ;
114110 }
115- if ($ this ->translations ->has ($ this ->translation )) {
116- throw new Exception ("Duplicated entry at byte {$ this ->position }" );
117- }
118111 $ this ->translations ->add ($ this ->translation );
119112 }
120113
@@ -154,7 +147,7 @@ private function readChar(string $char): bool
154147 private function readCharset (string $ charset , int $ min , int $ max , string $ name ): string
155148 {
156149 if (($ length = strspn ($ this ->data , $ charset , $ this ->position , $ max )) < $ min ) {
157- throw new Exception ("Expected at least {$ min } occurrence of {$ name } characters at byte {$ this ->position }" );
150+ throw new Exception ("Expected at least {$ min } occurrence of {$ name } characters {$ this ->getErrorPosition () }" );
158151 }
159152
160153 return substr ($ this ->data , ($ this ->position += $ length ) - $ length , $ length );
@@ -184,7 +177,7 @@ private function readQuotedString(?string $context = null): string
184177 $ this ->position = $ checkpoint ;
185178 break ;
186179 }
187- throw new Exception ("Expected an opening quote at byte {$ this ->position }" );
180+ throw new Exception ("Expected an opening quote {$ this ->getErrorPosition () }" );
188181 }
189182 $ isNewPart = false ;
190183 // Collects chars until an edge case is found
@@ -204,14 +197,14 @@ private function readQuotedString(?string $context = null): string
204197 // Unexpected newline
205198 case "\r" :
206199 case "\n" :
207- throw new Exception ("Newline character must be escaped at byte {$ this ->position }" );
200+ throw new Exception ("Newline character must be escaped {$ this ->getErrorPosition () }" );
208201 // Unexpected end of file
209202 case null :
210- throw new Exception ("Expected a closing quote at byte {$ this ->position }" );
203+ throw new Exception ("Expected a closing quote {$ this ->getErrorPosition () }" );
211204 }
212205 }
213206 if ($ context && strlen ($ data ) && strpbrk ($ data [0 ] . $ data [strlen ($ data ) - 1 ], "\r\n" ) && !$ this ->isHeader ()) {
214- $ this ->addWarning ("$ context cannot start nor end with a newline at byte {$ this ->position }" );
207+ $ this ->addWarning ("$ context cannot start nor end with a newline {$ this ->getErrorPosition () }" );
215208 }
216209
217210 return $ data ;
@@ -230,7 +223,7 @@ private function readEscape(): string
230223 case strpbrk ($ char , $ octalDigits = '01234567 ' ):
231224 // GNU gettext fails with an octal above the signed char range
232225 if (($ decimal = octdec ($ char . $ this ->readCharset ($ octalDigits , 0 , 2 , 'octal ' ))) > 127 ) {
233- throw new Exception ("Octal value out of range [0, 0177] at byte {$ this ->position }" );
226+ throw new Exception ("Octal value out of range [0, 0177] {$ this ->getErrorPosition () }" );
234227 }
235228
236229 return chr ($ decimal );
@@ -247,7 +240,7 @@ private function readEscape(): string
247240
248241 return mb_convert_encoding (hex2bin ($ value ), 'UTF-8 ' , 'UTF- ' . ($ digits * 4 ));
249242 }
250- throw new Exception ("Invalid escaped character at byte {$ this ->position }" );
243+ throw new Exception ("Invalid escaped character {$ this ->getErrorPosition () }" );
251244 }
252245
253246 /**
@@ -270,15 +263,15 @@ private function readComment(): bool
270263 break ;
271264 case '~ ' :
272265 if ($ this ->translation ->getPreviousOriginal () !== null ) {
273- throw new Exception ("Inconsistent use of #~ at byte {$ this ->position }" );
266+ throw new Exception ("Inconsistent use of #~ {$ this ->getErrorPosition () }" );
274267 }
275268 $ this ->translation ->disable ();
276269 $ this ->isDisabled = true ;
277270 break ;
278271 case '| ' :
279272 if ($ this ->translation ->getPreviousOriginal () !== null ) {
280273 throw new Exception ('Cannot redeclare the previous comment #|, '
281- . "ensure the definitions are in the right order at byte {$ this ->position }" );
274+ . "ensure the definitions are in the right order {$ this ->getErrorPosition () }" );
282275 }
283276 $ this ->inPreviousPart = true ;
284277 $ this ->translation ->setPreviousContext ($ this ->readIdentifier ('msgctxt ' ));
@@ -320,7 +313,7 @@ private function readIdentifier(string $identifier, bool $throwIfNotFound = fals
320313 return $ this ->readQuotedString ($ identifier );
321314 }
322315 if ($ throwIfNotFound ) {
323- throw new Exception ("Expected $ identifier at byte {$ this ->position }" );
316+ throw new Exception ("Expected $ identifier {$ this ->getErrorPosition () }" );
324317 }
325318 $ this ->position = $ checkpoint ;
326319
@@ -359,7 +352,7 @@ private function readTranslation(): void
359352 {
360353 $ this ->readWhitespace ();
361354 if (!$ this ->readString ('msgstr ' )) {
362- throw new Exception ("Expected msgstr at byte {$ this ->position }" );
355+ throw new Exception ("Expected msgstr {$ this ->getErrorPosition () }" );
363356 }
364357 $ this ->translation ->translate ($ this ->readQuotedString ('msgstr ' ));
365358 }
@@ -372,27 +365,27 @@ private function readPluralTranslation(bool $throwIfNotFound = false): bool
372365 $ this ->readWhitespace ();
373366 if (!$ this ->readString ('msgstr ' )) {
374367 if ($ throwIfNotFound ) {
375- throw new Exception ("Expected indexed msgstr at byte {$ this ->position }" );
368+ throw new Exception ("Expected indexed msgstr {$ this ->getErrorPosition () }" );
376369 }
377370
378371 return false ;
379372 }
380373 $ this ->readWhitespace ();
381374 if (!$ this ->readChar ('[ ' )) {
382- throw new Exception ("Expected character \"[ \" at byte {$ this ->position }" );
375+ throw new Exception ("Expected character \"[ \"{$ this ->getErrorPosition () }" );
383376 }
384377 $ this ->readWhitespace ();
385378 $ index = (int ) $ this ->readCharset ('0123456789 ' , 1 , PHP_INT_MAX , 'numeric ' );
386379 $ this ->readWhitespace ();
387380 if (!$ this ->readChar ('] ' )) {
388- throw new Exception ("Expected character \"] \" at byte {$ this ->position }" );
381+ throw new Exception ("Expected character \"] \"{$ this ->getErrorPosition () }" );
389382 }
390383 $ translations = $ this ->translation ->getPluralTranslations ();
391384 if (($ translation = $ this ->translation ->getTranslation ()) !== null ) {
392385 array_unshift ($ translations , $ translation );
393386 }
394387 if (count ($ translations ) !== (int ) $ index ) {
395- throw new Exception ("The msgstr has an invalid index at byte {$ this ->position }" );
388+ throw new Exception ("The msgstr has an invalid index {$ this ->getErrorPosition () }" );
396389 }
397390 $ data = $ this ->readQuotedString ('msgstr ' );
398391 $ translations [] = $ data ;
@@ -423,7 +416,7 @@ private function processHeader(): void
423416 $ this ->pluralCount = $ headers ->getPluralForm ()[0 ] ?? null ;
424417 foreach (['Language ' , 'Plural-Forms ' , 'Content-Type ' ] as $ header ) {
425418 if (($ headers ->get ($ header ) ?? '' ) === '' ) {
426- $ this ->addWarning ("$ header header not declared or empty at byte {$ this ->position }" );
419+ $ this ->addWarning ("$ header header not declared or empty {$ this ->getErrorPosition () }" );
427420 }
428421 }
429422 }
@@ -441,14 +434,14 @@ private function readHeaders(string $data): array
441434 if (preg_match ('/^[\w-]+:/ ' , $ line )) {
442435 [$ name , $ value ] = explode (': ' , $ line , 2 );
443436 if (isset ($ headers [$ name ])) {
444- $ this ->addWarning ("Header already defined at byte {$ this ->position }" );
437+ $ this ->addWarning ("Header already defined {$ this ->getErrorPosition () }" );
445438 }
446439 $ headers [$ name ] = trim ($ value );
447440 continue ;
448441 }
449442 // Data without a definition
450443 if ($ name === null ) {
451- $ this ->addWarning ("Malformed header name at byte {$ this ->position }" );
444+ $ this ->addWarning ("Malformed header name {$ this ->getErrorPosition () }" );
452445 continue ;
453446 }
454447 $ headers [$ name ] .= $ line ;
@@ -475,4 +468,18 @@ private function isHeader(): bool
475468 {
476469 return $ this ->translation ->getOriginal () === '' && $ this ->translation ->getContext () === null ;
477470 }
471+
472+ /**
473+ * Retrieves the position where an error was detected
474+ */
475+ private function getErrorPosition (): string
476+ {
477+ if ($ this ->displayErrorLine ) {
478+ $ pieces = preg_split ("/ \\r \\n| \\n \\r| \\n| \\r/ " , substr ($ this ->data , 0 , $ this ->position ));
479+ $ line = count ($ pieces );
480+ $ column = strlen (end ($ pieces ));
481+ return " at line {$ line } column {$ column }" ;
482+ }
483+ return " at byte {$ this ->position }" ;
484+ }
478485}
0 commit comments