*/ protected function locales(): array { return array_keys((array) config('locales.supported')); } /** * Every leaf of a translation file, as dotted keys. * * @param array $translations * @return array */ 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>> */ 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 */ 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 */ 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; } }