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