Initial commit: Pireon Admin — headless admin panel and public API
Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer collective, backed by the Pireon OAuth identity server. The public site is a separate, server-rendered application consuming /api/public/v1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
186
tests/Unit/TranslationCatalogueTest.php
Normal file
186
tests/Unit/TranslationCatalogueTest.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use SplFileInfo;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* The three languages have to say the same things.
|
||||
*
|
||||
* A key present in one file and absent from another does not fail anywhere: it
|
||||
* renders as the raw key — `panel.widgets.stats.projects` — on a screen nobody
|
||||
* is looking at in that language. It is the kind of fault that only ever gets
|
||||
* found by a user, which is why it is worth a test.
|
||||
*/
|
||||
class TranslationCatalogueTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
protected function locales(): array
|
||||
{
|
||||
return array_keys((array) config('locales.supported'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Every leaf of a translation file, as dotted keys.
|
||||
*
|
||||
* @param array<string, mixed> $translations
|
||||
* @return array<int, string>
|
||||
*/
|
||||
protected function flatten(array $translations, string $prefix = ''): array
|
||||
{
|
||||
$keys = [];
|
||||
|
||||
foreach ($translations as $key => $value) {
|
||||
$path = $prefix === '' ? (string) $key : "{$prefix}.{$key}";
|
||||
|
||||
if (is_array($value)) {
|
||||
$keys = [...$keys, ...$this->flatten($value, $path)];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$keys[] = $path;
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, array<int, string>>>
|
||||
*/
|
||||
protected function catalogue(): array
|
||||
{
|
||||
$catalogue = [];
|
||||
|
||||
foreach ($this->locales() as $locale) {
|
||||
foreach (glob(lang_path("{$locale}/*.php")) ?: [] as $file) {
|
||||
$catalogue[basename($file)][$locale] = $this->flatten(require $file);
|
||||
}
|
||||
}
|
||||
|
||||
return $catalogue;
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function every_language_carries_the_same_keys(): void
|
||||
{
|
||||
$missing = [];
|
||||
|
||||
foreach ($this->catalogue() as $file => $byLocale) {
|
||||
$everyKey = array_unique(array_merge(...array_values($byLocale)));
|
||||
|
||||
foreach ($this->locales() as $locale) {
|
||||
foreach (array_diff($everyKey, $byLocale[$locale] ?? []) as $key) {
|
||||
$missing[] = "[{$locale}] {$file} :: {$key}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $missing, "Untranslated keys:\n".implode("\n", $missing));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function no_translation_is_left_empty(): void
|
||||
{
|
||||
$empty = [];
|
||||
|
||||
foreach ($this->locales() as $locale) {
|
||||
foreach (glob(lang_path("{$locale}/*.php")) ?: [] as $file) {
|
||||
foreach (require $file as $key => $value) {
|
||||
foreach ($this->leaves($value, (string) $key) as $path => $text) {
|
||||
if (trim((string) $text) === '') {
|
||||
$empty[] = "[{$locale}] ".basename($file)." :: {$path}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $empty, "Empty translations:\n".implode("\n", $empty));
|
||||
}
|
||||
|
||||
/**
|
||||
* Every key the application actually asks for exists.
|
||||
*
|
||||
* The mirror image of the test above: that one catches a key translated in
|
||||
* one language and forgotten in another, this one catches a key nobody
|
||||
* translated at all.
|
||||
*/
|
||||
#[Test]
|
||||
public function every_key_the_code_uses_has_a_translation(): void
|
||||
{
|
||||
$default = (string) config('locales.default');
|
||||
$missing = [];
|
||||
|
||||
foreach ($this->sourceFiles() as $file) {
|
||||
$source = (string) file_get_contents($file->getPathname());
|
||||
|
||||
preg_match_all(
|
||||
'/(?:__|trans_choice)\(\s*[\'"]([a-z0-9_]+\.[a-z0-9_.\-]+)[\'"]/i',
|
||||
$source,
|
||||
$matches,
|
||||
);
|
||||
|
||||
foreach ($matches[1] as $key) {
|
||||
[$group] = explode('.', $key, 2);
|
||||
|
||||
// Keys belonging to a package (`filament-panels::…`) or built
|
||||
// dynamically are not this test's business.
|
||||
if (! file_exists(lang_path("{$default}/{$group}.php"))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! is_string(__($key)) || __($key) === $key) {
|
||||
$missing[] = $key.' ('.str_replace(base_path().'/', '', $file->getPathname()).')';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], array_unique($missing), "Keys with no translation:\n".implode("\n", array_unique($missing)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, SplFileInfo>
|
||||
*/
|
||||
protected function sourceFiles(): array
|
||||
{
|
||||
$files = [];
|
||||
|
||||
foreach ([app_path(), resource_path('views')] as $directory) {
|
||||
/** @var SplFileInfo $file */
|
||||
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory)) as $file) {
|
||||
if ($file->isFile() && in_array($file->getExtension(), ['php'], true)) {
|
||||
$files[] = $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function leaves(mixed $value, string $prefix): array
|
||||
{
|
||||
if (! is_array($value)) {
|
||||
return [$prefix => $value];
|
||||
}
|
||||
|
||||
$leaves = [];
|
||||
|
||||
foreach ($value as $key => $child) {
|
||||
$leaves += $this->leaves($child, "{$prefix}.{$key}");
|
||||
}
|
||||
|
||||
return $leaves;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user