Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.00% covered (success)
80.00%
44 / 55
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
TranslationController
80.00% covered (success)
80.00%
44 / 55
50.00% covered (danger)
50.00%
1 / 2
24.53
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 translate
79.63% covered (warning)
79.63%
43 / 54
0.00% covered (danger)
0.00%
0 / 1
23.38
1<?php
2
3/**
4 * The Admin Translation Controller
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2026-01-17
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Controller\Administration\Api;
21
22use phpMyFAQ\Enums\AdminLogType;
23use phpMyFAQ\Enums\PermissionType;
24use phpMyFAQ\Session\Token;
25use phpMyFAQ\Translation;
26use phpMyFAQ\Translation\ContentTranslationService;
27use phpMyFAQ\Translation\DTO\TranslationRequest;
28use phpMyFAQ\Translation\Exception\TranslationException;
29use Symfony\Component\HttpFoundation\JsonResponse;
30use Symfony\Component\HttpFoundation\Request;
31use Symfony\Component\HttpFoundation\Response;
32use Symfony\Component\Routing\Attribute\Route;
33
34final class TranslationController extends AbstractAdministrationApiController
35{
36    public function __construct(
37        private readonly ContentTranslationService $translationService,
38    ) {
39        parent::__construct();
40    }
41
42    /**
43     * Translates content using a configured AI translation provider
44     *
45     * @throws \Exception
46     */
47    #[Route(path: 'translation/translate', name: 'admin.api.translation.translate', methods: ['POST'])]
48    public function translate(Request $request): JsonResponse
49    {
50        $this->userHasPermission(PermissionType::FAQ_TRANSLATE);
51
52        $data = json_decode($request->getContent(), associative: true);
53        if (!is_array($data)) {
54            return $this->json([
55                'success' => false,
56                'error' => 'The request body must be a JSON object.',
57            ], Response::HTTP_BAD_REQUEST);
58        }
59
60        if (!Token::getInstance($this->session)->verifyToken('translate', (string) ($data['pmf-csrf-token'] ?? ''))) {
61            return $this->json([
62                'success' => false,
63                'error' => 'CSRF - ' . Translation::getString(key: 'msgNoPermission'),
64            ], Response::HTTP_UNAUTHORIZED);
65        }
66
67        // Validate required fields
68        $contentType = (string) ($data['contentType'] ?? '');
69        $sourceLang = (string) ($data['sourceLang'] ?? '');
70        $targetLang = (string) ($data['targetLang'] ?? '');
71        $fields = $data['fields'] ?? [];
72
73        if (
74            $contentType === ''
75            || $sourceLang === ''
76            || $targetLang === ''
77            || !is_array($fields)
78            || count($fields) === 0
79        ) {
80            return $this->json([
81                'success' => false,
82                'error' => 'Missing required parameters',
83            ], Response::HTTP_BAD_REQUEST);
84        }
85
86        // Validate content type
87        $validContentTypes = ['faq', 'customPage', 'category', 'news'];
88        if (!in_array($contentType, $validContentTypes, strict: true)) {
89            return $this->json(['success' => false, 'error' => 'Invalid content type'], Response::HTTP_BAD_REQUEST);
90        }
91
92        try {
93            $fieldValues = [];
94            foreach ($fields as $fieldName => $fieldValue) {
95                $fieldValues[(string) $fieldName] = (string) $fieldValue;
96            }
97
98            $translationRequest = new TranslationRequest($contentType, $sourceLang, $targetLang, $fieldValues);
99
100            $result = match ($contentType) {
101                'faq' => $this->translationService->translateFaq($translationRequest),
102                'customPage' => $this->translationService->translateCustomPage($translationRequest),
103                'category' => $this->translationService->translateCategory($translationRequest),
104                'news' => $this->translationService->translateNews($translationRequest),
105            };
106
107            if ($result->isSuccess()) {
108                $logType = match ($contentType) {
109                    'faq' => AdminLogType::FAQ_TRANSLATE,
110                    'customPage' => AdminLogType::PAGE_TRANSLATE,
111                    'category' => AdminLogType::CATEGORY_TRANSLATE,
112                    'news' => AdminLogType::NEWS_TRANSLATE,
113                };
114
115                $this->adminLog->log($this->currentUser, $logType->value . ':' . $sourceLang . '->' . $targetLang);
116
117                return $this->json([
118                    'success' => true,
119                    'translatedFields' => $result->getTranslatedFields(),
120                ], Response::HTTP_OK);
121            }
122
123            return $this->json([
124                'success' => false,
125                'error' => $result->getError(),
126            ], Response::HTTP_INTERNAL_SERVER_ERROR);
127        } catch (TranslationException $e) {
128            return $this->json(['success' => false, 'error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
129        }
130    }
131}