Created
June 1, 2026 19:55
-
-
Save Megafry/6a69495fd45486669f632c66ce1d6771 to your computer and use it in GitHub Desktop.
Test: Registering a custom API provider for digitalpulsebe craft-multi-translator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| {% import '_includes/forms.twig' as forms %} | |
| {{ forms.autosuggestField({ | |
| label: 'Gemini API Key', | |
| name: 'apiKey', | |
| suggestEnvVars: true, | |
| value: settings.apiKey ?? '', | |
| }) }} | |
| {{ forms.selectField({ | |
| label: 'Model', | |
| name: 'model', | |
| value: settings.model ?? 'gemini-2.5-flash', | |
| options: modelOptions, | |
| }) }} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| declare(strict_types=1); | |
| namespace modules\translator\providers; | |
| use Craft; | |
| use craft\helpers\App; | |
| use digitalpulsebe\craftmultitranslator\providers\Provider; | |
| use digitalpulsebe\craftmultitranslator\MultiTranslator; | |
| use GeminiAPI\Client; | |
| use GeminiAPI\GenerationConfig; | |
| use GeminiAPI\Resources\Parts\TextPart; | |
| use Throwable; | |
| class GeminiProvider extends Provider | |
| { | |
| private const DEFAULT_MODEL = 'gemini-3.1-flash-lite'; | |
| private const MODEL_CACHE_TTL = 3600; | |
| private const MODELS_ENDPOINT = 'https://generativelanguage.googleapis.com/v1beta/models'; | |
| private ?Client $client = null; | |
| // ------------------------------------------------------------------------- | |
| // Identity | |
| // ------------------------------------------------------------------------- | |
| public static function getHandle(): string | |
| { | |
| return 'gemini'; | |
| } | |
| public static function getDisplayName(): string | |
| { | |
| return 'Google Gemini'; | |
| } | |
| // ------------------------------------------------------------------------- | |
| // Settings / templates | |
| // ------------------------------------------------------------------------- | |
| public function getSettingsTemplatePath(): ?string | |
| { | |
| return 'translator/gemini/_settings'; | |
| } | |
| public function getSettingsTemplateVariables(): array | |
| { | |
| return [ | |
| 'settings' => $this->settings, | |
| 'modelOptions' => $this->getAvailableModels() | |
| ]; | |
| } | |
| // ------------------------------------------------------------------------- | |
| // Connectivity | |
| // ------------------------------------------------------------------------- | |
| public function isConnected(): bool | |
| { | |
| return !empty($this->getApiKey()); | |
| } | |
| private function getApiKey(): string | |
| { | |
| return App::parseEnv($this->getSetting('apiKey', '')); | |
| } | |
| /** | |
| * Lazily creates and reuses a single Client instance per request. | |
| * | |
| * @throws \RuntimeException if no API key is configured | |
| */ | |
| private function getClient(): Client | |
| { | |
| if ($this->client !== null) { | |
| return $this->client; | |
| } | |
| $apiKey = $this->getApiKey(); | |
| if (empty($apiKey)) { | |
| throw new \RuntimeException('Gemini API key is not configured.'); | |
| } | |
| return $this->client = new Client($apiKey); | |
| } | |
| // ------------------------------------------------------------------------- | |
| // Model discovery | |
| // ------------------------------------------------------------------------- | |
| public function getAvailableModels(): array | |
| { | |
| $apiKey = $this->getApiKey(); | |
| if (empty($apiKey)) { | |
| return []; | |
| } | |
| $cacheKey = 'gemini_available_models_' . md5($apiKey); | |
| return Craft::$app->getCache()->getOrSet( | |
| $cacheKey, | |
| fn() => $this->fetchAvailableModels($apiKey), | |
| self::MODEL_CACHE_TTL | |
| ); | |
| } | |
| /** | |
| * Fetches and normalises the list of content-generation-capable models. | |
| */ | |
| private function fetchAvailableModels(string $apiKey): array | |
| { | |
| try { | |
| $response = Craft::createGuzzleClient()->get( | |
| self::MODELS_ENDPOINT, | |
| ['query' => ['key' => $apiKey]] | |
| ); | |
| $data = json_decode( | |
| (string) $response->getBody(), | |
| true, | |
| 512, | |
| JSON_THROW_ON_ERROR | |
| ); | |
| return $this->normaliseModelList($data['models'] ?? []); | |
| } catch (Throwable $e) { | |
| Craft::error( | |
| sprintf('Failed to fetch Gemini models: %s', $e->getMessage()), | |
| __METHOD__ | |
| ); | |
| return []; | |
| } | |
| } | |
| /** | |
| * Filters, shapes, and sorts raw model records from the API. | |
| * | |
| * @param array<int, array<string, mixed>> $models | |
| * @return array<int, array{label: string, value: string}> | |
| */ | |
| private function normaliseModelList(array $models): array | |
| { | |
| $options = []; | |
| foreach ($models as $model) { | |
| if (!in_array('generateContent', $model['supportedGenerationMethods'] ?? [], true)) { | |
| continue; | |
| } | |
| $name = str_replace('models/', '', $model['name'] ?? ''); | |
| $options[] = [ | |
| 'label' => $model['displayName'] ?? $name, | |
| 'value' => $name, | |
| ]; | |
| } | |
| usort($options, fn(array $a, array $b): int => strcmp($a['label'], $b['label'])); | |
| return $options; | |
| } | |
| // ------------------------------------------------------------------------- | |
| // Translation | |
| // ------------------------------------------------------------------------- | |
| public function translate( | |
| string $sourceLocale = null, | |
| string $targetLocale = null, | |
| string $text = null | |
| ): ?string { | |
| if (empty($text) || empty($sourceLocale) || empty($targetLocale)) { | |
| return null; | |
| } | |
| try { | |
| $response = $this->getClient() | |
| ->generativeModel($this->getModel()) | |
| ->generateContent( | |
| new TextPart('You are a professional translator. Return only the translated text, no explanations, notes, quotes, or markdown.'), | |
| new TextPart("Translate from {$sourceLocale} to {$targetLocale}:"), | |
| new TextPart($text) | |
| ); | |
| $result = trim($response->text()); | |
| return $result !== '' ? $result : null; | |
| } catch (\RuntimeException $e) { | |
| // Configuration error — surface clearly | |
| MultiTranslator::error( | |
| sprintf('Gemini configuration error: %s', $e->getMessage()), | |
| ); | |
| throw $e; | |
| } catch (Throwable $e) { | |
| MultiTranslator::error( | |
| sprintf( | |
| 'Gemini translation failed [%s → %s]: %s', | |
| $sourceLocale, | |
| $targetLocale, | |
| $e->getMessage() | |
| ) | |
| ); | |
| throw $e; | |
| } | |
| } | |
| // ------------------------------------------------------------------------- | |
| // Helpers | |
| // ------------------------------------------------------------------------- | |
| private function getModel(): string | |
| { | |
| return (string) ($this->getSetting('model') ?: self::DEFAULT_MODEL); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment