Mo::extractTable()   B
last analyzed

Complexity

Conditions 8
Paths 10

Size

Total Lines 40
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 21
c 1
b 0
f 0
dl 0
loc 40
rs 8.4444
cc 8
nc 10
nop 3
1
<?php
2
3
4
/**
5
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
6
 * @copyright Aimeos (aimeos.org), 2016-2025
7
 * @package Base
8
 * @subpackage Translation
9
 */
10
11
12
namespace Aimeos\Base\Translation\File;
13
14
15
/**
16
 * Class for reading Gettext MO files
17
 *
18
 * @package Base
19
 * @subpackage Translation
20
 */
21
class Mo
22
{
23
	const MAGIC1 = -1794895138;
24
	const MAGIC2 = -569244523;
25
	const MAGIC3 = 2500072158;
26
27
28
	private string $str;
29
	private int $strlen;
30
	private int $pos = 0;
31
	private array $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\Base\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
		return $this->messages[$original] ?? null;
71
	}
72
73
74
	/**
75
	 * Extracts the messages and translations from the MO file
76
	 *
77
	 * @return array Associative list of original singular as keys and one or more translations as values
78
	 * @throws \Aimeos\Base\Translation\Exception If file content is invalid
79
	 */
80
	protected function extract() : array
81
	{
82
		$magic = $this->readInt( 'V' );
83
84
		if( ( $magic === self::MAGIC1 ) || ( $magic === self::MAGIC3 ) ) { //to make sure it works for 64-bit platforms
85
			$byteOrder = 'V'; //low endian
86
		} elseif( $magic === ( self::MAGIC2 & 0xFFFFFFFF ) ) {
87
			$byteOrder = 'N'; //big endian
88
		} else {
89
			throw new \Aimeos\Base\Translation\Exception( 'Invalid MO file' );
90
		}
91
92
		$this->readInt( $byteOrder );
93
		$total = $this->readInt( $byteOrder ); //total string count
94
		$originals = $this->readInt( $byteOrder ); //offset of original table
95
		$trans = $this->readInt( $byteOrder ); //offset of translation table
96
97
		$this->seekto( (int) $originals );
98
		$originalTable = $this->readIntArray( $byteOrder, $total * 2 );
99
		$this->seekto( (int) $trans );
100
		$translationTable = $this->readIntArray( $byteOrder, $total * 2 );
101
102
		return $this->extractTable( $originalTable, $translationTable, (int) $total );
103
	}
104
105
106
	/**
107
	 * Extracts the messages and their translations
108
	 *
109
	 * @param array $originalTable MO table for original strings
110
	 * @param array $translationTable MO table for translated strings
111
	 * @param int $total Total number of translations
112
	 * @return array Associative list of original singular as keys and one or more translations as values
113
	 */
114
	protected function extractTable( array $originalTable, array $translationTable, int $total ) : array
115
	{
116
		$messages = [];
117
118
		for( $i = 0; $i < $total; ++$i )
119
		{
120
			$plural = null;
121
			$next = $i * 2;
122
123
			$this->seekto( $originalTable[$next + 2] );
124
			$original = $this->read( $originalTable[$next + 1] );
125
			$this->seekto( $translationTable[$next + 2] );
126
			$translated = $this->read( $translationTable[$next + 1] );
127
128
			if( $original === '' || $translated === '' ) { // Headers
129
				continue;
130
			}
131
132
			if( strpos( $original, "\x04" ) !== false ) {
133
				list( $context, $original ) = explode( "\x04", $original, 2 );
134
			}
135
136
			if( strpos( $original, "\000" ) !== false ) {
137
				list( $original, $plural ) = explode( "\000", $original );
138
			}
139
140
			if( $plural === null )
141
			{
142
				$messages[$original] = $translated;
143
				continue;
144
			}
145
146
			$messages[$original] = [];
147
148
			foreach( explode( "\x00", $translated ) as $idx => $value ) {
149
				$messages[$original][$idx] = $value;
150
			}
151
		}
152
153
		return $messages;
154
	}
155
156
157
	/**
158
	 * Returns a single integer starting from the current position
159
	 *
160
	 * @param string $byteOrder Format code for unpack()
161
	 * @return integer Read integer
162
	 */
163
	protected function readInt( string $byteOrder ) : ?int
164
	{
165
		if( ( $content = $this->read( 4 ) ) === '' ) {
166
			return null;
167
		}
168
169
		$content = unpack( $byteOrder, $content );
170
		return array_shift( $content );
171
	}
172
173
174
	/**
175
	 * Returns the list of integers starting from the current position
176
	 *
177
	 * @param string $byteOrder Format code for unpack()
178
	 * @param int $count Number of four byte integers to read
179
	 * @return array List of integers
180
	 */
181
	protected function readIntArray( string $byteOrder, int $count ) : array
182
	{
183
		return unpack( $byteOrder . $count, $this->read( 4 * $count ) );
184
	}
185
186
187
	/**
188
	 * Returns a part of the file
189
	 *
190
	 * @param int $bytes Number of bytes to read
191
	 * @return string Read bytes or empty on failure
192
	 */
193
	protected function read( int $bytes ) : string
194
	{
195
		$data = substr( $this->str, $this->pos, $bytes );
196
197
		if( $data !== false && $data !== '' ) {
198
			$this->seekto( $this->pos + $bytes );
199
		}
200
201
		return (string) $data;
202
	}
203
204
205
	/**
206
	 * Move the cursor to the position in the file
207
	 *
208
	 * @param int $pos Number of bytes to move
209
	 * @return int New file position in bytes
210
	 */
211
	protected function seekto( int $pos ) : int
212
	{
213
		$this->pos = ( $this->strlen < $pos ? $this->strlen : $pos );
214
		return $this->pos;
215
	}
216
}
217