Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
50.00% |
56 / 112 |
|
30.00% |
3 / 10 |
CRAP | |
0.00% |
0 / 1 |
JavaFormat | |
50.45% |
56 / 111 |
|
30.00% |
3 / 10 |
147.91 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
supportsFuzzy | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getFileExtensions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
readFromVariable | |
88.24% |
30 / 34 |
|
0.00% |
0 / 1 |
11.20 | |||
writeReal | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
30 | |||
writeRow | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
readRow | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
6 | |||
doHeader | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
doAuthors | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getExtraSchema | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\FileFormatSupport; |
5 | |
6 | use FileBasedMessageGroup; |
7 | use MediaWiki\Extension\Translate\MessageGroupConfiguration\MetaYamlSchemaExtender; |
8 | use MediaWiki\Extension\Translate\MessageLoading\Message; |
9 | use MediaWiki\Extension\Translate\MessageLoading\MessageCollection; |
10 | use MediaWiki\Extension\Translate\Utilities\Utilities; |
11 | use RuntimeException; |
12 | use TextContent; |
13 | |
14 | /** |
15 | * JavaFormat class implements support for Java properties files. |
16 | * This class reads and writes only utf-8 files. Java projects |
17 | * need to run native2ascii on them before using them. |
18 | * |
19 | * This class adds a new item into FILES section of group configuration: |
20 | * \c keySeparator which defaults to '='. |
21 | * @ingroup FileFormatSupport |
22 | */ |
23 | class JavaFormat extends SimpleFormat implements MetaYamlSchemaExtender { |
24 | |
25 | private string $keySeparator; |
26 | |
27 | public function __construct( FileBasedMessageGroup $group ) { |
28 | parent::__construct( $group ); |
29 | $this->keySeparator = $this->extra['keySeparator'] ?? '='; |
30 | } |
31 | |
32 | public function supportsFuzzy(): string { |
33 | return 'write'; |
34 | } |
35 | |
36 | public function getFileExtensions(): array { |
37 | return [ '.properties' ]; |
38 | } |
39 | |
40 | /** @throws RuntimeException */ |
41 | public function readFromVariable( string $data ): array { |
42 | $data = TextContent::normalizeLineEndings( $data ); |
43 | $lines = array_map( 'ltrim', explode( "\n", $data ) ); |
44 | $authors = $messages = []; |
45 | $lineContinuation = false; |
46 | |
47 | $key = ''; |
48 | $value = ''; |
49 | foreach ( $lines as $line ) { |
50 | if ( $lineContinuation ) { |
51 | $lineContinuation = false; |
52 | $valuecont = $line; |
53 | $valuecont = str_replace( '\n', "\n", $valuecont ); |
54 | $value .= $valuecont; |
55 | } else { |
56 | if ( $line === '' ) { |
57 | continue; |
58 | } |
59 | |
60 | if ( $line[0] === '#' || $line[0] === '!' ) { |
61 | $match = []; |
62 | $ok = preg_match( '/#\s*Author:\s*(.*)/', $line, $match ); |
63 | |
64 | if ( $ok ) { |
65 | $authors[] = $match[1]; |
66 | } |
67 | |
68 | continue; |
69 | } |
70 | |
71 | if ( !str_contains( $line, $this->keySeparator ) ) { |
72 | throw new RuntimeException( "Line without separator '{$this->keySeparator}': $line." ); |
73 | } |
74 | |
75 | [ $key, $value ] = $this->readRow( $line, $this->keySeparator ); |
76 | if ( $key === '' ) { |
77 | throw new RuntimeException( "Empty key in line $line." ); |
78 | } |
79 | } |
80 | |
81 | // @todo This doesn't handle the pathological case of even number of trailing \ |
82 | if ( strlen( $value ) && $value[strlen( $value ) - 1] === "\\" ) { |
83 | $value = substr( $value, 0, strlen( $value ) - 1 ); |
84 | $lineContinuation = true; |
85 | } else { |
86 | $messages[$key] = ltrim( $value ); |
87 | } |
88 | } |
89 | |
90 | $messages = $this->group->getMangler()->mangleArray( $messages ); |
91 | |
92 | return [ |
93 | 'AUTHORS' => $authors, |
94 | 'MESSAGES' => $messages, |
95 | ]; |
96 | } |
97 | |
98 | protected function writeReal( MessageCollection $collection ): string { |
99 | $header = $this->doHeader( $collection ); |
100 | $header .= $this->doAuthors( $collection ); |
101 | $header .= "\n"; |
102 | |
103 | $output = ''; |
104 | $mangler = $this->group->getMangler(); |
105 | |
106 | /** @var Message $message */ |
107 | foreach ( $collection as $key => $message ) { |
108 | $value = $message->translation() ?? ''; |
109 | if ( $value === '' ) { |
110 | continue; |
111 | } |
112 | |
113 | $value = str_replace( TRANSLATE_FUZZY, '', $value ); |
114 | |
115 | // Just to give an overview of translation quality. |
116 | if ( $message->hasTag( 'fuzzy' ) ) { |
117 | $output .= "# Fuzzy\n"; |
118 | } |
119 | |
120 | $key = $mangler->unmangle( $key ); |
121 | $output .= $this->writeRow( $key, $value ); |
122 | } |
123 | |
124 | if ( $output ) { |
125 | return $header . $output; |
126 | } |
127 | |
128 | return ''; |
129 | } |
130 | |
131 | /** Writes well-formed properties file row with key and value. */ |
132 | public function writeRow( string $key, string $value ): string { |
133 | /* Keys containing the separator need escaping. Also escape comment |
134 | * characters, though strictly they would only need escaping when |
135 | * they are the first character. Plus the escape character itself. */ |
136 | $key = addcslashes( $key, "#!{$this->keySeparator}\\" ); |
137 | // Make sure we do not slip newlines trough... it would be fatal. |
138 | $value = str_replace( "\n", '\\n', $value ); |
139 | |
140 | return "$key{$this->keySeparator}$value\n"; |
141 | } |
142 | |
143 | /** |
144 | * Parses non-empty properties file row to key and value. |
145 | * @return string[] |
146 | */ |
147 | public function readRow( string $line, string $sep ): array { |
148 | if ( !str_contains( $line, '\\' ) ) { |
149 | /* Nothing appears to be escaped in this line. |
150 | * Just read the key and the value. */ |
151 | [ $key, $value ] = explode( $sep, $line, 2 ); |
152 | } else { |
153 | /* There might be escaped separators in the key. |
154 | * Using slower method to find the separator. */ |
155 | |
156 | /* Make the key default to empty instead of value, because |
157 | * empty key causes error on callers, while empty value |
158 | * wouldn't. */ |
159 | $key = ''; |
160 | $value = $line; |
161 | |
162 | /* Find the first unescaped separator. Example: |
163 | * First line is the string being read, second line is the |
164 | * value of $escaped after having read the above character. |
165 | * |
166 | * ki\ts\\s\=a = koira |
167 | * 0010010010000 |
168 | * ^ Not separator because $escaped was true |
169 | * ^ Split the string into key and value here |
170 | */ |
171 | |
172 | $len = strlen( $line ); |
173 | $escaped = false; |
174 | for ( $i = 0; $i < $len; $i++ ) { |
175 | $char = $line[$i]; |
176 | if ( $char === '\\' ) { |
177 | $escaped = !$escaped; |
178 | } elseif ( $escaped ) { |
179 | $escaped = false; |
180 | } elseif ( $char === $sep ) { |
181 | $key = substr( $line, 0, $i ); |
182 | // Excluding the separator character from the value |
183 | $value = substr( $line, $i + 1 ); |
184 | break; |
185 | } |
186 | } |
187 | } |
188 | |
189 | /* We usually don't want to expand things like \t in values since |
190 | * translators cannot easily input those. But in keys we do. |
191 | * \n is exception we do handle in values. */ |
192 | $key = trim( $key ); |
193 | $key = stripcslashes( $key ); |
194 | $value = ltrim( $value ); |
195 | $value = str_replace( '\n', "\n", $value ); |
196 | |
197 | return [ $key, $value ]; |
198 | } |
199 | |
200 | private function doHeader( MessageCollection $collection ): string { |
201 | if ( isset( $this->extra['header'] ) ) { |
202 | $output = $this->extra['header']; |
203 | } else { |
204 | global $wgSitename; |
205 | |
206 | $code = $collection->code; |
207 | $name = Utilities::getLanguageName( $code ); |
208 | $native = Utilities::getLanguageName( $code, $code ); |
209 | $output = "# Messages for $name ($native)\n"; |
210 | $output .= "# Exported from $wgSitename\n"; |
211 | } |
212 | |
213 | return $output; |
214 | } |
215 | |
216 | private function doAuthors( MessageCollection $collection ): string { |
217 | $output = ''; |
218 | $authors = $collection->getAuthors(); |
219 | $authors = $this->filterAuthors( $authors, $collection->code ); |
220 | |
221 | foreach ( $authors as $author ) { |
222 | $output .= "# Author: $author\n"; |
223 | } |
224 | |
225 | return $output; |
226 | } |
227 | |
228 | public static function getExtraSchema(): array { |
229 | return [ |
230 | 'root' => [ |
231 | '_type' => 'array', |
232 | '_children' => [ |
233 | 'FILES' => [ |
234 | '_type' => 'array', |
235 | '_children' => [ |
236 | 'header' => [ |
237 | '_type' => 'text', |
238 | ], |
239 | 'keySeparator' => [ |
240 | '_type' => 'text', |
241 | ], |
242 | ] |
243 | ] |
244 | ] |
245 | ] |
246 | ]; |
247 | } |
248 | } |
249 | |
250 | class_alias( JavaFormat::class, 'JavaFFS' ); |