diff --git a/app/Filament/Pages/SuperAdminProfile.php b/app/Filament/Pages/SuperAdminProfile.php new file mode 100644 index 0000000..ad08ed3 --- /dev/null +++ b/app/Filament/Pages/SuperAdminProfile.php @@ -0,0 +1,59 @@ +label(__('Username')) + ->maxLength(32) + ->unique(ignoreRecord: true); + } + + protected function getPreferredLocaleFormComponent(): Component + { + $supported = collect(explode(',', (string) env('APP_SUPPORTED_LOCALES', 'de,en'))) + ->map(fn ($l) => trim((string) $l)) + ->filter() + ->unique() + ->values() + ->all(); + + if (empty($supported)) { + $supported = array_values(array_unique(array_filter([ + config('app.locale'), + config('app.fallback_locale'), + ]))); + } + + $options = collect($supported)->mapWithKeys(fn ($l) => [$l => strtoupper($l)])->all(); + + return Select::make('preferred_locale') + ->label(__('Language')) + ->required() + ->options($options); + } + + public function form(Schema $schema): Schema + { + return $schema + ->components([ + $this->getNameFormComponent(), + $this->getEmailFormComponent(), + $this->getUsernameFormComponent(), + $this->getPreferredLocaleFormComponent(), + $this->getPasswordFormComponent(), + $this->getPasswordConfirmationFormComponent(), + $this->getCurrentPasswordFormComponent(), + ]); + } +} + diff --git a/app/Filament/Resources/EmotionResource.php b/app/Filament/Resources/EmotionResource.php index d1a90b2..2f1f745 100644 --- a/app/Filament/Resources/EmotionResource.php +++ b/app/Filament/Resources/EmotionResource.php @@ -24,43 +24,48 @@ class EmotionResource extends Resource { protected static ?string $model = Emotion::class; protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-face-smile'; - protected static UnitEnum|string|null $navigationGroup = 'Library'; + protected static UnitEnum|string|null $navigationGroup = null; + + public static function getNavigationGroup(): UnitEnum|string|null + { + return __('admin.nav.library'); + } protected static ?int $navigationSort = 10; public static function form(Schema $form): Schema { return $form->schema([ SchemaTabs::make('content_tabs') - ->label('Content Localization') + ->label(__('admin.emotions.sections.content_localization')) ->tabs([ - SchemaTab::make('German') + SchemaTab::make(__('admin.common.german')) ->icon('heroicon-o-language') ->schema([ TextInput::make('name.de') - ->label('Name (German)') + ->label(__('admin.emotions.fields.name_de')) ->required(), MarkdownEditor::make('description.de') - ->label('Description (German)') + ->label(__('admin.emotions.fields.description_de')) ->columnSpanFull(), ]), - SchemaTab::make('English') + SchemaTab::make(__('admin.common.english')) ->icon('heroicon-o-language') ->schema([ TextInput::make('name.en') - ->label('Name (English)') + ->label(__('admin.emotions.fields.name_en')) ->required(), MarkdownEditor::make('description.en') - ->label('Description (English)') + ->label(__('admin.emotions.fields.description_en')) ->columnSpanFull(), ]), ]) ->columnSpanFull(), - TextInput::make('icon')->label('Icon/Emoji')->maxLength(50), + TextInput::make('icon')->label(__('admin.emotions.fields.icon_emoji'))->maxLength(50), TextInput::make('color')->maxLength(7)->helperText('#RRGGBB'), TextInput::make('sort_order')->numeric()->default(0), Toggle::make('is_active')->default(true), Select::make('eventTypes') - ->label('Event Types') + ->label(__('admin.emotions.fields.event_types')) ->multiple() ->searchable() ->preload() @@ -72,12 +77,36 @@ class EmotionResource extends Resource { return $table ->columns([ - Tables\Columns\TextColumn::make('id')->sortable(), - Tables\Columns\TextColumn::make('name')->searchable(), - Tables\Columns\TextColumn::make('icon'), - Tables\Columns\TextColumn::make('color'), - Tables\Columns\IconColumn::make('is_active')->boolean(), - Tables\Columns\TextColumn::make('sort_order')->sortable(), + Tables\Columns\TextColumn::make('id') + ->label('#') + ->sortable(), + + Tables\Columns\TextColumn::make('name') + ->label(__('admin.emotions.table.name')) + ->getStateUsing(function ($record) { + $value = $record->name; + if (is_array($value)) { + $loc = app()->getLocale(); + return $value[$loc] ?? ($value['de'] ?? ($value['en'] ?? '')); + } + return (string) $value; + }) + ->searchable(['name->de', 'name->en']) + ->limit(60), + + Tables\Columns\TextColumn::make('icon') + ->label(__('admin.emotions.table.icon')), + + Tables\Columns\TextColumn::make('color') + ->label(__('admin.emotions.table.color')), + + Tables\Columns\IconColumn::make('is_active') + ->label(__('admin.emotions.table.is_active')) + ->boolean(), + + Tables\Columns\TextColumn::make('sort_order') + ->label(__('admin.emotions.table.sort_order')) + ->sortable(), ]) ->filters([]) ->actions([ diff --git a/app/Filament/Resources/EmotionResource/Pages/ImportEmotions.php b/app/Filament/Resources/EmotionResource/Pages/ImportEmotions.php index fa937ed..00224bd 100644 --- a/app/Filament/Resources/EmotionResource/Pages/ImportEmotions.php +++ b/app/Filament/Resources/EmotionResource/Pages/ImportEmotions.php @@ -15,7 +15,7 @@ class ImportEmotions extends Page { protected static string $resource = EmotionResource::class; protected string $view = 'filament.resources.emotion-resource.pages.import-emotions'; - protected ?string $heading = 'Import Emotions (CSV)'; + protected ?string $heading = null; public ?string $file = null; @@ -23,7 +23,7 @@ class ImportEmotions extends Page { return $form->schema([ FileUpload::make('file') - ->label('CSV file') + ->label(__('admin.common.csv_file')) ->acceptedFileTypes(['text/csv', 'text/plain']) ->directory('imports') ->required(), @@ -36,7 +36,7 @@ class ImportEmotions extends Page $path = $this->form->getState()['file'] ?? null; if (!$path || !Storage::disk('public')->exists($path)) { - Notification::make()->danger()->title('File not found')->send(); + Notification::make()->danger()->title(__('admin.notifications.file_not_found'))->send(); return; } @@ -45,11 +45,16 @@ class ImportEmotions extends Page Notification::make() ->success() - ->title("Imported {$ok} rows") - ->body($fail ? "{$fail} failed" : null) + ->title(__('admin.notifications.imported_rows', ['count' => $ok])) + ->body($fail ? __('admin.notifications.failed_count', ['count' => $fail]) : null) ->send(); } + public function getHeading(): string + { + return __('admin.emotions.import.heading'); + } + private function importEmotionsCsv(string $file): array { $handle = fopen($file, 'r'); diff --git a/app/Filament/Resources/EmotionResource/Pages/ManageEmotions.php b/app/Filament/Resources/EmotionResource/Pages/ManageEmotions.php index 944adb4..60f8703 100644 --- a/app/Filament/Resources/EmotionResource/Pages/ManageEmotions.php +++ b/app/Filament/Resources/EmotionResource/Pages/ManageEmotions.php @@ -15,11 +15,11 @@ class ManageEmotions extends ManageRecords return [ Actions\CreateAction::make(), Actions\Action::make('import') - ->label('Import CSV') + ->label(__('admin.common.import_csv')) ->icon('heroicon-o-arrow-up-tray') ->url(EmotionResource::getUrl('import')), Actions\Action::make('template') - ->label('Download CSV Template') + ->label(__('admin.common.download_csv_template')) ->icon('heroicon-o-document-arrow-down') ->url(url('/super-admin/templates/emotions.csv')), ]; diff --git a/app/Filament/Resources/EventResource.php b/app/Filament/Resources/EventResource.php index f5bd734..d4cf358 100644 --- a/app/Filament/Resources/EventResource.php +++ b/app/Filament/Resources/EventResource.php @@ -25,44 +25,49 @@ class EventResource extends Resource { protected static ?string $model = Event::class; protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-calendar'; - protected static UnitEnum|string|null $navigationGroup = 'Platform'; + protected static UnitEnum|string|null $navigationGroup = null; + + public static function getNavigationGroup(): UnitEnum|string|null + { + return __('admin.nav.platform'); + } protected static ?int $navigationSort = 20; public static function form(Schema $form): Schema { return $form->schema([ Select::make('tenant_id') - ->label('Tenant') + ->label(__('admin.events.fields.tenant')) ->options(Tenant::all()->pluck('name', 'id')) ->searchable() ->required(), TextInput::make('name') - ->label('Event Name') + ->label(__('admin.events.fields.name')) ->required() ->maxLength(255), TextInput::make('slug') - ->label('Slug') + ->label(__('admin.events.fields.slug')) ->required() ->unique(ignoreRecord: true) ->maxLength(255), DatePicker::make('date') - ->label('Event Date') + ->label(__('admin.events.fields.date')) ->required(), Select::make('event_type_id') - ->label('Event Type') + ->label(__('admin.events.fields.type')) ->options(EventType::all()->pluck('name', 'id')) ->searchable(), TextInput::make('default_locale') - ->label('Default Locale') + ->label(__('admin.events.fields.default_locale')) ->default('de') ->maxLength(5), Toggle::make('is_active') - ->label('Is Active') + ->label(__('admin.events.fields.is_active')) ->default(true), KeyValue::make('settings') - ->label('Settings') - ->keyLabel('Key') - ->valueLabel('Value'), + ->label(__('admin.events.fields.settings')) + ->keyLabel(__('admin.common.key')) + ->valueLabel(__('admin.common.value')), ])->columns(2); } @@ -71,30 +76,30 @@ class EventResource extends Resource return $table ->columns([ Tables\Columns\TextColumn::make('id')->sortable(), - Tables\Columns\TextColumn::make('tenant_id')->label('Tenant')->sortable(), + Tables\Columns\TextColumn::make('tenant_id')->label(__('admin.events.table.tenant'))->sortable(), Tables\Columns\TextColumn::make('name')->limit(30), Tables\Columns\TextColumn::make('slug')->searchable(), Tables\Columns\TextColumn::make('date')->date(), Tables\Columns\IconColumn::make('is_active')->boolean(), Tables\Columns\TextColumn::make('default_locale'), - Tables\Columns\TextColumn::make('join')->label('Join') + Tables\Columns\TextColumn::make('join')->label(__('admin.events.table.join')) ->getStateUsing(fn($record) => url("/e/{$record->slug}")) ->copyable() - ->copyMessage('Join link copied'), + ->copyMessage(__('admin.events.messages.join_link_copied')), Tables\Columns\TextColumn::make('created_at')->since(), ]) ->filters([]) ->actions([ Actions\EditAction::make(), Actions\Action::make('toggle') - ->label('Toggle Active') + ->label(__('admin.events.actions.toggle_active')) ->icon('heroicon-o-power') ->action(fn($record) => $record->update(['is_active' => !$record->is_active])), Actions\Action::make('join_link') - ->label('Join Link / QR') + ->label(__('admin.events.actions.join_link_qr')) ->icon('heroicon-o-qr-code') - ->modalHeading('Event Join Link') - ->modalSubmitActionLabel('Close') + ->modalHeading(__('admin.events.modal.join_link_heading')) + ->modalSubmitActionLabel(__('admin.common.close')) ->modalContent(fn($record) => view('filament.events.join-link', [ 'link' => url("/e/{$record->slug}"), ])), @@ -112,4 +117,4 @@ class EventResource extends Resource 'edit' => Pages\EditEvent::route('/{record}/edit'), ]; } -} \ No newline at end of file +} diff --git a/app/Filament/Resources/EventTypeResource.php b/app/Filament/Resources/EventTypeResource.php index 80c0f9c..dc33c79 100644 --- a/app/Filament/Resources/EventTypeResource.php +++ b/app/Filament/Resources/EventTypeResource.php @@ -21,35 +21,40 @@ class EventTypeResource extends Resource { protected static ?string $model = EventType::class; protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-swatch'; - protected static UnitEnum|string|null $navigationGroup = 'Library'; + protected static UnitEnum|string|null $navigationGroup = null; + + public static function getNavigationGroup(): UnitEnum|string|null + { + return __('admin.nav.library'); + } protected static ?int $navigationSort = 20; public static function form(Schema $form): Schema { return $form->schema([ SchemaTabs::make('name_tabs') - ->label('Name Localization') + ->label(__('admin.event_types.sections.name_localization')) ->tabs([ - SchemaTab::make('German') + SchemaTab::make(__('admin.common.german')) ->icon('heroicon-o-language') ->schema([ TextInput::make('name.de') - ->label('Name (German)') + ->label(__('admin.event_types.fields.name_de')) ->required(), ]), - SchemaTab::make('English') + SchemaTab::make(__('admin.common.english')) ->icon('heroicon-o-language') ->schema([ TextInput::make('name.en') - ->label('Name (English)') + ->label(__('admin.event_types.fields.name_en')) ->required(), ]), ]), - TextInput::make('slug')->required()->unique(ignoreRecord: true), - TextInput::make('icon')->maxLength(64), - KeyValue::make('settings')->label('Settings')->keyLabel('key')->valueLabel('value'), + TextInput::make('slug')->label(__('admin.event_types.fields.slug'))->required()->unique(ignoreRecord: true), + TextInput::make('icon')->label(__('admin.event_types.fields.icon'))->maxLength(64), + KeyValue::make('settings')->label(__('admin.event_types.fields.settings'))->keyLabel(__('admin.common.key'))->valueLabel(__('admin.common.value')), Select::make('emotions') - ->label('Emotions') + ->label(__('admin.event_types.fields.emotions')) ->multiple() ->searchable() ->preload() @@ -61,11 +66,33 @@ class EventTypeResource extends Resource { return $table ->columns([ - Tables\Columns\TextColumn::make('id')->sortable(), - Tables\Columns\TextColumn::make('name')->searchable(), - Tables\Columns\TextColumn::make('slug')->searchable(), - Tables\Columns\TextColumn::make('icon'), - Tables\Columns\TextColumn::make('created_at')->since(), + Tables\Columns\TextColumn::make('id') + ->label('#') + ->sortable(), + + Tables\Columns\TextColumn::make('name') + ->label(__('admin.event_types.table.name')) + ->getStateUsing(function ($record) { + $value = $record->name; + if (is_array($value)) { + $loc = app()->getLocale(); + return $value[$loc] ?? ($value['de'] ?? ($value['en'] ?? '')); + } + return (string) $value; + }) + ->searchable(['name->de', 'name->en']) + ->limit(60), + + Tables\Columns\TextColumn::make('slug') + ->label(__('admin.event_types.table.slug')) + ->searchable(), + + Tables\Columns\TextColumn::make('icon') + ->label(__('admin.event_types.table.icon')), + + Tables\Columns\TextColumn::make('created_at') + ->label(__('admin.event_types.table.created_at')) + ->since(), ]) ->filters([]) ->actions([ @@ -82,4 +109,4 @@ class EventTypeResource extends Resource 'index' => Pages\ManageEventTypes::route('/'), ]; } -} \ No newline at end of file +} diff --git a/app/Filament/Resources/LegalPageResource.php b/app/Filament/Resources/LegalPageResource.php index 5ecf105..c88e03f 100644 --- a/app/Filament/Resources/LegalPageResource.php +++ b/app/Filament/Resources/LegalPageResource.php @@ -23,52 +23,57 @@ class LegalPageResource extends Resource { protected static ?string $model = LegalPage::class; protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-scale'; - protected static UnitEnum|string|null $navigationGroup = 'Platform'; + protected static UnitEnum|string|null $navigationGroup = null; + + public static function getNavigationGroup(): UnitEnum|string|null + { + return __('admin.nav.platform'); + } protected static ?int $navigationSort = 40; public static function form(Schema $form): Schema { return $form->schema([ TextInput::make('slug') - ->label('Slug') + ->label(__('admin.legal_pages.fields.slug')) ->required() ->unique(ignoreRecord: true) ->maxLength(255), KeyValue::make('title') - ->label('Title (de/en)') - ->keyLabel('locale') - ->valueLabel('value') + ->label(__('admin.legal_pages.fields.title_localized')) + ->keyLabel(__('admin.common.locale')) + ->valueLabel(__('admin.common.value')) ->default(['de' => '', 'en' => '']) ->required(), SchemaTabs::make('content_tabs') - ->label('Content Localization') + ->label(__('admin.legal_pages.fields.content_localization')) ->tabs([ - SchemaTab::make('German') + SchemaTab::make(__('admin.common.german')) ->icon('heroicon-o-language') ->schema([ MarkdownEditor::make('body_markdown.de') - ->label('Content (German)') + ->label(__('admin.legal_pages.fields.content_de')) ->required() ->columnSpanFull(), ]), - SchemaTab::make('English') + SchemaTab::make(__('admin.common.english')) ->icon('heroicon-o-language') ->schema([ MarkdownEditor::make('body_markdown.en') - ->label('Content (English)') + ->label(__('admin.legal_pages.fields.content_en')) ->required() ->columnSpanFull(), ]), ]) ->columnSpanFull(), Toggle::make('is_published') - ->label('Is Published') + ->label(__('admin.legal_pages.fields.is_published')) ->default(true), DatePicker::make('effective_from') - ->label('Effective From') + ->label(__('admin.legal_pages.fields.effective_from')) ->required(), TextInput::make('version') - ->label('Version') + ->label(__('admin.legal_pages.fields.version')) ->required() ->default('1.0') ->maxLength(20), diff --git a/app/Filament/Resources/PhotoResource.php b/app/Filament/Resources/PhotoResource.php index 466e74d..3d2c5ef 100644 --- a/app/Filament/Resources/PhotoResource.php +++ b/app/Filament/Resources/PhotoResource.php @@ -24,30 +24,36 @@ class PhotoResource extends Resource { protected static ?string $model = Photo::class; protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-photo'; - protected static UnitEnum|string|null $navigationGroup = 'Content'; + protected static UnitEnum|string|null $navigationGroup = null; + + public static function getNavigationGroup(): UnitEnum|string|null + { + return __('admin.nav.content'); + } protected static ?int $navigationSort = 30; public static function form(Schema $form): Schema { return $form->schema([ Select::make('event_id') - ->label('Event') + ->label(__('admin.photos.fields.event')) ->options(Event::all()->pluck('name', 'id')) ->searchable() ->required(), - FileUpload::make('image_path') - ->label('Photo') - ->image() + FileUpload::make('file_path') + ->label(__('admin.photos.fields.photo')) + ->image() // enable FilePond image preview + ->disk('public') ->directory('photos') - ->required() - ->visibility('public'), + ->visibility('public') + ->required(), Toggle::make('is_featured') - ->label('Is Featured') + ->label(__('admin.photos.fields.is_featured')) ->default(false), KeyValue::make('metadata') - ->label('Metadata') - ->keyLabel('Key') - ->valueLabel('Value'), + ->label(__('admin.photos.fields.metadata')) + ->keyLabel(__('admin.common.key')) + ->valueLabel(__('admin.common.value')), ])->columns(2); } @@ -55,10 +61,10 @@ class PhotoResource extends Resource { return $table ->columns([ - Tables\Columns\ImageColumn::make('image_path')->label('Photo')->disk('public')->visibility('public'), + Tables\Columns\ImageColumn::make('file_path')->label(__('admin.photos.table.photo'))->disk('public')->visibility('public'), Tables\Columns\TextColumn::make('id')->sortable(), - Tables\Columns\TextColumn::make('event_id')->label('Event'), - Tables\Columns\TextColumn::make('likes_count')->label('Likes'), + Tables\Columns\TextColumn::make('event_id')->label(__('admin.photos.table.event')), + Tables\Columns\TextColumn::make('likes_count')->label(__('admin.photos.table.likes')), Tables\Columns\IconColumn::make('is_featured')->boolean(), Tables\Columns\TextColumn::make('created_at')->since(), ]) @@ -66,12 +72,12 @@ class PhotoResource extends Resource ->actions([ Actions\EditAction::make(), Actions\Action::make('feature') - ->label('Feature') + ->label(__('admin.photos.actions.feature')) ->visible(fn($record) => !$record->is_featured) ->action(fn($record) => $record->update(['is_featured' => true])) ->icon('heroicon-o-star'), Actions\Action::make('unfeature') - ->label('Unfeature') + ->label(__('admin.photos.actions.unfeature')) ->visible(fn($record) => $record->is_featured) ->action(fn($record) => $record->update(['is_featured' => false])) ->icon('heroicon-o-star'), @@ -79,11 +85,11 @@ class PhotoResource extends Resource ]) ->bulkActions([ Actions\BulkAction::make('feature') - ->label('Feature selected') + ->label(__('admin.photos.actions.feature_selected')) ->icon('heroicon-o-star') ->action(fn($records) => $records->each->update(['is_featured' => true])), Actions\BulkAction::make('unfeature') - ->label('Unfeature selected') + ->label(__('admin.photos.actions.unfeature_selected')) ->icon('heroicon-o-star') ->action(fn($records) => $records->each->update(['is_featured' => false])), Actions\DeleteBulkAction::make(), @@ -98,4 +104,4 @@ class PhotoResource extends Resource 'edit' => Pages\EditPhoto::route('/{record}/edit'), ]; } -} \ No newline at end of file +} diff --git a/app/Filament/Resources/TaskResource.php b/app/Filament/Resources/TaskResource.php index db227f4..a49e8e6 100644 --- a/app/Filament/Resources/TaskResource.php +++ b/app/Filament/Resources/TaskResource.php @@ -23,59 +23,69 @@ class TaskResource extends Resource { protected static ?string $model = Task::class; protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-clipboard-document-check'; - protected static UnitEnum|string|null $navigationGroup = 'Library'; + protected static UnitEnum|string|null $navigationGroup = null; protected static ?int $navigationSort = 30; + public static function getNavigationGroup(): UnitEnum|string|null + { + return __('admin.nav.library'); + } + + public static function getNavigationLabel(): string + { + return __('admin.tasks.menu'); + } + public static function form(Schema $form): Schema { return $form->schema([ Select::make('emotion_id') ->relationship('emotion', 'name') - ->getOptionLabelFromRecordUsing(fn ($record) => is_array($record->name) ? ($record->name['de'] ?? $record->name['en'] ?? 'Unnamed') : $record->name) + ->getOptionLabelFromRecordUsing(fn ($record) => is_array($record->name) ? ($record->name['de'] ?? $record->name['en'] ?? __('admin.common.unnamed')) : $record->name) ->required() ->searchable() ->preload(), Select::make('event_type_id') ->relationship('eventType', 'name') - ->getOptionLabelFromRecordUsing(fn ($record) => is_array($record->name) ? ($record->name['de'] ?? $record->name['en'] ?? 'Unnamed') : $record->name) + ->getOptionLabelFromRecordUsing(fn ($record) => is_array($record->name) ? ($record->name['de'] ?? $record->name['en'] ?? __('admin.common.unnamed')) : $record->name) ->searchable() ->preload() - ->label('Event Type (optional)'), + ->label(__('admin.tasks.fields.event_type_optional')), SchemaTabs::make('content_tabs') - ->label('Content Localization') + ->label(__('admin.tasks.fields.content_localization')) ->tabs([ - SchemaTab::make('German') + SchemaTab::make(__('admin.common.german')) ->icon('heroicon-o-language') ->schema([ TextInput::make('title.de') - ->label('Title (German)') + ->label(__('admin.tasks.fields.title_de')) ->required(), MarkdownEditor::make('description.de') - ->label('Description (German)') + ->label(__('admin.tasks.fields.description_de')) ->columnSpanFull(), MarkdownEditor::make('example_text.de') - ->label('Example Text (German)') + ->label(__('admin.tasks.fields.example_de')) ->columnSpanFull(), ]), - SchemaTab::make('English') + SchemaTab::make(__('admin.common.english')) ->icon('heroicon-o-language') ->schema([ TextInput::make('title.en') - ->label('Title (English)') + ->label(__('admin.tasks.fields.title_en')) ->required(), MarkdownEditor::make('description.en') - ->label('Description (English)') + ->label(__('admin.tasks.fields.description_en')) ->columnSpanFull(), MarkdownEditor::make('example_text.en') - ->label('Example Text (English)') + ->label(__('admin.tasks.fields.example_en')) ->columnSpanFull(), ]), ]) ->columnSpanFull(), - Select::make('difficulty')->options([ - 'easy' => 'Easy', - 'medium' => 'Medium', - 'hard' => 'Hard', + Select::make('difficulty')->label(__('admin.tasks.fields.difficulty.label'))->options([ + 'easy' => __('admin.tasks.fields.difficulty.easy'), + 'medium' => __('admin.tasks.fields.difficulty.medium'), + 'hard' => __('admin.tasks.fields.difficulty.hard'), ])->default('easy'), TextInput::make('sort_order')->numeric()->default(0), Toggle::make('is_active')->default(true), @@ -86,13 +96,59 @@ class TaskResource extends Resource { return $table ->columns([ - Tables\Columns\TextColumn::make('id')->sortable(), - Tables\Columns\TextColumn::make('emotion.name')->label('Emotion')->sortable()->searchable(), - Tables\Columns\TextColumn::make('eventType.name')->label('Event Type')->toggleable(), - Tables\Columns\TextColumn::make('title')->searchable()->limit(40), - Tables\Columns\TextColumn::make('difficulty')->badge(), - Tables\Columns\IconColumn::make('is_active')->boolean(), - Tables\Columns\TextColumn::make('sort_order')->sortable(), + Tables\Columns\TextColumn::make('id') + ->label('#') + ->sortable(), + + Tables\Columns\TextColumn::make('title') + ->label(__('admin.tasks.table.title')) + ->getStateUsing(function ($record) { + $value = $record->title; + if (is_array($value)) { + $loc = app()->getLocale(); + return $value[$loc] ?? ($value['de'] ?? ($value['en'] ?? '')); + } + return (string) $value; + }) + ->limit(60) + ->searchable(['title->de', 'title->en']), + + Tables\Columns\TextColumn::make('emotion.name') + ->label(__('admin.tasks.fields.emotion')) + ->getStateUsing(function ($record) { + $value = optional($record->emotion)->name; + if (is_array($value)) { + $loc = app()->getLocale(); + return $value[$loc] ?? ($value['de'] ?? ($value['en'] ?? '')); + } + return (string) ($value ?? ''); + }) + ->sortable() + ->searchable(['emotion->name->de', 'emotion->name->en']), + + Tables\Columns\TextColumn::make('eventType.name') + ->label(__('admin.tasks.fields.event_type')) + ->getStateUsing(function ($record) { + $value = optional($record->eventType)->name; + if (is_array($value)) { + $loc = app()->getLocale(); + return $value[$loc] ?? ($value['de'] ?? ($value['en'] ?? '')); + } + return (string) ($value ?? ''); + }) + ->toggleable(), + + Tables\Columns\TextColumn::make('difficulty') + ->label(__('admin.tasks.fields.difficulty.label')) + ->badge(), + + Tables\Columns\IconColumn::make('is_active') + ->label(__('admin.tasks.table.is_active')) + ->boolean(), + + Tables\Columns\TextColumn::make('sort_order') + ->label(__('admin.tasks.table.sort_order')) + ->sortable(), ]) ->filters([]) ->actions([ @@ -111,4 +167,4 @@ class TaskResource extends Resource 'import' => Pages\ImportTasks::route('/import'), ]; } -} \ No newline at end of file +} diff --git a/app/Filament/Resources/TaskResource/Pages/ImportTasks.php b/app/Filament/Resources/TaskResource/Pages/ImportTasks.php index 3675457..967755e 100644 --- a/app/Filament/Resources/TaskResource/Pages/ImportTasks.php +++ b/app/Filament/Resources/TaskResource/Pages/ImportTasks.php @@ -15,7 +15,7 @@ class ImportTasks extends Page { protected static string $resource = TaskResource::class; protected string $view = 'filament.resources.task-resource.pages.import-tasks'; - protected ?string $heading = 'Import Tasks (CSV)'; + protected ?string $heading = null; public ?string $file = null; @@ -23,7 +23,7 @@ class ImportTasks extends Page { return $form->schema([ FileUpload::make('file') - ->label('CSV file') + ->label(__('admin.common.csv_file')) ->acceptedFileTypes(['text/csv', 'text/plain']) ->directory('imports') ->required(), @@ -36,7 +36,7 @@ class ImportTasks extends Page $path = $this->form->getState()['file'] ?? null; if (!$path || !Storage::disk('public')->exists($path)) { - Notification::make()->danger()->title('File not found')->send(); + Notification::make()->danger()->title(__('admin.notifications.file_not_found'))->send(); return; } @@ -45,11 +45,16 @@ class ImportTasks extends Page Notification::make() ->success() - ->title("Imported {$ok} rows") - ->body($fail ? "{$fail} failed" : null) + ->title(__('admin.notifications.imported_rows', ['count' => $ok])) + ->body($fail ? __('admin.notifications.failed_count', ['count' => $fail]) : null) ->send(); } + public function getHeading(): string + { + return __('admin.tasks.import.heading'); + } + private function importTasksCsv(string $file): array { $handle = fopen($file, 'r'); diff --git a/app/Filament/Resources/TaskResource/Pages/ManageTasks.php b/app/Filament/Resources/TaskResource/Pages/ManageTasks.php index 9d96669..f24884a 100644 --- a/app/Filament/Resources/TaskResource/Pages/ManageTasks.php +++ b/app/Filament/Resources/TaskResource/Pages/ManageTasks.php @@ -15,11 +15,11 @@ class ManageTasks extends ManageRecords return [ Actions\CreateAction::make(), Actions\Action::make('import') - ->label('Import CSV') + ->label(__('admin.common.import_csv')) ->icon('heroicon-o-arrow-up-tray') ->url(TaskResource::getUrl('import')), Actions\Action::make('template') - ->label('Download CSV Template') + ->label(__('admin.common.download_csv_template')) ->icon('heroicon-o-document-arrow-down') ->url(url('/super-admin/templates/tasks.csv')), ]; diff --git a/app/Filament/Resources/TenantResource.php b/app/Filament/Resources/TenantResource.php index 005717f..ff6ee70 100644 --- a/app/Filament/Resources/TenantResource.php +++ b/app/Filament/Resources/TenantResource.php @@ -21,34 +21,39 @@ class TenantResource extends Resource { protected static ?string $model = Tenant::class; protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-building-office'; - protected static UnitEnum|string|null $navigationGroup = 'Platform'; + protected static UnitEnum|string|null $navigationGroup = null; + + public static function getNavigationGroup(): UnitEnum|string|null + { + return __('admin.nav.platform'); + } protected static ?int $navigationSort = 10; public static function form(Schema $form): Schema { return $form->schema([ TextInput::make('name') - ->label('Tenant Name') + ->label(__('admin.tenants.fields.name')) ->required() ->maxLength(255), TextInput::make('slug') - ->label('Slug') + ->label(__('admin.tenants.fields.slug')) ->required() ->unique(ignoreRecord: true) ->maxLength(255), TextInput::make('contact_email') - ->label('Contact Email') + ->label(__('admin.tenants.fields.contact_email')) ->email() ->required() ->maxLength(255), TextInput::make('event_credits_balance') - ->label('Event Credits Balance') + ->label(__('admin.tenants.fields.event_credits_balance')) ->numeric() ->default(0), KeyValue::make('features') - ->label('Features') - ->keyLabel('Key') - ->valueLabel('Value'), + ->label(__('admin.tenants.fields.features')) + ->keyLabel(__('admin.common.key')) + ->valueLabel(__('admin.common.value')), ])->columns(2); } @@ -60,8 +65,8 @@ class TenantResource extends Resource Tables\Columns\TextColumn::make('name')->searchable()->sortable(), Tables\Columns\TextColumn::make('slug')->searchable(), Tables\Columns\TextColumn::make('contact_email'), - Tables\Columns\TextColumn::make('event_credits_balance')->label('Credits'), - Tables\Columns\TextColumn::make('last_activity_at')->since()->label('Last activity'), + Tables\Columns\TextColumn::make('event_credits_balance')->label(__('admin.common.credits')), + Tables\Columns\TextColumn::make('last_activity_at')->since()->label(__('admin.common.last_activity')), Tables\Columns\TextColumn::make('created_at')->dateTime()->toggleable(isToggledHiddenByDefault: true), ]) ->filters([]) diff --git a/app/Filament/Widgets/EventsActiveToday.php b/app/Filament/Widgets/EventsActiveToday.php index fa41140..2e029b7 100644 --- a/app/Filament/Widgets/EventsActiveToday.php +++ b/app/Filament/Widgets/EventsActiveToday.php @@ -9,7 +9,12 @@ use App\Models\Event; class EventsActiveToday extends BaseWidget { - protected static ?string $heading = 'Events active today'; + protected static ?string $heading = null; + + public function getHeading() + { + return __('admin.widgets.events_active_today.heading'); + } protected ?string $pollingInterval = '60s'; public function table(Tables\Table $table): Tables\Table @@ -29,11 +34,13 @@ class EventsActiveToday extends BaseWidget ->limit(10) ) ->columns([ - Tables\Columns\TextColumn::make('id')->label('#')->width('60px'), - Tables\Columns\TextColumn::make('slug')->label('Slug')->searchable(), + Tables\Columns\TextColumn::make('id')->label(__('admin.common.hash'))->width('60px'), + Tables\Columns\TextColumn::make('slug')->label(__('admin.common.slug'))->searchable(), Tables\Columns\TextColumn::make('date')->date(), - Tables\Columns\TextColumn::make('uploads_today')->label('Uploads today')->numeric(), + Tables\Columns\TextColumn::make('uploads_today')->label(__('admin.common.uploads_today'))->numeric(), ]) ->paginated(false); } } + + diff --git a/app/Filament/Widgets/RecentPhotosTable.php b/app/Filament/Widgets/RecentPhotosTable.php index 1024649..f42c75e 100644 --- a/app/Filament/Widgets/RecentPhotosTable.php +++ b/app/Filament/Widgets/RecentPhotosTable.php @@ -9,7 +9,12 @@ use Filament\Actions; class RecentPhotosTable extends BaseWidget { - protected static ?string $heading = 'Recent uploads'; + protected static ?string $heading = null; + + public function getHeading() + { + return __('admin.widgets.recent_uploads.heading'); + } protected int|string|array $columnSpan = 'full'; public function table(Tables\Table $table): Tables\Table @@ -21,22 +26,23 @@ class RecentPhotosTable extends BaseWidget ->limit(10) ) ->columns([ - Tables\Columns\ImageColumn::make('thumbnail_path')->label('Thumb')->circular(), - Tables\Columns\TextColumn::make('id')->label('#'), - Tables\Columns\TextColumn::make('event_id')->label('Event'), - Tables\Columns\TextColumn::make('likes_count')->label('Likes'), + Tables\Columns\ImageColumn::make('thumbnail_path')->label(__('admin.common.thumb'))->circular(), + Tables\Columns\TextColumn::make('id')->label(__('admin.common.hash')), + Tables\Columns\TextColumn::make('event_id')->label(__('admin.common.event')), + Tables\Columns\TextColumn::make('likes_count')->label(__('admin.common.likes')), Tables\Columns\TextColumn::make('created_at')->since(), ]) ->actions([ Actions\Action::make('feature') - ->label('Feature') + ->label(__('admin.photos.actions.feature')) ->visible(fn(Photo $record) => ! (bool)($record->is_featured ?? 0)) ->action(fn(Photo $record) => $record->update(['is_featured' => 1])), Actions\Action::make('unfeature') - ->label('Unfeature') + ->label(__('admin.photos.actions.unfeature')) ->visible(fn(Photo $record) => (bool)($record->is_featured ?? 0)) ->action(fn(Photo $record) => $record->update(['is_featured' => 0])), ]) ->paginated(false); } } + diff --git a/app/Filament/Widgets/TopTenantsByUploads.php b/app/Filament/Widgets/TopTenantsByUploads.php index cf2a0ab..004b302 100644 --- a/app/Filament/Widgets/TopTenantsByUploads.php +++ b/app/Filament/Widgets/TopTenantsByUploads.php @@ -8,7 +8,12 @@ use App\Models\Tenant; class TopTenantsByUploads extends BaseWidget { - protected static ?string $heading = 'Top tenants by uploads'; + protected static ?string $heading = null; + + public function getHeading() + { + return __('admin.widgets.top_tenants_by_uploads.heading'); + } protected ?string $pollingInterval = '60s'; public function table(Tables\Table $table): Tables\Table @@ -21,10 +26,11 @@ class TopTenantsByUploads extends BaseWidget ->limit(5) ) ->columns([ - Tables\Columns\TextColumn::make('id')->label('#')->width('60px'), - Tables\Columns\TextColumn::make('name')->label('Tenant')->searchable(), - Tables\Columns\TextColumn::make('photos_count')->label('Uploads')->numeric(), + Tables\Columns\TextColumn::make('id')->label(__('admin.common.hash'))->width('60px'), + Tables\Columns\TextColumn::make('name')->label(__('admin.common.tenant'))->searchable(), + Tables\Columns\TextColumn::make('photos_count')->label(__('admin.common.uploads'))->numeric(), ]) ->paginated(false); } } + diff --git a/app/Filament/Widgets/UploadsPerDayChart.php b/app/Filament/Widgets/UploadsPerDayChart.php index a0eafc3..6712023 100644 --- a/app/Filament/Widgets/UploadsPerDayChart.php +++ b/app/Filament/Widgets/UploadsPerDayChart.php @@ -1,14 +1,13 @@ $labels, 'datasets' => [ [ - 'label' => 'Uploads', + 'label' => __('admin.common.uploads'), 'data' => $data, 'borderColor' => '#f59e0b', 'backgroundColor' => 'rgba(245, 158, 11, 0.2)', @@ -49,4 +48,9 @@ class UploadsPerDayChart extends ChartWidget { return 'line'; } -} + + public function getHeading(): string|\Illuminate\Contracts\Support\Htmlable|null + { + return __('admin.widgets.uploads_per_day.heading'); + } +} \ No newline at end of file diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index bd89013..a62e452 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -38,6 +38,20 @@ class HandleInertiaRequests extends Middleware { [$message, $author] = str(Inspiring::quotes()->random())->explode('-'); + $supportedLocales = collect(explode(',', (string) env('APP_SUPPORTED_LOCALES', 'de,en'))) + ->map(fn ($l) => trim((string) $l)) + ->filter() + ->unique() + ->values() + ->all(); + + if (empty($supportedLocales)) { + $supportedLocales = array_values(array_unique(array_filter([ + config('app.locale'), + config('app.fallback_locale'), + ]))); + } + return [ ...parent::share($request), 'name' => config('app.name'), @@ -45,6 +59,7 @@ class HandleInertiaRequests extends Middleware 'auth' => [ 'user' => $request->user(), ], + 'supportedLocales' => $supportedLocales, 'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true', ]; } diff --git a/app/Http/Middleware/SetLocaleFromUser.php b/app/Http/Middleware/SetLocaleFromUser.php new file mode 100644 index 0000000..99f4757 --- /dev/null +++ b/app/Http/Middleware/SetLocaleFromUser.php @@ -0,0 +1,22 @@ +user(); + + if ($user && ! empty($user->preferred_locale)) { + app()->setLocale($user->preferred_locale); + } + + return $next($request); + } +} + diff --git a/app/Http/Requests/Settings/ProfileUpdateRequest.php b/app/Http/Requests/Settings/ProfileUpdateRequest.php index 64cf26b..c502abf 100644 --- a/app/Http/Requests/Settings/ProfileUpdateRequest.php +++ b/app/Http/Requests/Settings/ProfileUpdateRequest.php @@ -16,6 +16,20 @@ class ProfileUpdateRequest extends FormRequest */ public function rules(): array { + $supportedLocales = collect(explode(',', (string) env('APP_SUPPORTED_LOCALES', 'de,en'))) + ->map(fn ($l) => trim((string) $l)) + ->filter() + ->unique() + ->values() + ->all(); + + if (empty($supportedLocales)) { + $supportedLocales = array_values(array_unique(array_filter([ + config('app.locale'), + config('app.fallback_locale'), + ]))); + } + return [ 'name' => ['required', 'string', 'max:255'], @@ -27,6 +41,19 @@ class ProfileUpdateRequest extends FormRequest 'max:255', Rule::unique(User::class)->ignore($this->user()->id), ], + + 'username' => [ + 'nullable', + 'string', + 'max:32', + Rule::unique(User::class)->ignore($this->user()->id), + ], + + 'preferred_locale' => [ + 'required', + 'string', + Rule::in($supportedLocales), + ], ]; } } diff --git a/app/Models/Photo.php b/app/Models/Photo.php index 55a05c4..77bbe0d 100644 --- a/app/Models/Photo.php +++ b/app/Models/Photo.php @@ -5,6 +5,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Facades\Storage; class Photo extends Model { diff --git a/app/Models/User.php b/app/Models/User.php index d89e8ee..f3f97ea 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -22,6 +22,8 @@ class User extends Authenticatable 'name', 'email', 'password', + 'username', + 'preferred_locale', ]; /** diff --git a/app/Providers/Filament/SuperAdminPanelProvider.php b/app/Providers/Filament/SuperAdminPanelProvider.php index 682a4c5..da76f47 100644 --- a/app/Providers/Filament/SuperAdminPanelProvider.php +++ b/app/Providers/Filament/SuperAdminPanelProvider.php @@ -27,6 +27,7 @@ class SuperAdminPanelProvider extends PanelProvider ->default() ->id('super-admin') ->path('super-admin') + ->profile(\App\Filament\Pages\SuperAdminProfile::class, true) ->colors([ 'primary' => Color::Amber, ]) diff --git a/bootstrap/app.php b/bootstrap/app.php index 134581a..24baf8c 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -2,6 +2,7 @@ use App\Http\Middleware\HandleAppearance; use App\Http\Middleware\HandleInertiaRequests; +use App\Http\Middleware\SetLocaleFromUser; use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; @@ -17,6 +18,7 @@ return Application::configure(basePath: dirname(__DIR__)) $middleware->encryptCookies(except: ['appearance', 'sidebar_state']); $middleware->web(append: [ + SetLocaleFromUser::class, HandleAppearance::class, HandleInertiaRequests::class, AddLinkHeadersForPreloadedAssets::class, diff --git a/database/migrations/2025_09_11_100500_add_username_and_locale_to_users_table.php b/database/migrations/2025_09_11_100500_add_username_and_locale_to_users_table.php new file mode 100644 index 0000000..9f88f6f --- /dev/null +++ b/database/migrations/2025_09_11_100500_add_username_and_locale_to_users_table.php @@ -0,0 +1,37 @@ +string('username', 32)->nullable()->unique()->after('email'); + } + + if (! Schema::hasColumn('users', 'preferred_locale')) { + $defaultLocale = config('app.locale', 'en'); + $table->string('preferred_locale', 5)->default($defaultLocale)->after('role'); + } + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + if (Schema::hasColumn('users', 'username')) { + try { $table->dropUnique(['username']); } catch (\Throwable $e) { /* ignore */ } + $table->dropColumn('username'); + } + + if (Schema::hasColumn('users', 'preferred_locale')) { + $table->dropColumn('preferred_locale'); + } + }); + } +}; + diff --git a/resources/js/pages/settings/profile.tsx b/resources/js/pages/settings/profile.tsx index 77e5d8c..d75fd43 100644 --- a/resources/js/pages/settings/profile.tsx +++ b/resources/js/pages/settings/profile.tsx @@ -22,7 +22,7 @@ const breadcrumbs: BreadcrumbItem[] = [ ]; export default function Profile({ mustVerifyEmail, status }: { mustVerifyEmail: boolean; status?: string }) { - const { auth } = usePage().props; + const { auth, supportedLocales } = usePage().props as SharedData & { supportedLocales: string[] }; return ( @@ -74,6 +74,40 @@ export default function Profile({ mustVerifyEmail, status }: { mustVerifyEmail: +
+ + + + + +
+ +
+ + + + + +
+ {mustVerifyEmail && auth.user.email_verified_at === null && (

diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts index 42f88e8..1ed750f 100644 --- a/resources/js/types/index.d.ts +++ b/resources/js/types/index.d.ts @@ -27,6 +27,7 @@ export interface SharedData { quote: { message: string; author: string }; auth: Auth; sidebarOpen: boolean; + supportedLocales?: string[]; [key: string]: unknown; } @@ -34,6 +35,8 @@ export interface User { id: number; name: string; email: string; + username?: string; + preferred_locale?: string; avatar?: string; email_verified_at: string | null; created_at: string; diff --git a/resources/lang/de/admin.php b/resources/lang/de/admin.php new file mode 100644 index 0000000..0c13490 --- /dev/null +++ b/resources/lang/de/admin.php @@ -0,0 +1,222 @@ + [ + 'platform' => 'Plattform', + 'library' => 'Bibliothek', + 'content' => 'Inhalte', + ], + + 'common' => [ + 'key' => 'Schlüssel', + 'value' => 'Wert', + 'locale' => 'Sprache', + 'german' => 'Deutsch', + 'english' => 'Englisch', + 'import' => 'Import', + 'import_csv' => 'CSV importieren', + 'download_csv_template' => 'CSV‑Vorlage herunterladen', + 'csv_file' => 'CSV‑Datei', + 'close' => 'Schließen', + 'hash' => '#', + 'slug' => 'Slug', + 'event' => 'Veranstaltung', + 'tenant' => 'Mandant', + 'uploads' => 'Uploads', + 'uploads_today' => 'Uploads heute', + 'thumb' => 'Vorschau', + 'likes' => 'Gefällt mir', + 'emotion' => 'Emotion', + 'event_type' => 'Eventtyp', + 'last_activity' => 'Letzte Aktivität', + 'credits' => 'Credits', + 'settings' => 'Einstellungen', + 'join' => 'Beitreten', + 'unnamed' => 'Ohne Namen', + ], + + 'photos' => [ + 'fields' => [ + 'event' => 'Veranstaltung', + 'photo' => 'Foto', + 'is_featured' => 'Hervorgehoben', + 'metadata' => 'Metadaten', + 'likes' => 'Gefällt mir', + ], + 'actions' => [ + 'feature' => 'Hervorheben', + 'unfeature' => 'Hervorhebung entfernen', + 'feature_selected' => 'Auswahl hervorheben', + 'unfeature_selected' => 'Hervorhebung der Auswahl entfernen', + ], + 'table' => [ + 'photo' => 'Foto', + 'event' => 'Veranstaltung', + 'likes' => 'Gefällt mir', + ], + ], + + 'events' => [ + 'fields' => [ + 'tenant' => 'Mandant', + 'name' => 'Eventname', + 'slug' => 'Slug', + 'date' => 'Eventdatum', + 'type' => 'Eventtyp', + 'default_locale' => 'Standardsprache', + 'is_active' => 'Aktiv', + 'settings' => 'Einstellungen', + ], + 'table' => [ + 'tenant' => 'Mandant', + 'join' => 'Beitreten', + ], + 'actions' => [ + 'toggle_active' => 'Aktiv umschalten', + 'join_link_qr' => 'Beitrittslink / QR', + ], + 'modal' => [ + 'join_link_heading' => 'Beitrittslink der Veranstaltung', + ], + 'messages' => [ + 'join_link_copied' => 'Beitrittslink kopiert', + ], + 'join_link' => [ + 'link_label' => 'Beitrittslink', + 'qr_code_label' => 'QR‑Code', + 'note_html' => 'Hinweis: Der QR‑Code wird über einen externen QR‑Service generiert. Für eine selbst gehostete Lösung können wir später eine interne QR‑Generierung ergänzen.', + ], + ], + + 'legal_pages' => [ + 'fields' => [ + 'slug' => 'Slug', + 'title_localized' => 'Titel (de/en)', + 'content_localization' => 'Inhaltslokalisierung', + 'content_de' => 'Inhalt (Deutsch)', + 'content_en' => 'Inhalt (Englisch)', + 'is_published' => 'Veröffentlicht', + 'effective_from' => 'Gültig ab', + 'version' => 'Version', + ], + ], + + 'emotions' => [ + 'sections' => [ + 'content_localization' => 'Inhaltslokalisierung', + ], + 'fields' => [ + 'name_de' => 'Name (Deutsch)', + 'description_de' => 'Beschreibung (Deutsch)', + 'name_en' => 'Name (Englisch)', + 'description_en' => 'Beschreibung (Englisch)', + 'icon_emoji' => 'Icon/Emoji', + 'color' => 'Farbe', + 'sort_order' => 'Sortierreihenfolge', + 'is_active' => 'Aktiv', + 'event_types' => 'Eventtypen', + ], + 'table' => [ + 'name' => 'Name', + 'icon' => 'Icon', + 'color' => 'Farbe', + 'is_active' => 'Aktiv', + 'sort_order' => 'Sortierung', + ], + 'import' => [ + 'heading' => 'Emotionen importieren (CSV)', + ], + ], + + 'event_types' => [ + 'sections' => [ + 'name_localization' => 'Namenslokalisierung', + ], + 'fields' => [ + 'name_de' => 'Name (Deutsch)', + 'name_en' => 'Name (Englisch)', + 'slug' => 'Slug', + 'icon' => 'Icon', + 'settings' => 'Einstellungen', + 'emotions' => 'Emotionen', + ], + 'table' => [ + 'name' => 'Name', + 'slug' => 'Slug', + 'icon' => 'Icon', + 'created_at' => 'Erstellt', + ], + ], + + 'tasks' => [ + 'menu' => 'Aufgaben', + 'fields' => [ + 'event_type_optional' => 'Eventtyp (optional)', + 'content_localization' => 'Inhaltslokalisierung', + 'title_de' => 'Titel (Deutsch)', + 'description_de' => 'Beschreibung (Deutsch)', + 'example_de' => 'Beispieltext (Deutsch)', + 'title_en' => 'Titel (Englisch)', + 'description_en' => 'Beschreibung (Englisch)', + 'example_en' => 'Beispieltext (Englisch)', + 'emotion' => 'Emotion', + 'event_type' => 'Eventtyp', + 'difficulty' => [ + 'label' => 'Schwierigkeit', + 'easy' => 'Leicht', + 'medium' => 'Mittel', + 'hard' => 'Schwer', + ], + ], + 'table' => [ + 'title' => 'Titel', + 'is_active' => 'Aktiv', + 'sort_order' => 'Sortierung', + ], + 'table' => [ + 'name' => 'Name', + 'icon' => 'Icon', + 'color' => 'Farbe', + 'is_active' => 'Aktiv', + 'sort_order' => 'Sortierung', + ], + 'import' => [ + 'heading' => 'Aufgaben importieren (CSV)', + ], + ], + + 'widgets' => [ + 'events_active_today' => [ + 'heading' => 'Heute aktive Events', + ], + 'recent_uploads' => [ + 'heading' => 'Neueste Uploads', + ], + 'top_tenants_by_uploads' => [ + 'heading' => 'Top‑Mandanten nach Uploads', + ], + 'uploads_per_day' => [ + 'heading' => 'Uploads (14 Tage)', + ], + ], + + 'notifications' => [ + 'file_not_found' => 'Datei nicht gefunden', + 'imported_rows' => ':count Zeilen importiert', + 'failed_count' => ':count fehlgeschlagen', + ], + + 'tenants' => [ + 'fields' => [ + 'name' => 'Mandantenname', + 'slug' => 'Slug', + 'contact_email' => 'Kontakt‑E‑Mail', + 'event_credits_balance' => 'Event‑Credits‑Kontostand', + 'features' => 'Funktionen', + ], + ], + + 'shell' => [ + 'tenant_admin_title' => 'Tenant‑Admin', + ], +]; diff --git a/resources/lang/en/admin.php b/resources/lang/en/admin.php new file mode 100644 index 0000000..ee50f34 --- /dev/null +++ b/resources/lang/en/admin.php @@ -0,0 +1,209 @@ + [ + 'platform' => 'Platform', + 'library' => 'Library', + 'content' => 'Content', + ], + + 'common' => [ + 'key' => 'Key', + 'value' => 'Value', + 'locale' => 'Locale', + 'german' => 'German', + 'english' => 'English', + 'import' => 'Import', + 'import_csv' => 'Import CSV', + 'download_csv_template' => 'Download CSV Template', + 'csv_file' => 'CSV file', + 'close' => 'Close', + 'hash' => '#', + 'slug' => 'Slug', + 'event' => 'Event', + 'tenant' => 'Tenant', + 'uploads' => 'Uploads', + 'uploads_today' => 'Uploads today', + 'thumb' => 'Thumb', + 'likes' => 'Likes', + 'emotion' => 'Emotion', + 'event_type' => 'Event Type', + 'last_activity' => 'Last activity', + 'credits' => 'Credits', + 'settings' => 'Settings', + 'join' => 'Join', + 'unnamed' => 'Unnamed', + ], + + 'photos' => [ + 'fields' => [ + 'event' => 'Event', + 'photo' => 'Photo', + 'is_featured' => 'Is Featured', + 'metadata' => 'Metadata', + 'likes' => 'Likes', + ], + 'actions' => [ + 'feature' => 'Feature', + 'unfeature' => 'Unfeature', + 'feature_selected' => 'Feature selected', + 'unfeature_selected' => 'Unfeature selected', + ], + 'table' => [ + 'photo' => 'Photo', + 'event' => 'Event', + 'likes' => 'Likes', + ], + ], + + 'events' => [ + 'fields' => [ + 'tenant' => 'Tenant', + 'name' => 'Event Name', + 'slug' => 'Slug', + 'date' => 'Event Date', + 'type' => 'Event Type', + 'default_locale' => 'Default Locale', + 'is_active' => 'Is Active', + 'settings' => 'Settings', + ], + 'table' => [ + 'tenant' => 'Tenant', + 'join' => 'Join', + ], + 'actions' => [ + 'toggle_active' => 'Toggle Active', + 'join_link_qr' => 'Join Link / QR', + ], + 'modal' => [ + 'join_link_heading' => 'Event Join Link', + ], + 'messages' => [ + 'join_link_copied' => 'Join link copied', + ], + 'join_link' => [ + 'link_label' => 'Join Link', + 'qr_code_label' => 'QR Code', + 'note_html' => 'Note: The QR code is generated via an external QR service. For a self-hosted option, we can add internal generation later.', + ], + ], + + 'legal_pages' => [ + 'fields' => [ + 'slug' => 'Slug', + 'title_localized' => 'Title (de/en)', + 'content_localization' => 'Content Localization', + 'content_de' => 'Content (German)', + 'content_en' => 'Content (English)', + 'is_published' => 'Is Published', + 'effective_from' => 'Effective From', + 'version' => 'Version', + ], + ], + + 'emotions' => [ + 'sections' => [ + 'content_localization' => 'Content Localization', + ], + 'fields' => [ + 'name_de' => 'Name (German)', + 'description_de' => 'Description (German)', + 'name_en' => 'Name (English)', + 'description_en' => 'Description (English)', + 'icon_emoji' => 'Icon/Emoji', + 'color' => 'Color', + 'sort_order' => 'Sort Order', + 'is_active' => 'Is Active', + 'event_types' => 'Event Types', + ], + 'table' => [ + 'name' => 'Name', + 'icon' => 'Icon', + 'color' => 'Color', + 'is_active' => 'Active', + 'sort_order' => 'Sort Order', + ], + 'import' => [ + 'heading' => 'Import Emotions (CSV)', + ], + ], + + 'event_types' => [ + 'sections' => [ + 'name_localization' => 'Name Localization', + ], + 'fields' => [ + 'name_de' => 'Name (German)', + 'name_en' => 'Name (English)', + 'slug' => 'Slug', + 'icon' => 'Icon', + 'settings' => 'Settings', + 'emotions' => 'Emotions', + ], + ], + + 'tasks' => [ + 'menu' => 'Tasks', + 'fields' => [ + 'event_type_optional' => 'Event Type (optional)', + 'content_localization' => 'Content Localization', + 'title_de' => 'Title (German)', + 'description_de' => 'Description (German)', + 'example_de' => 'Example Text (German)', + 'title_en' => 'Title (English)', + 'description_en' => 'Description (English)', + 'example_en' => 'Example Text (English)', + 'emotion' => 'Emotion', + 'event_type' => 'Event Type', + 'difficulty' => [ + 'label' => 'Difficulty', + 'easy' => 'Easy', + 'medium' => 'Medium', + 'hard' => 'Hard', + ], + ], + 'table' => [ + 'title' => 'Title', + 'is_active' => 'Active', + 'sort_order' => 'Sort Order', + ], + 'import' => [ + 'heading' => 'Import Tasks (CSV)', + ], + ], + + 'widgets' => [ + 'events_active_today' => [ + 'heading' => 'Events active today', + ], + 'recent_uploads' => [ + 'heading' => 'Recent uploads', + ], + 'top_tenants_by_uploads' => [ + 'heading' => 'Top tenants by uploads', + ], + 'uploads_per_day' => [ + 'heading' => 'Uploads (14 days)', + ], + ], + + 'notifications' => [ + 'file_not_found' => 'File not found', + 'imported_rows' => 'Imported :count rows', + 'failed_count' => ':count failed', + ], + + 'tenants' => [ + 'fields' => [ + 'name' => 'Tenant Name', + 'slug' => 'Slug', + 'contact_email' => 'Contact Email', + 'event_credits_balance' => 'Event Credits Balance', + 'features' => 'Features', + ], + ], + + 'shell' => [ + 'tenant_admin_title' => 'Tenant Admin', + ], +]; diff --git a/resources/views/admin.blade.php b/resources/views/admin.blade.php index cabb7e2..cae8e63 100644 --- a/resources/views/admin.blade.php +++ b/resources/views/admin.blade.php @@ -4,11 +4,10 @@ - Tenant Admin + {{ __('admin.shell.tenant_admin_title') }} @vite('resources/js/admin/main.tsx')

- diff --git a/resources/views/filament/events/join-link.blade.php b/resources/views/filament/events/join-link.blade.php index 067dd11..37cc21e 100644 --- a/resources/views/filament/events/join-link.blade.php +++ b/resources/views/filament/events/join-link.blade.php @@ -1,15 +1,16 @@
-
Join Link
+
{{ __('admin.events.join_link.link_label') }}
-
QR Code
+
{{ __('admin.events.join_link.qr_code_label') }}
{!! \SimpleSoftwareIO\QrCode\Facades\QrCode::size(300)->generate($link) !!}
- Hinweis: Der QR-Code wird über einen externen QR-Service generiert. Für eine selbst gehostete Lösung können wir später eine interne QR-Generierung ergänzen. + {!! __('admin.events.join_link.note_html') !!}
+ diff --git a/resources/views/filament/resources/emotion-resource/pages/import-emotions.blade.php b/resources/views/filament/resources/emotion-resource/pages/import-emotions.blade.php index 67dd9bd..f36ed43 100644 --- a/resources/views/filament/resources/emotion-resource/pages/import-emotions.blade.php +++ b/resources/views/filament/resources/emotion-resource/pages/import-emotions.blade.php @@ -8,7 +8,7 @@ ]" /> - Import + {{ __('admin.common.import') }} diff --git a/resources/views/filament/resources/task-resource/pages/import-tasks.blade.php b/resources/views/filament/resources/task-resource/pages/import-tasks.blade.php index 67dd9bd..f36ed43 100644 --- a/resources/views/filament/resources/task-resource/pages/import-tasks.blade.php +++ b/resources/views/filament/resources/task-resource/pages/import-tasks.blade.php @@ -8,7 +8,7 @@ ]" /> - Import + {{ __('admin.common.import') }} diff --git a/resources/views/guest.blade.php b/resources/views/guest.blade.php index 771ba57..7a1b044 100644 --- a/resources/views/guest.blade.php +++ b/resources/views/guest.blade.php @@ -3,11 +3,10 @@ - Fotospiel + {{ config('app.name', 'Fotospiel') }} @vite('resources/js/guest/main.tsx')
-