From abc87f6cc7dfe0abac347a20f20a2ed44959545b Mon Sep 17 00:00:00 2001 From: shnsumit Date: Wed, 29 Apr 2020 20:57:23 +0530 Subject: [PATCH 1/7] ACCL-79: Static string translation --- src/Translations.php | 18 ++ src/assetbundles/StaticTranslationsAssets.php | 32 +++ src/assetbundles/src/js/StaticTranslations.js | 49 ++++ .../StaticTranslationsController.php | 57 ++++ src/elements/StaticTranslations.php | 271 ++++++++++++++++++ src/elements/db/StaticTranslationQuery.php | 49 ++++ src/services/App.php | 8 +- .../StaticTranslationsRepository.php | 208 ++++++++++++++ src/templates/static-translations/index.twig | 60 ++++ 9 files changed, 751 insertions(+), 1 deletion(-) create mode 100644 src/assetbundles/StaticTranslationsAssets.php create mode 100644 src/assetbundles/src/js/StaticTranslations.js create mode 100644 src/controllers/StaticTranslationsController.php create mode 100644 src/elements/StaticTranslations.php create mode 100644 src/elements/db/StaticTranslationQuery.php create mode 100644 src/services/repository/StaticTranslationsRepository.php create mode 100644 src/templates/static-translations/index.twig diff --git a/src/Translations.php b/src/Translations.php index 8a03a458..fddaaae5 100644 --- a/src/Translations.php +++ b/src/Translations.php @@ -245,6 +245,12 @@ public function getCpNavItem() 'url' => 'translations/translators', ]; } + if ($currentUser->can('translations:static-translations')) { + $subNavs['static-translations'] = [ + 'label' => 'Static Translations', + 'url' => 'translations/static-translations', + ]; + } if ($currentUser->can('translations:settings')) { $subNavs['settings'] = [ @@ -343,6 +349,7 @@ private function _registerCpRoutes() 'translations/settings/send-logs' => 'translations/settings/send-logs', 'translations/orders/get-file-diff/' => 'translations/base/get-file-diff', 'translations/settings/configuration-options' => 'translations/settings/configuration-options', + 'translations/static-translations' => 'translations/static-translations', ]); } ); @@ -583,6 +590,17 @@ protected function customAdminCpPermissions(): array ] ] ], + 'translations:static-translations' => [ + 'label' => Craft::t('translations', 'Static Translations'), + 'nested' => [ + 'translations:static-translations:import' => [ + 'label' => Craft::t('translations', 'Import'), + ], + 'translations:static-translations:export' => [ + 'label' => Craft::t('translations', 'Export'), + ] + ] + ], 'translations:orders' => [ 'label' => Craft::t('translations', 'View Orders'), 'nested' => [ diff --git a/src/assetbundles/StaticTranslationsAssets.php b/src/assetbundles/StaticTranslationsAssets.php new file mode 100644 index 00000000..8bf4df4b --- /dev/null +++ b/src/assetbundles/StaticTranslationsAssets.php @@ -0,0 +1,32 @@ +sourcePath = '@acclaro/translations/assetbundles/src'; + + $this->depends = [ + CpAsset::class, + ]; + + $this->js = [ + 'js/StaticTranslations.js', + ]; + + parent::init(); + } +} \ No newline at end of file diff --git a/src/assetbundles/src/js/StaticTranslations.js b/src/assetbundles/src/js/StaticTranslations.js new file mode 100644 index 00000000..b738b6dc --- /dev/null +++ b/src/assetbundles/src/js/StaticTranslations.js @@ -0,0 +1,49 @@ +(function($) { + +if (typeof Craft.Translations === 'undefined') { + Craft.Translations = {}; +} + +Craft.Translations.StaticTranslations = { + + saveStaticTranslation: function() { + + var data = $("#static-translation").serializeArray(); + data.push( + {name: 'source', value: Craft.elementIndex.sourceKey}, + {name: 'siteId', value: Craft.elementIndex.siteId} + ); + Craft.postActionRequest('translations/static-translations/save', data, $.proxy(function(response, textStatus) { + if (textStatus === 'success') { + if (response.success) { + Craft.cp.displayNotice(Craft.t('app', 'Static Translations saved.')); + Craft.elementIndex.updateElements(); + } + } else { + Craft.cp.displayError(Craft.t('app', 'An unknown error occurred.')); + } + + $('.save-static-translation').removeClass('disabled'); + $('.save-static-translation').attr("disabled", false); + + }, this)); + }, + + init: function() { + var self = this; + $('.sortmenubtn').hide(); + $('.statusmenubtn').hide(); + + $('.save-static-translation').on('click', function(e) { + + $('.save-static-translation').addClass('disabled'); + $('.save-static-translation').attr("disabled", true); + + e.preventDefault(); + console.log(Craft.elementIndex); + self.saveStaticTranslation(); + }); + }, +}; + +})(jQuery); \ No newline at end of file diff --git a/src/controllers/StaticTranslationsController.php b/src/controllers/StaticTranslationsController.php new file mode 100644 index 00000000..9ac613dd --- /dev/null +++ b/src/controllers/StaticTranslationsController.php @@ -0,0 +1,57 @@ +requireLogin(); + $this->renderTemplate('translations/static-translations/index', $variables); + } + + /** + * @return \yii\web\Response + * @throws \yii\web\BadRequestHttpException + */ + public function actionSave() { + + $this->requirePostRequest(); + + $siteId = Craft::$app->request->getRequiredBodyParam('siteId'); + $site = Craft::$app->getSites()->getSiteById($siteId); + $lang = $site->language; + $translations = Craft::$app->request->getRequiredBodyParam('translation'); + + Translations::$plugin->staticTranslationsRepository->set($lang, $translations); + + return $this->asJson([ + 'success' => true, + 'errors' => [] + ]); + } +} \ No newline at end of file diff --git a/src/elements/StaticTranslations.php b/src/elements/StaticTranslations.php new file mode 100644 index 00000000..437bba3d --- /dev/null +++ b/src/elements/StaticTranslations.php @@ -0,0 +1,271 @@ +translator->translate('app', 'Static Translation'); + } + + public function __toString() + { + try{ + return $this->original; + } catch (\Exception $e) { + ErrorHandler::convertExceptionToError($e); + } + } + + /** + * @inheritdoc + */ + public static function isLocalized(): bool + { + return true; + } + + /** + * @inheritdoc + */ + public static function hasStatuses(): bool + { + return true; + } + + /** + * @return string + */ + public function getTranslateStatus() + { + if ($this->original != $this->translation) { + return static::TRANSLATED; + } + + return static::UNTRANSLATED; + } + + /** + * @param string|null $context + * @return array + */ + protected static function defineSources(string $context = null): array + { + $sources = []; + $sources[] = ['heading' => Craft::t('app','Status')]; + + $sources[] = [ + 'status' => 'orange', + 'key' => 'status:' . self::UNTRANSLATED, + 'label' => Craft::t('app','Untranslated'), + 'criteria' => [ + 'source' => [ + Craft::$app->path->getSiteTemplatesPath() + ], + 'translateStatus' => self::UNTRANSLATED + ], + ]; + + $sources[] = [ + 'status' => 'green', + 'key' => 'status:' . self::TRANSLATED, + 'label' => Craft::t('app', 'Translated'), + 'criteria' => [ + 'source' => [ + Craft::$app->path->getSiteTemplatesPath() + ], + 'translateStatus' => self::TRANSLATED + ], + ]; + + // Get all template files + $templateSourceFiles = []; + $options = [ + 'recursive' => false, + 'only' => ['*.html','*.twig','*.js','*.json','*.atom','*.rss'], + 'except' => ['vendor/', 'node_modules/'] + ]; + $allFiles = FileHelper::findFiles(Craft::$app->path->getSiteTemplatesPath(), $options); + + foreach ($allFiles as $file) { + $fileName = basename($file); + $templateKey = str_replace('/', '*', $file); + $templateSourceFiles['templatessources:'.$fileName] = [ + 'label' => $fileName, + 'key' => 'templates:'.$templateKey, + 'criteria' => [ + 'source' => [ + $file + ], + ], + ]; + } + + // Other Template folders & files + $options = [ + 'recursive' => false, + 'except' => ['vendor/', 'node_modules/'] + ]; + $allFiles = FileHelper::findDirectories(Craft::$app->path->getSiteTemplatesPath(), $options); + foreach ($allFiles as $file) { + $fileName = basename($file); + $templateKey = str_replace('/', '*', $file); + $templateSourceFiles['templatessources:'.$fileName] = [ + 'label' => $fileName.'/', + 'key' => 'templates:'.$templateKey, + 'criteria' => [ + 'source' => [ + $file + ], + ], + ]; + } + + $sources[] = ['heading' => Craft::t('app','Templates')]; + $sources[] = [ + 'label' => Craft::t('app', 'Templates'), + 'key' => 'all-templates:', + 'criteria' => [ + 'source' => [ + Craft::$app->path->getSiteTemplatesPath() + ] + ], + 'nested' => $templateSourceFiles + ]; + + return $sources; + } + + /** + * @inheritdoc + */ + protected static function defineSearchableAttributes(): array + { + return [ + 'original', + 'translation', + 'source', + 'file', + 'status', + 'locale' + ]; + } + + /** + * @inheritdoc + */ + protected function tableAttributeHtml(string $attribute): string + { + return $this->$attribute; + } + + /** + * @inheritdoc + */ + protected static function defineTableAttributes(): array + { + $primary = Craft::$app->getSites()->getPrimarySite(); + $lang = Craft::$app->getI18n()->getLocaleById($primary->language); + $attributes['original'] = ['label' => Translations::$plugin->translator->translate('app', "Source: $lang->displayName ($primary->language)")]; + $attributes['field'] = ['label' => Craft::t('app','Target: Translation')]; + + return $attributes; + } + + public static function find(): ElementQueryInterface + { + return new StaticTranslationQuery(get_called_class()); + } + + /** + * @inheritdoc + */ + public static function indexHtml(ElementQueryInterface $elementQuery, array $disabledElementIds = null, array $viewState, string $sourceKey = null, string $context = null, bool $includeContainer, bool $showCheckboxes): string + { + + // if not getting siteId then set primary site id + if (empty($elementQuery->siteId)) { + $primarySite = Craft::$app->getSites()->getPrimarySite(); + $elementQuery->siteId = $primarySite->id; + } + + if ($elementQuery->translateStatus) { + $elementQuery->status = $elementQuery->translateStatus; + } + + $elements = Translations::$plugin->staticTranslationsRepository->get($elementQuery); + + $variables = [ + 'viewMode' => $viewState['mode'], + 'context' => $context, + 'disabledElementIds' => $disabledElementIds, + 'attributes' => Craft::$app->getElementIndexes()->getTableAttributes(static::class, $sourceKey), + 'elements' => $elements, + 'showCheckboxes' => $showCheckboxes + ]; + + Craft::$app->view->registerJs("$('table.fullwidth thead th').css('width', '50%');"); + Craft::$app->view->registerJs("$('.buttons.hidden').removeClass('hidden');"); + + $template = '_elements/'.$viewState['mode'].'view/'.($includeContainer ? 'container' : 'elements'); + + return Craft::$app->view->renderTemplate($template, $variables); + } + + /** + * @return null|string + */ + public function getLocale() + { + $site = Craft::$app->getSites()->getSiteById($this->siteId); + + return $site->language; + } + + public static function statuses(): array + { + return [ + self::TRANSLATED => ['label' => ucfirst(self::TRANSLATED), 'color' => 'green'], + self::UNTRANSLATED => ['label' => ucfirst(self::UNTRANSLATED), 'color' => 'orange'], + ]; + } +} diff --git a/src/elements/db/StaticTranslationQuery.php b/src/elements/db/StaticTranslationQuery.php new file mode 100644 index 00000000..73e78a65 --- /dev/null +++ b/src/elements/db/StaticTranslationQuery.php @@ -0,0 +1,49 @@ +source = $value; + } + + /** + * @inheritdoc + */ + public function getStatus() + { + return $this->status; + } + + /** + * @inheritdoc + */ + protected function beforePrepare(): bool + { + return false; + } +} \ No newline at end of file diff --git a/src/services/App.php b/src/services/App.php index 70334efa..bb6cbcbf 100644 --- a/src/services/App.php +++ b/src/services/App.php @@ -99,7 +99,12 @@ class App extends Component * @var repository\UserRepository */ public $userRepository; - + + /** + * @var repository\StaticTranslationsRepository + */ + public $staticTranslationsRepository; + /** * @var WordCounter */ @@ -148,6 +153,7 @@ public function init() $this->orderRepository = new repository\OrderRepository(); $this->translatorRepository = new repository\TranslatorRepository(); $this->userRepository = new repository\UserRepository(); + $this->staticTranslationsRepository = new repository\StaticTranslationsRepository(); $this->wordCounter = new WordCounter(); $this->fieldTranslatorFactory = new fieldtranslator\Factory(); $this->translatorFactory = new translator\Factory(); diff --git a/src/services/repository/StaticTranslationsRepository.php b/src/services/repository/StaticTranslationsRepository.php new file mode 100644 index 00000000..81460524 --- /dev/null +++ b/src/services/repository/StaticTranslationsRepository.php @@ -0,0 +1,208 @@ +source) && !is_array($query->source)) { + $query->source = [$query->source]; + } + + foreach ($query->source as $filePath) { + // Check if source path is folder or file + if (is_dir($filePath)) { + $options = [ + 'recursive' => true, + 'only' => ['*.php','*.html','*.twig','*.js','*.json','*.atom','*.rss'], + 'except' => ['vendor/', 'node_modules/'] + ]; + $files = FileHelper::findFiles($filePath, $options); + foreach ($files as $file) { + + // process file + $elements = $this->getFileStrings($filePath, $file, $query, $category, $elementId); + $translations = array_merge($translations, $elements); + } + } elseif (file_exists($filePath)) { + + // process file + $elements = $this->getFileStrings($filePath, $filePath, $query, $category, $elementId); + $translations = array_merge($translations, $elements); + } + } + + return $translations; + } + + /** + * Search in file + * + * @param $path + * @param $file + * @param ElementQueryInterface $query + * @param $category + * @param $elementId + * @return array + */ + private function getFileStrings($path, $file, ElementQueryInterface $query, $category, &$elementId) + { + $translations = []; + $contents = file_get_contents($file); + $extension = pathinfo($file, PATHINFO_EXTENSION); + + $fileOptions = $this->getExpressions($extension); + + if (!isset($fileOptions['regex'])) return []; + + foreach ($fileOptions['regex'] as $regex) { + if (preg_match_all($regex, $contents, $matches)) { + $position = $fileOptions['position']; + foreach ($matches[$position] as $original) { + $translateId = ElementHelper::createSlug($original); + $view = Craft::$app->getView(); + $site = Craft::$app->getSites()->getSiteById($query->siteId); + $translation = Craft::t($category, $original, null, $site->language); + + $field = $view->renderTemplate('_includes/forms/text', [ + 'id' => $translateId, + 'name' => 'translation['.$original.']', + 'value' => $translation, + 'placeholder' => $translation, + ]); + + $element = new StaticTranslations([ + 'id' => $elementId, + 'translateId' => ElementHelper::createSlug($original), + 'original' => $original, + 'translation' => $translation, + 'source' => $path, + 'file' => $file, + 'siteId' => $query->siteId, + 'field' => $field, + ]); + + $elementId++; + if ($query->status && $query->status != $element->getTranslateStatus()) { + continue; + } + if ($query->search && !stristr($element->original, $query->search) && !stristr($element->translation, $query->search)) { + continue; + } + + if ($query->id) + { + foreach ($query->id as $id) { + if ($element->id == $id) { + $translations[$element->original] = $element; + } + } + } + else{ + $translations[$element->original] = $element; + } + } + } + } + + return $translations; + } + + /** + * @param $ext + * @return array + */ + public function getExpressions($ext) { + + $exp = []; + switch ($ext) { + case 'php': + $exp = [ + 'position' => '3', + 'regex' => [ + '/Craft::(t|translate)\(.*?\'(.*?)\'.*?\,.*?\'(.*?)\'.*?\)/', + '/Craft::(t|translate)\(.*?"(.*?)".*?\,.*?"(.*?)".*?\)/', + ] + ]; + break; + case 'twig': + case 'html': + case 'atom': + case 'rss': + $exp = [ + 'position' => '1', + 'regex' => [ + '/\'((?:[^\']|\\\\\')*)\'\s*\|\s*t(?:ranslate)?\b/', + '/"((?:[^"]|\\\\")*)"\s*\|\s*t(?:ranslate)?\b/', + ] + ]; + break; + case 'js': + $exp = [ + 'position' => '3', + 'regex' => [ + '/Craft\.(t|translate)\(.*?\'(.*?)\'.*?\,.*?\'(.*?)\'.*?\)/', + '/Craft::(t|translate)\(.*?"(.*?)".*?\,.*?"(.*?)".*?\)/', + ] + ]; + break; + } + + return $exp; + } + + /** + * @param $lang + * @param array $fileContent + * @return bool + * @throws Exception + */ + public function set($lang, array $fileContent) + { + try { + // get translation file path + $sitePath = Craft::$app->getPath()->getSiteTranslationsPath(); + $file = $sitePath.DIRECTORY_SEPARATOR.$lang.DIRECTORY_SEPARATOR.'site.php'; + + if ($existingContent = @include($file)) { + $fileContent = array_merge($existingContent, $fileContent); + } + + $content = "{{ 'Import'|t }} + +{% endblock %} + +{% do view.registerAssetBundle("acclaro\\translations\\assetbundles\\StaticTranslationsAssets") %} + +{% set sources = craft.app.elementIndexes.getSources(elementType, 'index') %} +{% set customizableSources = (sources is not empty and context == 'index' and currentUser.can('customizeSources')) %} + +{% block sidebar %} + {% if sources is not empty %} + + + {% if customizableSources %} + + + {{ 'Customize'|t('app') }} + + {% endif %} + {% endif %} +{% endblock %} + +{% block content %} +
+ {{ csrfInput() }} + + +
+
+
+
+{% endblock %} + +{% block footer %} +
 
+ +{% endblock %} + +{% js %} + $(document).ready(function() { + Craft.Translations.StaticTranslations.init(); + }); +{% endjs %} \ No newline at end of file From 72783d0d359b729d0f618fdc0b4a0de284daa0ef Mon Sep 17 00:00:00 2001 From: shnsumit Date: Sat, 2 May 2020 01:04:23 +0530 Subject: [PATCH 2/7] Static translation import & export implementation --- src/Translations.php | 2 + src/assetbundles/src/js/StaticTranslations.js | 44 ++++++ .../StaticTranslationsController.php | 144 +++++++++++++++++- src/elements/StaticTranslations.php | 56 +++---- src/templates/static-translations/index.twig | 13 +- 5 files changed, 224 insertions(+), 35 deletions(-) diff --git a/src/Translations.php b/src/Translations.php index fddaaae5..2b88a592 100644 --- a/src/Translations.php +++ b/src/Translations.php @@ -350,6 +350,8 @@ private function _registerCpRoutes() 'translations/orders/get-file-diff/' => 'translations/base/get-file-diff', 'translations/settings/configuration-options' => 'translations/settings/configuration-options', 'translations/static-translations' => 'translations/static-translations', + 'translations/static-translations/export-file' => 'translations/static-translations/export-file', + 'translations/static-translations/import' => 'translations/static-translations/import', ]); } ); diff --git a/src/assetbundles/src/js/StaticTranslations.js b/src/assetbundles/src/js/StaticTranslations.js index b738b6dc..ddb1a4bb 100644 --- a/src/assetbundles/src/js/StaticTranslations.js +++ b/src/assetbundles/src/js/StaticTranslations.js @@ -27,12 +27,37 @@ Craft.Translations.StaticTranslations = { $('.save-static-translation').attr("disabled", false); }, this)); + + + }, + + exportStaticTranslation: function() { + + var data = { + siteId: Craft.elementIndex.siteId, + sourceKey: Craft.elementIndex.sourceKey, + search: Craft.elementIndex.searchText + }; + + Craft.postActionRequest('translations/static-translations/export', data, $.proxy(function(response, textStatus) { + if (textStatus === 'success') { + if (response.success) { + var $iframe = $('