23 public function __construct() {
24 parent::__construct();
25 $this->addDescription(
'Backport translations from one branch to another.' );
29 'Comma separated list of message group IDs (supports * wildcard) to backport',
35 'Source path for reading updated translations. Defaults to $wgTranslateGroupRoot.',
41 'Target path for writing backported translations',
47 'Only export a group if its export path matches this prefix (relative to target-path)',
52 'never-export-languages',
53 'Languages to not export',
57 $this->requireExtension(
'Translate' );
62 $config = $this->getConfig();
63 $logger = LoggerFactory::getInstance(
'Translate.GroupSynchronization' );
64 $groupPattern = $this->getOption(
'group' ) ??
'';
66 'Starting backports for groups {groups}',
67 [
'groups' => $groupPattern ]
70 $sourcePath = $this->getOption(
'source-path' ) ?: $config->get(
'TranslateGroupRoot' );
71 if ( !is_readable( $sourcePath ) ) {
72 $this->fatalError(
"Source directory is not readable ($sourcePath)." );
75 $targetPath = $this->getOption(
'target-path' );
76 if ( !is_writable( $targetPath ) ) {
77 $this->fatalError(
"Target directory is not writable ($targetPath)." );
80 $groupIds = MessageGroups::expandWildcards( explode(
',', trim( $groupPattern ) ) );
81 $groups = MessageGroups::getGroupsById( $groupIds );
82 if ( $groups === [] ) {
83 $this->fatalError(
"Pattern $groupPattern did not match any message groups." );
86 $neverExportLanguages = $this->csv2array(
87 $this->getOption(
'never-export-languages' ) ??
''
89 $supportedLanguages = Utilities::getLanguageNames(
'en' );
91 foreach ( $groups as $group ) {
92 $groupId = $group->getId();
94 if ( !$this->hasOption(
'filter-path' ) ) {
95 $this->error(
"Skipping $groupId: Not instance of FileBasedMessageGroup" );
100 if ( !$group->getFFS() instanceof
JsonFormat ) {
101 $this->error(
"Skipping $groupId: Only JSON format is supported" );
105 if ( $this->hasOption(
'filter-path' ) ) {
106 $filter = $this->getOption(
'filter-path' );
107 $exportPath = $group->getTargetFilename(
'*' );
108 if ( !$this->matchPath( $filter, $exportPath ) ) {
113 $sourceLanguage = $group->getSourceLanguage();
115 $sourceDefinitions = $this->loadDefinitions( $group, $sourcePath, $sourceLanguage );
116 $targetDefinitions = $this->loadDefinitions( $group, $targetPath, $sourceLanguage );
117 }
catch ( RuntimeException $e ) {
119 "Skipping $groupId: Error while loading definitions: {$e->getMessage()}\n"
124 $keyCompatibilityMap = $this->getKeyCompatibilityMap(
125 $sourceDefinitions[
'MESSAGES'],
126 $targetDefinitions[
'MESSAGES'],
130 if ( array_filter( $keyCompatibilityMap ) === [] ) {
131 $this->output(
"Skipping $groupId: No compatible keys found\n" );
136 $languages = array_keys( $group->getTranslatableLanguages() ?? $supportedLanguages );
137 $languagesToSkip = $neverExportLanguages;
138 $languagesToSkip[] = $sourceLanguage;
139 $languages = array_diff( $languages, $languagesToSkip );
141 foreach ( $languages as $language ) {
142 $status = $this->backport(
146 $keyCompatibilityMap,
150 $summary[$status][] = $language;
153 $numUpdated = count( $summary[
'updated' ] ?? [] );
154 $numAdded = count( $summary[
'new' ] ?? [] );
155 if ( ( $numUpdated + $numAdded ) > 0 ) {
158 "%s: Compatible keys: %d. Updated %d languages, %d new (%s)\n",
160 count( $keyCompatibilityMap ),
163 implode(
', ', $summary[
'new' ] ?? [] )
170 private function csv2array(
string $input ): array {
172 array_map(
'trim', explode(
',', $input ) ),
173 static function ( $v ) {
179 private function matchPath(
string $prefix,
string $full ): bool {
180 $prefix =
"./$prefix";
181 $length = strlen( $prefix );
182 return substr( $full, 0, $length ) === $prefix;
185 private function loadDefinitions(
190 $file = $path .
'/' . $group->getTargetFilename( $language );
192 if ( !file_exists( $file ) ) {
193 throw new RuntimeException(
"File $file does not exist" );
196 $contents = file_get_contents( $file );
197 return $group->getFFS()->readFromVariable( $contents );
209 private function getKeyCompatibilityMap( array $source, array $target, SimpleFormat $fileFormat ): array {
211 foreach ( $target as $key => $value ) {
212 $keys[$key] = isset( $source[ $key ] ) && $fileFormat->isContentEqual( $source[ $key ], $value );
217 private function backport(
221 array $keyCompatibilityMap,
225 $sourceTemplate = $this->loadDefinitions( $group, $source, $language );
226 }
catch ( RuntimeException $e ) {
227 return 'no definitions';
231 $targetTemplate = $this->loadDefinitions( $group, $targetPath, $language );
232 }
catch ( RuntimeException $e ) {
242 $fileFormat = $group->getFFS();
246 if ( !$fileFormat instanceof JsonFormat ) {
247 throw new RuntimeException(
248 "Expected file format type: " . JsonFormat::class .
'; got: ' . get_class( $fileFormat )
252 $combinedMessages = [];
254 foreach ( $keyCompatibilityMap as $key => $isCompatible ) {
255 $sourceValue = $sourceTemplate[
'MESSAGES'][$key] ??
null;
256 $targetValue = $targetTemplate[
'MESSAGES'][$key] ??
null;
259 if ( $targetValue !==
null ) {
260 $combinedMessages[$key] = $targetValue;
265 if ( !$isCompatible ) {
269 if ( $sourceValue !==
null && !$fileFormat->isContentEqual( $sourceValue, $targetValue ) ) {
271 if ( str_contains( $sourceValue,
'#FORMAL:' ) ) {
277 $combinedMessages[$key] = $sourceValue;
281 if ( !$hasUpdates ) {
286 $combinedAuthors = array_merge(
287 $targetTemplate[
'AUTHORS' ] ?? [],
288 $sourceTemplate[
'AUTHORS' ] ?? []
290 $combinedAuthors = array_unique( $combinedAuthors );
291 $combinedAuthors = $fileFormat->filterAuthors( $combinedAuthors, $language );
293 $targetTemplate[
'AUTHORS'] = $combinedAuthors;
294 $targetTemplate[
'MESSAGES'] = $combinedMessages;
296 $backportedContent = $fileFormat->generateFile( $targetTemplate );
298 $targetFilename = $targetPath .
'/' . $group->getTargetFilename( $language );
299 if ( file_exists( $targetFilename ) ) {
300 $currentContent = file_get_contents( $targetFilename );
302 if ( $fileFormat->shouldOverwrite( $currentContent, $backportedContent ) ) {
303 file_put_contents( $targetFilename, $backportedContent );
307 file_put_contents( $targetFilename, $backportedContent );
return[ 'Translate:AggregateGroupManager'=> static function(MediaWikiServices $services):AggregateGroupManager { return new AggregateGroupManager( $services->getTitleFactory());}, 'Translate:AggregateGroupMessageGroupFactory'=> static function(MediaWikiServices $services):AggregateGroupMessageGroupFactory { return new AggregateGroupMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateComparator'=> static function(MediaWikiServices $services):ExternalMessageSourceStateComparator { return new ExternalMessageSourceStateComparator(new SimpleStringComparator(), $services->getRevisionLookup(), $services->getPageStore());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), $services->get( 'Translate:MessageIndex'), $services->getTitleFactory(), new ServiceOptions(ExternalMessageSourceStateImporter::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:FileBasedMessageGroupFactory'=> static function(MediaWikiServices $services):FileBasedMessageGroupFactory { return new FileBasedMessageGroupFactory(new MessageGroupConfigurationParser(), new ServiceOptions(FileBasedMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookDefinedMessageGroupFactory'=> static function(MediaWikiServices $services):HookDefinedMessageGroupFactory { return new HookDefinedMessageGroupFactory( $services->get( 'Translate:HookRunner'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleMessageGroupFactory'=> static function(MediaWikiServices $services):MessageBundleMessageGroupFactory { return new MessageBundleMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'), new ServiceOptions(MessageBundleMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:MessageBundleTranslationLoader'=> static function(MediaWikiServices $services):MessageBundleTranslationLoader { return new MessageBundleTranslationLoader( $services->getLanguageFallback());}, 'Translate:MessageGroupMetadata'=> static function(MediaWikiServices $services):MessageGroupMetadata { return new MessageGroupMetadata( $services->getDBLoadBalancer());}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getDBLoadBalancer(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getDBLoadBalancer(), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->get( 'Translate:MessageGroupMetadata'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageGroupSubscription'=> static function(MediaWikiServices $services):MessageGroupSubscription { return new MessageGroupSubscription($services->get( 'Translate:MessageGroupSubscriptionStore'), $services->getJobQueueGroup(), $services->getUserIdentityLookup(), LoggerFactory::getInstance( 'Translate.MessageGroupSubscription'), new ServiceOptions(MessageGroupSubscription::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:MessageGroupSubscriptionHookHandler'=> static function(MediaWikiServices $services):MessageGroupSubscriptionHookHandler { return new MessageGroupSubscriptionHookHandler($services->get( 'Translate:MessageGroupSubscription'), $services->getUserFactory());}, 'Translate:MessageGroupSubscriptionStore'=> static function(MediaWikiServices $services):MessageGroupSubscriptionStore { return new MessageGroupSubscriptionStore( $services->getDBLoadBalancerFactory());}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=(array) $services->getMainConfig() ->get( 'TranslateMessageIndex');$class=array_shift( $params);$implementationMap=['HashMessageIndex'=> HashMessageIndex::class, 'CDBMessageIndex'=> CDBMessageIndex::class, 'DatabaseMessageIndex'=> DatabaseMessageIndex::class, 'hash'=> HashMessageIndex::class, 'cdb'=> CDBMessageIndex::class, 'database'=> DatabaseMessageIndex::class,];$messageIndexStoreClass=$implementationMap[$class] ?? $implementationMap['database'];return new MessageIndex(new $messageIndexStoreClass, $services->getMainWANObjectCache(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), LoggerFactory::getInstance( 'Translate'), $services->getMainObjectStash(), $services->getDBLoadBalancerFactory(), $services->get( 'Translate:MessageGroupSubscription'), new ServiceOptions(MessageIndex::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getDBLoadBalancer(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore( $services->getDBLoadBalancer());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleDeleter'=> static function(MediaWikiServices $services):TranslatableBundleDeleter { return new TranslatableBundleDeleter($services->getMainObjectStash(), $services->getJobQueueGroup(), $services->get( 'Translate:SubpageListBuilder'), $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup(), $services->getNamespaceInfo(), $services->getTitleFactory());}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getDBLoadBalancerFactory(), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getDBLoadBalancer() ->getConnection(DB_PRIMARY), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageMarker'=> static function(MediaWikiServices $services):TranslatablePageMarker { return new TranslatablePageMarker($services->getDBLoadBalancer(), $services->getJobQueueGroup(), $services->getLinkRenderer(), MessageGroups::singleton(), $services->get( 'Translate:MessageIndex'), $services->getTitleFormatter(), $services->getTitleParser(), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:TranslatablePageStateStore'), $services->get( 'Translate:TranslationUnitStoreFactory'), $services->get( 'Translate:MessageGroupMetadata'), $services->getWikiPageFactory(), $services->get( 'Translate:TranslatablePageView'));}, 'Translate:TranslatablePageMessageGroupFactory'=> static function(MediaWikiServices $services):TranslatablePageMessageGroupFactory { return new TranslatablePageMessageGroupFactory(new ServiceOptions(TranslatablePageMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStateStore'=> static function(MediaWikiServices $services):TranslatablePageStateStore { return new TranslatablePageStateStore($services->get( 'Translate:PersistentCache'), $services->getPageStore());}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), $services->get( 'Translate:RevTagStore'), $services->getDBLoadBalancer(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:TranslatablePageView'=> static function(MediaWikiServices $services):TranslatablePageView { return new TranslatablePageView($services->getDBLoadBalancerFactory(), $services->get( 'Translate:TranslatablePageStateStore'), new ServiceOptions(TranslatablePageView::SERVICE_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslateSandbox'=> static function(MediaWikiServices $services):TranslateSandbox { return new TranslateSandbox($services->getUserFactory(), $services->getDBLoadBalancer(), $services->getPermissionManager(), $services->getAuthManager(), $services->getUserGroupManager(), $services->getActorStore(), $services->getUserOptionsManager(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), new ServiceOptions(TranslateSandbox::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnection(DB_REPLICA);return new TranslationStashStorage( $db);}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getDBLoadBalancer());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}]
@phpcs-require-sorted-array