Index: trunk/extensions/Translate/Translate.php |
— | — | @@ -11,7 +11,7 @@ |
12 | 12 | * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later |
13 | 13 | */ |
14 | 14 | |
15 | | -define( 'TRANSLATE_VERSION', '9 (2008-08-04:1)' ); |
| 15 | +define( 'TRANSLATE_VERSION', '9 (2008-08-08:1)' ); |
16 | 16 | |
17 | 17 | $wgExtensionCredits['specialpage'][] = array( |
18 | 18 | 'name' => 'Translate', |
Index: trunk/extensions/Translate/scripts/sync-group.php |
— | — | @@ -0,0 +1,294 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +$optionsWithArgs = array( 'group', 'lang', 'start', 'end' ); |
| 5 | +require( dirname(__FILE__) . '/cli.inc' ); |
| 6 | + |
| 7 | +function showUsage() { |
| 8 | + STDERR( <<<EOT |
| 9 | +Options: |
| 10 | + --group comma separated list of group ids or * |
| 11 | + --lang comma separated list of language codes or * |
| 12 | + --norc do not add entries to recent changes table |
| 13 | + --help this help |
| 14 | + --noask skip all conflicts |
| 15 | + --start start of the last export (changes in wiki after this will conflict) |
| 16 | + --end end of the last export (changes in source before this wont conflict) |
| 17 | + --nocolor without colours |
| 18 | +EOT |
| 19 | +); |
| 20 | + exit( 1 ); |
| 21 | +} |
| 22 | + |
| 23 | +if ( isset( $options['help'] ) ) showUsage(); |
| 24 | + |
| 25 | + |
| 26 | +if (!isset($options['group'])) { |
| 27 | + STDERR( "ESG1: Message group id must be supplied with group parameter." ); |
| 28 | + exit(1); |
| 29 | +} |
| 30 | + |
| 31 | +$group = MessageGroups::getGroup( $options['group'] ); |
| 32 | +if ( $group === null ) { |
| 33 | + if ( $options['group'] === '*' ) { |
| 34 | + $mg = MessageGroups::singleton(); |
| 35 | + $groups = $mg->getGroups(); |
| 36 | + } else { |
| 37 | + STDERR( "ESG2: Invalid message group was given." ); |
| 38 | + exit(1); |
| 39 | + } |
| 40 | +} else { |
| 41 | + $groups = array( $group ); |
| 42 | +} |
| 43 | + |
| 44 | +if (!isset($options['lang'])) { |
| 45 | + STDERR( "ESG3: List of language codes must be supplied with lang parameter." ); |
| 46 | + exit(1); |
| 47 | +} |
| 48 | + |
| 49 | +$start = isset($options['start']) ? strtotime($options['start']) : false; |
| 50 | +$end = isset($options['end']) ? strtotime($options['end']) : false; |
| 51 | + |
| 52 | +STDOUT( "Conflict times: " . wfTimestamp( TS_ISO_8601, $start ) . " - " . wfTimestamp( TS_ISO_8601, $end ) ); |
| 53 | + |
| 54 | +$codes = array_filter( array_map( 'trim', explode( ',', $options['lang'] ) ) ); |
| 55 | + |
| 56 | +if ( $codes[0] === '*' ) { |
| 57 | + $langs = Language::getLanguageNames(); |
| 58 | + ksort( $langs ); |
| 59 | + $codes = array_keys($langs); |
| 60 | +} |
| 61 | + |
| 62 | +foreach ( $groups as &$group ) { |
| 63 | + if ($group->isMeta()) continue; |
| 64 | + |
| 65 | + STDOUT( "{$group->getLabel()} ", $group ); |
| 66 | + |
| 67 | + foreach ( $codes as $code ) { |
| 68 | + |
| 69 | + $file = $group->getMessageFileWithPath( $code ); |
| 70 | + if ( !$file ) continue; |
| 71 | + |
| 72 | + if ( !file_exists( $file ) ) continue; |
| 73 | + |
| 74 | + $cs = new ChangeSyncer( $group ); |
| 75 | + if ( isset($options['norc']) ) $cs->norc = true; |
| 76 | + if ( isset($options['noask']) ) $cs->interactive = false; |
| 77 | + if ( isset($options['nocolor']) ) $cs->nocolor = true; |
| 78 | + |
| 79 | + $ts = $cs->getTimestampsFromSvn( $file ); |
| 80 | + if ( !$ts ) $ts = $cs->getTimestampsFromFs( $file ); |
| 81 | + |
| 82 | + STDOUT( "Modify time for $code: " . wfTimestamp( TS_ISO_8601, $ts ) ); |
| 83 | + |
| 84 | + $count = $cs->checkConflicts( $code, $start, $end, $ts ); |
| 85 | + |
| 86 | + } |
| 87 | + unset( $group ); |
| 88 | +} |
| 89 | + |
| 90 | +class ChangeSyncer { |
| 91 | + public $group; |
| 92 | + public $norc = false; |
| 93 | + public $interactive = true; |
| 94 | + public $nocolor = false; |
| 95 | + |
| 96 | + public function __construct( MessageGroup $group ) { |
| 97 | + $this->group = $group; |
| 98 | + } |
| 99 | + |
| 100 | + // svn component from pecl doesn't seem to have this in quick sight |
| 101 | + public function getTimestampsFromSvn( $file ) { |
| 102 | + $file = escapeshellarg( $file ); |
| 103 | + $retval = 0; |
| 104 | + $output = wfShellExec( "svn info $file", $retval ); |
| 105 | + if ( $retval ) return false; |
| 106 | + |
| 107 | + |
| 108 | + $matches = array(); |
| 109 | + // PHP doesn't allow foo || return false; |
| 110 | + // Thank |
| 111 | + // you |
| 112 | + // PHP (for being an ass)! |
| 113 | + $regex = '^Last Changed Date: (.*) \('; |
| 114 | + $ok = preg_match( "~$regex~m", $output, $matches ); |
| 115 | + if ($ok) return strtotime( $matches[1] ); |
| 116 | + |
| 117 | + return false; |
| 118 | + } |
| 119 | + |
| 120 | + public function getTimestampsFromFs( $file ) { |
| 121 | + if ( !file_exists( $file ) ) return false; |
| 122 | + $stat = stat($file); |
| 123 | + return $stat['mtime']; |
| 124 | + } |
| 125 | + |
| 126 | + public function checkConflicts( $code, $startTs = false, $endTs = false, $changeTs = false ) { |
| 127 | + $messages = $this->group->load( $code ); |
| 128 | + if ( !count($messages) ) return; |
| 129 | + |
| 130 | + $collection = $this->group->initCollection( $code ); |
| 131 | + $this->group->fillCollection( $collection ); |
| 132 | + |
| 133 | + foreach ( $messages as $key => $translation ) { |
| 134 | + |
| 135 | + if ( !isset($collection[$key]) ) { |
| 136 | + //STDOUT( "Unknown key $key" ); |
| 137 | + continue; |
| 138 | + } |
| 139 | + |
| 140 | + |
| 141 | + |
| 142 | + $title = Title::makeTitleSafe( $this->group->namespaces[0], "$key/$code" ); |
| 143 | + |
| 144 | + $page = $title->getPrefixedText(); |
| 145 | + |
| 146 | + if ( $collection[$key]->database === null) { |
| 147 | + STDOUT("Importing $page as a new translation"); |
| 148 | + $this->import( $title, $translation, 'Importing a new translation' ); |
| 149 | + continue; |
| 150 | + } |
| 151 | + |
| 152 | + $current = str_replace( TRANSLATE_FUZZY, '', $collection[$key]->translation ); |
| 153 | + $translation = str_replace( TRANSLATE_FUZZY, '', $translation ); |
| 154 | + if ( $translation === $current ) continue; |
| 155 | + |
| 156 | + STDOUT("Conflict in " . $this->color('bold', $page). "!", $page); |
| 157 | + |
| 158 | + global $wgLang; |
| 159 | + $iso = 'xnY-xnm-xnd"T"xnH:xni:xns'; |
| 160 | + |
| 161 | + // Finally all is ok, now lets start comparing timestamps |
| 162 | + // Make sure we are comparing timestamps in same format |
| 163 | + $wikiTs = $this->getLastGoodChange( $title, $startTs ); |
| 164 | + if ( $wikiTs ) { |
| 165 | + $wikiTs = wfTimestamp( TS_UNIX, $wikiTs ); |
| 166 | + $wikiDate = $wgLang->sprintfDate( $iso, wfTimestamp( TS_MW, $wikiTs ) ); |
| 167 | + } else { |
| 168 | + $wikiDate = 'Unknown'; |
| 169 | + } |
| 170 | + |
| 171 | + if ( $startTs ) { |
| 172 | + $startTs = wfTimestamp( TS_UNIX, $startTs ); |
| 173 | + $startDate = $wgLang->sprintfDate( $iso, wfTimestamp( TS_MW, $startTs ) ); |
| 174 | + } else { |
| 175 | + $startDate = 'Unknown'; |
| 176 | + } |
| 177 | + |
| 178 | + if ( $endTs ) { |
| 179 | + $endTs = wfTimestamp( TS_UNIX, $endTs ); |
| 180 | + $endDate = $wgLang->sprintfDate( $iso, wfTimestamp( TS_MW, $endTs ) ); |
| 181 | + } else { |
| 182 | + $endDate = 'Unknown'; |
| 183 | + } |
| 184 | + |
| 185 | + if ( $changeTs ) { |
| 186 | + $changeTs = wfTimestamp( TS_UNIX, $changeTs ); |
| 187 | + $changeDate = $wgLang->sprintfDate( $iso, wfTimestamp( TS_MW, $changeTs ) ); |
| 188 | + } else { |
| 189 | + $changeDate = 'Unknown'; |
| 190 | + } |
| 191 | + |
| 192 | + if ( $changeTs ) { |
| 193 | + if ( $wikiTs > $startTs && $changeTs <= $endTs ) { |
| 194 | + STDOUT(" →Changed in wiki after export: IGNORE", $page); |
| 195 | + continue; |
| 196 | + } elseif ( !$wikiTs || ($changeTs > $endTs && $wikiTs < $startTs) ) { |
| 197 | + STDOUT(" →Changed in source after export: IMPORT", $page); |
| 198 | + $this->import( $title, $translation, 'Updating translation from external source' ); |
| 199 | + continue; |
| 200 | + } |
| 201 | + |
| 202 | + } |
| 203 | + |
| 204 | + if ( !$this->interactive ) continue; |
| 205 | + STDOUT(" →Needs manual resolution", $page); |
| 206 | + |
| 207 | + STDOUT("Source translation at $changeDate:"); |
| 208 | + STDOUT($this->color('blue', $translation) . "\n"); |
| 209 | + STDOUT("Wiki translation at $wikiDate:"); |
| 210 | + STDOUT($this->color('green', $current) . "\n"); |
| 211 | + |
| 212 | + do { |
| 213 | + STDOUT("Resolution: [S]kip [I]mport [C]onflict: ", 'foo' ); |
| 214 | + $action = fgets( STDIN ); |
| 215 | + $action = strtoupper( trim( $action ) ); |
| 216 | + if ( $action === 'S' ) break; |
| 217 | + if ( $action === 'I' ) { |
| 218 | + $this->import( $title, $translation, 'Updating translation from external source' ); |
| 219 | + break; |
| 220 | + } |
| 221 | + if ( $action === 'C' ) { |
| 222 | + $this->import( $title, TRANSLATE_FUZZY . $translation, 'Edit conflict between wiki and source' ); |
| 223 | + break; |
| 224 | + } |
| 225 | + } while( true ); |
| 226 | + |
| 227 | + } |
| 228 | + } |
| 229 | + |
| 230 | + public function color( $color, $text ) { |
| 231 | + switch ($color) { |
| 232 | + case 'blue': |
| 233 | + return "\033[1;34m$text\033[0m"; |
| 234 | + case 'green': |
| 235 | + return "\033[1;32m$text\033[0m"; |
| 236 | + case 'bold': |
| 237 | + return "\033[1m$text\033[0m"; |
| 238 | + default: |
| 239 | + return $text; |
| 240 | + } |
| 241 | + } |
| 242 | + |
| 243 | + public function getLastGoodChange( $title, $startTs = false ) { |
| 244 | + global $wgTranslateFuzzyBotName; |
| 245 | + |
| 246 | + $wikiTs = false; |
| 247 | + $revision = Revision::newFromTitle( $title ); |
| 248 | + while ( $revision ) { |
| 249 | + // No need to go back further |
| 250 | + if ( $startTs && $wikiTs && ( $wikiTs < $startTs ) ) break; |
| 251 | + |
| 252 | + if ( $revision->getRawUserText() === $wgTranslateFuzzyBotName ) { |
| 253 | + $revision = $revision->getPrevious(); |
| 254 | + continue; |
| 255 | + } |
| 256 | + |
| 257 | + $wikiTs = wfTimestamp( TS_UNIX, $revision->getTimestamp()); |
| 258 | + break; |
| 259 | + } |
| 260 | + |
| 261 | + return $wikiTs; |
| 262 | + } |
| 263 | + |
| 264 | + public function getImportUser() { |
| 265 | + static $user = null; |
| 266 | + if ( $user === null ) { |
| 267 | + global $wgTranslateFuzzyBotName; |
| 268 | + $user = User::newFromName( $wgTranslateFuzzyBotName ); |
| 269 | + |
| 270 | + if ( !$user->isLoggedIn() ) { |
| 271 | + STDOUT( "Creating user $wgTranslateFuzzyBotName" ); |
| 272 | + $user->addToDatabase(); |
| 273 | + } |
| 274 | + } |
| 275 | + |
| 276 | + return $user; |
| 277 | + } |
| 278 | + |
| 279 | + public function import( $title, $translation, $comment ) { |
| 280 | + global $wgUser; |
| 281 | + $old = $wgUser; |
| 282 | + $wgUser = $this->getImportUser(); |
| 283 | + |
| 284 | + $flags = EDIT_FORCE_BOT; |
| 285 | + if ( $this->norc ) $flags |= EDIT_SUPPRESS_RC; |
| 286 | + |
| 287 | + $article = new Article( $title ); |
| 288 | + STDOUT( "Importing {$title->getPrefixedText()}: ", $title ); |
| 289 | + $success = $article->doEdit( $translation, $comment, $flags ); |
| 290 | + STDOUT( $success ? 'OK' : 'FAILED', $title ); |
| 291 | + |
| 292 | + $wgUser = $old; |
| 293 | + } |
| 294 | + |
| 295 | +} |
\ No newline at end of file |
Property changes on: trunk/extensions/Translate/scripts/sync-group.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 296 | + native |
Index: trunk/extensions/Translate/README |
— | — | @@ -33,7 +33,8 @@ |
34 | 34 | </code> |
35 | 35 | |
36 | 36 | == Changes in version 10 == |
37 | | -* 2008-08-04:add "Other translations" link to Special:Prefexindex in sidebar toolbox |
| 37 | +* 2008-08-08:1 experimental sync-group to import external changes and keep them in sync |
| 38 | +* 2008-08-04:1 add "Other translations" link to Special:Prefexindex in sidebar toolbox |
38 | 39 | * 2008-07-29:2 support for variables and purging and fallbacks in page translation |
39 | 40 | * 2008-07-29:1 bug fixes and enhanced magic word support for AdvancedTranslate |
40 | 41 | * 2008-07-26:2 proper parents for branched messages |