Index: admin_templates/languages/phrase_edit.tpl =================================================================== --- admin_templates/languages/phrase_edit.tpl (revision 14184) +++ admin_templates/languages/phrase_edit.tpl (working copy) @@ -81,8 +81,53 @@ - - + + + + + + + : + +   + + + + + + + + + Index: admin_templates/regional/languages_edit.tpl =================================================================== --- admin_templates/regional/languages_edit.tpl (revision 14184) +++ admin_templates/regional/languages_edit.tpl (working copy) @@ -93,7 +93,7 @@   " name="" value=""> - " type="checkbox" id="_cb_CopyLabels" name="_cb_CopyLabels" onclick="update_checkbox(this, document.getElementById(''))"> + " type="checkbox" id="_cb_CopyLabels" name="_cb_CopyLabels" onclick="update_checkbox(this, document.getElementById('')); reflectAutoTranslateLabels();"> + + + + + + + Index: install/english.lang =================================================================== --- install/english.lang (revision 14184) +++ install/english.lang (working copy) @@ -46,6 +46,7 @@ U2VsZWN0IEFsbA== U2V0IFZhbHVl U2hvdyBTdHJ1Y3R1cmU= + VHJhbnNsYXRl VW5zZWxlY3Q= VXA= VXNl @@ -254,6 +255,8 @@ UmVkaXJlY3QgdG8gSFRUUCB3aGVuIFNTTCBpcyBub3QgcmVxdWlyZWQ= RnVsbCBpbWFnZSBIZWlnaHQ= RnVsbCBpbWFnZSBXaWR0aA== + R29vZ2xlIFRyYW5zbGF0ZSBBUEkgS2V5 + R29vZ2xlIFRyYW5zbGF0ZSBBUEkgVXJs QnlwYXNzIEhUVFAgQXV0aGVudGljYXRpb24gZnJvbSBJUHMgKHNlcGFyYXRlZCBieSBzZW1pY29sb25zKQ== UGFzc3dvcmQgZm9yIEhUVFAgQXV0aGVudGljYXRpb24= VXNlcm5hbWUgZm9yIEhUVFAgQXV0aGVudGljYXRpb24= @@ -328,6 +331,7 @@ RXJyb3IgY29weWluZyBzdWJzZWN0aW9ucw== Q3VzdG9tIGZpZWxkIHdpdGggaWRlbnRpY2FsIG5hbWUgYWxyZWFkeSBleGlzdHM= RmlsZSBpcyB0b28gbGFyZ2U= + WW91IGFyZSBtaXNzaW5nIHlvdXIgR29vZ2xlIFRyYW5zbGF0ZSBBUEkgS2V5LiBQbGVhc2UgZW50ZXIgaXQgaW4gb3JkZXIgdG8gdXNlIEdvb2dsZSBUcmFuc2xhdGlvbiBzZXJ2aWNlIQ== Z3JvdXAgbm90IGZvdW5k SW52YWxpZCBGaWxlIEZvcm1hdA== aW52YWxpZCBvcHRpb24= @@ -336,6 +340,7 @@ RXJyb3IgbW92aW5nIHN1YnNlY3Rpb24= Q2FuJ3QgaW5oZXJpdCB0ZW1wbGF0ZSBmcm9tIHRvcCBjYXRlZ29yeQ== Tm8gbWF0Y2hpbmcgY29sdW1ucyBhcmUgZm91bmQ= + U29ycnksIG5vIHRyYW5zbGF0aW9uIGlzIGF2YWlsYWJsZSE= VGhpcyBvcGVyYXRpb24gaXMgbm90IGFsbG93ZWQh VmFsaWRhdGlvbiBlcnJvciwgcGxlYXNlIGRvdWJsZS1jaGVjayBJbi1Qb3J0YWwgdGFncw== UGFzc3dvcmRzIGRvIG5vdCBtYXRjaCE= @@ -389,6 +394,7 @@ QXR0YWNobWVudA== QXV0byBDcmVhdGUgRmlsZSBOYW1l QXV0b21hdGljIEZpbGVuYW1l + VHJhbnNsYXRlIGFmdGVyIENvcHlpbmc= QXZhaWxhYmxlIENvbHVtbnM= QmFja2dyb3VuZA== QmFja2dyb3VuZCBBdHRhY2htZW50 @@ -491,6 +497,7 @@ RnJvbSBFbWFpbA== RnJvbnQtRW5kIE9ubHk= QWxsb3cgUmVnaXN0cmF0aW9uIG9uIEZyb250LWVuZA== + R29vZ2xlIFRyYW5zbGF0aW9u VXNlciBHcm91cA== SUQ= R3JvdXAgTmFtZQ== @@ -697,6 +704,7 @@ SGVhZCBGcmFtZQ== SGlkZQ== QWxsIEZpbGVz + QWxsIExhYmVscyB3aWxsIGJlIGF1dG8tdHJhbnNsYXRlZCB1c2luZyBHb29nbGUgVHJhbnNsYXRlIFNlcnZpY2Ugb25jZSBuZXcgbGFuZ3VhZ2UgaXMgY3JlYXRlZCBhbmQgbGFiZWxzIGNvcGllZC4gQXV0by10cmFuc2xhdGUgd2lsbCB3b3JrIG9ubHkgaWYgIkNvcHkgTGFiZWxzIGZyb20gdGhpcyBMYW5ndWFnZSIgb3B0aW9uIGlzIGNoZWNrZWQu Q1NWIEZpbGVz U2luZ2xlIElQIG9yIHJhbmdlIHJlY29yZCBwZXIgbGluZSAoZm9ybWF0czogMS4yLjMuNCBvciAxLjIuMyBvciAxLjIuMy4zMi0xLjIuMy41NCBvciAxLjIuMy4zMi8yNyBvciAxLjIuMy4zMi8yNTUuMjU1LjI1NS4yMjQp U2luZ2xlIEVtYWlsIEV2ZW50IHBlciBsaW5lIChmb3JtYXRzOiBVU0VSLkFERCwgT1JERVIuU1VCTUlUKQ== @@ -1075,6 +1083,7 @@ UmVwbGFjZW1lbnQgVGFncw== U2VuZGVyIEluZm9ybWF0aW9u U2V0dGluZ3M= + M3JkIFBhcnR5IEFQSSBTZXR0aW5ncw== QWRtaW4gQ29uc29sZSBTZXR0aW5ncw== Q2FjaGluZyBTZXR0aW5ncw== Q1NWIEV4cG9ydCBTZXR0aW5ncw== Index: install/install_data.sql =================================================================== --- install/install_data.sql (revision 14184) +++ install/install_data.sql (working copy) @@ -94,6 +94,8 @@ INSERT INTO ConfigurationValues VALUES(DEFAULT, 'CSVExportEncoding', '0', 'In-Portal', 'in-portal:configure_advanced', 'la_section_SettingsCSVExport', 'la_config_CSVExportEncoding', 'radio', NULL, '0=la_Unicode||1=la_Regular', 70.04, 0, 1, NULL); INSERT INTO ConfigurationValues VALUES(DEFAULT, 'CacheHandler', 'Fake', 'In-Portal', 'in-portal:configure_advanced', 'la_section_SettingsCaching', 'la_config_CacheHandler', 'select', NULL, 'Fake=la_None||Memcache=+Memcached||Apc=+Alternative PHP Cache||XCache=+XCache', 80.01, 0, 0, NULL); INSERT INTO ConfigurationValues VALUES(DEFAULT, 'MemcacheServers', 'localhost:11211', 'In-Portal', 'in-portal:configure_advanced', 'la_section_SettingsCaching', 'la_config_MemcacheServers', 'text', NULL, '', 80.02, 0, 0, 'la_hint_MemcacheServers'); +INSERT INTO ConfigurationValues VALUES(DEFAULT, 'GoogleTranslateApiUrl', 'https://www.googleapis.com/language/translate/v2', 'In-Portal', 'in-portal:configure_advanced', 'la_section_Settings3rdPartyApi', 'la_config_GoogleTranslateApiUrl', 'text', NULL, '', 90.01, 0, 0, NULL); +INSERT INTO ConfigurationValues VALUES(DEFAULT, 'GoogleTranslateApiKey', '', 'In-Portal', 'in-portal:configure_advanced', 'la_section_Settings3rdPartyApi', 'la_config_GoogleTranslateApiKey', 'text', NULL, '', 90.02, 0, 0, NULL); # Section "in-portal:configure_users": INSERT INTO ConfigurationValues VALUES(DEFAULT, 'User_Allow_New', '3', 'In-Portal:Users', 'in-portal:configure_users', 'la_title_General', 'la_users_allow_new', 'radio', '', '1=la_opt_UserInstantRegistration||2=la_opt_UserNotAllowedRegistration||3=la_opt_UserUponApprovalRegistration||4=la_opt_UserEmailActivation', 10.01, 0, 1, NULL); Index: units/languages/languages_config.php =================================================================== --- units/languages/languages_config.php (revision 14184) +++ units/languages/languages_config.php (working copy) @@ -216,6 +216,7 @@ 'formatter' => 'kOptionsFormatter', 'options_sql' => 'SELECT %s FROM ' . TABLE_PREFIX . 'Language ORDER BY PackName', 'option_title_field' => 'PackName', 'option_key_field' => 'LanguageId', 'default' => '', ), + 'AutoTranslateLabels' => Array ('type' => 'int', 'default' => 0), ), 'Grids' => Array( Index: units/languages/languages_event_handler.php =================================================================== --- units/languages/languages_event_handler.php (revision 14184) +++ units/languages/languages_event_handler.php (working copy) @@ -249,10 +249,10 @@ $object =& $event->getObject(); /* @var $object kDBItem */ - $src_language = $object->GetDBField('CopyFromLanguage'); + $src_language_id = $object->GetDBField('CopyFromLanguage'); - if ($object->GetDBField('CopyLabels') && $src_language) { - $dst_language = $object->GetID(); + if ($object->GetDBField('CopyLabels') && $src_language_id) { + $dst_language_id = $object->GetID(); // 1. schedule data copy after OnSave event is executed $var_name = $event->getPrefixSpecial() . '_copy_data' . $this->Application->GetVar('m_wid'); @@ -261,10 +261,25 @@ if ($pending_actions) { $pending_actions = unserialize($pending_actions); } - - $pending_actions[$src_language] = $dst_language; + + $pending_actions[$src_language_id] = Array( 'target_id' => $dst_language_id ); + + if ( $object->GetDBField('AutoTranslateLabels') ) { + $live_table = $this->Application->getUnitOption($event->Prefix, 'TableName'); + $sql = 'SELECT Locale FROM ' . $live_table . ' + WHERE ' . $object->IDField. ' = ' . $src_language_id; + $src_lang_locale = $this->Conn->GetOne($sql); + + if ( $src_lang_locale ) { + $pending_actions[$src_language_id]['source_locale'] = strstr($src_lang_locale, '-', true); // save the locale for later + $pending_actions[$src_language_id]['target_locale'] = strstr($object->GetDBField('Locale'), '-', true); // save the locale for later + } + } + $this->Application->StoreVar($var_name, serialize($pending_actions)); + $object->SetDBField('CopyLabels', 0); + $object->SetDBField('AutoTranslateLabels', 0); } } @@ -280,6 +295,9 @@ if ($event->status != erSUCCESS) { return ; } + + $object =& $event->getObject(); + /* @var $object kDBItem */ $var_name = $event->getPrefixSpecial() . '_copy_data' . $this->Application->GetVar('m_wid'); $pending_actions = $this->Application->RecallVar($var_name, Array ()); @@ -293,7 +311,9 @@ $ml_helper->createFields('phrases'); $ml_helper->createFields('emailevents'); - foreach ($pending_actions as $src_language => $dst_language) { + foreach ($pending_actions as $src_language => $lang_data) { + $dst_language = $lang_data['target_id']; + // phrases import $sql = 'UPDATE ' . $this->Application->getUnitOption('phrases', 'TableName') . ' SET l' . $dst_language . '_Translation = l' . $src_language . '_Translation'; @@ -305,6 +325,14 @@ l' . $dst_language . '_Subject = l' . $src_language . '_Subject, l' . $dst_language . '_Body = l' . $src_language . '_Body'; $this->Conn->Query($sql); + + if ( isset($lang_data['source_locale']) ) { + // perform the translations + $event->setEventParam('source_locale', $lang_data['source_locale']); + $event->setEventParam('target_locale', $lang_data['target_locale']); + $event->setEventParam('target_lang_id', $dst_language); + $event->CallSubEvent('OnTranslateLanguagePack'); + } } $this->Application->RemoveVar($var_name); @@ -314,6 +342,137 @@ } /** + * Performs actual translation and data update + * + * @param string $prefix + * @param array $data + * @param int $lang_id + * @param string $source_locale + * @param string $target_locale + */ + function _performTranslation($prefix, $data, $lang_id, $source_locale, $target_locale) + { + $post_data = ''; + $counter = 0; + $posting_limits = ($prefix == 'phrases')? 100 : 50; // limits are: 100 phrases or 50 email events (subject + body) + $table = $this->Application->getUnitOption($prefix, 'TableName'); + + $url = $this->Application->ConfigValue('GoogleTranslateApiUrl') . '?key=' . $this->Application->ConfigValue('GoogleTranslateApiKey') . '&prettyprint=true&source=' . $source_locale . '&target=' . $target_locale; + $curl_helper =& $this->Application->recallObject('CurlHelper'); + /* @var $curl_helper kCurlHelper */ + + foreach ($data as $data_id => $source) { + if ($prefix == 'phrases') { + $post_data .= 'q=' . rawurlencode($source) . '&'; + } + else { + $post_data .= 'q=' . rawurlencode($source['l'.$lang_id.'_Subject']) . '&q=' . rawurlencode($source['l'.$lang_id.'_Body']) . '&'; // email subject and boby + } + + $counter++; + $ids[] = $data_id; // save ids we are going to translate in this round for later + + if ( count($ids) == $posting_limits || count($data) == $counter || strlen($post_data) > 4500) { + // with single Curl request process 100 or phrases or 4.5K (API POST data limits) + $curl_helper->followLocation = true; + $curl_helper->SetHeader('X-HTTP-Method-Override', 'GET'); // see - http://code.google.com/apis/language/translate/v2/using_rest.html#WorkingResults + $curl_helper->SetPostData($post_data); + + $response = $curl_helper->Send( $url ); + + if ( $response ) { + $parsed_response = json_decode($response, true); + $translations = $parsed_response['data']['translations']; // array of translations + + if ( isset($translations) && is_array($translations) ) { + $where_case = ''; + $where_case1 = ''; + + foreach ($translations as $index => $translation) { + // build multiple translation updates into one SQL + if ( $translation ) { + if ($prefix == 'phrases') { + $where_case .= ' WHEN ' . $ids[ $index ] . ' THEN ' . $this->Conn->qstr($translation['translatedText']); + } + elseif ($prefix == 'emailevents') { + // do this go through all odd array records to be case for body + // since we are processing both subject and body that belong to one event record + $even = $index % 2? FALSE : TRUE; + if ( $even ) { + $event_id = $index / 2; + $where_case .= ' WHEN ' . $ids[ $event_id ] . ' THEN ' . $this->Conn->qstr($translation['translatedText']); + } + else { + $where_case1 .= ' WHEN ' . $ids[ $event_id ] . ' THEN ' . $this->Conn->qstr($translation['translatedText']); + } + } + } + else { + // translation is missing + unset($ids[ $index ]); + } + } + + if ($prefix == 'phrases' && $where_case) { + $sql = 'UPDATE ' . $table . ' + SET l' . $lang_id . '_Translation = CASE PhraseId ' . $where_case . ' END + WHERE PhraseId IN (' . implode(',', $ids) . ')'; + $this->Conn->Query($sql); + } + elseif ($prefix == 'emailevents' && ($where_case || $where_case1)) { + $sql = 'UPDATE ' . $table . ' + SET l' . $lang_id . '_Subject = CASE EventId ' . $where_case . ' END, l' . $lang_id . '_Body = CASE EventId ' . $where_case1 . ' END + WHERE EventId IN (' . implode(',', $ids) . ')'; + $this->Conn->Query($sql); + } + } + // reset variables so we can prepare a new request + $post_data = ''; + unset($ids); + } + } + } + + } + + /** + * Auto-translate language labels using Google Translate API + * + * @param kEvent $event + */ + function OnTranslateLanguagePack(&$event) + { + $lang_id = $event->getEventParam('target_lang_id'); + $source_locale = $event->getEventParam('source_locale'); + $target_locale = $event->getEventParam('target_locale'); + + // 1. translate language phrases + $table = $this->Application->getUnitOption('phrases', 'TableName'); + $sql = 'SELECT l' . $lang_id . '_Translation, PhraseId + FROM ' . $table . ' + WHERE l' . $lang_id . '_Translation != \'\' AND l' . $lang_id . '_Translation IS NOT NULL + ORDER BY PhraseId'; + + $phrases_to_translate = $this->Conn->GetCol($sql, 'PhraseId'); + if ( $phrases_to_translate ) { + $this->_performTranslation('phrases', $phrases_to_translate, $lang_id, $source_locale, $target_locale); + } + + // 2. translate language events + $table = $this->Application->getUnitOption('emailevents', 'TableName'); + $sql = 'SELECT l' . $lang_id . '_Subject, l' . $lang_id . '_Body, EventId + FROM ' . $table . ' + WHERE (l' . $lang_id . '_Subject != \'\' AND l' . $lang_id . '_Subject IS NOT NULL) OR + (l' . $lang_id . '_Body != \'\' AND l' . $lang_id . '_Body IS NOT NULL) + ORDER BY EventId'; + + $events_to_translate = $this->Conn->Query($sql, 'EventId'); + if ( $events_to_translate ) { + $this->_performTranslation('emailevents', $events_to_translate, $lang_id, $source_locale, $target_locale); + } + } + + /** * Prepare temp tables for creating new item * but does not create it. Actual create is * done in OnPreSaveCreated Index: units/phrases/phrases_config.php =================================================================== --- units/phrases/phrases_config.php (revision 14184) +++ units/phrases/phrases_config.php (working copy) @@ -159,6 +159,12 @@ ), 'ExportPhrases' => Array ('type' => 'string', 'default' => ''), 'ExportEmailEvents' => Array ('type' => 'string', 'default' => ''), + + 'GoogleTranslation' => Array ('type' => 'string', 'default' => ''), + 'GoogleTranslationTargetLanguage' => Array ( + 'type' => 'string', + 'formatter' => 'kOptionsFormatter', 'options_sql' => 'SELECT PackName, SUBSTR(Locale, 1, 2) AS Target FROM ' . TABLE_PREFIX . 'Language WHERE (Locale != "") ORDER BY PrimaryLang DESC, Priority DESC', 'option_key_field' => 'Target', 'option_title_field' => 'PackName', + ), ), 'Grids' => Array ( Index: units/phrases/phrases_event_handler.php =================================================================== --- units/phrases/phrases_event_handler.php (revision 14184) +++ units/phrases/phrases_event_handler.php (working copy) @@ -55,6 +55,11 @@ return true; } } + + if ($this->Application->isAdmin && $event->Name == 'OnGetGoogleTranslation') { + // request to google translation api + return true; + } return parent::CheckPermission($event); } @@ -329,4 +334,31 @@ // use language from grid, instead of primary language used by default $event->SetRedirectParam('m_lang', $this->Application->GetVar('m_lang')); } + + /** + * Send request to Google Translate API to translate text + * + * @param kEvent $event + */ + function OnGetGoogleTranslation(&$event) + { + $curl_helper =& $this->Application->recallObject('CurlHelper'); + /* @var $curl_helper kCurlHelper */ + + $target_locale = $this->Application->GetVar('target_locale'); + $to_translate = rawurlencode( $this->Application->GetVar('q') ); + + $request_url = $this->Application->ConfigValue('GoogleTranslateApiUrl') . '?key=' . $this->Application->ConfigValue('GoogleTranslateApiKey') . '&target=' . $target_locale . '&q=' . $to_translate; + + $response = $curl_helper->Send($request_url); + + if ( $response ) { + $parsed_response = json_decode($response, true); + if ( isset($parsed_response['data']['translations'][0]['translatedText']) ) { + echo $parsed_response['data']['translations'][0]['translatedText']; + } + } + + $event->status = erSTOP; + } } \ No newline at end of file