Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 79
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
MicrosoftWebService
0.00% covered (danger)
0.00%
0 / 79
0.00% covered (danger)
0.00%
0 / 8
306
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 mapCode
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 doPairs
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
42
 getQuery
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
20
 parseResponse
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 wrapUntranslatable
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 unwrapUntranslatable
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\WebService;
5
6use MediaWiki\Http\HttpRequestFactory;
7
8/**
9 * Implements support for Microsoft translation api v3.
10 * @author Niklas Laxström
11 * @author Ulrich Strauss
12 * @license GPL-2.0-or-later
13 * @since 2013-01-01
14 * @see https://docs.microsoft.com/fi-fi/azure/cognitive-services/Translator/reference/v3-0-reference
15 * @ingroup TranslationWebService
16 */
17class MicrosoftWebService extends TranslationWebService {
18    private HttpRequestFactory $httpRequestFactory;
19
20    public function __construct(
21        HttpRequestFactory $httpRequestFactory,
22        string $serviceName,
23        array $config
24    ) {
25        parent::__construct( $serviceName, $config );
26        $this->httpRequestFactory = $httpRequestFactory;
27    }
28
29    /** @inheritDoc */
30    public function getType(): string {
31        return 'mt';
32    }
33
34    /** @inheritDoc */
35    protected function mapCode( string $code ): string {
36        $map = [
37            'tl' => 'fil',
38            'zh-hant' => 'zh-Hant',
39            'zh-hans' => 'zh-Hans',
40            'sr-ec' => 'sr-Cyrl',
41            'sr-el' => 'sr-Latn',
42            'pt-br' => 'pt',
43        ];
44
45        return $map[$code] ?? $code;
46    }
47
48    /** @inheritDoc */
49    protected function doPairs(): array {
50        if ( !isset( $this->config['key'] ) ) {
51            throw new TranslationWebServiceConfigurationException( 'key is not set' );
52        }
53
54        $key = $this->config['key'];
55
56        $options = [];
57        $options['method'] = 'GET';
58        $options['timeout'] = $this->config['timeout'];
59
60        $url = $this->config['url'] . '/languages?api-version=3.0';
61
62        $req = $this->httpRequestFactory->create( $url, $options, __METHOD__ );
63        $req->setHeader( 'Ocp-Apim-Subscription-Key', $key );
64
65        $status = $req->execute();
66        if ( !$status->isOK() ) {
67            $error = $req->getContent();
68            // Most likely a timeout or other general error
69            throw new TranslationWebServiceException(
70                'HttpRequestFactory::get failed:' . serialize( $error ) . serialize( $status )
71            );
72        }
73
74        $json = $req->getContent();
75        $response = json_decode( $json, true );
76        if ( !isset( $response[ 'translation' ] ) ) {
77            throw new TranslationWebServiceException(
78                'Unable to fetch list of available languages: ' . $json
79            );
80        }
81
82        $languages = array_keys( $response[ 'translation' ] );
83
84        // Let's make a cartesian product, assuming we can translate from any language to any language
85        $pairs = [];
86        foreach ( $languages as $from ) {
87            foreach ( $languages as $to ) {
88                $pairs[$from][$to] = true;
89            }
90        }
91
92        return $pairs;
93    }
94
95    /** @inheritDoc */
96    protected function getQuery( string $text, string $sourceLanguage, string $targetLanguage ): TranslationQuery {
97        if ( !isset( $this->config['key'] ) ) {
98            throw new TranslationWebServiceConfigurationException( 'key is not set' );
99        }
100
101        $key = $this->config['key'];
102        $text = trim( $text );
103        $text = $this->wrapUntranslatable( $text );
104
105        $url = $this->config['url'] . '/translate';
106        $params = [
107            'api-version' => '3.0',
108            'from' => $sourceLanguage,
109            'to' => $targetLanguage,
110            'textType' => 'html',
111        ];
112        $headers = [
113            'Ocp-Apim-Subscription-Key' => $key,
114            'Content-Type' => 'application/json',
115        ];
116        $body = json_encode( [ [ 'Text' => $text ] ] );
117
118        if ( $body === false ) {
119            throw new TranslationWebServiceInvalidInputException( 'Could not JSON encode source text' );
120        }
121
122        if ( strlen( $body ) > 5000 ) {
123            throw new TranslationWebServiceInvalidInputException( 'Source text too long' );
124        }
125
126        return TranslationQuery::factory( $url )
127            ->timeout( intval( $this->config['timeout'] ) )
128            ->queryParameters( $params )
129            ->queryHeaders( $headers )
130            ->postWithData( $body );
131    }
132
133    /** @inheritDoc */
134    protected function parseResponse( TranslationQueryResponse $reply ): string {
135        $body = $reply->getBody();
136
137        $response = json_decode( $body, true );
138        if ( !isset( $response[ 0 ][ 'translations' ][ 0 ][ 'text' ] ) ) {
139            throw new TranslationWebServiceException(
140                'Unable to parse translation response: ' . $body
141            );
142        }
143
144        $text = $response[ 0 ][ 'translations' ][ 0 ][ 'text' ];
145        $text = $this->unwrapUntranslatable( $text );
146
147        return $text;
148    }
149
150    /** @inheritDoc */
151    protected function wrapUntranslatable( string $text ): string {
152        $pattern = '~%[^% ]+%|\$\d|{VAR:[^}]+}|{?{(PLURAL|GRAMMAR|GENDER):[^|]+\||%(\d\$)?[sd]~';
153        $wrap = '<span class="notranslate">\0</span>';
154        return preg_replace( $pattern, $wrap, $text );
155    }
156
157    /** @inheritDoc */
158    protected function unwrapUntranslatable( string $text ): string {
159        $pattern = '~<span class="notranslate">\s*(.*?)\s*</span>~';
160        return preg_replace( $pattern, '\1', $text );
161    }
162}