Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.55% covered (success)
96.55%
28 / 29
87.50% covered (success)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
LibreTranslationProvider
96.55% covered (success)
96.55%
28 / 29
87.50% covered (success)
87.50%
7 / 8
12
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
 getApiUrl
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 doTranslate
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 doTranslateBatch
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 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%
1 / 1
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 * LibreTranslate 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 Exception;
23use phpMyFAQ\Translation\AbstractTranslationProvider;
24use phpMyFAQ\Translation\Exception\ApiException;
25
26/**
27 * Class LibreTranslationProvider
28 *
29 * LibreTranslate API implementation (self-hosted or public instance).
30 */
31class LibreTranslationProvider extends AbstractTranslationProvider
32{
33    /**
34     * @inheritDoc
35     */
36    public function getProviderName(): string
37    {
38        return 'LibreTranslate';
39    }
40
41    /**
42     * Get the API URL from the configuration.
43     *
44     * @return string API translate endpoint URL
45     * @throws ApiException
46     */
47    private function getApiUrl(): string
48    {
49        $baseUrl = (string) $this->configuration->get('translation.libreTranslateUrl');
50
51        if ($baseUrl === '') {
52            throw new ApiException('LibreTranslate server URL not configured');
53        }
54
55        return rtrim(string: $baseUrl, characters: '/') . '/translate';
56    }
57
58    /**
59     * @inheritDoc
60     */
61    protected function doTranslate(string $text, string $sourceLang, string $targetLang): string
62    {
63        $apiKey = $this->configuration->get('translation.libreTranslateApiKey'); // Optional
64
65        $body = [
66            'q' => $text,
67            'source' => $this->mapLanguageCode($sourceLang),
68            'target' => $this->mapLanguageCode($targetLang),
69            'format' => 'text',
70        ];
71
72        if ((string) $apiKey !== '') {
73            $body['api_key'] = $apiKey;
74        }
75
76        try {
77            $response = $this->httpClient->request('POST', $this->getApiUrl(), [
78                'headers' => ['Content-Type' => 'application/json'],
79                'json' => $body,
80            ]);
81
82            $data = $response->toArray();
83            return (string) ($data['translatedText'] ?? '');
84        } catch (Exception $e) {
85            throw new ApiException('LibreTranslate API error: ' . $e->getMessage());
86        }
87    }
88
89    /**
90     * @inheritDoc
91     */
92    protected function doTranslateBatch(array $texts, string $sourceLang, string $targetLang): array
93    {
94        // LibreTranslate doesn't support batch translation natively, translate one by one
95        return array_map(fn($text) => $this->doTranslate($text, $sourceLang, $targetLang), $texts);
96    }
97
98    /**
99     * @inheritDoc
100     */
101    public function translateBatch(
102        array $texts,
103        string $sourceLang,
104        string $targetLang,
105        bool $preserveHtml = false,
106    ): array {
107        if ($preserveHtml) {
108            // Process each text individually with HTML preservation
109            return array_map(fn($text) => $this->translate($text, $sourceLang, $targetLang, true), $texts);
110        }
111
112        return $this->doTranslateBatch($texts, $sourceLang, $targetLang);
113    }
114
115    /**
116     * @inheritDoc
117     */
118    public function supportsLanguagePair(string $sourceLang, string $targetLang): bool
119    {
120        // LibreTranslate support depends on installed models, assume common languages are supported
121        return true;
122    }
123
124    /**
125     * @inheritDoc
126     */
127    public function getSupportedLanguages(): array
128    {
129        // Common languages typically available in LibreTranslate
130        return ['ar', 'de', 'en', 'es', 'fr', 'it', 'ja', 'nl', 'pl', 'pt', 'ru', 'tr', 'zh'];
131    }
132
133    /**
134     * @inheritDoc
135     */
136    protected function mapLanguageCode(string $pmfLangCode): string
137    {
138        // LibreTranslate uses standard ISO 639-1 codes
139        return $pmfLangCode;
140    }
141}