Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.30% covered (success)
91.30%
42 / 46
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
AmdFormat
93.33% covered (success)
93.33%
42 / 45
57.14% covered (warning)
57.14%
4 / 7
13.05
0.00% covered (danger)
0.00%
0 / 1
 getFileExtensions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 readFromVariable
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 writeReal
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
5.01
 extractMessagePart
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 extractAuthors
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 header
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 authorsList
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\FileFormatSupport;
5
6use FormatJson;
7use MediaWiki\Extension\Translate\MessageLoading\Message;
8use MediaWiki\Extension\Translate\MessageLoading\MessageCollection;
9use MediaWiki\Extension\Translate\Utilities\Utilities;
10
11/**
12 * Support for the AMD i18n message file format (used by require.js and Dojo). See:
13 * http://requirejs.org/docs/api.html#i18n
14 *
15 * A limitation is that it only accepts json compatible structures inside the define
16 * wrapper function. For example the following example is not ok since there are no
17 * quotation marks around the keys:
18 * define({
19 *   key1: "somevalue",
20 *   key2: "anothervalue"
21 * });
22 *
23 * Instead it should look like:
24 * define({
25 *   "key1": "somevalue",
26 *   "key2": "anothervalue"
27 * });
28 *
29 * It also supports the top-level bundle with a root construction and language indicators.
30 * The following example will give the same messages as above:
31 * define({
32 *   "root": {
33 *      "key1": "somevalue",
34 *      "key2": "anothervalue"
35 *   },
36 *   "sv": true
37 * });
38 *
39 * Note that it does not support exporting with the root construction, there is only support
40 * for reading it. However, this is not a serious limitation as Translatewiki doesn't export
41 * the base language.
42 *
43 * AmdFormat implements a message format where messages are encoded
44 * as key-value pairs in JSON objects wrapped in a define call.
45 *
46 * @author Matthias Palmér
47 * @copyright Copyright © 2011-2015, MetaSolutions AB
48 * @license GPL-2.0-or-later
49 * @ingroup FileFormatSupport
50 */
51class AmdFormat extends SimpleFormat {
52
53    public function getFileExtensions(): array {
54        return [ '.js' ];
55    }
56
57    /** @inheritDoc */
58    public function readFromVariable( string $data ): array {
59        $authors = $this->extractAuthors( $data );
60        $data = $this->extractMessagePart( $data );
61        $messages = (array)FormatJson::decode( $data, /*as array*/true );
62        $metadata = [];
63
64        // Take care of regular language bundles, as well as the root bundle.
65        if ( isset( $messages['root'] ) ) {
66            $messages = $this->group->getMangler()->mangleArray( $messages['root'] );
67        } else {
68            $messages = $this->group->getMangler()->mangleArray( $messages );
69        }
70
71        return [
72            'MESSAGES' => $messages,
73            'AUTHORS' => $authors,
74            'METADATA' => $metadata,
75        ];
76    }
77
78    protected function writeReal( MessageCollection $collection ): string {
79        $messages = [];
80        $mangler = $this->group->getMangler();
81
82        /** @var Message $m */
83        foreach ( $collection as $key => $m ) {
84            $value = $m->translation();
85            if ( $value === null ) {
86                continue;
87            }
88
89            if ( $m->hasTag( 'fuzzy' ) ) {
90                $value = str_replace( TRANSLATE_FUZZY, '', $value );
91            }
92
93            $key = $mangler->unmangle( $key );
94            $messages[$key] = $value;
95        }
96
97        // Do not create empty files
98        if ( !count( $messages ) ) {
99            return '';
100        }
101        $header = $this->header( $collection->code, $collection->getAuthors() );
102        return $header . FormatJson::encode( $messages, "\t", FormatJson::UTF8_OK ) . ");\n";
103    }
104
105    private function extractMessagePart( string $data ): string {
106        // Find the start and end of the data section (enclosed in the define function call).
107        $dataStart = strpos( $data, 'define(' ) + 6;
108        $dataEnd = strrpos( $data, ')' );
109
110        // Strip everything outside of the data section.
111        return substr( $data, $dataStart + 1, $dataEnd - $dataStart - 1 );
112    }
113
114    private function extractAuthors( string $data ): array {
115        preg_match_all( '~\n \*  - (.+)~', $data, $result );
116        return $result[1];
117    }
118
119    private function header( string $code, array $authors ): string {
120        global $wgSitename;
121
122        $name = Utilities::getLanguageName( $code );
123        $authorsList = $this->authorsList( $authors );
124
125        return <<<EOT
126            /**
127             * Messages for $name
128             * Exported from $wgSitename
129             *
130            {$authorsList}
131             */
132            define(
133            EOT;
134    }
135
136    /** @param string[] $authors */
137    private function authorsList( array $authors ): string {
138        if ( $authors === [] ) {
139            return '';
140        }
141
142        $prefix = ' *  - ';
143        $authorList = implode( "\n$prefix", $authors );
144        return " * Translators:\n$prefix$authorList";
145    }
146}
147
148class_alias( AmdFormat::class, 'AmdFFS' );