Mehrsprachigkeit der Oberfläche (Deutsch Standard, Englisch wählbar)

- Sprache je Benutzer (users.locale), Middleware SetLocale, Auswahl unter
  Einstellungen → Darstellung; angebotene Sprachen in config/app.php
- Übersetzungen über Laravel Lang (lang/de.json, lang/de/*.php), Seitentitel
  aus #[Title] werden übersetzt
- Eigene Texte und Korrekturen in lang/project/de.json, übernommen mit
  `artisan lang:apply-project` (überstehen `lang:update`)
- Tests: jede im Code verwendete Übersetzung ist vorhanden, eigene
  Übersetzungen sind übernommen, Sprachwahl und Standardsprache;
  SecurityTest prüft Texte sprachunabhängig über __()
- lang/ von Pint ausgenommen (erzeugte Dateien)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Christoph
2026-10-01 09:52:20 +02:00
co-authored by Claude Opus 5.5
parent cdd2d5b676
commit db2d5d6228
32 changed files with 4108 additions and 18 deletions
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace Tests\Unit;
use Illuminate\Support\Facades\File;
use Tests\TestCase;
/**
* Jeder Text, der im Code über __() bzw. #[Title(...)] verwendet wird, braucht für jede
* angebotene Sprache außer der Quellsprache Englisch eine Übersetzung in lang/{code}.json.
*/
class TranslationCoverageTest extends TestCase
{
public function test_every_translation_key_used_in_the_code_is_translated(): void
{
$keys = $this->translationKeysUsedInCode();
$this->assertNotEmpty($keys);
foreach (array_keys(config('app.available_locales')) as $locale) {
if ($locale === 'en') {
continue;
}
$translations = json_decode(File::get(lang_path("{$locale}.json")), true, flags: JSON_THROW_ON_ERROR);
$missing = array_values(array_diff($keys, array_keys($translations)));
$this->assertSame([], $missing, "Fehlende Übersetzungen in lang/{$locale}.json");
}
}
public function test_project_translations_are_applied(): void
{
foreach (File::glob(lang_path('project/*.json')) as $projectFile) {
$locale = pathinfo($projectFile, PATHINFO_FILENAME);
$project = json_decode(File::get($projectFile), true, flags: JSON_THROW_ON_ERROR);
$translations = json_decode(File::get(lang_path("{$locale}.json")), true, flags: JSON_THROW_ON_ERROR);
$notApplied = array_keys(array_diff_assoc($project, array_intersect_key($translations, $project)));
$this->assertSame([], $notApplied, "lang/project/{$locale}.json nicht übernommen – `sail artisan lang:apply-project` ausführen");
}
}
/**
* @return list<string>
*/
private function translationKeysUsedInCode(): array
{
$patterns = [
'/__\(\s*\'((?:[^\'\\\\]|\\\\.)*)\'/',
'/__\(\s*"((?:[^"\\\\]|\\\\.)*)"/',
'/#\[Title\(\s*\'((?:[^\'\\\\]|\\\\.)*)\'/',
];
$keys = [];
foreach ([resource_path('views'), app_path()] as $directory) {
foreach (File::allFiles($directory) as $file) {
if ($file->getExtension() !== 'php') {
continue;
}
foreach ($patterns as $pattern) {
preg_match_all($pattern, $file->getContents(), $matches);
foreach ($matches[1] as $key) {
$keys[] = stripslashes($key);
}
}
}
}
return array_values(array_unique($keys));
}
}