Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
StatsTable.php
1<?php
2declare( strict_types=1 );
3
4namespace MediaWiki\Extension\Translate\Statistics;
5
6use Html;
7use HtmlArmor;
8use Language;
10use MediaWiki\Linker\LinkRenderer;
11use Message;
12use MessageGroup;
14use MessageLocalizer;
15use SpecialPage;
16use TitleValue;
18use Xml;
19
28 protected $translate;
30 private $linkRenderer;
32 private $configHelper;
34 private $messageLocalizer;
36 protected $language;
38 protected $mainColumnHeader;
40 protected $extraColumns = [];
41
42 public function __construct(
43 LinkRenderer $linkRenderer,
44 ConfigHelper $configHelper,
45 MessageLocalizer $messageLocalizer,
46 Language $language
47 ) {
48 $this->translate = SpecialPage::getTitleValueFor( 'Translate' );
49 $this->linkRenderer = $linkRenderer;
50 $this->configHelper = $configHelper;
51 $this->messageLocalizer = $messageLocalizer;
52 $this->language = $language;
53 }
54
62 public function element( string $in, string $bgcolor = '', string $sort = '' ): string {
63 $attributes = [];
64
65 if ( $sort ) {
66 $attributes['data-sort-value'] = $sort;
67 }
68
69 if ( $bgcolor ) {
70 $attributes['style'] = 'background-color: #' . $bgcolor;
71 }
72
73 $element = Html::element( 'td', $attributes, $in );
74
75 return $element;
76 }
77
78 public function getBackgroundColor( float $percentage, bool $fuzzy = false ): string {
79 if ( $fuzzy ) {
80 // Steeper scale for fuzzy
81 // (0), [0-2), [2-4), ... [12-100)
82 $index = min( 7, ceil( 50 * $percentage ) );
83 $colors = [
84 '', 'fedbd7', 'fecec8', 'fec1b9',
85 'fcb5ab', 'fba89d', 'f89b8f', 'f68d81'
86 ];
87 return $colors[ $index ];
88 }
89
90 // https://gka.github.io/palettes/#colors=#36c,#eaf3ff|steps=20|bez=1|coL=1
91 // Color groups for (0-10], (10-20], ... (90-100], (100)
92 $index = floor( $percentage * 10 );
93 $colors = [
94 'eaf3ff', 'e2ebfc', 'dae3fa', 'd2dbf7', 'c9d4f5',
95 'c1ccf2', 'b8c4ef', 'b1bced', 'a8b4ea', '9fade8',
96 '96a6e5'
97 ];
98
99 return $colors[ $index ];
100 }
101
102 private function getMainColumnHeader(): string {
103 return $this->mainColumnHeader;
104 }
105
106 public function setMainColumnHeader( Message $msg ): void {
107 $this->mainColumnHeader = $this->createColumnHeader( $msg );
108 }
109
111 private function createColumnHeader( Message $msg ): string {
112 return Html::element( 'th', [], $msg->text() );
113 }
114
115 public function addExtraColumn( Message $column ): void {
116 $this->extraColumns[] = $column;
117 }
118
120 private function getOtherColumnHeaders(): array {
121 return array_merge( [
122 $this->messageLocalizer->msg( 'translate-total' ),
123 $this->messageLocalizer->msg( 'translate-untranslated' ),
124 $this->messageLocalizer->msg( 'translate-percentage-complete' ),
125 $this->messageLocalizer->msg( 'translate-percentage-proofread' ),
126 $this->messageLocalizer->msg( 'translate-percentage-fuzzy' ),
127 ], $this->extraColumns );
128 }
129
131 public function createHeader(): string {
132 // Create table header
133 $out = Html::openElement(
134 'table',
135 [ 'class' => 'statstable' ]
136 );
137
138 $out .= "\n\t" . Html::openElement( 'thead' );
139 $out .= "\n\t" . Html::openElement( 'tr' );
140
141 $out .= "\n\t\t" . $this->getMainColumnHeader();
142 foreach ( $this->getOtherColumnHeaders() as $label ) {
143 $out .= "\n\t\t" . $this->createColumnHeader( $label );
144 }
145 $out .= "\n\t" . Html::closeElement( 'tr' );
146 $out .= "\n\t" . Html::closeElement( 'thead' );
147 $out .= "\n\t" . Html::openElement( 'tbody' );
148
149 return $out;
150 }
151
158 public function makeTotalRow( Message $message, array $stats ): string {
159 $out = "\t" . Html::openElement( 'tr' );
160 $out .= "\n\t\t" . Html::element( 'td', [], $message->text() );
161 $out .= $this->makeNumberColumns( $stats );
162 $out .= "\n\t" . Xml::closeElement( 'tr' ) . "\n";
163
164 return $out;
165 }
166
171 public function makeNumberColumns( array $stats ): string {
172 $total = $stats[MessageGroupStats::TOTAL];
173 $translated = $stats[MessageGroupStats::TRANSLATED];
174 $fuzzy = $stats[MessageGroupStats::FUZZY];
175 $proofread = $stats[MessageGroupStats::PROOFREAD];
176
177 if ( $total === null ) {
178 $na = "\n\t\t" . Html::element( 'td', [ 'data-sort-value' => -1 ], '...' );
179 $nap = "\n\t\t" . $this->element( '...', 'AFAFAF', '-1' );
180 $out = $na . $na . $nap . $nap;
181
182 return $out;
183 }
184
185 $out = "\n\t\t" . Html::element( 'td',
186 [ 'data-sort-value' => $total ],
187 $this->language->formatNum( $total ) );
188
189 $out .= "\n\t\t" . Html::element( 'td',
190 [ 'data-sort-value' => $total - $translated ],
191 $this->language->formatNum( $total - $translated ) );
192
193 if ( $total === 0 ) {
194 $transRatio = 0;
195 $fuzzyRatio = 0;
196 $proofRatio = 0;
197 } else {
198 $transRatio = $translated / $total;
199 $fuzzyRatio = $fuzzy / $total;
200 $proofRatio = $translated ? $proofread / $translated : 0;
201 }
202
203 $out .= "\n\t\t" . $this->element( $this->formatPercentage( $transRatio, 'floor' ),
204 $this->getBackgroundColor( $transRatio ),
205 sprintf( '%1.5f', $transRatio ) );
206
207 $out .= "\n\t\t" . $this->element( $this->formatPercentage( $proofRatio, 'floor' ),
208 $this->getBackgroundColor( $proofRatio ),
209 sprintf( '%1.5f', $proofRatio ) );
210
211 $out .= "\n\t\t" . $this->element( $this->formatPercentage( $fuzzyRatio, 'ceil' ),
212 $this->getBackgroundColor( $fuzzyRatio, true ),
213 sprintf( '%1.5f', $fuzzyRatio ) );
214
215 return $out;
216 }
217
218 public function makeWorkflowStateCell( ?string $state, MessageGroup $group, string $language ): string {
219 if ( $state === null ) {
220 return "\n\t\t" . $this->element( '', '', '-1' );
221 }
222
223 if ( $group->getSourceLanguage() === $language ) {
224 return "\n\t\t" . $this->element( '', '', '-1' );
225 }
226
227 $stateConfig = $group->getMessageGroupStates()->getStates();
228 $sortValue = '-1';
229 $stateColor = '';
230 if ( isset( $stateConfig[$state] ) ) {
231 $sortIndex = array_flip( array_keys( $stateConfig ) );
232 $sortValue = $sortIndex[$state] + 1;
233
234 if ( is_string( $stateConfig[$state] ) ) {
235 // BC for old configuration format
236 $stateColor = $stateConfig[$state];
237 } elseif ( isset( $stateConfig[$state]['color'] ) ) {
238 $stateColor = $stateConfig[$state]['color'];
239 }
240 }
241
242 $stateMessage = $this->messageLocalizer->msg( "translate-workflow-state-$state" );
243 $stateText = $stateMessage->isBlank() ? $state : $stateMessage->text();
244
245 return "\n\t\t" . $this->element(
246 $stateText,
247 $stateColor,
248 (string)$sortValue
249 );
250 }
251
258 public function formatPercentage( $num, string $to = 'floor' ): string {
259 $num = $to === 'floor' ? floor( 100 * $num ) : ceil( 100 * $num );
260 $fmt = $this->language->formatNum( $num );
261
262 return $this->messageLocalizer->msg( 'percent', $fmt )->text();
263 }
264
269 private function getGroupLabel( MessageGroup $group ): string {
270 $groupLabel = htmlspecialchars( $group->getLabel() );
271
272 // Bold for meta groups.
273 if ( $group->isMeta() ) {
274 $groupLabel = Html::rawElement( 'b', [], $groupLabel );
275 }
276
277 return $groupLabel;
278 }
279
287 public function makeGroupLink( MessageGroup $group, string $code, array $params ): string {
288 $queryParameters = $params + [
289 'group' => $group->getId(),
290 'language' => $code
291 ];
292
293 return $this->linkRenderer->makeLink(
294 $this->translate,
295 new HtmlArmor( $this->getGroupLabel( $group ) ),
296 [],
297 $queryParameters
298 );
299 }
300
307 public function isExcluded( MessageGroup $group, string $code ): bool {
308 $excluded = null;
309 $groupId = $group->getId();
310
311 $checks = [
312 $groupId,
313 strtok( $groupId, '-' ),
314 '*'
315 ];
316
317 $disabledLanguages = $this->configHelper->getDisabledTargetLanguages();
318
319 foreach ( $checks as $check ) {
320 if ( isset( $disabledLanguages[$check] ) && isset( $disabledLanguages[$check][$code] ) ) {
321 $excluded = $disabledLanguages[$check][$code];
322 }
323
324 if ( $excluded !== null ) {
325 break;
326 }
327 }
328
329 $languages = $group->getTranslatableLanguages();
330 if ( $languages !== null && !isset( $languages[$code] ) ) {
331 $excluded = true;
332 }
333
334 if ( TranslateMetadata::isExcluded( $groupId, $code ) ) {
335 $excluded = true;
336 }
337
338 return (bool)$excluded;
339 }
340}
return[ '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:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->getMainConfig(), $services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), $services->get( 'Translate:MessageIndex'));}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore(new RevTagStore(), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReview'=> static function(MediaWikiServices $services):MessageGroupReview { return new MessageGroupReview($services->getDBLoadBalancer(), $services->getHookContainer());}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getDBLoadBalancer(), $services->getLinkRenderer(), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=$services->getMainConfig() ->get( 'TranslateMessageIndex');if(is_string( $params)) { $params=(array) $params;} $class=array_shift( $params);return new $class( $params);}, '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'));}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, '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->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:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), new RevTagStore(), $services->getDBLoadBalancer(), $services->get( 'Translate:TranslatableBundleStatusStore'));}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnectionRef(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());}, '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
Implements generation of HTML stats table.
makeTotalRow(Message $message, array $stats)
Makes a row with aggregate numbers.
isExcluded(MessageGroup $group, string $code)
Check whether translations in given group in given language has been disabled.
element(string $in, string $bgcolor='', string $sort='')
Statistics table element (heading or regular cell)
formatPercentage( $num, string $to='floor')
Makes a nice print from plain float.
makeGroupLink(MessageGroup $group, string $code, array $params)
Gets the name of group linked to translation tool.
makeNumberColumns(array $stats)
Makes partial row from completion numbers.
A helper class added to work with configuration values of the Translate Extension.
This class abstract MessageGroup statistics calculation and storing.
Interface for message groups.
getTranslatableLanguages()
Get all the translatable languages for a group, considering the inclusion and exclusion list.
getId()
Returns the unique identifier for this group.