Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
19.05% covered (danger)
19.05%
20 / 105
25.00% covered (danger)
25.00%
2 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
GoogleApiService
19.05% covered (danger)
19.05%
20 / 105
25.00% covered (danger)
25.00%
2 / 8
254.95
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 translateStrings
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 translateContent
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
72
 translateArticle
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 translateTag
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 translateImageGallery
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 preprocessContent
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
2
 postprocessContent
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2declare(strict_types=1);
3
4namespace App\Service\Api\Google;
5
6use App\Utility\SettingsManager;
7use Exception;
8use Google\Cloud\Translate\V2\TranslateClient;
9use InvalidArgumentException;
10
11/**
12 * Service class for interacting with the Google Cloud Translate API.
13 */
14class GoogleApiService
15{
16    /**
17     * The Google Cloud Translate client instance.
18     *
19     * @var \Google\Cloud\Translate\V2\TranslateClient
20     */
21    private TranslateClient $translateClient;
22
23    /**
24     * @var array<string, string>
25     */
26    private array $preservedBlocks = [];
27
28    /**
29     * Constructor for the GoogleApiService class.
30     * Initializes the Google Cloud Translate client with the API key from the settings.
31     */
32    public function __construct()
33    {
34        $this->translateClient = new TranslateClient([
35            'key' => SettingsManager::read('Google.translateApiKey', env('TRANSLATE_API_KEY')),
36        ]);
37    }
38
39    /**
40     * Translates an array of strings from one language to another using batch translation.
41     *
42     * @param array $strings The array of strings to be translated.
43     * @param string $localeFrom The source language locale code (e.g., 'en' for English).
44     * @param string $localeTo The target language locale code (e.g., 'fr' for French).
45     * @return array An array containing the translated strings and their original values.
46     */
47    public function translateStrings(array $strings, string $localeFrom, string $localeTo): array
48    {
49        if (empty($strings)) {
50            return ['translations' => []];
51        }
52
53        // Google Translate API has batch size limits
54        if (count($strings) > 100) {
55            throw new InvalidArgumentException('Batch size exceeds Google API limit');
56        }
57
58        try {
59            $results = $this->translateClient->translateBatch($strings, [
60                'source' => $localeFrom,
61                'target' => $localeTo,
62            ]);
63
64            $translatedStrings = [];
65
66            foreach ($results as $result) {
67                $translatedStrings['translations'][] = [
68                    'original' => $result['input'],
69                    'translated' => $result['text'],
70                ];
71            }
72
73            return $translatedStrings;
74        } catch (Exception $e) {
75            throw new TranslationException('Translation failed: ' . $e->getMessage(), 0, $e);
76        }
77    }
78
79    /**
80     * Translates content fields into multiple languages using the Google Translate API.
81     *
82     * This is the core translation method that handles the common logic for all content types.
83     * It supports HTML content preprocessing for fields that may contain code blocks, videos,
84     * or galleries.
85     *
86     * @param array<string, string> $fields Associative array of field name => value to translate
87     * @param array<string> $htmlFields Field names that should be preprocessed for HTML content
88     * @param bool $useHtmlFormat Whether to use HTML format for translation (preserves HTML tags)
89     * @return array<string, array<string, string>> Translations keyed by locale, then field name
90     */
91    public function translateContent(array $fields, array $htmlFields = [], bool $useHtmlFormat = false): array
92    {
93        $locales = array_filter(SettingsManager::read('Translations', []));
94
95        if (empty($locales)) {
96            return [];
97        }
98
99        $this->preservedBlocks = [];
100
101        // Preprocess HTML fields
102        $processedFields = [];
103        foreach ($fields as $fieldName => $value) {
104            if (in_array($fieldName, $htmlFields, true)) {
105                $processedFields[$fieldName] = $this->preprocessContent($value);
106            } else {
107                $processedFields[$fieldName] = $value;
108            }
109        }
110
111        $fieldNames = array_keys($processedFields);
112        $fieldValues = array_values($processedFields);
113
114        $translations = [];
115        foreach ($locales as $locale => $enabled) {
116            $options = [
117                'source' => 'en',
118                'target' => $locale,
119            ];
120
121            if ($useHtmlFormat) {
122                $options['format'] = 'html';
123            }
124
125            $translationResult = $this->translateClient->translateBatch($fieldValues, $options);
126
127            foreach ($fieldNames as $index => $fieldName) {
128                $translatedText = $translationResult[$index]['text'];
129
130                // Postprocess HTML fields to restore preserved blocks
131                if (in_array($fieldName, $htmlFields, true)) {
132                    $translatedText = $this->postprocessContent($translatedText);
133                }
134
135                $translations[$locale][$fieldName] = $translatedText;
136            }
137        }
138
139        return $translations;
140    }
141
142    /**
143     * Translates an article into multiple languages using the Google Translate API.
144     *
145     * @param string $title The title of the article to be translated.
146     * @param string $lede The lede of the article to be translated.
147     * @param string $body The body of the article to be translated (can contain HTML).
148     * @param string $summary The summary of the article to be translated.
149     * @param string $meta_title The meta title of the article to be translated.
150     * @param string $meta_description The meta description of the article to be translated.
151     * @param string $meta_keywords The meta keywords of the article to be translated.
152     * @param string $facebook_description The Facebook description of the article to be translated.
153     * @param string $linkedin_description The LinkedIn description of the article to be translated.
154     * @param string $instagram_description The Instagram description of the article to be translated.
155     * @param string $twitter_description The Twitter description of the article to be translated.
156     * @return array An associative array where the keys are the locale codes and the values are arrays
157     *               containing the translated fields for each enabled locale.
158     */
159    public function translateArticle(
160        string $title,
161        string $lede,
162        string $body,
163        string $summary,
164        string $meta_title,
165        string $meta_description,
166        string $meta_keywords,
167        string $facebook_description,
168        string $linkedin_description,
169        string $instagram_description,
170        string $twitter_description,
171    ): array {
172        return $this->translateContent(
173            [
174                'title' => $title,
175                'lede' => $lede,
176                'body' => $body,
177                'summary' => $summary,
178                'meta_title' => $meta_title,
179                'meta_description' => $meta_description,
180                'meta_keywords' => $meta_keywords,
181                'facebook_description' => $facebook_description,
182                'linkedin_description' => $linkedin_description,
183                'instagram_description' => $instagram_description,
184                'twitter_description' => $twitter_description,
185            ],
186            ['body'], // HTML fields that need preprocessing
187            true, // Use HTML format
188        );
189    }
190
191    /**
192     * Translates a tag into multiple languages using the Google Translate API.
193     *
194     * @param string $title The title of the tag to be translated.
195     * @param string $description The description of the tag to be translated.
196     * @param string $meta_title The meta title of the tag to be translated.
197     * @param string $meta_description The meta description of the tag to be translated.
198     * @param string $meta_keywords The meta keywords of the tag to be translated.
199     * @param string $facebook_description The Facebook description of the tag to be translated.
200     * @param string $linkedin_description The LinkedIn description of the tag to be translated.
201     * @param string $instagram_description The Instagram description of the tag to be translated.
202     * @param string $twitter_description The Twitter description of the tag to be translated.
203     * @return array An associative array where the keys are the locale codes and the values are arrays
204     *               containing the translated fields for each enabled locale.
205     */
206    public function translateTag(
207        string $title,
208        string $description,
209        string $meta_title,
210        string $meta_description,
211        string $meta_keywords,
212        string $facebook_description,
213        string $linkedin_description,
214        string $instagram_description,
215        string $twitter_description,
216    ): array {
217        return $this->translateContent([
218            'title' => $title,
219            'description' => $description,
220            'meta_title' => $meta_title,
221            'meta_description' => $meta_description,
222            'meta_keywords' => $meta_keywords,
223            'facebook_description' => $facebook_description,
224            'linkedin_description' => $linkedin_description,
225            'instagram_description' => $instagram_description,
226            'twitter_description' => $twitter_description,
227        ]);
228    }
229
230    /**
231     * Translates an image gallery into multiple languages using the Google Translate API.
232     *
233     * @param string $name The name of the gallery to be translated.
234     * @param string $description The description of the gallery to be translated.
235     * @param string $meta_title The meta title of the gallery to be translated.
236     * @param string $meta_description The meta description of the gallery to be translated.
237     * @param string $meta_keywords The meta keywords of the gallery to be translated.
238     * @param string $facebook_description The Facebook description of the gallery to be translated.
239     * @param string $linkedin_description The LinkedIn description of the gallery to be translated.
240     * @param string $instagram_description The Instagram description of the gallery to be translated.
241     * @param string $twitter_description The Twitter description of the gallery to be translated.
242     * @return array An associative array where the keys are the locale codes and the values are arrays
243     *               containing the translated fields for each enabled locale.
244     */
245    public function translateImageGallery(
246        string $name,
247        string $description,
248        string $meta_title,
249        string $meta_description,
250        string $meta_keywords,
251        string $facebook_description,
252        string $linkedin_description,
253        string $instagram_description,
254        string $twitter_description,
255    ): array {
256        return $this->translateContent([
257            'name' => $name,
258            'description' => $description,
259            'meta_title' => $meta_title,
260            'meta_description' => $meta_description,
261            'meta_keywords' => $meta_keywords,
262            'facebook_description' => $facebook_description,
263            'linkedin_description' => $linkedin_description,
264            'instagram_description' => $instagram_description,
265            'twitter_description' => $twitter_description,
266        ]);
267    }
268
269    /**
270     * Preprocesses content to identify and store code blocks, video placeholders, and gallery placeholders before translation.
271     *
272     * This method extracts code blocks (markdown, pre, code tags), video placeholders, and image gallery
273     * placeholders from the content and replaces them with unique placeholders. The original content is
274     * stored in the $preservedBlocks property for later restoration.
275     *
276     * @param string $content The content containing blocks to be processed
277     * @return string The content with preserved blocks replaced by placeholders
278     */
279    private function preprocessContent(string $content): string
280    {
281        $this->preservedBlocks = [];
282
283        // Process code blocks, video placeholders, and gallery placeholders
284        $patterns = [
285            // Code blocks pattern
286            '/(```[a-z]*\n[\s\S]*?\n```)|(<pre[\s\S]*?<\/pre>)|(<code[\s\S]*?<\/code>)/m',
287            // YouTube video placeholder pattern
288            '/\[youtube:[a-zA-Z0-9_-]+:\d+:\d+:[^\]]*\]/m',
289            // Image gallery placeholder pattern
290            '/\[gallery:[a-f0-9-]+:[^:]*:[^\]]*\]/m',
291        ];
292
293        foreach ($patterns as $pattern) {
294            $content = preg_replace_callback(
295                $pattern,
296                function ($matches) {
297                    // Use HTML comment syntax for placeholders to prevent translation
298                    $placeholder = sprintf('<!--PRESERVED_BLOCK_%d-->', count($this->preservedBlocks));
299                    $this->preservedBlocks[$placeholder] = $matches[0];
300
301                    return $placeholder;
302                },
303                $content,
304            );
305        }
306
307        return $content;
308    }
309
310    /**
311     * Restores previously stored content blocks back into the content.
312     *
313     * This method replaces the placeholder tokens with their original content
314     * that was stored during preprocessing. This ensures code blocks and video
315     * placeholders maintain their original formatting and content after translation.
316     *
317     * @param string $content The content containing placeholders
318     * @return string The content with original blocks restored
319     */
320    private function postprocessContent(string $content): string
321    {
322        // Restore all preserved blocks
323        foreach ($this->preservedBlocks as $placeholder => $originalContent) {
324            $content = str_replace($placeholder, $originalContent, $content);
325        }
326
327        return $content;
328    }
329}