new I18nValue('Identique -> dirty'), 'example.first' => new I18nValue('Identique -> dirty'), 'example.missing' => new I18nValue('Absent -> dirty'), 'example.empty' => new I18nValue('Vide -> dirty'), 'example.todo' => new I18nValue('Not translated -> todo'), 'example.ignore' => new I18nValue('Ignored -> ignore'), 'example.reviewed' => new I18nValue('Reviewed'), ]; $reference = [ 'example.first' => new I18nValue('First English string'), 'example.second' => new I18nValue('Second English string'), 'example.empty' => new I18nValue(" \t\r\n"), 'example.todo' => new I18nValue('Not translated'), 'example.ignore' => new I18nValue('Ignored'), 'example.reviewed' => new I18nValue('Reviewed English string'), ]; $output = $format->invoke($file, $translation, $reference); self::assertIsString($output); self::assertStringContainsString("'first' => 'Identique',\t// DIRTY - First English string", $output); self::assertStringContainsString("'second' => 'Identique',\t// DIRTY - Second English string", $output); self::assertStringContainsString("'missing' => 'Absent',\t// DIRTY\n", $output); self::assertStringContainsString("'empty' => 'Vide',\t// DIRTY\n", $output); self::assertStringContainsString("'todo' => 'Not translated',\t// TODO\n", $output); self::assertStringContainsString("'ignore' => 'Ignored',\t// IGNORE\n", $output); self::assertStringContainsString("'reviewed' => 'Reviewed',\n", $output); } public function testDirtyCommentsAreSafeAndIdempotent(): void { $file = new I18nFile(); $format = new ReflectionMethod(I18nFile::class, 'format'); $process = new ReflectionMethod(I18nFile::class, 'process'); $flatten = new ReflectionMethod(I18nFile::class, 'flatten'); $translation = ['example.key' => new I18nValue("L’apostrophe ' et \\ -> dirty")]; $reference = ['example.key' => new I18nValue("English ' and \\ %s HTML\r\nnext\tline ?> trailing ")]; $output = $format->invoke($file, $translation, $reference); self::assertIsString($output); self::assertStringContainsString("// DIRTY - English ' and \\ %s HTML next line ? > trailing\n", $output); self::assertStringNotContainsString('?>', $output); $filename = tempnam(sys_get_temp_dir(), 'freshrss-i18n-'); self::assertIsString($filename); try { file_put_contents($filename, $output); $values = include $filename; self::assertSame(['key' => "L’apostrophe ' et \\"], $values); $loaded = $process->invoke($file, $filename); self::assertIsArray($loaded); $roundTrip = $flatten->invoke($file, $loaded, 'example'); self::assertSame($output, $format->invoke($file, $roundTrip, $reference)); } finally { unlink($filename); } $reference['example.key'] = new I18nValue('Updated English'); $updated = $format->invoke($file, $translation, $reference); self::assertIsString($updated); self::assertStringContainsString('// DIRTY - Updated English', $updated); self::assertStringNotContainsString('trailing', $updated); } public function testDirtyCommentsUseEnglishFallbackForAdditionalPluralForms(): void { $format = new ReflectionMethod(I18nFile::class, 'format'); $output = $format->invoke(new I18nFile(), [ 'example.count.0' => new I18nValue('Un article -> dirty'), 'example.count.1' => new I18nValue('Des articles -> dirty'), 'example.count.2' => new I18nValue('Plusieurs articles -> dirty'), 'example.only.2' => new I18nValue('Autre forme -> dirty'), ], [ 'example.count.0' => new I18nValue('One article'), 'example.count.1' => new I18nValue('Many articles'), 'example.only.0' => new I18nValue('One form'), ]); self::assertIsString($output); self::assertStringContainsString("0 => 'Un article',\t// DIRTY - One article", $output); self::assertStringContainsString("1 => 'Des articles',\t// DIRTY - Many articles", $output); self::assertStringContainsString("2 => 'Plusieurs articles',\t// DIRTY - Many articles", $output); self::assertStringContainsString("2 => 'Autre forme',\t// DIRTY - One form", $output); } public function testDirtyCommentsShowBothEnglishFormsForSingleFormTranslations(): void { $format = new ReflectionMethod(I18nFile::class, 'format'); $output = $format->invoke(new I18nFile(), [ 'example.count.0' => new I18nValue('記事が %d 件あります -> dirty'), 'example.only.0' => new I18nValue('一つの形 -> dirty'), 'example.fallback.0' => new I18nValue('別の形 -> dirty'), ], [ 'example.count.0' => new I18nValue("One article\n?>"), 'example.count.1' => new I18nValue("Many articles\t?>"), 'example.only.0' => new I18nValue('One form'), 'example.fallback.1' => new I18nValue('Plural only'), ]); self::assertIsString($output); self::assertStringContainsString("0 => '記事が %d 件あります',\t// DIRTY - Singular: One article ? > | Plural: Many articles ? >\n", $output); self::assertStringContainsString("0 => '一つの形',\t// DIRTY - One form\n", $output); self::assertStringContainsString("0 => '別の形',\t// DIRTY - Plural only\n", $output); } public function test(): void { $before = $this->computeFilesHash(); $file = new I18nFile(); $data = $file->load(); self::assertArrayNotHasKey('plurals.php', $data['en']); $file->dump($data); $after = $this->computeFilesHash(); self::assertSame($before, $after); } /** @return array */ 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; } }