Shopify 页面翻译实现
实现在自己网站后台控制shopify页面翻译
1 <?php 2 namespace Cli\Controller; 3 4 use Curl; 5 use Exception; 6 use Think\Controller; 7 use Think\Log; 8 use Think\Think; 9 use translate\Baidu; 10 use translate\Translate; 11 12 class ShopifyTranslateController extends Controller 13 { 14 private $_api = [ 15 //api info ... 20 ]; 21 private $_logque; 22 private $_curlLastErr; 23 private $_storefrontToken = 'token....'; 24 private $_rePrefix = 'ShopifyTranslate'; 25 private $_redis; 26 //locale shopLocales 27 const CHINESE = 'zh-CN'; 28 const JAPANESE = 'ja'; 29 30 31 public function __construct() 32 { 33 $this->_api['authorization'] = base64_encode( "{$this->_api['key']}:{$this->_api['pwd']}" ); 34 $this->_redis = getRedis(); 35 36 } 37 38 public function test() 39 { 40 $list = M('shopifyTranslate')->where( ['resourceId' => 'gid://shopify/OnlineStoreTheme/80042688647'] )->select(); 41 42 $translations = []; 43 foreach( $list as $_item ) { 44 $source = json_decode( $_item['source'], true ); 45 46 $cn = str_replace("\n", "\\n", addslashes( $_item['zh-cn'] ) ); 47 $ja = str_replace("\n", "\\n", addslashes( $_item['ja'] ) ); 48 49 $translations[] = [ 50 'key' => $source['key'], 51 'value' => $cn, 52 'locale' => "zh-CN", 53 'translatableContentDigest' => $source['digest'] 54 ]; 55 56 $translations[] = [ 57 'key' => $source['key'], 58 'value' => $ja, 59 'locale' => "ja", 60 'translatableContentDigest' => $source['digest'] 61 ]; 62 } 63 64 $chunked = array_chunk( $translations, 100 ); 65 foreach( $chunked as $_chunk ) { 66 $_translations = json_encode( $_chunk ); 67 $data = <<<EOF 68 { 69 "query": "mutation CreateTranslation(\$id: ID!, \$translations: [TranslationInput!]!) { translationsRegister(resourceId: \$id, translations: \$translations) { translations { locale key outdated value } userErrors { field message } } }", 70 "variables": { 71 "id": "gid://shopify/OnlineStoreTheme/80042688647", 72 "translations": $_translations 73 } 74 } 75 EOF; 76 $ret = $this->sendRequest( $data, 'post', 'graphql.json', [], false, 'json' ); 77 var_dump( $ret ); 78 } 79 80 81 } 82 83 public function pull() 84 { 85 86 } 87 88 //第三方翻译。 https://github.com/John-Theo/google-translate-server 89 public function translate() 90 { 91 $list = M('shopifyTranslate')->field('id, en')->where( 'ja is null' )->select(); 92 import( 'Lib.Extend.Translate.sdk.Baidu', dirname( THINK_PATH ) ); 93 94 $baidu = new Baidu(); 95 foreach( $list as $_item ) { 96 97 $up = []; 98 99 $en = urlencode( $_item['en'] ); 100 101 $ret = (new Curl())->get( "http://127.0.0.1:30031?to=zh-CN&text={$en}" ); 102 $ret = json_decode( $ret, true ); 103 if( !empty( $ret['text'] ) ) { 104 $up['zh-CN'] = $ret['text']; 105 }else { 106 $str = addslashes( $_item['en'] ); 107 $res = $baidu->run( $str, 'zh', 'en' ); 108 $up['zh-CN'] = addslashes( $res ); 109 } 110 sleep( 3 ); 111 112 $ret = (new Curl())->get( "http://127.0.0.1:30031?to=ja&text={$en}" ); 113 $ret = json_decode( $ret, true ); 114 if( !empty( $ret['text'] ) ) { 115 $up['ja'] = $ret['text']; 116 }else { 117 $res = $baidu->run( $str, 'jp', 'en' ); 118 $up['ja'] = addslashes( $res ); 119 sleep(1); 120 } 121 sleep( 3 ); 122 123 124 M('shopifyTranslate')->where( ['id' => $_item['id']] )->save( $up ); 125 } 126 } 127 128 public function setTranslate( $id = 0 ) 129 { 130 $this->_setLog( 'setTranslate start' ); 131 132 // $list = M('shopifyTranslate')->where( ['synch' => 0, 'ja' => ['exp', 'is not null']] )->select(); 133 134 // $keys = [ 135 // '!', '"', '#', '$', '%', ''', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\', ']', '^', '_', '`', '{', '|', '}', '~', 136 // '⦅', '⦆', '¢', '£', '¬', ' ̄', '¦', '¥', '₩', '│', '←', '↑', '→', '↓', '■', '○','“','”','/ ',' _ ',' // ' 137 // ]; 138 // $values = [ 139 // '!', '"', '#', '$', '%', '\'', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~', 140 // '⦅', '⦆', '¢', '£', '¬', '¯', '¦', '¥', '₩', '│', '←', '↑', '→', '↓', '■', '○','"','"','/','_','//' 141 // ]; 142 // foreach( $list as $_item ) { 143 // $up = []; 144 145 // $up['zh-CN'] = strtr( $_item['zh-cn'], array_combine( $keys, $values ) ); 146 // $up['ja'] = strtr( $_item['ja'], array_combine( $keys, $values ) ); 147 148 // M('shopifyTranslate')->where( ['id' => $_item['id']] )->save($up); 149 // } 150 // die; 151 152 $list = M('shopifyTranslate')->where( ['synch' => 0, 'ja' => ['exp', 'is not null'], 'type' => ['neq', 'EMAIL_TEMPLATE']] )->select(); 153 // $list = M('shopifyTranslate')->where( ['id' => $id] )->select(); 154 155 foreach( $list as $_item ) { 156 157 $source = json_decode( $_item['source'], true ); 158 $cn = str_replace("\n", "\\n", addslashes( $_item['zh-cn'] ) ); 159 $ja = str_replace("\n", "\\n", addslashes( $_item['ja'] ) ); 160 161 $data = <<<EOF 162 { 163 "query": "mutation CreateTranslation(\$id: ID!, \$translations: [TranslationInput!]!) { translationsRegister(resourceId: \$id, translations: \$translations) { translations { locale key outdated value } userErrors { field message } } }", 164 "variables": { 165 "id": "{$_item['resourceid']}", 166 "translations": [ 167 { 168 "key": "{$source['key']}", 169 "value": "{$cn}", 170 "locale": "zh-CN", 171 "translatableContentDigest": "{$source['digest']}" 172 }, 173 { 174 "key": "{$source['key']}", 175 "value": "{$ja}", 176 "locale": "ja", 177 "translatableContentDigest": "{$source['digest']}" 178 } 179 ] 180 } 181 } 182 EOF; 183 $ret = $this->sendRequest( $data, 'post', 'graphql.json', [], false, 'json' ); 184 if( $ret ) { 185 M('shopifyTranslate')->where( ['id' => $_item['id']] )->save(['synch' => 1]); 186 var_dump( $_item['id'] . ' ok ..' ); 187 // return true; 188 usleep(500); 189 }else { 190 var_dump( $_item['id'] . ' fail ..' . $ret ); 191 // return false; 192 } 193 } 194 195 } 196 197 public function setToDb() 198 { 199 $types = $this->getTranslateType(); 200 foreach( $types as $type ) { 201 $this->getTranslatableResources( $type, self::CHINESE ); 202 $this->parseDataToDb( $type, self::CHINESE ); 203 } 204 } 205 206 //解析数据入库 207 public function parseDataToDb( $type, $locale ) 208 { 209 $this->_setLog( 'parseDataToDb start' . json_encode( func_get_args() ) ); 210 $cacheFile = DATA_PATH . "ShopifyTranslate:to{$locale}:{$type}"; 211 if( !file_exists( $cacheFile ) ) { 212 $this->_setLog( 'err :: 获取原始数据失败' . json_encode( func_get_args() ), Log::WARN ); 213 return false; 214 } 215 216 $fhandle = fopen( $cacheFile, 'r+' ); 217 if( !$fhandle ) { 218 $this->_setLog( 'err :: 打开文件失败' . $cacheFile, Log::WARN ); 219 return false; 220 } 221 222 $preData = []; 223 $totals = 0; 224 while( ($line = fgets( $fhandle )) !== false ) { 225 $ret = json_decode( $line, true ); 226 if( !$ret ) { 227 $this->_setLog( 'err :: 读取文件异常' . $cacheFile . ' >>> ' . $line, Log::WARN ); 228 continue; 229 } 230 231 232 if( isset( $ret['data']['translatableResources']['edges'] ) ) { 233 // $this->_setLog( 'parseDataToDb edges count == ' . count( $ret['data']['translatableResources']['edges'] ) ); 234 foreach( $ret['data']['translatableResources']['edges'] as $edges ) { 235 $resourceId = $edges['node']['resourceId']; 236 // $this->_setLog( 'parseDataToDb edges > translatableContent > count == ' . count( $edges['node']['translatableContent'] ) ); 237 foreach( $edges['node']['translatableContent'] as $node ) { 238 if( empty( $node['value'] ) ) 239 continue; 240 241 $preData_key = md5( $node['key'] . $resourceId ); 242 $preData[$preData_key] = [ 243 'en' => $node['value'], 244 'resourceId' => $resourceId, 245 'en_md5' => md5( $node['key'] . $node['value'] . $resourceId . $type ), 246 'source' => json_encode( $node ), 247 'type' => $type, 248 ]; 249 $totals++; 250 } 251 252 // foreach( $edges['node']['translations'] as $node ) { 253 // $preData_key = md5( $node['key'] . $resourceId ); 254 // if( isset( $preData[$preData_key] ) ) 255 // $preData[$preData_key][$node['locale']] = $node['value']; 256 257 // } 258 } 259 }else { 260 $this->_setLog( 'err : 解析结果失败...' . $cacheFile, Log::WARN ); 261 } 262 263 } 264 $this->_setLog( "parseDataToDb totals == {$totals} , preData count == " . count( $preData ), Log::WARN ); 265 266 fclose( $fhandle ); 267 268 $i = 0; 269 foreach( $preData as $_item ) { 270 if( !M('shopifyTranslate')->where( ['en_md5' => $_item['en_md5']] )->find() ) { 271 echo $i++; 272 M('shopifyTranslate')->add( $_item ); 273 } 274 } 275 } 276 277 //获取原始信息 278 private function getTranslatableResources( $type, $locale, $force = false ) 279 { 280 $this->_setLog( 'getTranslatableResources start' . json_encode( func_get_args() ) ); 281 282 $cacheFile = DATA_PATH . "ShopifyTranslate:to{$locale}:{$type}"; 283 if( $force && file_exists( $cacheFile ) ) 284 unlink( $cacheFile ); 285 286 if( !file_exists( $cacheFile ) ) { 287 $hasNextPage = false; 288 $after = ''; 289 do{ 290 $data = <<<EOF 291 { 292 translatableResources(first: 50, resourceType: {$type} {$after}) { 293 edges { 294 cursor 295 node { 296 resourceId 297 translatableContent { 298 key 299 value 300 digest 301 locale 302 } 303 translations(locale: "{$locale}") 304 { 305 locale 306 key 307 value 308 } 309 } 310 } 311 pageInfo { 312 hasNextPage 313 } 314 } 315 } 316 EOF; 317 318 $ret = $this->sendRequest( $data ); 319 $hasNextPage = !empty( $ret['data']['translatableResources']['pageInfo']['hasNextPage'] ) ? true : false; 320 file_put_contents( $cacheFile, json_encode( $ret ) . PHP_EOL, FILE_APPEND ); 321 foreach( $ret['data']['translatableResources']['edges'] as $edges ) { 322 $after = ", after: \"{$edges['cursor']}\""; 323 } 324 sleep(1); 325 }while( $hasNextPage ); 326 } 327 328 $this->_setLog( 'getTranslatableResources over ~' . json_encode( func_get_args() ) ); 329 } 330 331 //get type 332 private function getTranslateType() 333 { 334 $rekey = $this->_rePrefix . '::getTranslateType'; 335 $data = $this->_redis->get( $rekey ); 336 if( $data ) 337 return json_decode( $data, true ); 338 339 $data = <<<EOF 340 { 341 __type(name: "TranslatableResourceType") { 342 enumValues { 343 name 344 } 345 } 346 } 347 EOF; 348 349 $ret = $this->sendRequest( $data ); 350 if( isset( $ret['data']['__type']['enumValues'] ) ) { 351 $data = []; 352 foreach( $ret['data']['__type']['enumValues'] as $_name ) { 353 $data[] = $_name['name']; 354 } 355 $this->_redis->set( $rekey, json_encode( $data ), 86400 ); 356 return $data; 357 }else { 358 $this->_setLog( 'err :: 解析结果失败...' . $ret, Log::WARN ); 359 } 360 } 361 362 363 364 private function sendRequest( $data = '', $method = 'post', $urlExtend = 'graphql.json', $header = [], $needResHeader = false, $contentType = 'graphql' ) 365 { 366 static $tryTimes = 1; 367 $tmpHeader = [ 368 'Cache-Controller: max-age=0', 369 'Authorization: Basic ' . $this->_api['authorization'], 370 'X-Shopify-Storefront-Access-Token: ' . $this->_storefrontToken, 371 'Accept: application/json', 372 ]; 373 if( is_array( $data ) && !empty( $data ) ) { 374 $data = json_encode( $data ); 375 $tmpHeader[] = 'Content-Length: ' . strlen( $data ); 376 }else { 377 $data = (string)$data; 378 } 379 380 if( !empty( $header ) ) { 381 $tmpHeader = array_merge( $tmpHeader, $header ); 382 } 383 384 if( $contentType == 'graphql' ) { 385 $tmpHeader[] = 'Content-Type:application/graphql'; 386 }else { 387 $tmpHeader[] = 'Content-Type:application/' . $contentType; 388 } 389 390 $url = "https://{$this->_api['key']}:{$this->_api['pwd']}@{$this->_api['url']}{$this->_api['version']}/{$urlExtend}"; 391 $this->_setLog( '开始curl '. $method .' ,url == ' . $url . ',, param == ' . $data . ',, header == ' . var_export( $tmpHeader, true ) ); 392 $ret = (new Curl())->execute( $method, $url, $data, '', $tmpHeader, '', '', $needResHeader ); 393 if( is_array( $ret ) || $ret === false ) { 394 $tryTimes++; 395 if( $tryTimes >= 1 ) { 396 $this->_curlLastErr = "send curl error .."; 397 if( isset( $ret[1] ) ) 398 $this->_curlLastErr .= "{$ret[1]}"; 399 $this->_setLog( 'curl 请求shopify失败,请检查! ' . json_encode( $ret ), Log::WARN ); 400 return false; 401 }else { 402 return $this->sendRequest( $method, $urlExtend, $data, $header, $needResHeader ); 403 } 404 } 405 406 $deRet = json_decode( $ret, true ); 407 408 if( $needResHeader ) { 409 if( isset( $deRet['ret']['errors'] ) ) { 410 $this->_curlLastErr = $ret; 411 $this->_setLog( '请求成功,但是返回了失败,.' . $ret, Log::WARN ); 412 return false; 413 } 414 }else { 415 if( isset( $deRet['errors'] ) || isset( $deRet['error'] ) ) { 416 $this->_curlLastErr = $ret; 417 $this->_setLog( '请求成功,但是返回了失败,.' . $ret, Log::WARN ); 418 return false; 419 } 420 } 421 422 // $this->_setLog( '请求成功了,' . $ret ); 423 424 return $deRet; 425 } 426 427 public function htmlMof() 428 { 429 430 header('Access-Control-Allow-Origin: *'); 431 header('Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept'); 432 433 if( substr($_POST['html'], 0, 3) == '<p>' ) { 434 $_POST['html'] = substr( $_POST['html'], 3 ); 435 $_POST['html'] = substr( $_POST['html'], 0, -4 ); 436 } 437 438 $ret = M('shopifyTranslate')->where(['id' => $_POST['id']])->save( ['ja' => $_POST['html'],'mof_time' => time()] ); 439 if( $ret !== false ) { 440 $this->setTranslate( $_POST['id'] ); 441 echo json_encode( ['code' => 1] ); 442 }else 443 echo json_encode( ['code' => 0] ); 444 die; 445 } 446 447 private function _setLog( $msg, $level = Log::NOTICE ) 448 { 449 Log::write( $msg . PHP_EOL, Log::NOTICE, '', durableLog( 'api-shopify-translate' ) ); 450 return; 451 452 if( !$this->_logque instanceof \SplQueue ) 453 $this->_logque = new \SplQueue(); 454 455 $this->_logque->enqueue( $msg . PHP_EOL ); 456 457 if( $level == Log::ERR ) { 458 //send mail 459 exit; 460 } 461 } 462 463 public function __destruct() 464 { 465 if( $this->_logque instanceof \SplQueue ) { 466 $msg = ''; 467 while( !$this->_logque->isEmpty() ) { 468 $msg .= $this->_logque->dequeue(); 469 } 470 Log::write( $msg, Log::NOTICE, '', durableLog( 'api-shopify-translate' ) ); 471 } 472 } 473 474 }