Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 5 additions & 1 deletion app/Ai/Agents/BrandAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace App\Ai\Agents;

use App\Enums\Workspace\BrandVoiceTrait;
use App\Enums\Workspace\ContentLanguage;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
Expand All @@ -20,6 +21,9 @@ public function instructions(): string
return view('prompts.brand_analyzer', [
'voice_groups' => BrandVoiceTrait::grouped(),
'single_select_groups' => BrandVoiceTrait::singleSelectGroups(),
'content_languages' => collect(ContentLanguage::values())
->map(fn (string $code) => "`{$code}`")
->implode(', '),
])->render();
}

Expand Down Expand Up @@ -47,7 +51,7 @@ public function schema(JsonSchema $schema): array
->description('A concise 2-3 sentence brand description summarizing what the company does, who they serve, and what makes them unique. Written in the detected content language.')
->required(),
'language' => $schema->string()
->enum(['en', 'pt-BR', 'es'])
->enum(ContentLanguage::values())
->description('The primary language of the content.')
->required(),
'brand_color' => $schema->string()
Expand Down
146 changes: 146 additions & 0 deletions app/Enums/Workspace/ContentLanguage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php

declare(strict_types=1);

namespace App\Enums\Workspace;

/**
* The set of languages the app supports, and the single source of truth for it:
* request validation, the brand analyzer's structured-output enum, homepage
* language detection, the AI image prompt's language name, the content-language
* picker options, and the right-to-left direction of the UI all derive from it.
*
* The string value is the language code stored on the workspace and passed
* straight to the content prompts (`content_language`); the same codes also back
* the application's UI locales, so `direction()` drives the document `dir` attribute.
*/
enum ContentLanguage: string
{
case English = 'en';
case PortugueseBrazil = 'pt-BR';
case Spanish = 'es';
case French = 'fr';
case German = 'de';
case Italian = 'it';
case Dutch = 'nl';
case Polish = 'pl';
case Greek = 'el';
case Japanese = 'ja';
case Korean = 'ko';
case Chinese = 'zh';
case Russian = 'ru';
case Turkish = 'tr';
case Arabic = 'ar';

public const DEFAULT = self::English;

/**
* The language's own name, shown in the content-language picker.
*/
public function label(): string
{
return match ($this) {
self::English => 'English',
self::PortugueseBrazil => 'Português (Brasil)',
self::Spanish => 'Español',
self::French => 'Français',
self::German => 'Deutsch',
self::Italian => 'Italiano',
self::Dutch => 'Nederlands',
self::Polish => 'Polski',
self::Greek => 'Ελληνικά',
self::Japanese => '日本語',
self::Korean => '한국어',
self::Chinese => '中文',
self::Russian => 'Русский',
self::Turkish => 'Türkçe',
self::Arabic => 'العربية',
};
}

/**
* The English name of the language, injected into AI image prompts so the
* in-image text is rendered in the workspace's content language.
*/
public function englishName(): string
{
return match ($this) {
self::English => 'English',
self::PortugueseBrazil => 'Brazilian Portuguese',
self::Spanish => 'Spanish',
self::French => 'French',
self::German => 'German',
self::Italian => 'Italian',
self::Dutch => 'Dutch',
self::Polish => 'Polish',
self::Greek => 'Greek',
self::Japanese => 'Japanese',
self::Korean => 'Korean',
self::Chinese => 'Chinese',
self::Russian => 'Russian',
self::Turkish => 'Turkish',
self::Arabic => 'Arabic',
};
}

/**
* The text direction (`ltr` or `rtl`) for the document root when this is the
* active UI locale — only Arabic is written right to left.
*/
public function direction(): string
{
return $this === self::Arabic ? 'rtl' : 'ltr';
}

/**
* Resolve a raw `<html lang>` value (e.g. "pt-PT", "zh-Hans") to a supported
* language by matching its primary subtag, or null if none is supported.
*/
public static function fromHtmlLang(string $lang): ?self
{
$subtag = self::primarySubtag($lang);

if (strlen($subtag) < 2) {
return null;
}

foreach (self::cases() as $language) {
if (self::primarySubtag($language->value) === $subtag) {
return $language;
}
}

return null;
}

/**
* The lowercased primary subtag of a BCP 47 language tag ("pt-BR" => "pt").
*/
private static function primarySubtag(string $tag): string
{
return strtolower(explode('-', trim($tag), 2)[0]);
}

/**
* @return array<int, string>
*/
public static function values(): array
{
return array_map(fn (self $language) => $language->value, self::cases());
}

/**
* @return array<int, array{value: string, label: string, englishName: string}>
*/
public static function options(): array
{
return array_map(
fn (self $language) => [
'value' => $language->value,
'label' => $language->label(),
'englishName' => $language->englishName(),
],
self::cases(),
);
}
}
3 changes: 3 additions & 0 deletions app/Http/Controllers/App/WorkspaceController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use App\Actions\Workspace\DeleteWorkspace;
use App\Enums\Workspace\BrandFont;
use App\Enums\Workspace\BrandVoiceTrait;
use App\Enums\Workspace\ContentLanguage;
use App\Enums\Workspace\ImageStyle;
use App\Http\Requests\App\Workspace\AutofillBrandRequest;
use App\Http\Requests\App\Workspace\StoreWorkspaceRequest;
Expand Down Expand Up @@ -80,6 +81,7 @@ public function create(Request $request): Response|RedirectResponse
'availableFonts' => BrandFont::values(),
'availableImageStyles' => ImageStyle::values(),
'availableVoiceTraits' => BrandVoiceTrait::grouped(),
'availableContentLanguages' => ContentLanguage::options(),
]);
}

Expand Down Expand Up @@ -163,6 +165,7 @@ public function brandSettings(Request $request): Response|RedirectResponse
'availableFonts' => BrandFont::values(),
'availableImageStyles' => ImageStyle::values(),
'availableVoiceTraits' => BrandVoiceTrait::grouped(),
'availableContentLanguages' => ContentLanguage::options(),
]);
}

Expand Down
7 changes: 6 additions & 1 deletion app/Http/Middleware/App/SetLocale.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

namespace App\Http\Middleware\App;

use App\Enums\Workspace\ContentLanguage;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\View;
use Symfony\Component\HttpFoundation\Response;

class SetLocale
Expand All @@ -19,8 +21,11 @@ public function handle(Request $request, Closure $next): Response
$available = config('languages.available');
$locale = $request->cookie('locale');
$isValid = $locale && array_key_exists($locale, $available);
$activeLocale = $isValid ? $locale : config('languages.default');
$language = ContentLanguage::tryFrom($activeLocale) ?? ContentLanguage::DEFAULT;

App::setLocale($isValid ? $locale : config('languages.default'));
App::setLocale($activeLocale);
View::share('htmlDir', $language->direction());

$response = $next($request);

Expand Down
3 changes: 2 additions & 1 deletion app/Http/Requests/App/Workspace/StoreWorkspaceRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use App\Enums\Workspace\BrandFont;
use App\Enums\Workspace\BrandVoiceTrait;
use App\Enums\Workspace\ContentLanguage;
use App\Enums\Workspace\ImageStyle;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
Expand All @@ -32,7 +33,7 @@ public function rules(): array
'text_color' => $hex,
'brand_font' => ['sometimes', 'string', Rule::in(BrandFont::values())],
'image_style' => ['sometimes', 'string', Rule::in(ImageStyle::values())],
'content_language' => ['nullable', 'string', 'in:en,pt-BR,es'],
'content_language' => ['nullable', 'string', Rule::in(ContentLanguage::values())],
'logo_url' => ['nullable', 'url', 'max:1024'],
];
}
Expand Down
3 changes: 2 additions & 1 deletion app/Http/Requests/App/Workspace/UpdateWorkspaceRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use App\Enums\Workspace\BrandFont;
use App\Enums\Workspace\BrandVoiceTrait;
use App\Enums\Workspace\ContentLanguage;
use App\Enums\Workspace\ImageStyle;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
Expand All @@ -32,7 +33,7 @@ public function rules(): array
'text_color' => $hex,
'brand_font' => ['sometimes', 'required', 'string', Rule::in(BrandFont::values())],
'image_style' => ['sometimes', 'required', 'string', Rule::in(ImageStyle::values())],
'content_language' => ['sometimes', 'string', 'in:en,pt-BR,es'],
'content_language' => ['sometimes', 'string', Rule::in(ContentLanguage::values())],
];
}

Expand Down
7 changes: 2 additions & 5 deletions app/Services/Ai/AiImageClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Services\Ai;

use App\Enums\Workspace\ContentLanguage;
use App\Enums\Workspace\ImageStyle;
use App\Support\HexColorName;
use Illuminate\Support\Facades\Log;
Expand Down Expand Up @@ -89,11 +90,7 @@ public function generate(

private function languageName(string $code): string
{
return match ($code) {
'pt-BR' => 'Brazilian Portuguese',
'es' => 'Spanish',
default => 'English',
};
return (ContentLanguage::tryFrom($code) ?? ContentLanguage::DEFAULT)->englishName();
}

/**
Expand Down
10 changes: 2 additions & 8 deletions app/Services/Brand/HomepageMetaExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Services\Brand;

use App\Enums\Workspace\ContentLanguage;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\DomCrawler\UriResolver;

Expand Down Expand Up @@ -291,14 +292,7 @@ private function extractLanguage(Crawler $crawler): ?string
return null;
}

$lower = strtolower($lang);

return match (true) {
str_starts_with($lower, 'pt') => 'pt-BR',
str_starts_with($lower, 'es') => 'es',
str_starts_with($lower, 'en') => 'en',
default => null,
};
return ContentLanguage::fromHtmlLang($lang)?->value;
}

private function extractLogoUrl(Crawler $crawler, string $baseUrl): ?string
Expand Down
12 changes: 12 additions & 0 deletions config/languages.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@
'en' => 'English',
'es' => 'Español',
'pt-BR' => 'Português',
'fr' => 'Français',
'de' => 'Deutsch',
'it' => 'Italiano',
'nl' => 'Nederlands',
'pl' => 'Polski',
'el' => 'Ελληνικά',
'ja' => '日本語',
'ko' => '한국어',
'zh' => '中文',
'ru' => 'Русский',
'tr' => 'Türkçe',
'ar' => 'العربية',
],

/*
Expand Down
Loading
Loading