PHPIndex

This page lists files in the current directory. You can view content, get download/execute commands for Wget, Curl, or PowerShell, or filter the list using wildcards (e.g., `*.sh`).

I18nCompletionValidatorTest.php
wget 'https://lists2.roe3.org/FreshRSS/tests/cli/i18n/I18nCompletionValidatorTest.php'
View Content
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../../cli/i18n/I18nCompletionValidator.php';
require_once __DIR__ . '/../../../cli/i18n/I18nValue.php';

class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase {
	/** @var I18nValue&PHPUnit\Framework\MockObject\MockObject */
	private $value;

	#[\Override]
	public function setUp(): void {
		$this->value = $this->getMockBuilder(I18nValue::class)
			->disableOriginalConstructor()
			->getMock();
	}

	public function testDisplayReport(): void {
		$validator = new I18nCompletionValidator([], []);

		self::assertEquals("There is no data.\n", $validator->displayReport());

		$reflectionTotalEntries = new ReflectionProperty(I18nCompletionValidator::class, 'totalEntries');
		$reflectionTotalEntries->setAccessible(true);
		$reflectionTotalEntries->setValue($validator, 100);

		self::assertEquals("Translation is   0.0% complete.\n", $validator->displayReport());

		$reflectionPassEntries = new ReflectionProperty(I18nCompletionValidator::class, 'passEntries');
		$reflectionPassEntries->setAccessible(true);
		$reflectionPassEntries->setValue($validator, 25);

		self::assertEquals("Translation is  25.0% complete.\n", $validator->displayReport());

		$reflectionPassEntries->setValue($validator, 100);

		self::assertEquals("Translation is 100.0% complete.\n", $validator->displayReport());

		$reflectionPassEntries->setValue($validator, 200);

		$this->expectException(\RuntimeException::class);
		$this->expectExceptionMessage('The number of translated strings cannot be higher than the number of strings');
		$validator->displayReport();
	}

	public function testValidateWhenNoData(): void {
		$validator = new I18nCompletionValidator([], []);
		self::assertTrue($validator->validate());
		self::assertEquals('', $validator->displayResult());
	}

	public function testValidateWhenKeyIsMissing(): void {
		$validator = new I18nCompletionValidator([
			'file1.php' => [
				'file1.l1.l2.k1' => $this->value,
			],
			'file2.php' => [
				'file2.l1.l2.k1' => $this->value,
			],
		], []);

		self::assertFalse($validator->validate());
		self::assertEquals("Missing key file1.l1.l2.k1\nMissing key file2.l1.l2.k1\n", $validator->displayResult());
	}

	public function testValidateWhenKeyIsIgnored(): void {
		$this->value->expects(self::exactly(2))
			->method('isIgnore')
			->willReturn(true);

		$validator = new I18nCompletionValidator([
			'file1.php' => [
				'file1.l1.l2.k1' => $this->value,
			],
			'file2.php' => [
				'file2.l1.l2.k1' => $this->value,
			],
		], [
			'file1.php' => [
				'file1.l1.l2.k1' => $this->value,
			],
			'file2.php' => [
				'file2.l1.l2.k1' => $this->value,
			],
		]);

		self::assertTrue($validator->validate());
		self::assertEquals('', $validator->displayResult());
	}

	public function testValidateWhenValueIsEqual(): void {
		$this->value->expects(self::exactly(2))
			->method('isIgnore')
			->willReturn(false);
		$this->value->expects(self::exactly(2))
			->method('equal')
			->willReturn(true);

		$validator = new I18nCompletionValidator([
			'file1.php' => [
				'file1.l1.l2.k1' => $this->value,
			],
			'file2.php' => [
				'file2.l1.l2.k1' => $this->value,
			],
		], [
			'file1.php' => [
				'file1.l1.l2.k1' => $this->value,
			],
			'file2.php' => [
				'file2.l1.l2.k1' => $this->value,
			],
		]);

		self::assertFalse($validator->validate());
		self::assertEquals("Untranslated key file1.l1.l2.k1 - \nUntranslated key file2.l1.l2.k1 - \n", $validator->displayResult());
	}

	public function testValidateWhenValueIsDifferent(): void {
		$this->value->expects(self::exactly(2))
			->method('isIgnore')
			->willReturn(false);
		$this->value->expects(self::exactly(2))
			->method('equal')
			->willReturn(false);

		$validator = new I18nCompletionValidator([
			'file1.php' => [
				'file1.l1.l2.k1' => $this->value,
			],
			'file2.php' => [
				'file2.l1.l2.k1' => $this->value,
			],
		], [
			'file1.php' => [
				'file1.l1.l2.k1' => $this->value,
			],
			'file2.php' => [
				'file2.l1.l2.k1' => $this->value,
			],
		]);

		self::assertTrue($validator->validate());
		self::assertEquals('', $validator->displayResult());
	}
}
I18nDataTest.php
wget 'https://lists2.roe3.org/FreshRSS/tests/cli/i18n/I18nDataTest.php'
View Content
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../../cli/i18n/I18nData.php';
require_once __DIR__ . '/../../../cli/i18n/I18nValue.php';

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 testConstructWhenReferenceOnly(): void {
		$data = new I18nData($this->referenceData);
		self::assertEquals($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 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::assertEquals([
			'en',
			'fr',
			'nl',
		], $data->getAvailableLanguages());
	}

	public function testGetAvailableLanguagesWhenTheyAreNotSorted(): void {
		$rawData = array_merge($this->referenceData, [
			'nl' => [],
			'fr' => [],
			'de' => [],
		]);
		$data = new I18nData($rawData);
		self::assertEquals([
			'de',
			'en',
			'fr',
			'nl',
		], $data->getAvailableLanguages());
	}

	public function testAddLanguageWhenLanguageExists(): void {
		$this->expectException(\Exception::class);
		$this->expectExceptionMessage('The selected language already exist.');
		$data = new I18nData($this->referenceData);
		$data->addLanguage('en');
	}

	public function testAddLanguageWhenNoReferenceProvided(): void {
		$data = new I18nData($this->referenceData);
		$data->addLanguage('fr');
		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 testAddLanguageWhenUnknownReferenceProvided(): void {
		$data = new I18nData($this->referenceData);
		$data->addLanguage('fr', 'unknown');
		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 testAddLanguageWhenKnownReferenceProvided(): void {
		$data = new I18nData($this->referenceData);
		$data->addLanguage('fr', 'en');
		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 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 exist.');
		$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::assertEquals('value', $enValue->getValue());
		self::assertTrue($enValue->isTodo());
		self::assertEquals($frValue, $enValue);
	}

	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::assertEquals('new value', $afterEnValue->getValue());
		self::assertInstanceOf(I18nValue::class, $afterFrValue);
		self::assertEquals('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::assertEquals('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::assertEquals('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::assertEquals($this->referenceData['en'], $data->getLanguage('en'));
	}

	public function testGetReferenceLanguage(): void {
		$rawData = array_merge($this->referenceData, [
			'fr' => [],
			'nl' => [],
		]);
		$data = new I18nData($rawData);
		self::assertEquals($this->referenceData['en'], $data->getReferenceLanguage());
	}
}
I18nFileTest.php
wget 'https://lists2.roe3.org/FreshRSS/tests/cli/i18n/I18nFileTest.php'
View Content
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../../cli/i18n/I18nFile.php';

class I18nFileTest extends PHPUnit\Framework\TestCase {
	public function test(): void {
		$before = $this->computeFilesHash();

		$file = new I18nFile();
		$data = $file->load();
		$file->dump($data);

		$after = $this->computeFilesHash();

		self::assertEquals($before, $after);
	}

	/** @return array<string,string|false> */
	private function computeFilesHash(): array {
		$hashes = [];

		$dirs = new DirectoryIterator(I18N_PATH);
		foreach ($dirs as $dir) {
			if ($dir->isDot()) {
				continue;
			}
			$files = new DirectoryIterator($dir->getPathname());
			foreach ($files as $file) {
				if (!$file->isFile()) {
					continue;
				}

				$hashes[$file->getPathname()] = sha1_file($file->getPathname());
			}
		}

		return $hashes;
	}
}
I18nUsageValidatorTest.php
wget 'https://lists2.roe3.org/FreshRSS/tests/cli/i18n/I18nUsageValidatorTest.php'
View Content
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../../cli/i18n/I18nValue.php';
require_once __DIR__ . '/../../../cli/i18n/I18nUsageValidator.php';

class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase {

	private I18nValue $value;

	#[\Override]
	public function setUp(): void {
		$this->value = $this->getMockBuilder(I18nValue::class)
			->disableOriginalConstructor()
			->getMock();
	}

	public function testDisplayReport(): void {
		$validator = new I18nUsageValidator([], []);

		self::assertEquals("There is no data.\n", $validator->displayReport());

		$reflectionTotalEntries = new ReflectionProperty(I18nUsageValidator::class, 'totalEntries');
		$reflectionTotalEntries->setAccessible(true);
		$reflectionTotalEntries->setValue($validator, 100);

		self::assertEquals("  0.0% of translation keys are unused.\n", $validator->displayReport());

		$reflectionFailedEntries = new ReflectionProperty(I18nUsageValidator::class, 'failedEntries');
		$reflectionFailedEntries->setAccessible(true);
		$reflectionFailedEntries->setValue($validator, 25);

		self::assertEquals(" 25.0% of translation keys are unused.\n", $validator->displayReport());

		$reflectionFailedEntries->setValue($validator, 100);

		self::assertEquals("100.0% of translation keys are unused.\n", $validator->displayReport());

		$reflectionFailedEntries->setValue($validator, 200);

		$this->expectException(\RuntimeException::class);
		$this->expectExceptionMessage('The number of unused strings cannot be higher than the number of strings');
		$validator->displayReport();
	}

	public function testValidateWhenNoData(): void {
		$validator = new I18nUsageValidator([], []);
		self::assertTrue($validator->validate());
		self::assertEquals('', $validator->displayResult());
	}

	public function testValidateWhenParentKeyExistsWithoutTransformation(): void {
		$validator = new I18nUsageValidator([
			'file1' => [
				'file1.l1.l2._' => $this->value,
			],
			'file2' => [
				'file2.l1.l2._' => $this->value,
			],
		], [
			'file1.l1.l2._',
			'file2.l1.l2._',
		]);
		self::assertTrue($validator->validate());
		self::assertEquals('', $validator->displayResult());
	}

	public function testValidateWhenParentKeyExistsWithTransformation(): void {
		$validator = new I18nUsageValidator([
			'file1' => [
				'file1.l1.l2._' => $this->value,
			],
			'file2' => [
				'file2.l1.l2._' => $this->value,
			],
		], [
			'file1.l1.l2',
			'file2.l1.l2',
		]);
		self::assertTrue($validator->validate());
		self::assertEquals('', $validator->displayResult());
	}

	public function testValidateWhenParentKeyDoesNotExist(): void {
		$validator = new I18nUsageValidator([
			'file1' => [
				'file1.l1.l2._' => $this->value,
			],
			'file2' => [
				'file2.l1.l2._' => $this->value,
			],
		], []);
		self::assertFalse($validator->validate());
		self::assertEquals("Unused key file1.l1.l2._ - \nUnused key file2.l1.l2._ - \n", $validator->displayResult());
	}

	public function testValidateWhenChildKeyExists(): void {
		$validator = new I18nUsageValidator([
			'file1' => [
				'file1.l1.l2.k1' => $this->value,
			],
			'file2' => [
				'file2.l1.l2.k1' => $this->value,
			],
		], [
			'file1.l1.l2.k1',
			'file2.l1.l2.k1',
		]);
		self::assertTrue($validator->validate());
		self::assertEquals('', $validator->displayResult());
	}

	public function testValidateWhenChildKeyDoesNotExist(): void {
		$validator = new I18nUsageValidator([
			'file1' => [
				'file1.l1.l2.k1' => $this->value,
			],
			'file2' => [
				'file2.l1.l2.k1' => $this->value,
			],
		], []);
		self::assertFalse($validator->validate());
		self::assertEquals("Unused key file1.l1.l2.k1 - \nUnused key file2.l1.l2.k1 - \n", $validator->displayResult());
	}
}
I18nValueTest.php
wget 'https://lists2.roe3.org/FreshRSS/tests/cli/i18n/I18nValueTest.php'
View Content
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../../cli/i18n/I18nValue.php';

class I18nValueTest extends PHPUnit\Framework\TestCase {
	public function testConstructorWithoutState(): void {
		$value = new I18nValue('some value');
		self::assertEquals('some value', $value->getValue());
		self::assertFalse($value->isIgnore());
		self::assertFalse($value->isTodo());
	}

	public function testConstructorWithUnknownState(): void {
		$value = new I18nValue('some value -> unknown');
		self::assertEquals('some value', $value->getValue());
		self::assertFalse($value->isIgnore());
		self::assertFalse($value->isTodo());
	}

	public function testConstructorWithTodoState(): void {
		$value = new I18nValue('some value -> todo');
		self::assertEquals('some value', $value->getValue());
		self::assertFalse($value->isIgnore());
		self::assertTrue($value->isTodo());
	}

	public function testConstructorWithIgnoreState(): void {
		$value = new I18nValue('some value -> ignore');
		self::assertEquals('some value', $value->getValue());
		self::assertTrue($value->isIgnore());
		self::assertFalse($value->isTodo());
	}

	public function testClone(): void {
		$value = new I18nValue('some value');
		$clonedValue = clone $value;
		self::assertEquals('some value', $value->getValue());
		self::assertEquals('some value', $clonedValue->getValue());
		self::assertFalse($value->isIgnore());
		self::assertFalse($clonedValue->isIgnore());
		self::assertFalse($value->isTodo());
		self::assertTrue($clonedValue->isTodo());
	}

	public function testEqualWhenValueIsIdentical(): void {
		$value = new I18nValue('some value');
		$clonedValue = clone $value;
		self::assertTrue($value->equal($clonedValue));
		self::assertTrue($clonedValue->equal($value));
	}

	public function testEqualWhenValueIsDifferent(): void {
		$value = new I18nValue('some value');
		$otherValue = new I18nValue('some other value');
		self::assertFalse($value->equal($otherValue));
		self::assertFalse($otherValue->equal($value));
	}

	public function testStates(): void {
		$reflectionProperty = new ReflectionProperty(I18nValue::class, 'state');
		$reflectionProperty->setAccessible(true);

		$value = new I18nValue('some value');
		self::assertNull($reflectionProperty->getValue($value));
		$value->markAsDirty();
		self::assertEquals('dirty', $reflectionProperty->getValue($value));
		$value->unmarkAsIgnore();
		self::assertEquals('dirty', $reflectionProperty->getValue($value));
		$value->markAsIgnore();
		self::assertEquals('ignore', $reflectionProperty->getValue($value));
		$value->unmarkAsIgnore();
		self::assertNull($reflectionProperty->getValue($value));
		$value->markAsTodo();
		self::assertEquals('todo', $reflectionProperty->getValue($value));
	}

	public function testToString(): void {
		$value = new I18nValue('some value');
		self::assertEquals('some value', $value->__toString());
		$value->markAsTodo();
		self::assertEquals('some value -> todo', $value->__toString());
	}
}