Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.05% covered (success)
96.05%
73 / 76
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
AzureTranslationProvider
96.05% covered (success)
96.05%
73 / 76
71.43% covered (warning)
71.43%
5 / 7
11
0.00% covered (danger)
0.00%
0 / 1
 getProviderName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doTranslate
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 doTranslateBatch
92.00% covered (success)
92.00%
23 / 25
0.00% covered (danger)
0.00%
0 / 1
4.01
 translateBatch
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 supportsLanguagePair
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSupportedLanguages
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
1
 mapLanguageCode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5/**
6 * Azure Translator API provider.
7 *
8 * This Source Code Form is subject to the terms of the Mozilla Public License,
9 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
10 * obtain one at https://mozilla.org/MPL/2.0/.
11 *
12 * @package   phpMyFAQ
13 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
14 * @copyright 2026 phpMyFAQ Team
15 * @license   http://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
16 * @link      https://www.phpmyfaq.de
17 * @since     2026-01-17
18 */
19
20namespace phpMyFAQ\Translation\Provider;
21
22use phpMyFAQ\Translation\AbstractTranslationProvider;
23use phpMyFAQ\Translation\Exception\ApiException;
24
25/**
26 * Class AzureTranslationProvider
27 *
28 * Azure Translator API implementation.
29 */
30class AzureTranslationProvider extends AbstractTranslationProvider
31{
32    private const string API_URL = 'https://api.cognitive.microsofttranslator.com/translate?api-version=3.0';
33
34    /**
35     * @inheritDoc
36     */
37    public function getProviderName(): string
38    {
39        return 'Azure Translator';
40    }
41
42    /**
43     * @inheritDoc
44     */
45    protected function doTranslate(string $text, string $sourceLang, string $targetLang): string
46    {
47        $results = $this->doTranslateBatch([$text], $sourceLang, $targetLang);
48        return $results[0] ?? '';
49    }
50
51    /**
52     * @inheritDoc
53     */
54    protected function doTranslateBatch(array $texts, string $sourceLang, string $targetLang): array
55    {
56        $apiKey = $this->configuration->get('translation.azureKey');
57        $region = $this->configuration->get('translation.azureRegion');
58
59        if ((string) $apiKey === '') {
60            throw new ApiException('Azure Translator API key not configured');
61        }
62
63        if ((string) $region === '') {
64            throw new ApiException('Azure Translator region not configured');
65        }
66
67        $url =
68            self::API_URL
69            . '&from='
70            . $this->mapLanguageCode($sourceLang)
71            . '&to='
72            . $this->mapLanguageCode($targetLang);
73
74        $body = array_map(static fn($text) => ['text' => $text], $texts);
75
76        try {
77            $response = $this->httpClient->request('POST', $url, [
78                'headers' => [
79                    'Ocp-Apim-Subscription-Key' => $apiKey,
80                    'Ocp-Apim-Subscription-Region' => $region,
81                    'Content-Type' => 'application/json',
82                ],
83                'json' => $body,
84            ]);
85
86            $data = $response->toArray();
87            return array_map(static fn($item): string => (string) ($item['translations'][0]['text'] ?? ''), $data);
88        } catch (\Exception $e) {
89            throw new ApiException('Azure Translator API error: ' . $e->getMessage());
90        }
91    }
92
93    /**
94     * @inheritDoc
95     */
96    public function translateBatch(
97        array $texts,
98        string $sourceLang,
99        string $targetLang,
100        bool $preserveHtml = false,
101    ): array {
102        if ($preserveHtml) {
103            // Process each text individually with HTML preservation
104            return array_map(fn($text) => $this->translate($text, $sourceLang, $targetLang, true), $texts);
105        }
106
107        return $this->doTranslateBatch($texts, $sourceLang, $targetLang);
108    }
109
110    /**
111     * @inheritDoc
112     */
113    public function supportsLanguagePair(string $sourceLang, string $targetLang): bool
114    {
115        // Azure Translator supports most language pairs
116        return true;
117    }
118
119    /**
120     * @inheritDoc
121     */
122    public function getSupportedLanguages(): array
123    {
124        // Common languages supported by Azure Translator
125        return [
126            'ar',
127            'bn',
128            'bs',
129            'cs',
130            'cy',
131            'da',
132            'de',
133            'el',
134            'en',
135            'es',
136            'eu',
137            'fa',
138            'fi',
139            'fr',
140            'he',
141            'hi',
142            'hu',
143            'id',
144            'it',
145            'ja',
146            'ko',
147            'lt',
148            'lv',
149            'mn',
150            'ms',
151            'nb',
152            'nl',
153            'pl',
154            'pt',
155            'ro',
156            'ru',
157            'sk',
158            'sl',
159            'sr',
160            'sv',
161            'th',
162            'tr',
163            'uk',
164            'ur',
165            'vi',
166            'zh',
167        ];
168    }
169
170    /**
171     * @inheritDoc
172     */
173    protected function mapLanguageCode(string $pmfLangCode): string
174    {
175        // Azure uses standard ISO 639-1
176        return $pmfLangCode;
177    }
178}