Files
FreshRSS/tests/cli/i18n/I18nDataTest.php
polybjorn 3a7431ce04 fix(i18n): validate language directory names against gen.lang.* keys (#8767)
Detect when an `app/i18n/<lang>/` directory has no matching `gen.lang.<lang>`
key in the reference language (or vice versa), and refuse to regenerate the
README from that invalid state.

This catches a class of silent corruption where the README translation
table renders literal i18n keys instead of localised language names. The
trigger is most often a case-folded directory on macOS APFS - git tracks
`zh-TW`, the local FS reads back `zh-tw`, the script's `_t('gen.lang.zh-tw')`
lookup misses, and the README ends up with `gen.lang.zh-tw (zh-tw)` instead
of `正體中文 (zh-TW)`. The same check also flags orphan directories (no
display-name key) and orphan keys (no directory).

The new validateLanguageNames() method on I18nData performs a bidirectional
set comparison and returns human-readable issues. cli/check.translation.php
prints them to STDERR and gates --generate-readme on the result, leaving
routine completeness validation behaviour unchanged. Adds four PHPUnit
tests covering: clean state, case mismatch, orphan directory, orphan key.

Co-authored-by: Bjørn A. Andersen <polybjorn@users.noreply.github.com>
2026-05-02 23:43:19 +02:00

919 lines
26 KiB
PHP

<?php
declare(strict_types=1);
require_once dirname(__DIR__, 3) . '/cli/i18n/I18nData.php';
require_once dirname(__DIR__, 3) . '/cli/i18n/I18nValue.php';
final class I18nDataTest extends \PHPUnit\Framework\TestCase {
/** @var array<string,array<string,array<string,I18nValue>>> */
private array $referenceData;
/** @var I18nValue&PHPUnit\Framework\MockObject\MockObject */
private $value;
#[\Override]
public function setUp(): void {
$this->value = $this->getMockBuilder(I18nValue::class)
->disableOriginalConstructor()
->getMock();
$this->referenceData = [
'en' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
'file1.l1.l2.k2' => $this->value,
],
'file2.php' => [
'file2.l1.l2._' => $this->value,
'file2.l1.l2.k1' => $this->value,
'file2.l1.l2.k2' => $this->value,
],
'file3.php' => [
'file3.l1.l2._' => $this->value,
'file3.l1.l2.k1' => $this->value,
],
],
];
}
public function testMoveKey(): void {
$data = new I18nData($this->referenceData);
$value = $data->getData()['en']['file2.php']['file2.l1.l2.k2'];
self::assertTrue($data->isKnown('file2.l1.l2.k2'));
self::assertFalse($data->isKnown('file2.l1.nkl2'));
$data->moveKey('file2.l1.l2.k2', 'file2.l1.nkl2');
self::assertFalse($data->isKnown('file2.l1.l2.k2'));
self::assertTrue($data->isKnown('file2.l1.nkl2'));
}
public function testConstructWhenReferenceOnly(): void {
$data = new I18nData($this->referenceData);
self::assertSame($this->referenceData, $data->getData());
}
public function testConstructorWhenLanguageIsMissingFile(): void {
$rawData = array_merge($this->referenceData, [
'fr' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
],
],
]);
$data = new I18nData($rawData);
self::assertEquals([
'en' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
'file1.l1.l2.k2' => $this->value,
],
'file2.php' => [
'file2.l1.l2._' => $this->value,
'file2.l1.l2.k1' => $this->value,
'file2.l1.l2.k2' => $this->value,
],
'file3.php' => [
'file3.l1.l2._' => $this->value,
'file3.l1.l2.k1' => $this->value,
],
],
'fr' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
'file1.l1.l2.k2' => $this->value,
],
'file2.php' => [
'file2.l1.l2._' => $this->value,
'file2.l1.l2.k1' => $this->value,
'file2.l1.l2.k2' => $this->value,
],
'file3.php' => [
'file3.l1.l2._' => $this->value,
'file3.l1.l2.k1' => $this->value,
],
],
], $data->getData());
}
public function testConstructorWhenLanguageIsMissingKeys(): void {
$rawData = array_merge($this->referenceData, [
'fr' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
],
'file2.php' => [
'file2.l1.l2.k1' => $this->value,
],
],
]);
$data = new I18nData($rawData);
self::assertEquals([
'en' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
'file1.l1.l2.k2' => $this->value,
],
'file2.php' => [
'file2.l1.l2._' => $this->value,
'file2.l1.l2.k1' => $this->value,
'file2.l1.l2.k2' => $this->value,
],
'file3.php' => [
'file3.l1.l2._' => $this->value,
'file3.l1.l2.k1' => $this->value,
],
],
'fr' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
'file1.l1.l2.k2' => $this->value,
],
'file2.php' => [
'file2.l1.l2._' => $this->value,
'file2.l1.l2.k1' => $this->value,
'file2.l1.l2.k2' => $this->value,
],
'file3.php' => [
'file3.l1.l2._' => $this->value,
'file3.l1.l2.k1' => $this->value,
],
],
], $data->getData());
}
public function testConstructorWhenLanguageHasExtraKeys(): void {
$rawData = array_merge($this->referenceData, [
'fr' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
'file1.l1.l2.k2' => $this->value,
'file1.l1.l2.k3' => $this->value,
],
'file2.php' => [
'file2.l1.l2.k1' => $this->value,
'file2.l1.l2.k2' => $this->value,
'file2.l1.l2.k3' => $this->value,
],
'file3.php' => [
'file3.l1.l2._' => $this->value,
'file3.l1.l2.k1' => $this->value,
],
],
]);
$data = new I18nData($rawData);
self::assertEquals([
'en' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
'file1.l1.l2.k2' => $this->value,
],
'file2.php' => [
'file2.l1.l2._' => $this->value,
'file2.l1.l2.k1' => $this->value,
'file2.l1.l2.k2' => $this->value,
],
'file3.php' => [
'file3.l1.l2._' => $this->value,
'file3.l1.l2.k1' => $this->value,
],
],
'fr' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
'file1.l1.l2.k2' => $this->value,
],
'file2.php' => [
'file2.l1.l2._' => $this->value,
'file2.l1.l2.k1' => $this->value,
'file2.l1.l2.k2' => $this->value,
],
'file3.php' => [
'file3.l1.l2._' => $this->value,
'file3.l1.l2.k1' => $this->value,
],
],
], $data->getData());
}
public function testConstructorKeepsLocalePluralVariants(): void {
$rawData = [
'en' => [
'gen.php' => [
'gen.interval.day.0' => $this->value,
'gen.interval.day.1' => $this->value,
],
],
'ru' => [
'gen.php' => [
'gen.interval.day.0' => $this->value,
'gen.interval.day.1' => $this->value,
'gen.interval.day.2' => $this->value,
],
],
];
$data = new I18nData($rawData);
self::assertArrayHasKey('gen.interval.day.2', $data->getLanguage('ru')['gen.php']);
self::assertArrayNotHasKey('gen.interval.day.2', $data->getReferenceLanguage()['gen.php']);
}
public function testConstructorPrefillsMissingLocalePluralVariantsFromEnglishPlural(): void {
$rawData = [
'en' => [
'gen.php' => [
'gen.interval.day.0' => new I18nValue('%d day ago'),
'gen.interval.day.1' => new I18nValue('%d days ago'),
'gen.interval.hour.0' => new I18nValue('%d hour ago'),
'gen.interval.hour.1' => new I18nValue('%d hours ago'),
],
],
'ru' => [
'gen.php' => [
'gen.interval.day.0' => new I18nValue('%d день назад'),
'gen.interval.day.1' => new I18nValue('%d дня назад'),
'gen.interval.day.2' => new I18nValue('%d дней назад'),
],
],
];
$data = new I18nData($rawData);
$ruTranslations = $data->getLanguage('ru')['gen.php'];
self::assertSame('%d hour ago', $ruTranslations['gen.interval.hour.0']->getValue());
self::assertSame('%d hours ago', $ruTranslations['gen.interval.hour.1']->getValue());
self::assertSame('%d hours ago', $ruTranslations['gen.interval.hour.2']->getValue());
self::assertTrue($ruTranslations['gen.interval.hour.0']->isTodo());
self::assertTrue($ruTranslations['gen.interval.hour.1']->isTodo());
self::assertTrue($ruTranslations['gen.interval.hour.2']->isTodo());
}
public function testConstructorMarksHigherLocalePluralVariantsAsTodoWhenEqualToEnglishPlural(): void {
$rawData = [
'en' => [
'gen.php' => [
'gen.interval.day.0' => new I18nValue('%d day ago'),
'gen.interval.day.1' => new I18nValue('%d days ago'),
],
],
'ru' => [
'gen.php' => [
'gen.interval.day.0' => new I18nValue('%d день назад'),
'gen.interval.day.1' => new I18nValue('%d дня назад'),
'gen.interval.day.2' => new I18nValue('%d days ago'),
],
],
];
$data = new I18nData($rawData);
$ruTranslations = $data->getLanguage('ru')['gen.php'];
self::assertFalse($ruTranslations['gen.interval.day.0']->isTodo());
self::assertFalse($ruTranslations['gen.interval.day.1']->isTodo());
self::assertTrue($ruTranslations['gen.interval.day.2']->isTodo());
}
public function testConstructorSkipsEnglishPluralVariantsNotUsedByOneFormLanguage(): void {
$rawData = [
'en' => [
'gen.php' => [
'gen.interval.day.0' => new I18nValue('%d day ago'),
'gen.interval.day.1' => new I18nValue('%d days ago'),
],
],
'id' => [
'gen.php' => [
'gen.interval.day.0' => new I18nValue('%d hari yang lalu'),
],
],
];
$data = new I18nData($rawData);
$idTranslations = $data->getLanguage('id')['gen.php'];
self::assertArrayHasKey('gen.interval.day.0', $idTranslations);
self::assertArrayNotHasKey('gen.interval.day.1', $idTranslations);
self::assertFalse($idTranslations['gen.interval.day.0']->isTodo());
}
public function testConstructorWhenValueIsIdenticalAndIsMarkedAsIgnore(): void {
$value = $this->getMockBuilder(I18nValue::class)
->disableOriginalConstructor()
->getMock();
$value->expects(self::exactly(2))
->method('isIgnore')
->willReturn(true);
$value->expects(self::never())
->method('markAsTodo');
$value->expects(self::exactly(3))
->method('equal')
->with($value)
->willReturn(true);
$rawData = array_merge($this->referenceData, [
'fr' => [
'file2.php' => [
'file2.l1.l2.k1' => $value,
],
],
]);
$rawData['en']['file2.php']['file2.l1.l2.k1'] = $value;
new I18nData($rawData);
}
public function testConstructorWhenValueIsIdenticalAndIsNotMarkedAsIgnore(): void {
$value = $this->getMockBuilder(I18nValue::class)
->disableOriginalConstructor()
->getMock();
$value->expects(self::exactly(2))
->method('isIgnore')
->willReturn(false);
$value->expects(self::exactly(2))
->method('markAsTodo');
$value->expects(self::exactly(2))
->method('equal')
->with($value)
->willReturn(true);
$rawData = array_merge($this->referenceData, [
'fr' => [
'file2.php' => [
'file2.l1.l2.k1' => $value,
],
],
]);
$rawData['en']['file2.php']['file2.l1.l2.k1'] = $value;
new I18nData($rawData);
}
public function testConstructorWhenValueIsDifferentAndIsMarkedAsToDo(): void {
$value = $this->getMockBuilder(I18nValue::class)
->disableOriginalConstructor()
->getMock();
$value->expects(self::once())
->method('isTodo')
->willReturn(true);
$value->expects(self::once())
->method('markAsDirty');
$rawData = array_merge($this->referenceData, [
'fr' => [
'file2.php' => [
'file2.l1.l2.k1' => $value,
],
],
]);
new I18nData($rawData);
}
public function testConstructorWhenValueIsDifferentAndIsNotMarkedAsTodo(): void {
$value = $this->getMockBuilder(I18nValue::class)
->disableOriginalConstructor()
->getMock();
$value->expects(self::once())
->method('isTodo')
->willReturn(false);
$value->expects(self::never())
->method('markAsDirty');
$rawData = array_merge($this->referenceData, [
'fr' => [
'file2.php' => [
'file2.l1.l2.k1' => $value,
],
],
]);
new I18nData($rawData);
}
public function testGetAvailableLanguagesWhenTheyAreSorted(): void {
$rawData = array_merge($this->referenceData, [
'fr' => [],
'nl' => [],
]);
$data = new I18nData($rawData);
self::assertSame([
'en',
'fr',
'nl',
], $data->getAvailableLanguages());
}
public function testGetAvailableLanguagesWhenTheyAreNotSorted(): void {
$rawData = array_merge($this->referenceData, [
'nl' => [],
'fr' => [],
'de' => [],
]);
$data = new I18nData($rawData);
self::assertSame([
'de',
'en',
'fr',
'nl',
], $data->getAvailableLanguages());
}
public function testAddLanguageWhenLanguageExists(): void {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('The selected language already exists.');
$data = new I18nData($this->referenceData);
$data->addLanguage('en');
}
public function testAddLanguageWhenNoReferenceProvided(): void {
$data = new I18nData($this->referenceData);
$data->addLanguage('fr');
self::assertSame([
'en' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
'file1.l1.l2.k2' => $this->value,
],
'file2.php' => [
'file2.l1.l2._' => $this->value,
'file2.l1.l2.k1' => $this->value,
'file2.l1.l2.k2' => $this->value,
],
'file3.php' => [
'file3.l1.l2._' => $this->value,
'file3.l1.l2.k1' => $this->value,
],
],
'fr' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
'file1.l1.l2.k2' => $this->value,
],
'file2.php' => [
'file2.l1.l2._' => $this->value,
'file2.l1.l2.k1' => $this->value,
'file2.l1.l2.k2' => $this->value,
],
'file3.php' => [
'file3.l1.l2._' => $this->value,
'file3.l1.l2.k1' => $this->value,
],
],
], $data->getData());
}
public function testAddLanguageWhenUnknownReferenceProvided(): void {
$data = new I18nData($this->referenceData);
$data->addLanguage('fr', 'unknown');
self::assertSame([
'en' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
'file1.l1.l2.k2' => $this->value,
],
'file2.php' => [
'file2.l1.l2._' => $this->value,
'file2.l1.l2.k1' => $this->value,
'file2.l1.l2.k2' => $this->value,
],
'file3.php' => [
'file3.l1.l2._' => $this->value,
'file3.l1.l2.k1' => $this->value,
],
],
'fr' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
'file1.l1.l2.k2' => $this->value,
],
'file2.php' => [
'file2.l1.l2._' => $this->value,
'file2.l1.l2.k1' => $this->value,
'file2.l1.l2.k2' => $this->value,
],
'file3.php' => [
'file3.l1.l2._' => $this->value,
'file3.l1.l2.k1' => $this->value,
],
],
], $data->getData());
}
public function testAddLanguageWhenKnownReferenceProvided(): void {
$data = new I18nData($this->referenceData);
$data->addLanguage('fr', 'en');
self::assertSame([
'en' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
'file1.l1.l2.k2' => $this->value,
],
'file2.php' => [
'file2.l1.l2._' => $this->value,
'file2.l1.l2.k1' => $this->value,
'file2.l1.l2.k2' => $this->value,
],
'file3.php' => [
'file3.l1.l2._' => $this->value,
'file3.l1.l2.k1' => $this->value,
],
],
'fr' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
'file1.l1.l2.k2' => $this->value,
],
'file2.php' => [
'file2.l1.l2._' => $this->value,
'file2.l1.l2.k1' => $this->value,
'file2.l1.l2.k2' => $this->value,
],
'file3.php' => [
'file3.l1.l2._' => $this->value,
'file3.l1.l2.k1' => $this->value,
],
],
], $data->getData());
}
public function testIsKnownWhenKeyExists(): void {
$data = new I18nData($this->referenceData);
self::assertTrue($data->isKnown('file2.l1.l2.k2'));
}
public function testIsKnownWhenKeyDoesNotExist(): void {
$data = new I18nData($this->referenceData);
self::assertFalse($data->isKnown('file2.l1.l2.k3'));
}
public function testAddKeyWhenKeyExists(): void {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('The selected key already exists.');
$data = new I18nData($this->referenceData);
$data->addKey('file2.l1.l2.k1', 'value');
}
public function testAddKeyWhenParentKeyExists(): void {
$rawData = array_merge($this->referenceData, [
'fr' => [],
]);
$data = new I18nData($rawData);
self::assertTrue($data->isKnown('file2.l1.l2.k1'));
self::assertFalse($data->isKnown('file2.l1.l2.k1._'));
self::assertFalse($data->isKnown('file2.l1.l2.k1.sk1'));
$data->addKey('file2.l1.l2.k1.sk1', 'value');
self::assertFalse($data->isKnown('file2.l1.l2.k1'));
self::assertTrue($data->isKnown('file2.l1.l2.k1._'));
self::assertTrue($data->isKnown('file2.l1.l2.k1.sk1'));
}
public function testAddKeyWhenKeyIsParent(): void {
$rawData = array_merge($this->referenceData, [
'fr' => [],
]);
$data = new I18nData($rawData);
self::assertFalse($data->isKnown('file1.l1.l2._'));
self::assertTrue($data->isKnown('file1.l1.l2.k1'));
self::assertTrue($data->isKnown('file1.l1.l2.k2'));
$data->addKey('file1.l1.l2', 'value');
self::assertTrue($data->isKnown('file1.l1.l2._'));
self::assertTrue($data->isKnown('file1.l1.l2.k1'));
self::assertTrue($data->isKnown('file1.l1.l2.k2'));
}
public function testAddKey(): void {
$getTargetedValue = static fn(I18nData $data, string $language) => $data->getData()[$language]['file2.php']['file2.l1.l2.k3'];
$rawData = array_merge($this->referenceData, [
'fr' => [],
]);
$data = new I18nData($rawData);
self::assertFalse($data->isKnown('file2.l1.l2.k3'));
$data->addKey('file2.l1.l2.k3', 'value');
self::assertTrue($data->isKnown('file2.l1.l2.k3'));
$enValue = $getTargetedValue($data, 'en');
$frValue = $getTargetedValue($data, 'fr');
self::assertInstanceOf(I18nValue::class, $enValue);
self::assertSame('value', $enValue->getValue());
self::assertTrue($enValue->isTodo());
self::assertSame($frValue, $enValue);
}
public function testAddFileWhenNotPhpFile(): void {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('The selected file name is not supported.');
$data = new I18nData($this->referenceData);
$data->addFile('file2');
}
public function testAddFileWhenAlreadyExists(): void {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('The selected file exists already.');
$data = new I18nData($this->referenceData);
self::assertTrue($data->exists('file2.php'));
$data->addFile('file2.php');
}
public function testAddFileWhenNotExists(): void {
$data = new I18nData($this->referenceData);
self::assertFalse($data->exists('newfile.php'));
$data->addFile('newfile.php');
self::assertTrue($data->exists('newfile.php'));
}
public function testAddValueWhenLanguageDoesNotExist(): void {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('The selected language does not exist.');
$data = new I18nData($this->referenceData);
$data->addValue('file2.l1.l2.k2', 'new value', 'fr');
}
public function testAddValueWhenKeyDoesNotExist(): void {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('The selected key does not exist for the selected language.');
$data = new I18nData($this->referenceData);
$data->addValue('unknown key', 'new value', 'en');
}
public function testAddValueWhenLanguageIsReferenceAndValueInOtherLanguageHasNotChange(): void {
$getTargetedValue = static fn(I18nData $data, string $language) => $data->getData()[$language]['file2.php']['file2.l1.l2.k2'];
$this->value->expects(self::atLeast(2))
->method('equal')
->with($this->value)
->willReturn(true);
$rawData = array_merge($this->referenceData, [
'fr' => [],
]);
$data = new I18nData($rawData);
$beforeEnValue = $getTargetedValue($data, 'en');
$beforeFrValue = $getTargetedValue($data, 'fr');
$data->addValue('file2.l1.l2.k2', 'new value', 'en');
$afterEnValue = $getTargetedValue($data, 'en');
$afterFrValue = $getTargetedValue($data, 'fr');
self::assertEquals($this->value, $beforeEnValue);
self::assertEquals($this->value, $beforeFrValue);
self::assertInstanceOf(I18nValue::class, $afterEnValue);
self::assertSame('new value', $afterEnValue->getValue());
self::assertInstanceOf(I18nValue::class, $afterFrValue);
self::assertSame('new value', $afterFrValue->getValue());
}
public function testAddValueWhenLanguageIsReferenceAndValueInOtherLanguageHasChange(): void {
$getTargetedValue = static fn(I18nData $data, string $language) => $data->getData()[$language]['file2.php']['file2.l1.l2.k2'];
$this->value->expects(self::any())
->method('equal')
->with($this->value)
->willReturn(true);
$value = $this->getMockBuilder(I18nValue::class)
->disableOriginalConstructor()
->getMock();
$rawData = array_merge($this->referenceData, [
'fr' => [
'file2.php' => [
'file2.l1.l2.k2' => $value,
]
],
]);
$data = new I18nData($rawData);
$beforeEnValue = $getTargetedValue($data, 'en');
$beforeFrValue = $getTargetedValue($data, 'fr');
$data->addValue('file2.l1.l2.k2', 'new value', 'en');
$afterEnValue = $getTargetedValue($data, 'en');
$afterFrValue = $getTargetedValue($data, 'fr');
self::assertEquals($this->value, $beforeEnValue);
self::assertEquals($value, $beforeFrValue);
self::assertInstanceOf(I18nValue::class, $afterEnValue);
self::assertSame('new value', $afterEnValue->getValue());
self::assertEquals($value, $afterFrValue);
}
public function testAddValueWhenLanguageIsNotReference(): void {
$getTargetedValue = static fn(I18nData $data, string $language) => $data->getData()[$language]['file2.php']['file2.l1.l2.k2'];
$rawData = array_merge($this->referenceData, [
'fr' => [],
]);
$data = new I18nData($rawData);
$beforeEnValue = $getTargetedValue($data, 'en');
$beforeFrValue = $getTargetedValue($data, 'fr');
$data->addValue('file2.l1.l2.k2', 'new value', 'fr');
$afterEnValue = $getTargetedValue($data, 'en');
$afterFrValue = $getTargetedValue($data, 'fr');
self::assertEquals($this->value, $beforeEnValue);
self::assertEquals($this->value, $beforeFrValue);
self::assertEquals($this->value, $afterEnValue);
self::assertInstanceOf(I18nValue::class, $afterFrValue);
self::assertSame('new value', $afterFrValue->getValue());
}
public function testRemoveKeyWhenKeyDoesNotExist(): void {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('The selected key does not exist.');
$data = new I18nData($this->referenceData);
$data->removeKey('Unknown key');
}
public function testRemoveKeyWhenKeyHasNoEmptySibling(): void {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('The selected key does not exist.');
$data = new I18nData($this->referenceData);
$data->removeKey('file1.l1.l2');
}
public function testRemoveKeyWhenKeyIsEmptySibling(): void {
$rawData = array_merge($this->referenceData, [
'fr' => [],
]);
$data = new I18nData($rawData);
$data->removeKey('file2.l1.l2');
self::assertEquals([
'en' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
'file1.l1.l2.k2' => $this->value,
],
'file2.php' => [
'file2.l1.l2.k1' => $this->value,
'file2.l1.l2.k2' => $this->value,
],
'file3.php' => [
'file3.l1.l2._' => $this->value,
'file3.l1.l2.k1' => $this->value,
],
],
'fr' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
'file1.l1.l2.k2' => $this->value,
],
'file2.php' => [
'file2.l1.l2.k1' => $this->value,
'file2.l1.l2.k2' => $this->value,
],
'file3.php' => [
'file3.l1.l2._' => $this->value,
'file3.l1.l2.k1' => $this->value,
],
],
], $data->getData());
}
public function testRemoveKeyWhenKeyIsTheOnlyChild(): void {
$rawData = array_merge($this->referenceData, [
'fr' => [],
]);
$data = new I18nData($rawData);
$data->removeKey('file3.l1.l2.k1');
self::assertEquals([
'en' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
'file1.l1.l2.k2' => $this->value,
],
'file2.php' => [
'file2.l1.l2._' => $this->value,
'file2.l1.l2.k1' => $this->value,
'file2.l1.l2.k2' => $this->value,
],
'file3.php' => [
'file3.l1.l2' => $this->value,
],
],
'fr' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
'file1.l1.l2.k2' => $this->value,
],
'file2.php' => [
'file2.l1.l2._' => $this->value,
'file2.l1.l2.k1' => $this->value,
'file2.l1.l2.k2' => $this->value,
],
'file3.php' => [
'file3.l1.l2' => $this->value,
],
],
], $data->getData());
}
public function testIgnore(): void {
$value = $this->getMockBuilder(I18nValue::class)
->disableOriginalConstructor()
->getMock();
$value->expects(self::once())
->method('unmarkAsIgnore');
$value->expects(self::exactly(2))
->method('markAsIgnore');
$rawData = array_merge($this->referenceData, [
'fr' => [
'file1.php' => [
'file1.l1.l2.k1' => $value,
],
],
]);
$data = new I18nData($rawData);
$data->ignore('file1.l1.l2.k1', 'fr');
$data->ignore('file1.l1.l2.k1', 'fr', true);
$data->ignore('file1.l1.l2.k1', 'fr', false);
}
public function testIgnoreUnmodified(): void {
$value = $this->getMockBuilder(I18nValue::class)
->disableOriginalConstructor()
->getMock();
$value->expects(self::once())
->method('unmarkAsIgnore');
$value->expects(self::exactly(2))
->method('markAsIgnore');
$this->value->expects(self::atLeast(2))
->method('equal')
->with($value)
->willReturn(true);
$rawData = array_merge($this->referenceData, [
'fr' => [
'file1.php' => [
'file1.l1.l2.k1' => $value,
],
],
]);
$data = new I18nData($rawData);
$data->ignore_unmodified('fr');
$data->ignore_unmodified('fr', true);
$data->ignore_unmodified('fr', false);
}
public function testGetLanguage(): void {
$rawData = array_merge($this->referenceData, [
'fr' => [],
'nl' => [],
]);
$data = new I18nData($rawData);
self::assertSame($this->referenceData['en'], $data->getLanguage('en'));
}
public function testGetReferenceLanguage(): void {
$rawData = array_merge($this->referenceData, [
'fr' => [],
'nl' => [],
]);
$data = new I18nData($rawData);
self::assertSame($this->referenceData['en'], $data->getReferenceLanguage());
}
/** @return array<string,array<string,array<string,I18nValue>>> */
private function dataWithLangKeys(string ...$langCodes): array {
$genFile = [];
foreach ($langCodes as $code) {
$genFile['gen.lang.' . $code] = $this->value;
}
return [
'en' => ['gen.php' => $genFile],
];
}
public function testValidateLanguageNamesPassesWhenDirsAndKeysMatch(): void {
$rawData = $this->dataWithLangKeys('en', 'fr', 'zh-TW');
$rawData['fr'] = [];
$rawData['zh-TW'] = [];
$data = new I18nData($rawData);
self::assertSame([], $data->validateLanguageNames());
}
public function testValidateLanguageNamesFlagsCaseMismatch(): void {
$rawData = $this->dataWithLangKeys('en', 'zh-TW');
$rawData['zh-tw'] = [];
$data = new I18nData($rawData);
$issues = $data->validateLanguageNames();
self::assertCount(2, $issues);
self::assertStringContainsString('app/i18n/zh-tw/', $issues[0]);
self::assertStringContainsString('gen.lang.zh-TW', $issues[1]);
}
public function testValidateLanguageNamesFlagsOrphanDirectory(): void {
$rawData = $this->dataWithLangKeys('en');
$rawData['fr'] = [];
$data = new I18nData($rawData);
$issues = $data->validateLanguageNames();
self::assertCount(1, $issues);
self::assertStringContainsString('app/i18n/fr/', $issues[0]);
}
public function testValidateLanguageNamesFlagsOrphanKey(): void {
$rawData = $this->dataWithLangKeys('en', 'fr');
$data = new I18nData($rawData);
$issues = $data->validateLanguageNames();
self::assertCount(1, $issues);
self::assertStringContainsString('gen.lang.fr', $issues[0]);
}
}