Centralize supported content languages in a ContentLanguage enum
The new content-language options were hand-duplicated across request validation, the UI picker, and homepage detection, while the brand analyzer's structured-output enum and the AI image prompt's language name still only knew about en/pt-BR/es. That left autofill unable to detect the new languages and made image text fall back to English for them. Introduce App\Enums\Workspace\ContentLanguage as the single source of truth and derive every site from it: - Store/UpdateWorkspaceRequest validate against ContentLanguage::values() - BrandAnalyzer's language enum uses ContentLanguage::values() - AiImageClient::languageName() resolves via the enum's englishName() - HomepageMetaExtractor detects through ContentLanguage::fromHtmlLang() - BrandForm consumes availableContentLanguages from the backend, like availableFonts/availableImageStyles, instead of a hardcoded list Also fix two labels: nl "Nederlandse" -> "Nederlands", zh -> "中文".
This commit is contained in:
parent
eb0ec6fb24
commit
f7ef13c116
15 changed files with 236 additions and 66 deletions
|
|
@ -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;
|
||||
|
|
@ -47,7 +48,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()
|
||||
|
|
|
|||
125
app/Enums/Workspace/ContentLanguage.php
Normal file
125
app/Enums/Workspace/ContentLanguage.php
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\Workspace;
|
||||
|
||||
/**
|
||||
* Languages a workspace can pick for AI-generated content. This is the single
|
||||
* source of truth for the supported set: request validation, the brand
|
||||
* analyzer's structured-output enum, homepage language detection, the AI image
|
||||
* prompt's language name, and the picker options in 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`). This is unrelated to
|
||||
* the application's UI locale — we are not translating the interface here.
|
||||
*/
|
||||
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',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a raw `<html lang>` value (e.g. "pt-PT", "en-US") to a supported
|
||||
* language by matching its two-letter primary subtag, or null if none fits.
|
||||
*/
|
||||
public static function fromHtmlLang(string $lang): ?self
|
||||
{
|
||||
$prefix = strtolower(substr(trim($lang), 0, 2));
|
||||
|
||||
if (strlen($prefix) < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (self::cases() as $language) {
|
||||
if (str_starts_with($language->value, $prefix)) {
|
||||
return $language;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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}>
|
||||
*/
|
||||
public static function options(): array
|
||||
{
|
||||
return array_map(
|
||||
fn (self $language) => ['value' => $language->value, 'label' => $language->label()],
|
||||
self::cases(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -80,6 +81,7 @@ public function create(Request $request): Response|RedirectResponse
|
|||
'availableFonts' => BrandFont::values(),
|
||||
'availableImageStyles' => ImageStyle::values(),
|
||||
'availableVoiceTraits' => BrandVoiceTrait::grouped(),
|
||||
'availableContentLanguages' => ContentLanguage::options(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -163,6 +165,7 @@ public function brandSettings(Request $request): Response|RedirectResponse
|
|||
'availableFonts' => BrandFont::values(),
|
||||
'availableImageStyles' => ImageStyle::values(),
|
||||
'availableVoiceTraits' => BrandVoiceTrait::grouped(),
|
||||
'availableContentLanguages' => ContentLanguage::options(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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,fr,de,it,nl,pl,el,ja,ko,zh,ru,tr,ar'],
|
||||
'content_language' => ['nullable', 'string', Rule::in(ContentLanguage::values())],
|
||||
'logo_url' => ['nullable', 'url', 'max:1024'],
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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,fr,de,it,nl,pl,el,ja,ko,zh,ru,tr,ar'],
|
||||
'content_language' => ['sometimes', 'string', Rule::in(ContentLanguage::values())],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
namespace App\Services\Brand;
|
||||
|
||||
use App\Enums\Workspace\ContentLanguage;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\DomCrawler\UriResolver;
|
||||
|
||||
|
|
@ -291,26 +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',
|
||||
str_starts_with($lower, 'fr') => 'fr',
|
||||
str_starts_with($lower, 'de') => 'de',
|
||||
str_starts_with($lower, 'it') => 'it',
|
||||
str_starts_with($lower, 'nl') => 'nl',
|
||||
str_starts_with($lower, 'pl') => 'pl',
|
||||
str_starts_with($lower, 'el') => 'el',
|
||||
str_starts_with($lower, 'ja') => 'ja',
|
||||
str_starts_with($lower, 'ko') => 'ko',
|
||||
str_starts_with($lower, 'zh') => 'zh',
|
||||
str_starts_with($lower, 'ru') => 'ru',
|
||||
str_starts_with($lower, 'tr') => 'tr',
|
||||
str_starts_with($lower, 'ar') => 'ar',
|
||||
default => null,
|
||||
};
|
||||
return ContentLanguage::fromHtmlLang($lang)?->value;
|
||||
}
|
||||
|
||||
private function extractLogoUrl(Crawler $crawler, string $baseUrl): ?string
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ const props = withDefaults(
|
|||
availableFonts: string[];
|
||||
availableImageStyles: string[];
|
||||
availableVoiceTraits: Record<string, string[]>;
|
||||
availableContentLanguages: { value: string; label: string }[];
|
||||
autofill?: boolean;
|
||||
showName?: boolean;
|
||||
}>(),
|
||||
|
|
@ -95,26 +96,9 @@ const toggleTrait = (group: string, value: string): void => {
|
|||
props.fields.brand_voice_traits = [...current.filter((v) => !groupValues.includes(v)), value];
|
||||
};
|
||||
|
||||
const languageLabel = computed(() => {
|
||||
const map: Record<string, string> = {
|
||||
en: 'English',
|
||||
'pt-BR': 'Português (Brasil)',
|
||||
es: 'Español',
|
||||
fr: 'Français',
|
||||
de: 'Deutsch',
|
||||
it: 'Italiano',
|
||||
nl: 'Nederlandse',
|
||||
pl: 'Polski',
|
||||
el: 'Ελληνικά',
|
||||
ja: '日本語',
|
||||
ko: '한국어',
|
||||
zh: '现代标准汉语',
|
||||
ru: 'Русский',
|
||||
tr: 'Türkçe',
|
||||
ar: 'العربية',
|
||||
};
|
||||
return map[props.fields.content_language] ?? '';
|
||||
});
|
||||
const languageLabel = computed(
|
||||
() => props.availableContentLanguages.find((language) => language.value === props.fields.content_language)?.label ?? '',
|
||||
);
|
||||
|
||||
const runAutofill = async () => {
|
||||
const url = props.fields.brand_website?.trim() ?? '';
|
||||
|
|
@ -209,21 +193,9 @@ const runAutofill = async () => {
|
|||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
<SelectItem value="pt-BR">Português (Brasil)</SelectItem>
|
||||
<SelectItem value="es">Español</SelectItem>
|
||||
<SelectItem value="fr">Français</SelectItem>
|
||||
<SelectItem value="de">Deutsch</SelectItem>
|
||||
<SelectItem value="it">Italiano</SelectItem>
|
||||
<SelectItem value="nl">Nederlandse</SelectItem>
|
||||
<SelectItem value="pl">Polski</SelectItem>
|
||||
<SelectItem value="el">Ελληνικά</SelectItem>
|
||||
<SelectItem value="ja">日本語</SelectItem>
|
||||
<SelectItem value="ko">한국어</SelectItem>
|
||||
<SelectItem value="zh">现代标准汉语</SelectItem>
|
||||
<SelectItem value="ru">Русский</SelectItem>
|
||||
<SelectItem value="tr">Türkçe</SelectItem>
|
||||
<SelectItem value="ar">العربية</SelectItem>
|
||||
<SelectItem v-for="language in availableContentLanguages" :key="language.value" :value="language.value">
|
||||
{{ language.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p class="text-xs font-medium text-foreground/60">
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ const props = defineProps<{
|
|||
availableFonts: string[];
|
||||
availableImageStyles: string[];
|
||||
availableVoiceTraits: Record<string, string[]>;
|
||||
availableContentLanguages: { value: string; label: string }[];
|
||||
}>();
|
||||
|
||||
const form = useForm({
|
||||
|
|
@ -58,6 +59,7 @@ const submit = () => {
|
|||
:available-fonts="availableFonts"
|
||||
:available-image-styles="availableImageStyles"
|
||||
:available-voice-traits="availableVoiceTraits"
|
||||
:available-content-languages="availableContentLanguages"
|
||||
:autofill="!workspace.brand_website"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ defineProps<{
|
|||
availableFonts: string[];
|
||||
availableImageStyles: string[];
|
||||
availableVoiceTraits: Record<string, string[]>;
|
||||
availableContentLanguages: { value: string; label: string }[];
|
||||
}>();
|
||||
|
||||
const tabs = computed(() => [
|
||||
|
|
@ -57,6 +58,7 @@ const tabs = computed(() => [
|
|||
:available-fonts="availableFonts"
|
||||
:available-image-styles="availableImageStyles"
|
||||
:available-voice-traits="availableVoiceTraits"
|
||||
:available-content-languages="availableContentLanguages"
|
||||
/>
|
||||
</div>
|
||||
</AppLayout>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ defineProps<{
|
|||
availableFonts: string[];
|
||||
availableImageStyles: string[];
|
||||
availableVoiceTraits: Record<string, string[]>;
|
||||
availableContentLanguages: { value: string; label: string }[];
|
||||
}>();
|
||||
|
||||
const form = useForm({
|
||||
|
|
@ -45,6 +46,7 @@ const submit = () => {
|
|||
:available-fonts="availableFonts"
|
||||
:available-image-styles="availableImageStyles"
|
||||
:available-voice-traits="availableVoiceTraits"
|
||||
:available-content-languages="availableContentLanguages"
|
||||
:autofill="true"
|
||||
:show-name="true"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -104,8 +104,10 @@
|
|||
['pt-PT', 'pt-BR'],
|
||||
['en-US', 'en'],
|
||||
['es-MX', 'es'],
|
||||
['fr', null],
|
||||
['ja-JP', null],
|
||||
['fr', 'fr'],
|
||||
['ja-JP', 'ja'],
|
||||
['zh-Hans', 'zh'],
|
||||
['sv', null],
|
||||
]);
|
||||
|
||||
test('rejects non-http schemes', function () {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use App\Ai\Agents\BrandAnalyzer;
|
||||
use App\Enums\UserWorkspace\Role;
|
||||
use App\Enums\Workspace\ContentLanguage;
|
||||
use App\Models\Account;
|
||||
use App\Models\AiUsageLog;
|
||||
use App\Models\User;
|
||||
|
|
@ -59,6 +60,8 @@
|
|||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('workspaces/Create', false)
|
||||
->has('availableContentLanguages', count(ContentLanguage::cases()))
|
||||
->where('availableContentLanguages.0', ['value' => 'en', 'label' => 'English'])
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -187,6 +190,7 @@
|
|||
$response->assertInertia(fn ($page) => $page
|
||||
->component('settings/workspace/Brand', false)
|
||||
->has('workspace')
|
||||
->has('availableContentLanguages', count(ContentLanguage::cases()))
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -288,6 +292,26 @@
|
|||
])->assertSessionHasErrors(['image_style']);
|
||||
});
|
||||
|
||||
test('update workspace settings persists a newly supported content language', function () {
|
||||
$this->actingAs($this->user)
|
||||
->from(route('app.workspace.brand'))
|
||||
->put(route('app.workspace.settings.update'), [
|
||||
'name' => $this->workspace->name,
|
||||
'content_language' => 'fr',
|
||||
])->assertRedirect(route('app.workspace.brand'))
|
||||
->assertSessionHasNoErrors();
|
||||
|
||||
expect($this->workspace->refresh()->content_language)->toBe('fr');
|
||||
});
|
||||
|
||||
test('update workspace settings rejects an unsupported content language', function () {
|
||||
$this->actingAs($this->user)
|
||||
->put(route('app.workspace.settings.update'), [
|
||||
'name' => $this->workspace->name,
|
||||
'content_language' => 'sv',
|
||||
])->assertSessionHasErrors(['content_language']);
|
||||
});
|
||||
|
||||
test('update workspace settings validates required fields', function () {
|
||||
$response = $this->actingAs($this->user)->put(route('app.workspace.settings.update'), [
|
||||
'name' => '',
|
||||
|
|
|
|||
47
tests/Unit/Enums/Workspace/ContentLanguageTest.php
Normal file
47
tests/Unit/Enums/Workspace/ContentLanguageTest.php
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Enums\Workspace\ContentLanguage;
|
||||
|
||||
test('values exposes every supported content-language code', function () {
|
||||
expect(ContentLanguage::values())->toBe([
|
||||
'en', 'pt-BR', 'es', 'fr', 'de', 'it', 'nl',
|
||||
'pl', 'el', 'ja', 'ko', 'zh', 'ru', 'tr', 'ar',
|
||||
]);
|
||||
});
|
||||
|
||||
test('default language is English', function () {
|
||||
expect(ContentLanguage::DEFAULT)->toBe(ContentLanguage::English);
|
||||
expect(ContentLanguage::DEFAULT->value)->toBe('en');
|
||||
});
|
||||
|
||||
test('options pairs each code with its native label', function () {
|
||||
$options = ContentLanguage::options();
|
||||
|
||||
expect($options)->toHaveCount(count(ContentLanguage::cases()));
|
||||
expect($options[0])->toBe(['value' => 'en', 'label' => 'English']);
|
||||
expect($options)->toContain(['value' => 'pt-BR', 'label' => 'Português (Brasil)']);
|
||||
expect($options)->toContain(['value' => 'ja', 'label' => '日本語']);
|
||||
});
|
||||
|
||||
test('english name is the language name in English for the AI image prompt', function () {
|
||||
expect(ContentLanguage::PortugueseBrazil->englishName())->toBe('Brazilian Portuguese');
|
||||
expect(ContentLanguage::French->englishName())->toBe('French');
|
||||
expect(ContentLanguage::Chinese->englishName())->toBe('Chinese');
|
||||
});
|
||||
|
||||
test('fromHtmlLang resolves the two-letter primary subtag', function (string $lang, ?ContentLanguage $expected) {
|
||||
expect(ContentLanguage::fromHtmlLang($lang))->toBe($expected);
|
||||
})->with([
|
||||
['pt', ContentLanguage::PortugueseBrazil],
|
||||
['pt-PT', ContentLanguage::PortugueseBrazil],
|
||||
['en-US', ContentLanguage::English],
|
||||
['es-MX', ContentLanguage::Spanish],
|
||||
['fr', ContentLanguage::French],
|
||||
['ja-JP', ContentLanguage::Japanese],
|
||||
['zh-Hans', ContentLanguage::Chinese],
|
||||
['sv', null],
|
||||
['e', null],
|
||||
['', null],
|
||||
]);
|
||||
|
|
@ -81,12 +81,21 @@
|
|||
Image::assertGenerated(fn (ImagePrompt $prompt) => $prompt->contains('Spanish'));
|
||||
});
|
||||
|
||||
test('generate defaults to English instruction when language is unknown', function () {
|
||||
test('generate appends French instruction when language is fr', function () {
|
||||
Image::fake();
|
||||
|
||||
$client = new AiImageClient;
|
||||
$client->generate(['x'], ImageStyle::Cinematic, language: 'fr');
|
||||
|
||||
Image::assertGenerated(fn (ImagePrompt $prompt) => $prompt->contains('French'));
|
||||
});
|
||||
|
||||
test('generate defaults to English instruction when language is unsupported', function () {
|
||||
Image::fake();
|
||||
|
||||
$client = new AiImageClient;
|
||||
$client->generate(['x'], ImageStyle::Cinematic, language: 'sv');
|
||||
|
||||
Image::assertGenerated(fn (ImagePrompt $prompt) => $prompt->contains('English'));
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue