Skip to content

Commit

Permalink
MO file loader
Browse files Browse the repository at this point in the history
  • Loading branch information
Stadly committed Jun 20, 2019
1 parent 2d5a525 commit 679f8a3
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 1 deletion.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip
## Unreleased

### Added
- Nothing
- Loader for MO files.

### Changed
- Nothing
Expand Down
102 changes: 102 additions & 0 deletions src/Loader/MoFileLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

declare(strict_types=1);

namespace Stadly\Translation\Loader;

use Symfony\Component\Translation\Exception\InvalidResourceException;
use Symfony\Component\Translation\Loader\FileLoader;

final class MoFileLoader extends FileLoader
{
// phpcs:disable Squiz.Commenting.FunctionComment.ScalarTypeHintMissing
// phpcs:disable SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint
/**
* Parses machine object (MO) format, independent of the machine's endian it
* was created on. Both 32bit and 64bit systems are supported.
*
* @param string $filename Path to resource to load.
* @return array<string|int, string> Strings loaded from resource.
*/
protected function loadResource($filename): array
{
$stream = fopen($filename, 'r');
if ($stream === false) {
// @codeCoverageIgnoreStart
throw new InvalidResourceException('Could not open file.');
// @codeCoverageIgnoreEnd
}

$this->readLong($stream); // magicNumber
$this->readLong($stream); // formatRevision
$count = $this->readLong($stream);
$offsetId = $this->readLong($stream);
$offsetTranslated = $this->readLong($stream);
$this->readLong($stream); // sizeHashes
$this->readLong($stream); // offsetHashes

$messages = [];

for ($i = 0; $i < $count; ++$i) {
fseek($stream, $offsetId + $i * 8);
$id = $this->readEntry($stream);

fseek($stream, $offsetTranslated + $i * 8);
$translated = $this->readEntry($stream);

if ($id !== null && $translated !== null) {
$messages[$id] = $translated;
}
}

fclose($stream);

return $messages;
}
// phpcs:enable

/**
* Reads an unsigned long from stream.
*
* @param resource $stream Stream to read from.
* @return int The read number.
*/
private function readLong($stream): int
{
$string = fread($stream, 4);
if ($string === false) {
// @codeCoverageIgnoreStart
throw new InvalidResourceException('Could not read from file.');
// @codeCoverageIgnoreEnd
}
if (strlen($string) !== 4) {
throw new InvalidResourceException('MO stream content has an invalid format.');
}
$number = unpack('V', $string);

return $number[1];
}

/**
* @param resource $stream Stream to read from.
* @return string|null The read entry or null on failure.
*/
private function readEntry($stream): ?string
{
$length = $this->readLong($stream);
$offset = $this->readLong($stream);

if ($length < 1) {
return null;
}

fseek($stream, $offset);
$string = fread($stream, $length);
if ($string === false) {
// @codeCoverageIgnoreStart
throw new InvalidResourceException('Could not read from file.');
// @codeCoverageIgnoreEnd
}
return implode('|', explode("\000", $string));
}
}
89 changes: 89 additions & 0 deletions tests/Loader/MoFileLoaderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

declare(strict_types=1);

namespace Stadly\Translation\Loader;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Translation\Exception\InvalidResourceException;
use Symfony\Component\Translation\Exception\NotFoundResourceException;

/**
* @coversDefaultClass \Stadly\Translation\Loader\MoFileLoader
* @covers ::<protected>
* @covers ::<private>
*/
final class MoFileLoaderTest extends TestCase
{
/**
* @covers ::load
*/
public function testCanLoadFile(): void
{
$loader = new MoFileLoader();
$resource = __DIR__ . '/../resources/messages.mo';
$catalogue = $loader->load($resource, 'en_US', 'messages');

self::assertEquals([
'foo'
=> 'bar',
'one foo|%count% foos'
=> 'one bar|%count% bars',
'{0} no foos|one foo|%count% foos'
=> '{0} no bars|one bar|%count% bars',
'missing foo plural|missing foo plurals'
=> 'missing bar plural|',
'string containing || foo'
=> 'string containing || bar',
'one string containing || foo|%count% strings containing || foos'
=> 'one string containing || bar|%count% strings containing || bars',
'{0} no strings containing || foos|one string containing || foo|%count% strings containing || foos'
=> '{0} no strings containing || bars|one string containing || bar|%count% strings containing || bars',
'escaped "foo"'
=> 'escaped "bar"',
'one escaped "foo"|%count% escaped "foos"'
=> 'one escaped "bar"|%count% escaped "bars"',
'{0} no escaped "foos"|one one escaped "foo"|%count% escaped "foos"'
=> '{0} no escaped "bars"|one one escaped "bar"|%count% escaped "bars"',
], $catalogue->all('messages'));
}

/**
* @covers ::load
*/
public function testCannotLoadEmptyFile(): void
{
$loader = new MoFileLoader();
$resource = __DIR__ . '/../resources/empty.mo';

$this->expectException(InvalidResourceException::class);

$loader->load($resource, 'en_US', 'messages');
}

/**
* @covers ::load
*/
public function testCannotLoadInvalidFile(): void
{
$loader = new MoFileLoader();
$resource = __DIR__ . '/../resources/invalid.mo';

$this->expectException(InvalidResourceException::class);

$loader->load($resource, 'en_US', 'messages');
}

/**
* @covers ::load
*/
public function testCannotLoadNonExistingFile(): void
{
$loader = new MoFileLoader();
$resource = __DIR__ . '/../resources/non-existing.mo';

$this->expectException(NotFoundResourceException::class);

$loader->load($resource, 'en_US', 'messages');
}
}
Empty file added tests/resources/empty.mo
Empty file.
Empty file added tests/resources/empty.po
Empty file.
3 changes: 3 additions & 0 deletions tests/resources/invalid.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
foobar
test
this is an invalid translation file
3 changes: 3 additions & 0 deletions tests/resources/invalid.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
foobar
test
this is an invalid translation file
Binary file added tests/resources/messages.mo
Binary file not shown.
58 changes: 58 additions & 0 deletions tests/resources/messages.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

msgid "foo"
msgstr "bar"

msgid "one foo"
msgid_plural "%count% foos"
msgstr[0] "one bar"
msgstr[1] "%count% bars"

msgid "{0} no foos|one foo|%count% foos"
msgstr "{0} no bars|one bar|%count% bars"

msgid "missing foo plural"
msgid_plural "missing foo plurals"
msgstr[0] "missing bar plural"
msgstr[1] ""

msgid "missing foo singular"
msgid_plural "missing foo singulars"
msgstr[0] ""
msgstr[1] "missing bar singulars"

msgid "message without translation"
msgstr ""

msgid "one message without translation"
msgid_plural "%count% messages without translations"
msgstr[0] ""
msgstr[1] ""

msgid "{0} no messages without translations|one message without translation|%count% messages without translations"
msgstr ""

msgid "string containing || foo"
msgstr "string containing || bar"

msgid "one string containing || foo"
msgid_plural "%count% strings containing || foos"
msgstr[0] "one string containing || bar"
msgstr[1] "%count% strings containing || bars"

msgid "{0} no strings containing || foos|one string containing || foo|%count% strings containing || foos"
msgstr "{0} no strings containing || bars|one string containing || bar|%count% strings containing || bars"

msgid "escaped \"foo\""
msgstr "escaped \"bar\""

msgid "one escaped \"foo\""
msgid_plural "%count% escaped \"foos\""
msgstr[0] "one escaped \"bar\""
msgstr[1] "%count% escaped \"bars\""

msgid "{0} no escaped \"foos\"|one one escaped \"foo\"|%count% escaped \"foos\""
msgstr "{0} no escaped \"bars\"|one one escaped \"bar\"|%count% escaped \"bars\""

0 comments on commit 679f8a3

Please sign in to comment.