Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
40.22% covered (danger)
40.22%
37 / 92
41.67% covered (danger)
41.67%
5 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
AppleFormat
40.66% covered (danger)
40.66%
37 / 91
41.67% covered (danger)
41.67%
5 / 12
204.73
0.00% covered (danger)
0.00%
0 / 1
 supportsFuzzy
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 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%
27 / 27
100.00% covered (success)
100.00%
1 / 1
9
 readRow
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
3.14
 writeReal
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
42
 writeRow
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 quoteString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 escapeString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 unescapeString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doHeader
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 doAuthors
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getExtraSchema
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\FileFormatSupport;
5
6use InvalidArgumentException;
7use MediaWiki\Extension\Translate\MessageLoading\Message;
8use MediaWiki\Extension\Translate\MessageLoading\MessageCollection;
9use MediaWiki\Extension\Translate\Utilities\Utilities;
10use RuntimeException;
11
12/**
13 * AppleFFS class implements support for Apple .strings files.
14 * This class reads and writes only UTF-8 files.
15 *
16 * This class has not yet been battle-tested, so beware.
17 *
18 * @author Brion Vibber <bvibber@wikimedia.org>
19 * @ingroup FileFormatSupport
20 */
21class AppleFormat extends SimpleFormat {
22    public function supportsFuzzy(): string {
23        return 'write';
24    }
25
26    public function getFileExtensions(): array {
27        return [ '.strings' ];
28    }
29
30    /** @inheritDoc */
31    public function readFromVariable( string $data ): array {
32        $lines = explode( "\n", $data );
33        $authors = $messages = [];
34        $linecontinuation = false;
35
36        foreach ( $lines as $line ) {
37            $line = trim( $line );
38            if ( $linecontinuation ) {
39                if ( str_contains( $line, '*/' ) ) {
40                    $linecontinuation = false;
41                }
42            } else {
43                if ( $line === '' ) {
44                    continue;
45                }
46
47                if ( substr( $line, 0, 2 ) === '//' ) {
48                    // Single-line comment
49                    $match = [];
50                    $ok = preg_match( '~//\s*Author:\s*(.*)~', $line, $match );
51                    if ( $ok ) {
52                        $authors[] = $match[1];
53                    }
54                    continue;
55                }
56
57                if ( substr( $line, 0, 2 ) === '/*' ) {
58                    if ( strpos( $line, '*/', 2 ) === false ) {
59                        $linecontinuation = true;
60                    }
61                    continue;
62                }
63
64                [ $key, $value ] = $this->readRow( $line );
65                $messages[$key] = $value;
66            }
67        }
68
69        $messages = $this->group->getMangler()->mangleArray( $messages );
70
71        return [
72            'AUTHORS' => $authors,
73            'MESSAGES' => $messages,
74        ];
75    }
76
77    /**
78     * Parses non-empty strings file row to key and value.
79     * Can be overridden by child classes.
80     * @throws InvalidArgumentException
81     * @return array array( string $key, string $val )
82     */
83    public function readRow( string $line ): array {
84        $match = [];
85        if ( preg_match( '/^"((?:\\\"|[^"])*)"\s*=\s*"((?:\\\"|[^"])*)"\s*;\s*$/', $line, $match ) ) {
86            $key = self::unescapeString( $match[1] );
87            $value = self::unescapeString( $match[2] );
88            if ( $key === '' ) {
89                throw new InvalidArgumentException( "Empty key in line $line" );
90            }
91            return [ $key, $value ];
92        } else {
93            throw new InvalidArgumentException( "Unrecognized line format: $line" );
94        }
95    }
96
97    protected function writeReal( MessageCollection $collection ): string {
98        $header = $this->doHeader( $collection );
99        $header .= $this->doAuthors( $collection );
100        $header .= "\n";
101
102        $output = '';
103        $mangler = $this->group->getMangler();
104
105        $collection->filter( MessageCollection::FILTER_HAS_TRANSLATION, MessageCollection::INCLUDE_MATCHING );
106        /** @var Message $m */
107        foreach ( $collection as $key => $m ) {
108            $value = $m->translation();
109            if ( $value === null ) {
110                throw new RuntimeException( "Expected translation to be present for $key, but found null." );
111            }
112            $value = str_replace( TRANSLATE_FUZZY, '', $value );
113
114            if ( $value === '' ) {
115                continue;
116            }
117
118            // Just to give an overview of translation quality.
119            if ( $m->hasTag( 'fuzzy' ) ) {
120                $output .= "// Fuzzy\n";
121            }
122
123            $key = $mangler->unmangle( $key );
124            $output .= $this->writeRow( $key, $value );
125        }
126
127        if ( $output ) {
128            $data = $header . $output;
129        } else {
130            $data = $header;
131        }
132
133        return $data;
134    }
135
136    /**
137     * Writes well-formed properties file row with key and value.
138     * Can be overridden by child classes.
139     */
140    public function writeRow( string $key, string $value ): string {
141        return self::quoteString( $key ) . ' = ' . self::quoteString( $value ) . ';' . "\n";
142    }
143
144    /** Quote and escape Obj-C-style strings for .strings format. */
145    protected static function quoteString( string $str ): string {
146        return '"' . self::escapeString( $str ) . '"';
147    }
148
149    /** Escape Obj-C-style strings; use backslash-escapes etc. */
150    private static function escapeString( string $str ): string {
151        return str_replace( "\n", '\\n', addcslashes( $str, '\\"' ) );
152    }
153
154    /**
155     * Unescape Obj-C-style strings; can include backslash-escapes
156     *
157     * @todo support \UXXXX
158     */
159    protected static function unescapeString( string $str ): string {
160        return stripcslashes( $str );
161    }
162
163    private function doHeader( MessageCollection $collection ): string {
164        if ( isset( $this->extra['header'] ) ) {
165            $output = $this->extra['header'];
166        } else {
167            global $wgSitename;
168
169            $code = $collection->code;
170            $name = Utilities::getLanguageName( $code );
171            $native = Utilities::getLanguageName( $code, $code );
172            $output = "// Messages for $name ($native)\n";
173            $output .= "// Exported from $wgSitename\n";
174        }
175
176        return $output;
177    }
178
179    private function doAuthors( MessageCollection $collection ): string {
180        $output = '';
181        $authors = $collection->getAuthors();
182        $authors = $this->filterAuthors( $authors, $collection->code );
183
184        foreach ( $authors as $author ) {
185            $output .= "// Author: $author\n";
186        }
187
188        return $output;
189    }
190
191    public static function getExtraSchema(): array {
192        return [
193            'root' => [
194                '_type' => 'array',
195                '_children' => [
196                    'FILES' => [
197                        '_type' => 'array',
198                        '_children' => [
199                            'header' => [
200                                '_type' => 'text',
201                            ],
202                        ]
203                    ]
204                ]
205            ]
206        ];
207    }
208}
209
210class_alias( AppleFormat::class, 'AppleFFS' );