Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 78
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImportTranslationsFromCsv
0.00% covered (danger)
0.00%
0 / 78
0.00% covered (danger)
0.00%
0 / 4
210
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
90
 progressReporter
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 filterEmptyTranslations
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
5
6use MediaWiki\Extension\Translate\Services;
7use MediaWiki\Extension\Translate\Utilities\BaseMaintenanceScript;
8use MediaWiki\MediaWikiServices;
9use MediaWiki\Title\Title;
10
11/**
12 * Script to import translations from a CSV file
13 * @since 2022.06
14 * @license GPL-2.0-or-later
15 * @author Abijeet Patro
16 */
17class ImportTranslationsFromCsv extends BaseMaintenanceScript {
18    public function __construct() {
19        parent::__construct();
20        $this->addDescription( 'Import translations for a CSV file' );
21
22        $this->addArg(
23            'file',
24            'Path to CSV file to import',
25            self::REQUIRED
26        );
27
28        $this->addOption(
29            'summary',
30            'The change summary when updating the translations',
31            self::REQUIRED,
32            self::HAS_ARG
33        );
34
35        $this->addOption(
36            'user',
37            'User ID of the user performing the import',
38            self::REQUIRED,
39            self::HAS_ARG
40        );
41
42        $this->addOption(
43            'really',
44            'Should the import actually be performed',
45            self::OPTIONAL,
46            self::NO_ARG
47        );
48
49        $this->requireExtension( 'Translate' );
50    }
51
52    public function execute() {
53        $csvFilePath = $this->getArg( 0 );
54
55        $username = $this->getOption( 'user' );
56        $summary = $this->getOption( 'summary' );
57
58        // Validate the parameters
59        if ( trim( $summary ) === '' ) {
60            $this->fatalError( 'Please provide a non-empty "summary"' );
61        }
62
63        $userFactory = MediaWikiServices::getInstance()->getUserFactory();
64        $user = $userFactory->newFromName( $username );
65
66        if ( $user === null || !$user->isRegistered() ) {
67            $this->fatalError( "User $username does not exist." );
68        }
69
70        // Validate and parse the CSV file
71        $csvImporter = Services::getInstance()->getCsvTranslationImporter();
72        $status = $csvImporter->parseFile( $csvFilePath );
73
74        if ( $status->isOK() ) {
75            $messagesWithTranslations = $status->getValue();
76            $output = "\n";
77            foreach ( $messagesWithTranslations as $messageTranslations ) {
78                $translations = $messageTranslations[ 'translations' ];
79                $output .= '* ' . count( $this->filterEmptyTranslations( $translations ) ) .
80                    ' translation(s) to import for ' .
81                    $messageTranslations['messageTitle'] . "\n";
82            }
83
84            $this->output( $output . "\n" );
85        } else {
86            $this->error( "Error during processing:\n" );
87            $this->error( (string)$status );
88            $this->fatalError( 'Exiting...' );
89        }
90
91        if ( !$this->hasOption( 'really' ) ) {
92            $this->output( "\nUse option --really to perform the import.\n" );
93            return true;
94        }
95
96        // Start the actual import of translations
97        $this->output( "\nProceeding with import...\n\n" );
98        $importStatus = $csvImporter->importData(
99            $status->getValue(), $user, trim( $summary ), [ $this, 'progressReporter' ]
100        );
101
102        if ( $importStatus->isOK() ) {
103            $this->output( "\nSuccess: Import done\n" );
104        } else {
105            $this->output( "\nImport failed. See errors:\n" );
106            $failedImportStatuses = $importStatus->getValue();
107            foreach ( $failedImportStatuses as $translationTitleText => $status ) {
108                $this->output( "\nImport failed for $translationTitleText:\n" );
109                $this->output( $status );
110            }
111
112            return false;
113        }
114
115        return true;
116    }
117
118    public function progressReporter(
119        Title $title,
120        array $messageImportStatuses,
121        int $total,
122        int $processed
123    ): void {
124        $paddedProcessed = str_pad( (string)$processed, strlen( (string)$total ), ' ', STR_PAD_LEFT );
125        $progressCounter = "($paddedProcessed/$total)";
126
127        $successCount = 0;
128        $failCount = 0;
129        foreach ( $messageImportStatuses as $messageImportStatus ) {
130            if ( $messageImportStatus->isOK() ) {
131                $successCount++;
132            } else {
133                $failCount++;
134            }
135        }
136
137        $this->output(
138            "$progressCounter Imported translations for {$title->getPrefixedText()} with $failCount " .
139            "failure(s) and $successCount successful import(s) ...\n"
140        );
141    }
142
143    private function filterEmptyTranslations( array $translations ): array {
144        return array_filter( $translations, 'strlen' );
145    }
146}