refactor: use english for routes, parameters and internal messages

This commit is contained in:
fromme committed 2026-09-13 18:58:24 +02:00
1 parent bce8f58be3
commit a88ff7ff24
22 files changed
+106 -135

No files matched your search

+3 -3
View File
@@ -98,11 +98,11 @@ final readonly class BoardPresenter
*/
public static function urlParameters(ResourceType $type, \DateTimeImmutable $date, TimeRange $time): array
{
$parameters = ['type' => $type->slug(), 'tag' => $date->format('Y-m-d')];
$parameters = ['type' => $type->value, 'date' => $date->format('Y-m-d')];
if (!$time->isAllDay()) {
$parameters['von'] = $time->start();
$parameters['bis'] = $time->end();
$parameters['from'] = $time->start();
$parameters['to'] = $time->end();
}
return $parameters;
+2 -2
View File
@@ -21,11 +21,11 @@ final readonly class TimeRange implements \Stringable
public int $endMinute,
) {
if ($startMinute < self::DAY_START || $endMinute > self::DAY_END) {
throw new \InvalidArgumentException('Eine Buchung muss innerhalb eines Tages liegen.');
throw new \InvalidArgumentException('A time range must stay within a single day.');
}
if ($startMinute >= $endMinute) {
throw new \InvalidArgumentException('Das Ende muss nach dem Beginn liegen.');
throw new \InvalidArgumentException('A time range must end after it starts.');
}
}
+1 -1
View File
@@ -66,7 +66,7 @@ final class SeedCommand extends Command
$this->em->flush();
$io->success(\sprintf('%d neue Datensätze angelegt.', $created));
$io->success(\sprintf('%d new records created.', $created));
return Command::SUCCESS;
}
+9 -11
View File
@@ -29,11 +29,11 @@ final class BoardController extends AbstractController
{
}
#[Route('/buchen/{type}', name: 'app_board', requirements: ['type' => '[a-z]+'], methods: ['GET'])]
#[Route('/book/{type}', name: 'app_board', requirements: ['type' => '[a-z]+'], methods: ['GET'])]
public function index(Request $request, string $type): Response
{
$resourceType = $this->resourceType($type);
$date = DateParam::parseOrToday($request->query->getString('tag'));
$date = DateParam::parseOrToday($request->query->getString('date'));
return $this->render('board/index.html.twig', $this->board->context(
$resourceType,
@@ -51,7 +51,7 @@ final class BoardController extends AbstractController
* client-side event choreography. Requests without the htmx header get a plain redirect
* rather than a bare pair of fragments, so the URL stays shareable.
*/
#[Route('/buchen/{type}/tag/{date}', name: 'app_board_day', requirements: ['type' => '[a-z]+', 'date' => DateParam::PATTERN], methods: ['GET'])]
#[Route('/book/{type}/day/{date}', name: 'app_board_day', requirements: ['type' => '[a-z]+', 'date' => DateParam::PATTERN], methods: ['GET'])]
public function day(Request $request, string $type, string $date): Response
{
$resourceType = $this->resourceType($type);
@@ -76,19 +76,17 @@ final class BoardController extends AbstractController
*/
public static function timeOf(Request $request): TimeRange
{
return TimeRange::parse($request->query->getString('von'), $request->query->getString('bis'))
return TimeRange::parse($request->query->getString('from'), $request->query->getString('to'))
?? TimeRange::wholeDay();
}
/**
* Route requirements cannot call a method, so the slug is matched loosely and resolved here.
* Route requirements cannot enumerate the enum, so {type} is matched loosely and resolved
* here -- an unknown kind is a 404 rather than a 500.
*/
private function resourceType(string $slug): ResourceType
private function resourceType(string $value): ResourceType
{
try {
return ResourceType::fromSlug($slug);
} catch (\ValueError $e) {
throw $this->createNotFoundException($e->getMessage(), $e);
}
return ResourceType::tryFrom($value)
?? throw $this->createNotFoundException(\sprintf('Unknown bookable kind "%s".', $value));
}
}
+10 -10
View File
@@ -40,7 +40,7 @@ final class BookingController extends AbstractController
) {
}
#[Route('/buchungen', name: 'app_booking', methods: ['GET'])]
#[Route('/bookings', name: 'app_booking', methods: ['GET'])]
public function index(): Response
{
$today = new \DateTimeImmutable('today');
@@ -60,11 +60,11 @@ final class BookingController extends AbstractController
* out-of-band board blocks, so the modal's outerHTML target is swapped with an empty
* fragment and the modal simply disappears.
*/
#[Route('/buchungen/neu/{id}/{date}', name: 'app_booking_new', requirements: ['date' => DateParam::PATTERN], methods: ['GET', 'POST'])]
#[Route('/bookings/new/{id}/{date}', name: 'app_booking_new', requirements: ['date' => DateParam::PATTERN], methods: ['GET', 'POST'])]
public function create(Request $request, Resource $resource, string $date): Response
{
if (!$resource->isActive()) {
throw $this->createNotFoundException('Diese Ressource wird nicht mehr angeboten.');
throw $this->createNotFoundException('This resource has been retired.');
}
$selected = DateParam::parseOrToday($date);
@@ -110,7 +110,7 @@ final class BookingController extends AbstractController
* Moves one occurrence, which is what an "override" of a series is: the other occurrences
* are untouched and this one is flagged as differing from its series.
*/
#[Route('/buchungen/{id}/bearbeiten', name: 'app_booking_edit', methods: ['GET', 'POST'])]
#[Route('/bookings/{id}/edit', name: 'app_booking_edit', methods: ['GET', 'POST'])]
public function edit(Request $request, Booking $booking): Response
{
$this->denyUnlessOwn($booking);
@@ -145,7 +145,7 @@ final class BookingController extends AbstractController
]);
}
#[Route('/buchungen/{id}/freigeben', name: 'app_booking_release', methods: ['POST'])]
#[Route('/bookings/{id}/release', name: 'app_booking_release', methods: ['POST'])]
public function release(Request $request, Booking $booking): Response
{
$this->denyUnlessOwn($booking);
@@ -169,11 +169,11 @@ final class BookingController extends AbstractController
/**
* Ends a series from today onwards. Past occurrences are history and stay put.
*/
#[Route('/serien/{id}/beenden', name: 'app_series_end', methods: ['POST'])]
#[Route('/series/{id}/end', name: 'app_series_end', methods: ['POST'])]
public function endSeries(Request $request, BookingSeries $series): Response
{
if ($series->getUser()->getId() !== $this->currentUser()->getId()) {
throw $this->createAccessDeniedException('Fremde Serien können nicht beendet werden.');
throw $this->createAccessDeniedException('A series may only be ended by the person who created it.');
}
$this->denyUnlessCsrfValid($request, 'end-series-'.$series->getId());
@@ -252,20 +252,20 @@ final class BookingController extends AbstractController
*/
private function timeParameters(TimeRange $time): array
{
return $time->isAllDay() ? [] : ['von' => $time->start(), 'bis' => $time->end()];
return $time->isAllDay() ? [] : ['from' => $time->start(), 'to' => $time->end()];
}
private function denyUnlessOwn(Booking $booking): void
{
if (!$booking->belongsTo($this->currentUser())) {
throw $this->createAccessDeniedException('Fremde Buchungen können nicht geändert werden.');
throw $this->createAccessDeniedException('A booking may only be changed by the person holding it.');
}
}
private function denyUnlessCsrfValid(Request $request, string $tokenId): void
{
if (!$this->isCsrfTokenValid($tokenId, $request->request->getString('_token'))) {
throw $this->createAccessDeniedException('Ungültiges CSRF-Token.');
throw $this->createAccessDeniedException('Invalid CSRF token.');
}
}
}
+5 -24
View File
@@ -8,13 +8,14 @@ namespace App\Entity;
* The kinds of thing that can be booked.
*
* **This enum is the extension point.** The dashboard builds one tile per case, the board route
* accepts one slug per case, and the floor plan is looked up by convention from the case's
* value. Adding a third bookable kind is therefore: a case here, a
* accepts one backing value per case, and the floor plan is looked up by convention from that
* same value. Adding a third bookable kind is therefore: a case here, a
* `templates/board/plan/_<value>.html.twig` with its geometry, and rows in `app:seed`. Nothing
* else needs editing — in particular no template lists the kinds by hand.
*
* The backing values are persisted in `resource.type`, which is why they stay English while the
* URLs do not: `slug()` is what appears in a path, so the routes read German like the rest.
* The backing value is both what is persisted in `resource.type` and what appears in a URL, so
* there is no slug mapping to keep in sync. The label, plural and description are the only
* things here anyone sees, which is why they are the only German ones.
*/
enum ResourceType: string
{
@@ -52,24 +53,4 @@ enum ResourceType: string
self::PARKING => 'lucide:car',
};
}
/** What appears in a URL. */
public function slug(): string
{
return match ($this) {
self::DESK => 'schreibtische',
self::PARKING => 'parkplaetze',
};
}
public static function fromSlug(string $slug): self
{
foreach (self::cases() as $case) {
if ($case->slug() === $slug) {
return $case;
}
}
throw new \ValueError(\sprintf('"%s" ist kein bekannter Buchungsbereich.', $slug));
}
}
+4 -4
View File
@@ -74,11 +74,11 @@ class EpClient
$code = $request->query->get('code');
if (null === $code || '' === $code) {
throw new AuthorizationRequestException('Die Antwort von myep enthielt keinen Code.');
throw new AuthorizationRequestException('The callback from myep carried no authorization code.');
}
if (null === $expectedState || $request->query->get('state') !== $expectedState) {
throw new AuthorizationRequestException('Der state-Parameter passt nicht zur Sitzung.');
throw new AuthorizationRequestException('The state parameter does not match the one this session started with.');
}
return $this->getProvider()->getAccessToken('authorization_code', ['code' => $code]);
@@ -107,13 +107,13 @@ class EpClient
['headers' => ['Accept' => 'application/json']],
));
} catch (ClientExceptionInterface $e) {
throw new UserinfoUnavailableException('userinfo konnte nicht abgerufen werden.', previous: $e);
throw new UserinfoUnavailableException('The userinfo endpoint could not be reached.', previous: $e);
}
$userinfo = json_decode((string) $response->getBody(), true);
if (!\is_array($userinfo)) {
throw new UserinfoUnavailableException('userinfo lieferte keine lesbare Antwort.');
throw new UserinfoUnavailableException('The userinfo endpoint returned an unreadable payload.');
}
return $userinfo;