Mo::read()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 7
rs 10
1
<?php
2
3
4
/**
5
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
6
 * @copyright Aimeos (aimeos.org), 2016-2022
7
 * @package MW
8
 * @subpackage Translation
9
 */
10
11
12
namespace Aimeos\MW\Translation\File;
13
14
15
/**
16
 * Class for reading Gettext MO files
17
 *
18
 * @package MW
19
 * @subpackage Translation
20
 */
21
class Mo
22
{
23
	const MAGIC1 = -1794895138;
24
	const MAGIC2 = -569244523;
25
	const MAGIC3 = 2500072158;
26
27
28
	private $str;
29
	private $strlen;
30
	private $pos = 0;
31
	private $messages = [];
32
33
34
	/**
35
	 * Initializes the .mo file reader
36
	 *
37
	 * @param string $filepath Absolute path to the Gettext .mo file
38
	 */
39
	public function __construct( string $filepath )
40
	{
41
		if( ( $str = file_get_contents( $filepath ) ) === false ) {
42
			throw new \Aimeos\MW\Translation\Exception( sprintf( 'Unable to read from file "%1$s"', $filepath ) );
43
		}
44
45
		$this->str = $str;
46
		$this->strlen = strlen( $str );
47
		$this->messages = $this->extract();
48
	}
49
50
51
	/**
52
	 * Returns all translations
53
	 *
54
	 * @return array List of translations with original as key and translations as values
55
	 */
56
	public function all() : array
57
	{
58
		return $this->messages;
59
	}
60
61
62
	/**
63
	 * Returns the translations for the given original string
64
	 *
65
	 * @param string $original Untranslated string
66
	 * @return array|string|null List of translations or false if none is available
67
	 */
68
	public function get( string $original )
69
	{
70
		if( isset( $this->messages[$original] ) ) {
71
			return $this->messages[$original];
72
		}
73
74
		return null;
75
	}
76
77
78
	/**
79
	 * Extracts the messages and translations from the MO file
80
	 *
81
	 * @return array Associative list of original singular as keys and one or more translations as values
82
	 * @throws \Aimeos\MW\Translation\Exception If file content is invalid
83
	 */
84
	protected function extract() : array
85
	{
86
		$magic = $this->readInt( 'V' );
87
88
		if( ( $magic === self::MAGIC1 ) || ( $magic === self::MAGIC3 ) ) { //to make sure it works for 64-bit platforms
89
			$byteOrder = 'V'; //low endian
90
		} elseif( $magic === ( self::MAGIC2 & 0xFFFFFFFF ) ) {
91
			$byteOrder = 'N'; //big endian
92
		} else {
93
			throw new \Aimeos\MW\Translation\Exception( 'Invalid MO file' );
94
		}
95
96
		$this->readInt( $byteOrder );
97
		$total = $this->readInt( $byteOrder ); //total string count
98
		$originals = $this->readInt( $byteOrder ); //offset of original table
99
		$trans = $this->readInt( $byteOrder ); //offset of translation table
100
101
		$this->seekto( $originals );
0 ignored issues
show
Bug introduced by
It seems like $originals can also be of type null; however, parameter $pos of Aimeos\MW\Translation\File\Mo::seekto() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

101
		$this->seekto( /** @scrutinizer ignore-type */ $originals );
Loading history...
102
		$originalTable = $this->readIntArray( $byteOrder, $total * 2 );
103
		$this->seekto( $trans );
104
		$translationTable = $this->readIntArray( $byteOrder, $total * 2 );
105
106
		return $this->extractTable( $originalTable, $translationTable, $total );
0 ignored issues
show
Bug introduced by
It seems like $total can also be of type null; however, parameter $total of Aimeos\MW\Translation\File\Mo::extractTable() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

106
		return $this->extractTable( $originalTable, $translationTable, /** @scrutinizer ignore-type */ $total );
Loading history...
107
	}
108
109
110
	/**
111
	 * Extracts the messages and their translations
112
	 *
113
	 * @param array $originalTable MO table for original strings
114
	 * @param array $translationTable MO table for translated strings
115
	 * @param int $total Total number of translations
116
	 * @return array Associative list of original singular as keys and one or more translations as values
117
	 */
118
	protected function extractTable( array $originalTable, array $translationTable, int $total ) : array
119
	{
120
		$messages = [];
121
122
		for( $i = 0; $i < $total; ++$i )
123
		{
124
			$plural = null;
125
			$next = $i * 2;
126
127
			$this->seekto( $originalTable[$next + 2] );
128
			$original = $this->read( $originalTable[$next + 1] );
129
			$this->seekto( $translationTable[$next + 2] );
130
			$translated = $this->read( $translationTable[$next + 1] );
131
132
			if( $original === '' || $translated === '' ) { // Headers
133
				continue;
134
			}
135
136
			if( strpos( $original, "\x04" ) !== false ) {
137
				list( $context, $original ) = explode( "\x04", $original, 2 );
138
			}
139
140
			if( strpos( $original, "\000" ) !== false ) {
141
				list( $original, $plural ) = explode( "\000", $original );
142
			}
143
144
			if( $plural === null )
145
			{
146
				$messages[$original] = $translated;
147
				continue;
148
			}
149
150
			$messages[$original] = [];
151
152
			foreach( explode( "\x00", $translated ) as $idx => $value ) {
153
				$messages[$original][$idx] = $value;
154
			}
155
		}
156
157
		return $messages;
158
	}
159
160
161
	/**
162
	 * Returns a single integer starting from the current position
163
	 *
164
	 * @param string $byteOrder Format code for unpack()
165
	 * @return integer Read integer
166
	 */
167
	protected function readInt( string $byteOrder ) : ?int
168
	{
169
		if( ( $content = $this->read( 4 )) === null ) {
170
			return null;
171
		}
172
173
		$content = unpack( $byteOrder, $content );
174
		return array_shift( $content );
175
	}
176
177
178
	/**
179
	 * Returns the list of integers starting from the current position
180
	 *
181
	 * @param string $byteOrder Format code for unpack()
182
	 * @param int $count Number of four byte integers to read
183
	 * @return array List of integers
184
	 */
185
	protected function readIntArray( string $byteOrder, int $count ) : array
186
	{
187
		return unpack( $byteOrder . $count, $this->read( 4 * $count ) );
188
	}
189
190
191
	/**
192
	 * Returns a part of the file
193
	 *
194
	 * @param int $bytes Number of bytes to read
195
	 * @return string|null Read bytes or null on failure
196
	 */
197
	protected function read( int $bytes ) : ?string
198
	{
199
		if( ( $data = substr( $this->str, $this->pos, $bytes ) ) !== null ) {
200
			$this->seekto( $this->pos + $bytes );
201
		}
202
203
		return $data;
204
	}
205
206
207
	/**
208
	 * Move the cursor to the position in the file
209
	 *
210
	 * @param int $pos Number of bytes to move
211
	 * @return int New file position in bytes
212
	 */
213
	protected function seekto( int $pos ) : int
214
	{
215
		$this->pos = ( $this->strlen < $pos ? $this->strlen : $pos );
216
		return $this->pos;
217
	}
218
}
219