13
13
14
14
use Psr \Log \LoggerInterface ;
15
15
use Symfony \Component \HttpFoundation \Response ;
16
- use Symfony \Component \Translation \Exception \TransportException ;
16
+ use Symfony \Component \Translation \Dumper \XliffFileDumper ;
17
+ use Symfony \Component \Translation \Exception \ProviderException ;
17
18
use Symfony \Component \Translation \Loader \LoaderInterface ;
19
+ use Symfony \Component \Translation \MessageCatalogue ;
18
20
use Symfony \Component \Translation \Provider \AbstractProvider ;
19
21
use Symfony \Component \Translation \TranslatorBag ;
20
22
use Symfony \Contracts \HttpClient \HttpClientInterface ;
23
25
* @author Fabien Potencier <[email protected] >
24
26
*
25
27
* @experimental in 5.2
26
- *
27
- * In Crowdin:
28
- * Source strings refers to Symfony's translation keys
29
28
*/
30
29
final class CrowdinProvider extends AbstractProvider
31
30
{
@@ -36,14 +35,17 @@ final class CrowdinProvider extends AbstractProvider
36
35
private $ loader ;
37
36
private $ logger ;
38
37
private $ defaultLocale ;
38
+ private $ xliffFileDumper ;
39
+ private $ files = [];
39
40
40
- public function __construct (string $ projectId , string $ token , HttpClientInterface $ client = null , LoaderInterface $ loader = null , LoggerInterface $ logger = null , string $ defaultLocale = null )
41
+ public function __construct (string $ projectId , string $ token , HttpClientInterface $ client = null , LoaderInterface $ loader = null , LoggerInterface $ logger = null , string $ defaultLocale = null , XliffFileDumper $ xliffFileDumper = null )
41
42
{
42
43
$ this ->projectId = $ projectId ;
43
44
$ this ->token = $ token ;
44
45
$ this ->loader = $ loader ;
45
46
$ this ->logger = $ logger ;
46
47
$ this ->defaultLocale = $ defaultLocale ;
48
+ $ this ->xliffFileDumper = $ xliffFileDumper ;
47
49
48
50
parent ::__construct ($ client );
49
51
}
@@ -60,15 +62,19 @@ public function getName(): string
60
62
61
63
public function write (TranslatorBag $ translations , bool $ override = false ): void
62
64
{
63
- foreach ($ translations ->getCatalogues () as $ catalogue ) {
64
- foreach ($ catalogue ->all () as $ domain => $ messages ) {
65
- $ locale = $ catalogue ->getLocale ();
66
-
67
- // check if domain exists, if not, create it
68
-
69
- foreach ($ messages as $ id => $ message ) {
70
- $ this ->addString ($ id );
71
- $ this ->addTranslation ($ id , $ message , $ locale );
65
+ foreach ($ translations ->getDomains () as $ domain ) {
66
+ foreach ($ translations ->getCatalogues () as $ catalogue ) {
67
+ $ content = $ this ->xliffFileDumper ->formatCatalogue ($ catalogue , $ domain );
68
+ $ fileId = $ this ->getFileId ($ domain );
69
+
70
+ if ($ catalogue ->getLocale () === $ this ->defaultLocale ) {
71
+ if (!$ fileId ) {
72
+ $ this ->addFile ($ domain , $ content );
73
+ } else {
74
+ $ this ->updateFile ($ fileId , $ domain , $ content );
75
+ }
76
+ } else {
77
+ $ this ->uploadTranslations ($ fileId , $ domain , $ content , $ catalogue ->getLocale ());
72
78
}
73
79
}
74
80
}
@@ -79,21 +85,10 @@ public function write(TranslatorBag $translations, bool $override = false): void
79
85
*/
80
86
public function read (array $ domains , array $ locales ): TranslatorBag
81
87
{
82
- $ filter = $ domains ? implode (', ' , $ domains ) : '* ' ;
83
88
$ translatorBag = new TranslatorBag ();
84
89
85
90
foreach ($ locales as $ locale ) {
86
- $ fileId = $ this ->getFileId ();
87
-
88
- $ responseContent = $ response ->getContent (false );
89
-
90
- if (Response::HTTP_OK !== $ response ->getStatusCode ()) {
91
- throw new TransportException ('Unable to read the Loco response: ' .$ responseContent , $ response );
92
- }
93
-
94
- foreach ($ domains as $ domain ) {
95
- $ translatorBag ->addCatalogue ($ this ->loader ->load ($ responseContent , $ locale , $ domain ));
96
- }
91
+ // TODO: Implement read() method.
97
92
}
98
93
99
94
return $ translatorBag ;
@@ -107,65 +102,148 @@ public function delete(TranslatorBag $translations): void
107
102
protected function getDefaultHeaders (): array
108
103
{
109
104
return [
110
- 'Authorization ' => 'Bearer ' . $ this ->token ,
105
+ 'Authorization ' => 'Bearer ' . $ this ->token ,
111
106
];
112
107
}
113
108
109
+ private function getFileId (string $ domain ): ?int
110
+ {
111
+ if (isset ($ this ->files [$ domain ])) {
112
+ return $ this ->files [$ domain ];
113
+ }
114
+
115
+ try {
116
+ $ files = $ this ->getFilesList ();
117
+ } catch (ProviderException $ e ) {
118
+ return null ;
119
+ }
120
+
121
+ foreach ($ files as $ file ) {
122
+ if ($ file ['data ' ]['name ' ] === sprintf ('%s.%s ' , $ domain , 'xlf ' )) {
123
+ return $ this ->files [$ domain ] = (int ) $ file ['data ' ]['id ' ];
124
+ }
125
+ }
126
+
127
+ return null ;
128
+ }
129
+
114
130
/**
115
- * This function allows creation of a new translation key.
116
- *
117
- * @see https://support.crowdin.com/api/v2/#operation/api.projects.strings.post
131
+ * @see https://support.crowdin.com/api/v2/#operation/api.projects.files.post
118
132
*/
119
- private function addString (string $ id ): void
133
+ private function addFile (string $ domain , string $ content ): void
120
134
{
121
- $ response = $ this ->client ->request ('POST ' , sprintf ('https://%s/projects/%s/strings ' , $ this ->getEndpoint (), $ this ->projectId ), [
122
- 'headers ' => $ this ->getDefaultHeaders (),
123
- 'body ' => [
124
- 'text ' => $ id ,
125
- 'identifier ' => $ id ,
126
- ],
135
+ $ storageId = $ this ->addStorage ($ domain , $ content );
136
+ $ response = $ this ->client ->request ('POST ' , sprintf ('https://%s/projects/%s/files ' , $ this ->getEndpoint (), $ this ->projectId ), [
137
+ 'headers ' => array_merge ($ this ->getDefaultHeaders (), [
138
+ 'Content-Type ' => 'application/json ' ,
139
+ ]),
140
+ 'body ' => json_encode ([
141
+ 'storageId ' => $ storageId ,
142
+ 'name ' => sprintf ('%s.%s ' , $ domain , 'xlf ' ),
143
+ ]),
127
144
]);
128
145
129
- if (Response::HTTP_CONFLICT === $ response ->getStatusCode ()) {
130
- $ this ->logger ->warning (sprintf ('Translation key (%s) already exists in Crowdin. ' , $ id ), [
131
- 'id ' => $ id ,
132
- ]);
133
- } elseif (Response::HTTP_CREATED !== $ response ->getStatusCode ()) {
134
- throw new TransportException (sprintf ('Unable to add new translation key (%s) to Crowdin: (status code: "%s") "%s". ' , $ id , $ response ->getStatusCode (), $ response ->getContent (false )), $ response );
146
+ if (Response::HTTP_CREATED !== $ response ->getStatusCode ()) {
147
+ throw new ProviderException (sprintf ('Unable to add a File in Crowdin for domain "%s". ' , $ domain ), $ response );
135
148
}
149
+
150
+ $ this ->files [$ domain ] = (int ) json_decode ($ response ->getContent (), true )['data ' ]['id ' ];
136
151
}
137
152
138
153
/**
139
- * This function allows translation of a message.
140
- *
141
- * @see https://support.crowdin.com/api/v2/#operation/api.projects.translations.post
154
+ * @see https://support.crowdin.com/api/v2/#operation/api.projects.files.put
142
155
*/
143
- private function addTranslation ( string $ id , string $ message , string $ locale ): void
156
+ private function updateFile ( int $ fileId , string $ domain , string $ content ): void
144
157
{
145
- $ response = $ this ->client ->request ('POST ' , sprintf ('https://%s/projects/%s/translations ' , $ this ->getEndpoint (), $ this ->projectId ), [
146
- 'headers ' => $ this ->getDefaultHeaders (),
147
- 'body ' => [
148
- 'stringId ' => $ id ,
149
- 'languageId ' => $ locale ,
150
- 'text ' => $ message ,
151
- ],
158
+ $ storageId = $ this ->addStorage ($ domain , $ content );
159
+ $ response = $ this ->client ->request ('PUT ' , sprintf ('https://%s/projects/%s/files/%d ' , $ this ->getEndpoint (), $ this ->projectId , $ fileId ), [
160
+ 'headers ' => array_merge ($ this ->getDefaultHeaders (), [
161
+ 'Content-Type ' => 'application/json ' ,
162
+ ]),
163
+ 'body ' => json_encode ([
164
+ 'storageId ' => $ storageId ,
165
+ ]),
166
+ ]);
167
+
168
+ if (Response::HTTP_OK !== $ response ->getStatusCode ()) {
169
+ throw new ProviderException (
170
+ sprintf ('Unable to update file in Crowdin for file ID "%d" and domain "%s". ' , $ fileId , $ domain ),
171
+ $ response
172
+ );
173
+ }
174
+ }
175
+
176
+ /**
177
+ * @see https://support.crowdin.com/api/v2/#operation/api.projects.translations.postOnLanguage
178
+ */
179
+ private function uploadTranslations (?int $ fileId , string $ domain , string $ content , string $ locale ): void
180
+ {
181
+ if (!$ fileId ) {
182
+ return ;
183
+ }
184
+
185
+ $ storageId = $ this ->addStorage ($ domain , $ content );
186
+ $ response = $ this ->client ->request ('POST ' , sprintf ('https://%s/projects/%s/translations/%s ' , $ this ->getEndpoint (), $ this ->projectId , $ locale ), [
187
+ 'headers ' => array_merge ($ this ->getDefaultHeaders (), [
188
+ 'Content-Type ' => 'application/json ' ,
189
+ ]),
190
+ 'body ' => json_encode ([
191
+ 'storageId ' => $ storageId ,
192
+ 'fileId ' => $ fileId ,
193
+ ]),
194
+ ]);
195
+
196
+ if (Response::HTTP_OK !== $ response ->getStatusCode ()) {
197
+ throw new ProviderException (
198
+ sprintf ('Unable to upload translations to Crowdin for domain "%s" and locale "%s". ' , $ domain , $ locale ),
199
+ $ response
200
+ );
201
+ }
202
+ }
203
+
204
+ /**
205
+ * @see https://support.crowdin.com/api/v2/#operation/api.storages.post
206
+ */
207
+ private function addStorage (string $ domain , string $ content ): int
208
+ {
209
+ $ response = $ this ->client ->request ('POST ' , sprintf ('https://%s/storages ' , $ this ->getEndpoint ()), [
210
+ 'headers ' => array_merge ($ this ->getDefaultHeaders (), [
211
+ 'Crowdin-API-FileName ' => urlencode (sprintf ('%s.%s ' , $ domain , 'xlf ' )),
212
+ 'Content-Type ' => 'application/octet-stream ' ,
213
+ ]),
214
+ 'body ' => $ content ,
152
215
]);
153
216
154
217
if (Response::HTTP_CREATED !== $ response ->getStatusCode ()) {
155
- throw new TransportException (sprintf ('Unable to add new translation message "%s" ( for key: "%s") to Crowdin: (status code: "%s") "%s" . ' , $ message , $ id , $ response -> getStatusCode (), $ response -> getContent ( false ) ), $ response );
218
+ throw new ProviderException (sprintf ('Unable to add a Storage in Crowdin for domain "%s". ' , $ domain ), $ response );
156
219
}
220
+
221
+ $ storage = json_decode ($ response ->getContent (), true );
222
+
223
+ return $ storage ['data ' ]['id ' ];
157
224
}
158
225
159
226
/**
160
- * @todo: Not sure at all of this
227
+ * @see https://support.crowdin.com/api/v2/#operation/api.projects.files.getMany
161
228
*/
162
- private function getFileId (): int
229
+ private function getFilesList (): array
163
230
{
164
- $ response = $ this ->client ->request ('GET ' , sprintf ('https://%s/projects/%s/files ' , $ this ->getEndpoint (), $ this ->projectId ), [
165
- 'headers ' => $ this ->getDefaultHeaders (),
231
+ $ response = $ this ->client ->request ('GET ' , sprintf ('https://%s/projects/%d/files ' , $ this ->getEndpoint (), $ this ->projectId ), [
232
+ 'headers ' => array_merge ($ this ->getDefaultHeaders (), [
233
+ 'Content-Type ' => 'application/json ' ,
234
+ ]),
166
235
]);
167
- $ files = json_decode ($ response ->getContent ());
168
236
169
- return $ files ->data [0 ]->data ->id ;
237
+ if (Response::HTTP_OK !== $ response ->getStatusCode ()) {
238
+ throw new ProviderException ('Unable to list Crowdin files. ' , $ response );
239
+ }
240
+
241
+ $ files = json_decode ($ response ->getContent (), true )['data ' ];
242
+
243
+ if (count ($ files ) === 0 ) {
244
+ throw new ProviderException ('Crowdin files list is empty. ' , $ response );
245
+ }
246
+
247
+ return $ files ;
170
248
}
171
249
}
0 commit comments