feat: full DE/EN i18n with browser language detection and confirmation dialogs
- Add LocaleSubscriber: detects browser language, honours session override (priority 20) - Add LocaleSwitchController: stores locale in session, linked from user menu - Add admin.en.yaml / admin.de.yaml translation files (95 keys each) - Wire translation fallback to EN in config/packages/translation.yaml - Replace all hard-coded strings in CRUD controllers with TranslatableMessage - Inject TranslatorInterface into DashboardController, ArticleCrudController, AIPipelineJobCrudController and PipelineStreamController; add locale switcher links (English / Deutsch) to the user menu - Add confirmation dialog to "Re-run AI" and "Retry" pipeline actions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4c16f8cd68
commit
a3984adbed
19 changed files with 424 additions and 96 deletions
|
|
@ -2,4 +2,5 @@ framework:
|
|||
default_locale: en
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
fallbacks: [en]
|
||||
providers:
|
||||
|
|
|
|||
42
src/Infrastructure/EventListener/LocaleSubscriber.php
Normal file
42
src/Infrastructure/EventListener/LocaleSubscriber.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Infrastructure\EventListener;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
||||
final class LocaleSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private const array SUPPORTED = ['en', 'de'];
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
// Priority 20 — runs after the session is started (priority 128) but
|
||||
// before routing (priority 32) so $request->setLocale() is effective.
|
||||
return [KernelEvents::REQUEST => ['onRequest', 20]];
|
||||
}
|
||||
|
||||
public function onRequest(RequestEvent $event): void
|
||||
{
|
||||
if (!$event->isMainRequest()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$request = $event->getRequest();
|
||||
|
||||
// 1. Explicit user override stored in session
|
||||
$sessionLocale = $request->getSession()->get('_locale');
|
||||
if (\is_string($sessionLocale) && \in_array($sessionLocale, self::SUPPORTED, true)) {
|
||||
$request->setLocale($sessionLocale);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Browser's Accept-Language header
|
||||
$preferred = $request->getPreferredLanguage(self::SUPPORTED);
|
||||
$request->setLocale($preferred ?: 'en');
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,8 @@ use EasyCorp\Bundle\EasyAdminBundle\Orm\EntityRepository;
|
|||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/** @extends AbstractCrudController<AIPipelineJob> */
|
||||
final class AIPipelineJobCrudController extends AbstractCrudController
|
||||
|
|
@ -37,6 +39,7 @@ final class AIPipelineJobCrudController extends AbstractCrudController
|
|||
public function __construct(
|
||||
private readonly MessageBusInterface $bus,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -48,17 +51,18 @@ final class AIPipelineJobCrudController extends AbstractCrudController
|
|||
public function configureCrud(Crud $crud): Crud
|
||||
{
|
||||
return $crud
|
||||
->setEntityLabelInSingular('Pipeline Job')
|
||||
->setEntityLabelInPlural('AI Pipeline — Active')
|
||||
->setEntityLabelInSingular(new TranslatableMessage('crud.pipeline_job.singular', [], 'admin'))
|
||||
->setEntityLabelInPlural(new TranslatableMessage('crud.pipeline_job.plural_active', [], 'admin'))
|
||||
->setDefaultSort(['createdAt' => 'DESC'])
|
||||
->showEntityActionsInlined();
|
||||
}
|
||||
|
||||
public function configureActions(Actions $actions): Actions
|
||||
{
|
||||
$retryAction = Action::new('retry', 'Retry', 'fa fa-rotate-right')
|
||||
$retryAction = Action::new('retry', new TranslatableMessage('action.retry', [], 'admin'), 'fa fa-rotate-right')
|
||||
->linkToCrudAction('retryJob')
|
||||
->setCssClass('btn btn-sm btn-warning')
|
||||
->setConfirmation(new TranslatableMessage('action.retry_confirm', [], 'admin'))
|
||||
->displayIf(static fn (AIPipelineJob $job) => \in_array(
|
||||
$job->getStatus(),
|
||||
[AIPipelineJobStatus::NeedsReview, AIPipelineJobStatus::Failed, AIPipelineJobStatus::Processing],
|
||||
|
|
@ -75,22 +79,22 @@ final class AIPipelineJobCrudController extends AbstractCrudController
|
|||
public function configureFields(string $pageName): iterable
|
||||
{
|
||||
yield IdField::new('id')->hideOnIndex();
|
||||
yield TextField::new('inventoryNumber', 'Inventory #')
|
||||
yield TextField::new('inventoryNumber', new TranslatableMessage('field.inventory_number', [], 'admin'))
|
||||
->hideOnForm()
|
||||
->formatValue(static fn ($v, AIPipelineJob $job): string => (string) ($job->getInputData()['inventoryNumber'] ?? '—'));
|
||||
yield TextField::new('statusLabel', 'Status')
|
||||
yield TextField::new('statusLabel', new TranslatableMessage('field.status', [], 'admin'))
|
||||
->hideOnForm()
|
||||
->setCssClass('fw-bold');
|
||||
yield TextField::new('currentStep', 'Step')
|
||||
yield TextField::new('currentStep', new TranslatableMessage('field.step', [], 'admin'))
|
||||
->hideOnForm()
|
||||
->formatValue(static fn ($v, AIPipelineJob $job): string => $job->getCurrentStep() ?? '—');
|
||||
yield IntegerField::new('attemptCount', 'Attempts')->hideOnForm();
|
||||
yield DateTimeField::new('createdAt', 'Started')->hideOnForm();
|
||||
yield TextField::new('errorMessage', 'Error')
|
||||
yield IntegerField::new('attemptCount', new TranslatableMessage('field.attempts', [], 'admin'))->hideOnForm();
|
||||
yield DateTimeField::new('createdAt', new TranslatableMessage('field.started', [], 'admin'))->hideOnForm();
|
||||
yield TextField::new('errorMessage', new TranslatableMessage('field.error', [], 'admin'))
|
||||
->hideOnIndex()
|
||||
->hideOnForm()
|
||||
->formatValue(static fn ($v, AIPipelineJob $job): string => $job->getErrorMessage() ?? '');
|
||||
yield TextareaField::new('aiResults', 'AI Results')
|
||||
yield TextareaField::new('aiResults', new TranslatableMessage('field.ai_results', [], 'admin'))
|
||||
->onlyOnDetail()
|
||||
->formatValue(static fn ($v, AIPipelineJob $job): string => self::formatStepResults($job));
|
||||
}
|
||||
|
|
@ -158,7 +162,7 @@ final class AIPipelineJobCrudController extends AbstractCrudController
|
|||
$step = 'vision';
|
||||
}
|
||||
|
||||
$this->addFlash('success', sprintf('Job %s re-queued from %s.', substr($jobId, 0, 8), $step));
|
||||
$this->addFlash('success', $this->translator->trans('flash.job_requeued', ['%id%' => substr($jobId, 0, 8), '%step%' => $step], 'admin'));
|
||||
|
||||
return $this->redirect($context->getRequest()->headers->get('referer', $this->generateUrl('easyadmin')));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ use Symfony\Component\HttpFoundation\Request;
|
|||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/** @extends AbstractCrudController<Article> */
|
||||
final class ArticleCrudController extends AbstractCrudController
|
||||
|
|
@ -42,6 +44,7 @@ final class ArticleCrudController extends AbstractCrudController
|
|||
private readonly EntityManagerInterface $em,
|
||||
private readonly AIPipelineJobRepositoryInterface $jobRepository,
|
||||
private readonly MessageBusInterface $bus,
|
||||
private readonly TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -53,26 +56,27 @@ final class ArticleCrudController extends AbstractCrudController
|
|||
public function configureCrud(Crud $crud): Crud
|
||||
{
|
||||
return $crud
|
||||
->setEntityLabelInSingular('Article')
|
||||
->setEntityLabelInPlural('Articles')
|
||||
->setEntityLabelInSingular(new TranslatableMessage('crud.article.singular', [], 'admin'))
|
||||
->setEntityLabelInPlural(new TranslatableMessage('crud.article.plural', [], 'admin'))
|
||||
->showEntityActionsInlined();
|
||||
}
|
||||
|
||||
public function configureActions(Actions $actions): Actions
|
||||
{
|
||||
$activate = Action::new('activate', 'Activate', 'fa fa-check')
|
||||
$activate = Action::new('activate', new TranslatableMessage('action.activate', [], 'admin'), 'fa fa-check')
|
||||
->linkToRoute('admin_article_activate', static fn (Article $a) => ['id' => $a->getId()->toRfc4122()])
|
||||
->setCssClass('btn btn-sm btn-success')
|
||||
->displayIf(static fn (Article $a) => ArticleStatus::Draft === $a->getStatus());
|
||||
|
||||
$markDraft = Action::new('markDraft', 'Mark as Draft', 'fa fa-pen-to-square')
|
||||
$markDraft = Action::new('markDraft', new TranslatableMessage('action.mark_as_draft', [], 'admin'), 'fa fa-pen-to-square')
|
||||
->linkToCrudAction('markAsDraft')
|
||||
->setCssClass('btn btn-sm btn-secondary')
|
||||
->displayIf(static fn (Article $a) => ArticleStatus::Ingesting === $a->getStatus());
|
||||
|
||||
$rerunAi = Action::new('rerunAi', 'Re-run AI', 'fa fa-rotate')
|
||||
$rerunAi = Action::new('rerunAi', new TranslatableMessage('action.rerun_ai', [], 'admin'), 'fa fa-rotate')
|
||||
->linkToCrudAction('rerunAiPipeline')
|
||||
->setCssClass('btn btn-sm btn-info')
|
||||
->setConfirmation(new TranslatableMessage('action.rerun_ai_confirm', [], 'admin'))
|
||||
->displayIf(static fn (Article $a) => \in_array(
|
||||
$a->getStatus(),
|
||||
[ArticleStatus::Draft, ArticleStatus::Ingesting, ArticleStatus::NeedsReview],
|
||||
|
|
@ -93,31 +97,30 @@ final class ArticleCrudController extends AbstractCrudController
|
|||
public function configureFields(string $pageName): iterable
|
||||
{
|
||||
yield IdField::new('id')->hideOnForm()->hideOnIndex();
|
||||
yield TextField::new('inventoryNumber', 'Inventory #')->hideOnForm();
|
||||
yield TextField::new('inventoryNumber', new TranslatableMessage('field.inventory_number', [], 'admin'))->hideOnForm();
|
||||
yield AssociationField::new('articleType')->hideOnForm();
|
||||
yield TextField::new('ebayTitle', 'Description')->setRequired(false);
|
||||
yield ChoiceField::new('status')->setChoices(
|
||||
yield TextField::new('ebayTitle', new TranslatableMessage('field.description', [], 'admin'))->setRequired(false);
|
||||
yield ChoiceField::new('status', new TranslatableMessage('field.status', [], 'admin'))->setChoices(
|
||||
array_combine(
|
||||
array_map(static fn ($s) => $s->value, ArticleStatus::cases()),
|
||||
ArticleStatus::cases(),
|
||||
)
|
||||
)->hideOnForm();
|
||||
yield MoneyField::new('listingPrice', 'Price')->setCurrency('EUR')->setRequired(false);
|
||||
yield ChoiceField::new('condition')->setChoices(
|
||||
yield MoneyField::new('listingPrice', new TranslatableMessage('field.price', [], 'admin'))->setCurrency('EUR')->setRequired(false);
|
||||
yield ChoiceField::new('condition', new TranslatableMessage('field.condition', [], 'admin'))->setChoices(
|
||||
array_combine(
|
||||
array_map(static fn ($c) => $c->value, ArticleCondition::cases()),
|
||||
ArticleCondition::cases(),
|
||||
)
|
||||
)->hideOnIndex();
|
||||
yield TextField::new('manufacturer', 'Manufacturer')->setRequired(false)->hideOnIndex();
|
||||
yield TextField::new('modelNumber', 'Model #')->setRequired(false)->hideOnIndex();
|
||||
yield TextField::new('manufacturer', new TranslatableMessage('field.manufacturer', [], 'admin'))->setRequired(false)->hideOnIndex();
|
||||
yield TextField::new('modelNumber', new TranslatableMessage('field.model_number', [], 'admin'))->setRequired(false)->hideOnIndex();
|
||||
yield TextField::new('sku')->hideOnForm()->hideOnIndex();
|
||||
yield TextField::new('serialNumber', 'Serial #')->setRequired(false)->hideOnIndex();
|
||||
yield TextField::new('conditionNotes', 'Condition Notes')->setRequired(false)->hideOnIndex();
|
||||
yield TextareaField::new('ebayDescription', 'eBay Description')->setRequired(false)->hideOnIndex();
|
||||
yield TextField::new('serialNumber', new TranslatableMessage('field.serial_number', [], 'admin'))->setRequired(false)->hideOnIndex();
|
||||
yield TextField::new('conditionNotes', new TranslatableMessage('field.condition_notes', [], 'admin'))->setRequired(false)->hideOnIndex();
|
||||
yield TextareaField::new('ebayDescription', new TranslatableMessage('field.ebay_description', [], 'admin'))->setRequired(false)->hideOnIndex();
|
||||
|
||||
// Flat key-value display for detail view
|
||||
yield TextField::new('attributeValues', 'Attributes')
|
||||
yield TextField::new('attributeValues', new TranslatableMessage('field.attributes', [], 'admin'))
|
||||
->onlyOnDetail()
|
||||
->formatValue(static fn ($v, Article $a): string => implode(
|
||||
' | ',
|
||||
|
|
@ -130,7 +133,7 @@ final class ArticleCrudController extends AbstractCrudController
|
|||
)
|
||||
));
|
||||
|
||||
yield CollectionField::new('attributeValues', 'Attributes')
|
||||
yield CollectionField::new('attributeValues', new TranslatableMessage('field.attributes', [], 'admin'))
|
||||
->onlyOnForms()
|
||||
->setEntryType(AttributeValueFormType::class)
|
||||
->setEntryIsComplex(false)
|
||||
|
|
@ -149,14 +152,14 @@ final class ArticleCrudController extends AbstractCrudController
|
|||
$referrer = $context->getRequest()->headers->get('referer', $this->generateUrl('easyadmin'));
|
||||
|
||||
if (null === $originalJob) {
|
||||
$this->addFlash('danger', 'No original pipeline job found — cannot determine which photo to use.');
|
||||
$this->addFlash('danger', $this->translator->trans('flash.pipeline_job_not_found', [], 'admin'));
|
||||
|
||||
return $this->redirect($referrer);
|
||||
}
|
||||
|
||||
$storedPhotoPath = (string) ($originalJob->getInputData()['storedPhotoPath'] ?? '');
|
||||
if ('' === $storedPhotoPath || !file_exists($storedPhotoPath)) {
|
||||
$this->addFlash('danger', 'Stored photo not found at: '.$storedPhotoPath);
|
||||
$this->addFlash('danger', $this->translator->trans('flash.photo_not_found', ['%path%' => $storedPhotoPath], 'admin'));
|
||||
|
||||
return $this->redirect($referrer);
|
||||
}
|
||||
|
|
@ -179,9 +182,10 @@ final class ArticleCrudController extends AbstractCrudController
|
|||
originalFilename: (string) ($originalJob->getInputData()['originalFilename'] ?? ''),
|
||||
));
|
||||
|
||||
$this->addFlash('success', sprintf(
|
||||
'AI pipeline re-queued for %s — attributes and eBay texts will be updated when complete.',
|
||||
$article->getInventoryNumber(),
|
||||
$this->addFlash('success', $this->translator->trans(
|
||||
'flash.pipeline_requeued',
|
||||
['%label%' => $article->getInventoryNumber()],
|
||||
'admin',
|
||||
));
|
||||
|
||||
return $this->redirect($referrer);
|
||||
|
|
@ -197,7 +201,7 @@ final class ArticleCrudController extends AbstractCrudController
|
|||
if (ArticleStatus::Ingesting === $article->getStatus()) {
|
||||
$article->transitionTo(ArticleStatus::Draft);
|
||||
$this->em->flush();
|
||||
$this->addFlash('success', 'Article marked as draft.');
|
||||
$this->addFlash('success', $this->translator->trans('flash.article_marked_draft', [], 'admin'));
|
||||
}
|
||||
|
||||
return $this->redirect($referrer);
|
||||
|
|
@ -209,9 +213,9 @@ final class ArticleCrudController extends AbstractCrudController
|
|||
$result = $this->articleService->activate(Uuid::fromString($id));
|
||||
|
||||
if ([] !== $result['missing']) {
|
||||
$this->addFlash('warning', 'Cannot activate: missing attributes: '.implode(', ', $result['missing']));
|
||||
$this->addFlash('warning', $this->translator->trans('flash.article_missing_attributes', ['%attrs%' => implode(', ', $result['missing'])], 'admin'));
|
||||
} else {
|
||||
$this->addFlash('success', 'Article activated and queued for channel publishing.');
|
||||
$this->addFlash('success', $this->translator->trans('flash.article_activated', [], 'admin'));
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('easyadmin', [
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
|
|||
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
|
||||
/** @extends AbstractCrudController<ArticleTypeAttribute> */
|
||||
final class ArticleTypeAttributeCrudController extends AbstractCrudController
|
||||
|
|
@ -24,8 +25,8 @@ final class ArticleTypeAttributeCrudController extends AbstractCrudController
|
|||
public function configureCrud(Crud $crud): Crud
|
||||
{
|
||||
return $crud
|
||||
->setEntityLabelInSingular('Attribute Assignment')
|
||||
->setEntityLabelInPlural('Attribute Assignments')
|
||||
->setEntityLabelInSingular(new TranslatableMessage('crud.attribute_assignment.singular', [], 'admin'))
|
||||
->setEntityLabelInPlural(new TranslatableMessage('crud.attribute_assignment.plural', [], 'admin'))
|
||||
->setDefaultSort(['articleType' => 'ASC']);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
|
|||
use EasyCorp\Bundle\EasyAdminBundle\Field\IntegerField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
|
||||
/** @extends AbstractCrudController<ArticleType> */
|
||||
final class ArticleTypeCrudController extends AbstractCrudController
|
||||
|
|
@ -26,7 +27,7 @@ final class ArticleTypeCrudController extends AbstractCrudController
|
|||
|
||||
public function configureCrud(Crud $crud): Crud
|
||||
{
|
||||
return $crud->setEntityLabelInSingular('Article Type')->setEntityLabelInPlural('Article Types');
|
||||
return $crud->setEntityLabelInSingular(new TranslatableMessage('crud.article_type.singular', [], 'admin'))->setEntityLabelInPlural(new TranslatableMessage('crud.article_type.plural', [], 'admin'));
|
||||
}
|
||||
|
||||
public function configureAssets(Assets $assets): Assets
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\Field;
|
|||
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EnumType;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
|
||||
/** @extends AbstractCrudController<AttributeDefinition> */
|
||||
final class AttributeDefinitionCrudController extends AbstractCrudController
|
||||
|
|
@ -26,8 +27,8 @@ final class AttributeDefinitionCrudController extends AbstractCrudController
|
|||
public function configureCrud(Crud $crud): Crud
|
||||
{
|
||||
return $crud
|
||||
->setEntityLabelInSingular('Attribute')
|
||||
->setEntityLabelInPlural('Attributes');
|
||||
->setEntityLabelInSingular(new TranslatableMessage('crud.attribute.singular', [], 'admin'))
|
||||
->setEntityLabelInPlural(new TranslatableMessage('crud.attribute.plural', [], 'admin'));
|
||||
}
|
||||
|
||||
public function createEntity(string $entityFqcn): AttributeDefinition
|
||||
|
|
@ -38,25 +39,25 @@ final class AttributeDefinitionCrudController extends AbstractCrudController
|
|||
public function configureFields(string $pageName): iterable
|
||||
{
|
||||
yield IdField::new('id')->hideOnForm();
|
||||
yield TextField::new('name', 'Name');
|
||||
yield TextField::new('name', new TranslatableMessage('field.name', [], 'admin'));
|
||||
|
||||
// choice.html.twig (used by ChoiceField) only renders formattedValue — safe for enums.
|
||||
// text.html.twig also renders field.value in the title attr, which chokes on enum objects.
|
||||
if (\in_array($pageName, [Crud::PAGE_NEW, Crud::PAGE_EDIT], true)) {
|
||||
yield Field::new('type', 'Type')
|
||||
yield Field::new('type', new TranslatableMessage('field.type', [], 'admin'))
|
||||
->setFormType(EnumType::class)
|
||||
->setFormTypeOptions(['class' => AttributeType::class]);
|
||||
} else {
|
||||
yield ChoiceField::new('type', 'Type')
|
||||
yield ChoiceField::new('type', new TranslatableMessage('field.type', [], 'admin'))
|
||||
->setChoices([])
|
||||
->formatValue(static fn (mixed $v): string => $v instanceof AttributeType ? $v->value : (string) $v);
|
||||
}
|
||||
|
||||
yield TextField::new('unit', 'Unit')->setRequired(false)->hideOnIndex();
|
||||
yield Field::new('options', 'Options (one per line)')
|
||||
yield TextField::new('unit', new TranslatableMessage('field.unit', [], 'admin'))->setRequired(false)->hideOnIndex();
|
||||
yield Field::new('options', new TranslatableMessage('field.options', [], 'admin'))
|
||||
->setFormType(StringArrayType::class)
|
||||
->setRequired(false)
|
||||
->hideOnIndex()
|
||||
->setHelp('Only relevant for type select / multi_select.');
|
||||
->setHelp(new TranslatableMessage('field.options_help', [], 'admin'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
|
|||
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
|
||||
/** @extends AbstractCrudController<Customer> */
|
||||
final class CustomerCrudController extends AbstractCrudController
|
||||
|
|
@ -23,8 +24,8 @@ final class CustomerCrudController extends AbstractCrudController
|
|||
public function configureCrud(Crud $crud): Crud
|
||||
{
|
||||
return $crud
|
||||
->setEntityLabelInSingular('Kunde')
|
||||
->setEntityLabelInPlural('Kunden')
|
||||
->setEntityLabelInSingular(new TranslatableMessage('crud.customer.singular', [], 'admin'))
|
||||
->setEntityLabelInPlural(new TranslatableMessage('crud.customer.plural', [], 'admin'))
|
||||
->setDefaultSort(['name' => 'ASC'])
|
||||
->showEntityActionsInlined();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,16 @@ use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
|
|||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[AdminDashboard(routePath: '/admin', routeName: 'easyadmin')]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
final class DashboardController extends AbstractDashboardController
|
||||
{
|
||||
public function __construct(private readonly TranslatorInterface $translator)
|
||||
{
|
||||
}
|
||||
|
||||
public function index(): Response
|
||||
{
|
||||
return $this->render('admin/dashboard.html.twig');
|
||||
|
|
@ -25,9 +30,14 @@ final class DashboardController extends AbstractDashboardController
|
|||
|
||||
public function configureUserMenu(UserInterface $user): UserMenu
|
||||
{
|
||||
$t = fn (string $key) => $this->translator->trans($key, [], 'admin');
|
||||
|
||||
return parent::configureUserMenu($user)
|
||||
->addMenuItems([
|
||||
MenuItem::linkToRoute('Change Password', 'fa fa-key', 'app_change_password'),
|
||||
MenuItem::linkToRoute($t('menu.change_password'), 'fa fa-key', 'app_change_password'),
|
||||
MenuItem::section(),
|
||||
MenuItem::linkToRoute($t('menu.language_en'), 'fa fa-globe', 'admin_locale_switch', ['locale' => 'en']),
|
||||
MenuItem::linkToRoute($t('menu.language_de'), 'fa fa-globe', 'admin_locale_switch', ['locale' => 'de']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -45,20 +55,22 @@ final class DashboardController extends AbstractDashboardController
|
|||
|
||||
public function configureMenuItems(): iterable
|
||||
{
|
||||
yield MenuItem::linkToDashboard('Dashboard', 'fa fa-home');
|
||||
yield MenuItem::linkToRoute('Ingest Article', 'fa fa-camera', 'admin_manual_ingest');
|
||||
yield MenuItem::linkTo(ArticleCrudController::class, 'Articles', 'fa fa-box');
|
||||
yield MenuItem::linkTo(ArticleTypeCrudController::class, 'Article Types', 'fa fa-tags');
|
||||
yield MenuItem::linkTo(AttributeDefinitionCrudController::class, 'Attributes', 'fa fa-list-check');
|
||||
yield MenuItem::section('Pipeline');
|
||||
yield MenuItem::linkTo(AIPipelineJobCrudController::class, 'Active Jobs', 'fa fa-robot');
|
||||
yield MenuItem::linkTo(PipelineArchiveCrudController::class, 'Archive', 'fa fa-box-archive');
|
||||
yield MenuItem::linkTo(PromptTemplateCrudController::class, 'AI Prompts', 'fa fa-message');
|
||||
yield MenuItem::linkTo(UserCrudController::class, 'Users', 'fa fa-users');
|
||||
yield MenuItem::linkTo(LogEntryCrudController::class, 'Logs', 'fa fa-list');
|
||||
yield MenuItem::section('Sales');
|
||||
yield MenuItem::linkTo(OrderCrudController::class, 'Orders', 'fa fa-shopping-cart');
|
||||
yield MenuItem::linkTo(CustomerCrudController::class, 'Customers', 'fa fa-users');
|
||||
yield MenuItem::linkTo(InvoiceCrudController::class, 'Invoices', 'fa fa-file-invoice');
|
||||
$t = fn (string $key) => $this->translator->trans($key, [], 'admin');
|
||||
|
||||
yield MenuItem::linkToDashboard($t('menu.dashboard'), 'fa fa-home');
|
||||
yield MenuItem::linkToRoute($t('menu.ingest_article'), 'fa fa-camera', 'admin_manual_ingest');
|
||||
yield MenuItem::linkTo(ArticleCrudController::class, $t('menu.articles'), 'fa fa-box');
|
||||
yield MenuItem::linkTo(ArticleTypeCrudController::class, $t('menu.article_types'), 'fa fa-tags');
|
||||
yield MenuItem::linkTo(AttributeDefinitionCrudController::class, $t('menu.attributes'), 'fa fa-list-check');
|
||||
yield MenuItem::section($t('menu.section_pipeline'));
|
||||
yield MenuItem::linkTo(AIPipelineJobCrudController::class, $t('menu.active_jobs'), 'fa fa-robot');
|
||||
yield MenuItem::linkTo(PipelineArchiveCrudController::class, $t('menu.archive'), 'fa fa-box-archive');
|
||||
yield MenuItem::linkTo(PromptTemplateCrudController::class, $t('menu.ai_prompts'), 'fa fa-message');
|
||||
yield MenuItem::linkTo(UserCrudController::class, $t('menu.users'), 'fa fa-users');
|
||||
yield MenuItem::linkTo(LogEntryCrudController::class, $t('menu.logs'), 'fa fa-list');
|
||||
yield MenuItem::section($t('menu.section_sales'));
|
||||
yield MenuItem::linkTo(OrderCrudController::class, $t('menu.orders'), 'fa fa-shopping-cart');
|
||||
yield MenuItem::linkTo(CustomerCrudController::class, $t('menu.customers'), 'fa fa-users');
|
||||
yield MenuItem::linkTo(InvoiceCrudController::class, $t('menu.invoices'), 'fa fa-file-invoice');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
|
|||
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
|
||||
/** @extends AbstractCrudController<Invoice> */
|
||||
final class InvoiceCrudController extends AbstractCrudController
|
||||
|
|
@ -25,8 +26,8 @@ final class InvoiceCrudController extends AbstractCrudController
|
|||
public function configureCrud(Crud $crud): Crud
|
||||
{
|
||||
return $crud
|
||||
->setEntityLabelInSingular('Rechnung')
|
||||
->setEntityLabelInPlural('Rechnungen')
|
||||
->setEntityLabelInSingular(new TranslatableMessage('crud.invoice.singular', [], 'admin'))
|
||||
->setEntityLabelInPlural(new TranslatableMessage('crud.invoice.plural', [], 'admin'))
|
||||
->setDefaultSort(['createdAt' => 'DESC'])
|
||||
->showEntityActionsInlined();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Infrastructure\Http\Controller\Admin;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[IsGranted('ROLE_USER')]
|
||||
final class LocaleSwitchController extends AbstractController
|
||||
{
|
||||
private const array SUPPORTED = ['en', 'de'];
|
||||
|
||||
#[Route('/admin/locale/{locale}', name: 'admin_locale_switch', requirements: ['locale' => 'en|de'])]
|
||||
public function switch(string $locale, Request $request): Response
|
||||
{
|
||||
if (!\in_array($locale, self::SUPPORTED, true)) {
|
||||
$locale = 'en';
|
||||
}
|
||||
|
||||
$request->getSession()->set('_locale', $locale);
|
||||
|
||||
$referer = $request->headers->get('referer');
|
||||
|
||||
return $this->redirect($referer ?: $this->generateUrl('easyadmin'));
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
|
|||
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\IntegerField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
|
||||
/** @extends AbstractCrudController<LogEntry> */
|
||||
final class LogEntryCrudController extends AbstractCrudController
|
||||
|
|
@ -24,8 +25,8 @@ final class LogEntryCrudController extends AbstractCrudController
|
|||
public function configureCrud(Crud $crud): Crud
|
||||
{
|
||||
return $crud
|
||||
->setEntityLabelInSingular('Log Entry')
|
||||
->setEntityLabelInPlural('Log Entries')
|
||||
->setEntityLabelInSingular(new TranslatableMessage('crud.log_entry.singular', [], 'admin'))
|
||||
->setEntityLabelInPlural(new TranslatableMessage('crud.log_entry.plural', [], 'admin'))
|
||||
->setDefaultSort(['loggedAt' => 'DESC']);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
|
|||
use EasyCorp\Bundle\EasyAdminBundle\Field\MoneyField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Filter\ChoiceFilter;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
|
||||
/** @extends AbstractCrudController<Order> */
|
||||
final class OrderCrudController extends AbstractCrudController
|
||||
|
|
@ -30,8 +31,8 @@ final class OrderCrudController extends AbstractCrudController
|
|||
public function configureCrud(Crud $crud): Crud
|
||||
{
|
||||
return $crud
|
||||
->setEntityLabelInSingular('Bestellung')
|
||||
->setEntityLabelInPlural('Bestellungen')
|
||||
->setEntityLabelInSingular(new TranslatableMessage('crud.order.singular', [], 'admin'))
|
||||
->setEntityLabelInPlural(new TranslatableMessage('crud.order.plural', [], 'admin'))
|
||||
->setDefaultSort(['saleDate' => 'DESC'])
|
||||
->showEntityActionsInlined();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\IntegerField;
|
|||
use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Orm\EntityRepository;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
|
||||
/** @extends AbstractCrudController<AIPipelineJob> */
|
||||
final class PipelineArchiveCrudController extends AbstractCrudController
|
||||
|
|
@ -33,8 +34,8 @@ final class PipelineArchiveCrudController extends AbstractCrudController
|
|||
public function configureCrud(Crud $crud): Crud
|
||||
{
|
||||
return $crud
|
||||
->setEntityLabelInSingular('Pipeline Job')
|
||||
->setEntityLabelInPlural('AI Pipeline — Archive')
|
||||
->setEntityLabelInSingular(new TranslatableMessage('crud.pipeline_job.singular', [], 'admin'))
|
||||
->setEntityLabelInPlural(new TranslatableMessage('crud.pipeline_job.plural_archive', [], 'admin'))
|
||||
->setDefaultSort(['completedAt' => 'DESC'])
|
||||
->showEntityActionsInlined();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,21 +12,23 @@ use Symfony\Component\HttpFoundation\Request;
|
|||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[IsGranted('ROLE_USER')]
|
||||
final class PipelineStreamController extends AbstractController
|
||||
{
|
||||
private const array STEP_LABELS = [
|
||||
'vision' => 'Foto analysiert',
|
||||
'specs_research' => 'Specs recherchiert',
|
||||
'json_coding' => 'Attribute kodiert',
|
||||
'draft_article' => 'Artikel erstellt',
|
||||
'ebay_text' => 'eBay-Texte generiert',
|
||||
'validation' => 'Validierung',
|
||||
private const array STEP_KEYS = [
|
||||
'vision' => 'pipeline.step.vision',
|
||||
'specs_research' => 'pipeline.step.specs_research',
|
||||
'json_coding' => 'pipeline.step.json_coding',
|
||||
'draft_article' => 'pipeline.step.draft_article',
|
||||
'ebay_text' => 'pipeline.step.ebay_text',
|
||||
'validation' => 'pipeline.step.validation',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly AIPipelineJobRepositoryInterface $jobRepository,
|
||||
private readonly TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -102,17 +104,25 @@ final class PipelineStreamController extends AbstractController
|
|||
$inv = $job->getInventoryNumber() ?? substr($job->getId()->toRfc4122(), 0, 8);
|
||||
$label = "Job #{$inv}";
|
||||
|
||||
$t = fn (string $key, array $p = []) => $this->translator->trans($key, $p, 'admin');
|
||||
$stepLabel = $step !== null
|
||||
? $t(self::STEP_KEYS[$step] ?? $step)
|
||||
: '';
|
||||
|
||||
$message = match (true) {
|
||||
$status === AIPipelineJobStatus::Queued->value => "{$label} zur Pipeline hinzugefügt",
|
||||
$status === AIPipelineJobStatus::Queued->value
|
||||
=> $t('pipeline.event.queued', ['%inv%' => $inv]),
|
||||
$status === AIPipelineJobStatus::Processing->value && $step === null
|
||||
=> "{$label} gestartet",
|
||||
=> $t('pipeline.event.processing_start', ['%inv%' => $inv]),
|
||||
$status === AIPipelineJobStatus::Processing->value
|
||||
=> "{$label}: ".self::STEP_LABELS[$step] ?? $step,
|
||||
$status === AIPipelineJobStatus::Completed->value => "{$label} abgeschlossen ✓",
|
||||
$status === AIPipelineJobStatus::Failed->value => "{$label} fehlgeschlagen: ".($job->getErrorMessage() ?? ''),
|
||||
=> $t('pipeline.event.processing_step', ['%inv%' => $inv, '%step%' => $stepLabel]),
|
||||
$status === AIPipelineJobStatus::Completed->value
|
||||
=> $t('pipeline.event.completed', ['%inv%' => $inv]),
|
||||
$status === AIPipelineJobStatus::Failed->value
|
||||
=> $t('pipeline.event.failed', ['%inv%' => $inv, '%reason%' => $job->getErrorMessage() ?? '']),
|
||||
$status === AIPipelineJobStatus::NeedsReview->value
|
||||
=> "{$label} benötigt manuelle Prüfung",
|
||||
default => "{$label}: {$status}",
|
||||
=> $t('pipeline.event.needs_review', ['%inv%' => $inv]),
|
||||
default => "{$label}: {$status}",
|
||||
};
|
||||
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ namespace App\Infrastructure\Http\Controller\Admin;
|
|||
|
||||
use App\Domain\AI\PromptTemplate;
|
||||
use App\Infrastructure\AI\PromptTemplateService;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
|
||||
|
|
@ -24,8 +25,8 @@ final class PromptTemplateCrudController extends AbstractCrudController
|
|||
public function configureCrud(Crud $crud): Crud
|
||||
{
|
||||
return $crud
|
||||
->setEntityLabelInSingular('Prompt Template')
|
||||
->setEntityLabelInPlural('Prompt Templates')
|
||||
->setEntityLabelInSingular(new TranslatableMessage('crud.prompt_template.singular', [], 'admin'))
|
||||
->setEntityLabelInPlural(new TranslatableMessage('crud.prompt_template.plural', [], 'admin'))
|
||||
->setDefaultSort(['key' => 'ASC']);
|
||||
}
|
||||
|
||||
|
|
@ -37,18 +38,18 @@ final class PromptTemplateCrudController extends AbstractCrudController
|
|||
public function configureFields(string $pageName): iterable
|
||||
{
|
||||
yield IdField::new('id')->hideOnForm();
|
||||
yield TextField::new('key', 'Key')
|
||||
->setHelp('Slug identifying the prompt (e.g. <code>specs_research</code>). Must be unique.')
|
||||
yield TextField::new('key', new TranslatableMessage('field.prompt_key', [], 'admin'))
|
||||
->setHelp(new TranslatableMessage('field.prompt_key_help', [], 'admin'))
|
||||
->setColumns(4);
|
||||
yield TextareaField::new('body', 'Prompt Body')
|
||||
yield TextareaField::new('body', new TranslatableMessage('field.prompt_body', [], 'admin'))
|
||||
->setHelp($this->buildVariableHelp())
|
||||
->setNumOfRows(18)
|
||||
->hideOnIndex();
|
||||
yield TextField::new('body', 'Body Preview')
|
||||
yield TextField::new('body', new TranslatableMessage('field.prompt_body_preview', [], 'admin'))
|
||||
->formatValue(static fn (string $v): string => mb_substr($v, 0, 120).(mb_strlen($v) > 120 ? '…' : ''))
|
||||
->hideOnForm()
|
||||
->setSortable(false);
|
||||
yield DateTimeField::new('updatedAt', 'Last Updated')
|
||||
yield DateTimeField::new('updatedAt', new TranslatableMessage('field.last_updated', [], 'admin'))
|
||||
->hideOnForm()
|
||||
->setFormat('yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
|
|||
use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
|
||||
/** @extends AbstractCrudController<User> */
|
||||
final class UserCrudController extends AbstractCrudController
|
||||
|
|
@ -23,7 +24,7 @@ final class UserCrudController extends AbstractCrudController
|
|||
|
||||
public function configureCrud(Crud $crud): Crud
|
||||
{
|
||||
return $crud->setEntityLabelInSingular('User')->setEntityLabelInPlural('Users');
|
||||
return $crud->setEntityLabelInSingular(new TranslatableMessage('crud.user.singular', [], 'admin'))->setEntityLabelInPlural(new TranslatableMessage('crud.user.plural', [], 'admin'));
|
||||
}
|
||||
|
||||
public function configureActions(Actions $actions): Actions
|
||||
|
|
|
|||
119
translations/admin.de.yaml
Normal file
119
translations/admin.de.yaml
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
menu:
|
||||
dashboard: Dashboard
|
||||
ingest_article: Artikel einlesen
|
||||
articles: Artikel
|
||||
article_types: Artikeltypen
|
||||
attributes: Attribute
|
||||
section_pipeline: Pipeline
|
||||
active_jobs: Aktive Jobs
|
||||
archive: Archiv
|
||||
ai_prompts: KI-Prompts
|
||||
users: Benutzer
|
||||
logs: Logs
|
||||
section_sales: Verkauf
|
||||
orders: Bestellungen
|
||||
customers: Kunden
|
||||
invoices: Rechnungen
|
||||
change_password: Passwort ändern
|
||||
language_en: English
|
||||
language_de: Deutsch
|
||||
|
||||
crud:
|
||||
article:
|
||||
singular: Artikel
|
||||
plural: Artikel
|
||||
article_type:
|
||||
singular: Artikeltyp
|
||||
plural: Artikeltypen
|
||||
attribute:
|
||||
singular: Attribut
|
||||
plural: Attribute
|
||||
attribute_assignment:
|
||||
singular: Attributzuweisung
|
||||
plural: Attributzuweisungen
|
||||
customer:
|
||||
singular: Kunde
|
||||
plural: Kunden
|
||||
order:
|
||||
singular: Bestellung
|
||||
plural: Bestellungen
|
||||
invoice:
|
||||
singular: Rechnung
|
||||
plural: Rechnungen
|
||||
log_entry:
|
||||
singular: Log-Eintrag
|
||||
plural: Log-Einträge
|
||||
user:
|
||||
singular: Benutzer
|
||||
plural: Benutzer
|
||||
pipeline_job:
|
||||
singular: Pipeline-Job
|
||||
plural_active: KI-Pipeline — Aktiv
|
||||
plural_archive: KI-Pipeline — Archiv
|
||||
prompt_template:
|
||||
singular: Prompt-Vorlage
|
||||
plural: Prompt-Vorlagen
|
||||
|
||||
field:
|
||||
id: ID
|
||||
name: Name
|
||||
type: Typ
|
||||
unit: Einheit
|
||||
options: "Optionen (eine pro Zeile)"
|
||||
options_help: "Nur relevant für Typ select / multi_select."
|
||||
required_attributes: Pflichtattribute
|
||||
optional_attributes: Optionale Attribute
|
||||
inventory_number: Inventarnr.
|
||||
status: Status
|
||||
step: Schritt
|
||||
attempts: Versuche
|
||||
started: Gestartet
|
||||
error: Fehler
|
||||
ai_results: KI-Ergebnisse
|
||||
price: Preis
|
||||
condition: Zustand
|
||||
manufacturer: Hersteller
|
||||
model_number: Modellnr.
|
||||
serial_number: Seriennr.
|
||||
condition_notes: Zustandshinweise
|
||||
description: Beschreibung
|
||||
ebay_description: eBay-Beschreibung
|
||||
attributes: Attribute
|
||||
prompt_key: Schlüssel
|
||||
prompt_key_help: "Eindeutiger Bezeichner für den Prompt (z. B. <code>specs_research</code>)."
|
||||
prompt_body: Prompt-Text
|
||||
prompt_body_preview: Vorschau
|
||||
last_updated: Zuletzt geändert
|
||||
|
||||
action:
|
||||
retry: Wiederholen
|
||||
retry_confirm: "Diesen Pipeline-Job ab dem aktuellen Schritt neu einreihen?"
|
||||
rerun_ai: KI neu starten
|
||||
rerun_ai_confirm: "Die gesamte KI-Pipeline für diesen Artikel neu starten? Attribute und eBay-Texte werden überschrieben."
|
||||
mark_as_draft: Als Entwurf markieren
|
||||
activate: Aktivieren
|
||||
|
||||
flash:
|
||||
pipeline_job_not_found: "Kein ursprünglicher Pipeline-Job gefunden — Foto kann nicht ermittelt werden."
|
||||
photo_not_found: "Gespeichertes Foto nicht gefunden unter: %path%"
|
||||
pipeline_requeued: "KI-Pipeline für %label% neu gestartet — Attribute und eBay-Texte werden aktualisiert."
|
||||
article_marked_draft: Artikel als Entwurf markiert.
|
||||
article_missing_attributes: "Aktivierung nicht möglich: fehlende Attribute: %attrs%"
|
||||
article_activated: Artikel aktiviert und zur Kanalveröffentlichung eingereicht.
|
||||
job_requeued: "Job %id% ab Schritt %step% neu eingereiht."
|
||||
|
||||
pipeline:
|
||||
step:
|
||||
vision: Foto analysiert
|
||||
specs_research: Specs recherchiert
|
||||
json_coding: Attribute kodiert
|
||||
draft_article: Artikel erstellt
|
||||
ebay_text: eBay-Texte generiert
|
||||
validation: Validierung
|
||||
event:
|
||||
queued: "Job #%inv% zur Pipeline hinzugefügt"
|
||||
processing_start: "Job #%inv% gestartet"
|
||||
processing_step: "Job #%inv%: %step%"
|
||||
completed: "Job #%inv% abgeschlossen ✓"
|
||||
failed: "Job #%inv% fehlgeschlagen: %reason%"
|
||||
needs_review: "Job #%inv% benötigt manuelle Prüfung"
|
||||
95
translations/admin.en.yaml
Normal file
95
translations/admin.en.yaml
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
menu.dashboard: Dashboard
|
||||
menu.ingest_article: 'Ingest Article'
|
||||
menu.articles: Articles
|
||||
menu.article_types: 'Article Types'
|
||||
menu.attributes: Attributes
|
||||
menu.section_pipeline: Pipeline
|
||||
menu.active_jobs: 'Active Jobs'
|
||||
menu.archive: Archive
|
||||
menu.ai_prompts: 'AI Prompts'
|
||||
menu.users: Users
|
||||
menu.logs: Logs
|
||||
menu.section_sales: Sales
|
||||
menu.orders: Orders
|
||||
menu.customers: Customers
|
||||
menu.invoices: Invoices
|
||||
menu.change_password: 'Change Password'
|
||||
menu.language_en: English
|
||||
menu.language_de: Deutsch
|
||||
crud.article.singular: Article
|
||||
crud.article.plural: Articles
|
||||
crud.article_type.singular: 'Article Type'
|
||||
crud.article_type.plural: 'Article Types'
|
||||
crud.attribute.singular: Attribute
|
||||
crud.attribute.plural: Attributes
|
||||
crud.attribute_assignment.singular: 'Attribute Assignment'
|
||||
crud.attribute_assignment.plural: 'Attribute Assignments'
|
||||
crud.customer.singular: Customer
|
||||
crud.customer.plural: Customers
|
||||
crud.order.singular: Order
|
||||
crud.order.plural: Orders
|
||||
crud.invoice.singular: Invoice
|
||||
crud.invoice.plural: Invoices
|
||||
crud.log_entry.singular: 'Log Entry'
|
||||
crud.log_entry.plural: 'Log Entries'
|
||||
crud.user.singular: User
|
||||
crud.user.plural: Users
|
||||
crud.pipeline_job.singular: 'Pipeline Job'
|
||||
crud.pipeline_job.plural_active: 'AI Pipeline — Active'
|
||||
crud.pipeline_job.plural_archive: 'AI Pipeline — Archive'
|
||||
crud.prompt_template.singular: 'Prompt Template'
|
||||
crud.prompt_template.plural: 'Prompt Templates'
|
||||
field.id: ID
|
||||
field.name: Name
|
||||
field.type: Type
|
||||
field.unit: Unit
|
||||
field.options: 'Options (one per line)'
|
||||
field.options_help: 'Only relevant for type select / multi_select.'
|
||||
field.required_attributes: 'Required Attributes'
|
||||
field.optional_attributes: 'Optional Attributes'
|
||||
field.inventory_number: 'Inventory #'
|
||||
field.status: Status
|
||||
field.step: Step
|
||||
field.attempts: Attempts
|
||||
field.started: Started
|
||||
field.error: Error
|
||||
field.ai_results: 'AI Results'
|
||||
field.price: Price
|
||||
field.condition: Condition
|
||||
field.manufacturer: Manufacturer
|
||||
field.model_number: 'Model #'
|
||||
field.serial_number: 'Serial #'
|
||||
field.condition_notes: 'Condition Notes'
|
||||
field.description: Description
|
||||
field.ebay_description: 'eBay Description'
|
||||
field.attributes: Attributes
|
||||
field.prompt_key: Key
|
||||
field.prompt_key_help: 'Slug identifying the prompt (e.g. <code>specs_research</code>). Must be unique.'
|
||||
field.prompt_body: 'Prompt Body'
|
||||
field.prompt_body_preview: 'Body Preview'
|
||||
field.last_updated: 'Last Updated'
|
||||
action.retry: Retry
|
||||
action.retry_confirm: 'Re-queue this pipeline job from the current step?'
|
||||
action.rerun_ai: 'Re-run AI'
|
||||
action.rerun_ai_confirm: 'Re-run the full AI pipeline for this article? This will overwrite existing attributes and eBay texts.'
|
||||
action.mark_as_draft: 'Mark as Draft'
|
||||
action.activate: Activate
|
||||
flash.pipeline_job_not_found: 'No original pipeline job found — cannot determine which photo to use.'
|
||||
flash.photo_not_found: 'Stored photo not found at: %path%'
|
||||
flash.pipeline_requeued: 'AI pipeline re-queued for %label% — attributes and eBay texts will be updated when complete.'
|
||||
flash.article_marked_draft: 'Article marked as draft.'
|
||||
flash.article_missing_attributes: 'Cannot activate: missing attributes: %attrs%'
|
||||
flash.article_activated: 'Article activated and queued for channel publishing.'
|
||||
flash.job_requeued: 'Job %id% re-queued from %step%.'
|
||||
pipeline.step.vision: 'Photo analyzed'
|
||||
pipeline.step.specs_research: 'Specs researched'
|
||||
pipeline.step.json_coding: 'Attributes coded'
|
||||
pipeline.step.draft_article: 'Article created'
|
||||
pipeline.step.ebay_text: 'eBay texts generated'
|
||||
pipeline.step.validation: Validation
|
||||
pipeline.event.queued: 'Job #%inv% added to pipeline'
|
||||
pipeline.event.processing_start: 'Job #%inv% started'
|
||||
pipeline.event.processing_step: 'Job #%inv%: %step%'
|
||||
pipeline.event.completed: 'Job #%inv% completed ✓'
|
||||
pipeline.event.failed: 'Job #%inv% failed: %reason%'
|
||||
pipeline.event.needs_review: 'Job #%inv% requires manual review'
|
||||
Loading…
Reference in a new issue