Skip to content

Commit 26adeea

Browse files
authored
Merge pull request #460 from wp-cli/copilot/fix-1570774-111592377-5ffacbc1-5e8a-4e4c-b0c9-3ffb5c94276f
1 parent 76c354a commit 26adeea

2 files changed

Lines changed: 255 additions & 1 deletion

File tree

features/updatepo.feature

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,148 @@ Feature: Update existing PO files from a POT file
466466
"X-Domain: foo-plugin\n"
467467
"""
468468

469+
Scenario: Preserves obsolete translations and file-level comments with --no-purge
470+
Given an empty foo-plugin directory
471+
And a foo-plugin/foo-plugin.pot file:
472+
"""
473+
# Copyright (C) 2018 Foo Plugin
474+
# This file is distributed under the same license as the Foo Plugin package.
475+
msgid ""
476+
msgstr ""
477+
"Project-Id-Version: Foo Plugin\n"
478+
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
479+
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
480+
"Language-Team: LANGUAGE <LL@li.org>\n"
481+
"MIME-Version: 1.0\n"
482+
"Content-Type: text/plain; charset=UTF-8\n"
483+
"Content-Transfer-Encoding: 8bit\n"
484+
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
485+
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
486+
"X-Domain: foo-plugin\n"
487+
488+
#: foo-plugin.php:1
489+
msgid "Some string"
490+
msgstr ""
491+
"""
492+
And a foo-plugin/foo-plugin-de_DE.po file:
493+
"""
494+
# Copyright (C) 2018 Foo Plugin
495+
# This file is distributed under the same license as the Foo Plugin package.
496+
msgid ""
497+
msgstr ""
498+
"Project-Id-Version: Foo Plugin\n"
499+
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
500+
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
501+
"Language-Team: LANGUAGE <LL@li.org>\n"
502+
"Language: de_DE\n"
503+
"MIME-Version: 1.0\n"
504+
"Content-Type: text/plain; charset=UTF-8\n"
505+
"Content-Transfer-Encoding: 8bit\n"
506+
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
507+
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
508+
"X-Domain: foo-plugin\n"
509+
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
510+
511+
#: foo-plugin.php:1
512+
msgid "Some string"
513+
msgstr "Some translated string"
514+
515+
#~ msgid "Obsolete string"
516+
#~ msgstr "Veralteter String"
517+
"""
518+
519+
When I run `wp i18n update-po foo-plugin/foo-plugin.pot foo-plugin/foo-plugin-de_DE.po --no-purge`
520+
Then STDOUT should be:
521+
"""
522+
Success: Updated 1 file.
523+
"""
524+
And STDERR should be empty
525+
And the foo-plugin/foo-plugin-de_DE.po file should contain:
526+
"""
527+
# Copyright (C) 2018 Foo Plugin
528+
# This file is distributed under the same license as the Foo Plugin package.
529+
"""
530+
And the foo-plugin/foo-plugin-de_DE.po file should contain:
531+
"""
532+
#~ msgid "Obsolete string"
533+
#~ msgstr "Veralteter String"
534+
"""
535+
And the foo-plugin/foo-plugin-de_DE.po file should contain:
536+
"""
537+
#: foo-plugin.php:1
538+
msgid "Some string"
539+
msgstr "Some translated string"
540+
"""
541+
542+
Scenario: Removes obsolete translations and comments by default
543+
Given an empty foo-plugin directory
544+
And a foo-plugin/foo-plugin.pot file:
545+
"""
546+
msgid ""
547+
msgstr ""
548+
"Project-Id-Version: Foo Plugin\n"
549+
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
550+
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
551+
"Language-Team: LANGUAGE <LL@li.org>\n"
552+
"MIME-Version: 1.0\n"
553+
"Content-Type: text/plain; charset=UTF-8\n"
554+
"Content-Transfer-Encoding: 8bit\n"
555+
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
556+
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
557+
"X-Domain: foo-plugin\n"
558+
559+
#: foo-plugin.php:1
560+
msgid "Some string"
561+
msgstr ""
562+
"""
563+
And a foo-plugin/foo-plugin-de_DE.po file:
564+
"""
565+
# Copyright (C) 2018 Foo Plugin
566+
# This file is distributed under the same license as the Foo Plugin package.
567+
msgid ""
568+
msgstr ""
569+
"Project-Id-Version: Foo Plugin\n"
570+
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
571+
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
572+
"Language-Team: LANGUAGE <LL@li.org>\n"
573+
"Language: de_DE\n"
574+
"MIME-Version: 1.0\n"
575+
"Content-Type: text/plain; charset=UTF-8\n"
576+
"Content-Transfer-Encoding: 8bit\n"
577+
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
578+
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
579+
"X-Domain: foo-plugin\n"
580+
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
581+
582+
#: foo-plugin.php:1
583+
msgid "Some string"
584+
msgstr "Some translated string"
585+
586+
#~ msgid "Obsolete string"
587+
#~ msgstr "Veralteter String"
588+
"""
589+
590+
When I run `wp i18n update-po foo-plugin/foo-plugin.pot foo-plugin/foo-plugin-de_DE.po`
591+
Then STDOUT should be:
592+
"""
593+
Success: Updated 1 file.
594+
"""
595+
And STDERR should be empty
596+
And the foo-plugin/foo-plugin-de_DE.po file should not contain:
597+
"""
598+
# Copyright (C) 2018 Foo Plugin
599+
"""
600+
And the foo-plugin/foo-plugin-de_DE.po file should not contain:
601+
"""
602+
#~ msgid "Obsolete string"
603+
"""
604+
And the foo-plugin/foo-plugin-de_DE.po file should contain:
605+
"""
606+
#: foo-plugin.php:1
607+
msgid "Some string"
608+
msgstr "Some translated string"
609+
"""
610+
469611
Scenario: Updates PO-Revision-Date when updating a PO file
470612
Given an empty foo-plugin directory
471613
And a foo-plugin/foo-plugin.pot file:

src/UpdatePoCommand.php

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ class UpdatePoCommand extends WP_CLI_Command {
2727
* : PO file to update or a directory containing multiple PO files.
2828
* Defaults to all PO files in the source directory.
2929
*
30+
* [--purge]
31+
* : Remove obsolete strings and replace translator comments. Defaults to true.
32+
* By default, strings not found in the POT file are removed, and translator comments are replaced with those from the POT file.
33+
* Use `--no-purge` to preserve obsolete translations (marked with #~) and existing translator comments like copyright notices.
34+
*
3035
* ## EXAMPLES
3136
*
3237
* # Update all PO files from a POT file in the current directory.
@@ -41,6 +46,10 @@ class UpdatePoCommand extends WP_CLI_Command {
4146
* $ wp i18n update-po example-plugin.pot languages
4247
* Success: Updated 2 files.
4348
*
49+
* # Update PO files while keeping obsolete strings and translator comments.
50+
* $ wp i18n update-po example-plugin.pot --no-purge
51+
* Success: Updated 3 files.
52+
*
4453
* # Shows message when some files don't need updating.
4554
* $ wp i18n update-po example-plugin.pot languages
4655
* Success: Updated 2 files. 1 file unchanged.
@@ -73,6 +82,16 @@ public function __invoke( $args, $assoc_args ) {
7382

7483
$pot_translations = Translations::fromPoFile( $source );
7584

85+
// Build merge flags based on options
86+
$merge_flags = Merge::ADD | Merge::EXTRACTED_COMMENTS_THEIRS | Merge::REFERENCES_THEIRS | Merge::DOMAIN_OVERRIDE;
87+
88+
$purge = Utils\get_flag_value( $assoc_args, 'purge', true );
89+
90+
if ( $purge ) {
91+
// By default, remove obsolete entries and replace translator comments
92+
$merge_flags |= Merge::REMOVE | Merge::COMMENTS_THEIRS;
93+
}
94+
7695
$updated_count = 0;
7796
$unchanged_count = 0;
7897
/** @var DirectoryIterator $file */
@@ -86,17 +105,27 @@ public function __invoke( $args, $assoc_args ) {
86105
continue;
87106
}
88107

108+
// Preserve file-level comments when --no-purge is set
109+
$file_comments = '';
110+
if ( ! $purge ) {
111+
$file_comments = $this->extract_file_comments( $file->getPathname() );
112+
}
89113
$po_translations = Translations::fromPoFile( $file->getPathname() );
90114
$original_translations = clone $po_translations;
91115

92116
$po_translations->mergeWith(
93117
$pot_translations,
94-
Merge::ADD | Merge::REMOVE | Merge::COMMENTS_THEIRS | Merge::EXTRACTED_COMMENTS_THEIRS | Merge::REFERENCES_THEIRS | Merge::DOMAIN_OVERRIDE
118+
$merge_flags
95119
);
96120

97121
// Check if the translations actually changed by comparing the objects.
98122
$has_changes = $this->translations_differ( $original_translations, $po_translations );
99123

124+
// When using --no-purge, file-level comments being restored counts as a change.
125+
if ( ! $purge && ! empty( $file_comments ) ) {
126+
$has_changes = true;
127+
}
128+
100129
// Update PO-Revision-Date to current date and time in UTC.
101130
// Uses gmdate() for consistency across different server timezones.
102131
$po_translations->setHeader( 'PO-Revision-Date', gmdate( 'Y-m-d\TH:i:sP' ) );
@@ -109,6 +138,13 @@ public function __invoke( $args, $assoc_args ) {
109138
continue;
110139
}
111140

141+
// Restore file-level comments when --no-purge is set
142+
if ( ! $purge && ! empty( $file_comments ) ) {
143+
if ( ! $this->restore_file_comments( $file->getPathname(), $file_comments ) ) {
144+
WP_CLI::warning( sprintf( 'Could not restore file-level comments for %s', $file->getPathname() ) );
145+
}
146+
}
147+
112148
if ( $has_changes ) {
113149
++$updated_count;
114150
} else {
@@ -126,6 +162,74 @@ public function __invoke( $args, $assoc_args ) {
126162
WP_CLI::success( implode( '. ', $message_parts ) . '.' );
127163
}
128164

165+
/**
166+
* Extract file-level comments from a PO file.
167+
*
168+
* These are comments that appear before the first msgid in the file.
169+
*
170+
* @param string $file_path Path to the PO file.
171+
* @return string The file-level comments.
172+
*/
173+
private function extract_file_comments( $file_path ) {
174+
$content = file_get_contents( $file_path );
175+
if ( false === $content ) {
176+
return '';
177+
}
178+
179+
$lines = explode( "\n", $content );
180+
$file_comments = [];
181+
$found_msgid = false;
182+
183+
foreach ( $lines as $line ) {
184+
$trimmed = trim( $line );
185+
186+
// Stop when we hit the first msgid
187+
if ( preg_match( '/^msgid\s/', $trimmed ) ) {
188+
$found_msgid = true;
189+
break;
190+
}
191+
192+
// Collect comment lines
193+
if ( preg_match( '/^#([^.,:~]|$)/', $trimmed ) ) {
194+
$file_comments[] = $line;
195+
}
196+
}
197+
198+
return ! empty( $file_comments ) ? implode( "\n", $file_comments ) . "\n" : '';
199+
}
200+
201+
/**
202+
* Restore file-level comments to a PO file.
203+
*
204+
* @param string $file_path Path to the PO file.
205+
* @param string $comments The file-level comments to restore.
206+
* @return bool True on success, false on failure.
207+
*/
208+
private function restore_file_comments( $file_path, $comments ) {
209+
$content = file_get_contents( $file_path );
210+
if ( false === $content ) {
211+
return false;
212+
}
213+
214+
// Prepend the comments to the file content
215+
$updated_content = $comments . $content;
216+
217+
// Use atomic file operation with temporary file
218+
$temp_file = $file_path . '.tmp';
219+
if ( false === file_put_contents( $temp_file, $updated_content ) ) {
220+
return false;
221+
}
222+
223+
// Rename is atomic on most filesystems
224+
if ( ! rename( $temp_file, $file_path ) ) {
225+
// Clean up temp file on failure
226+
unlink( $temp_file );
227+
return false;
228+
}
229+
230+
return true;
231+
}
232+
129233
/**
130234
* Check if two Translations objects differ.
131235
*
@@ -216,6 +320,14 @@ private function reorder_translations( Translations $po_translations, Translatio
216320
}
217321
}
218322

323+
// Add any remaining translations from PO that aren't in POT (e.g., obsolete/disabled translations).
324+
foreach ( $po_translations as $po_entry ) {
325+
// Check if this entry is already in the ordered set.
326+
if ( ! $ordered->find( $po_entry ) ) {
327+
$ordered[] = $po_entry->getClone();
328+
}
329+
}
330+
219331
return $ordered;
220332
}
221333
}

0 commit comments

Comments
 (0)