├── .gitignore ├── LICENSE ├── README.md ├── app ├── code │ └── community │ │ └── EW │ │ └── UntranslatedStrings │ │ ├── Block │ │ └── Adminhtml │ │ │ ├── Report.php │ │ │ ├── Report │ │ │ └── Grid.php │ │ │ ├── Summary.php │ │ │ └── Summary │ │ │ └── Grid.php │ │ ├── Helper │ │ └── Data.php │ │ ├── Model │ │ ├── Core │ │ │ └── Translate.php │ │ ├── Observer.php │ │ ├── Resource │ │ │ ├── String.php │ │ │ └── String │ │ │ │ └── Collection.php │ │ └── String.php │ │ ├── controllers │ │ └── Adminhtml │ │ │ └── UntranslatedController.php │ │ ├── etc │ │ ├── adminhtml.xml │ │ ├── config.xml │ │ └── system.xml │ │ └── sql │ │ └── ew_untranslatedstrings_setup │ │ ├── install-1.0.0.php │ │ └── upgrade-1.0.0-1.0.1.php ├── design │ └── adminhtml │ │ └── default │ │ └── default │ │ └── layout │ │ └── ew_untranslatedstrings.xml └── etc │ └── modules │ └── EW_UntranslatedStrings.xml ├── composer.json └── modman /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Eric Wiese 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EW_UntranslatedStrings 2 | 3 | This is a module to optionally log untranslated strings as they are discovered by locale. 4 | 5 | ## Installation 6 | 7 | Install via [modman](https://github.com/colinmollenhour/modman): 8 | 9 | ``` 10 | $ cd 11 | $ modman init # if you've never used modman before 12 | $ modman clone https://github.com/ericthehacker/magento-untranslatedstrings.git 13 | ``` 14 | 15 | ## Configuration 16 | 17 | Visit *System -> Config -> Developer -> Untranslated Strings -> Log Untranslated Strings* and set it to Yes to enable untranslated string logging. When pages are rendered, any string missing translations for the current locale is logged. 18 | 19 | To log results from more than one locale at a time, enable *System -> Config -> Developer -> Untranslated Strings -> Batch Check Translation Locales*, then select some locales in the multiselect below. With this enabled, when a page is rendered, each translated string is checked against each of the selected locales and logged individually if there is a translation gap. 20 | 21 | ## Usage 22 | 23 | After enabling the functionality in the system config as stated above, any time a string is run through the translator but no translation for the selected locale(s) is found, the string along with other useful information will be logged. 24 | 25 | To see a summary of untranslated strings, visit *Reports -> Untranslated Strings -> Untranslated Strings Summary* in Magento admin. This report shows a summary of untranslated strings by locale. 26 | Additionally, you can perform the following actions: 27 | 28 | - Purge: this removes strings from the untranslated strings report which have since had translations added. This allows you to 29 | clean up the report after adding new translations to address previous gaps. 30 | - Truncate: this removes all strings from the report for the given locale(s). 31 | 32 | To see a full report of this log, visit *Reports -> Untranslated Strings -> Untranslated Strings Report* in the Magento admin. This report shows all logged untranslated strings and allows you to filter, sort, and export. 33 | 34 | ## Caveat 35 | 36 | Currently, the module introduces a small to moderate performance penalty, depending on the number of locales you have configured to check. However, this penalty is realized **only if enabled in system configuration**. That means that it should be safe to have installed on both production and stage, but you probably don't want to enable it on production all the time. 37 | 38 | Note that it's disabled by default. 39 | -------------------------------------------------------------------------------- /app/code/community/EW/UntranslatedStrings/Block/Adminhtml/Report.php: -------------------------------------------------------------------------------- 1 | _controller = 'Report'; 7 | $this->_blockGroup = 'ew_untranslatedstrings'; 8 | $this->_controller = 'adminhtml_report'; 9 | $this->_headerText = Mage::helper('ew_untranslatedstrings')->__('Untranslated Strings'); 10 | parent::__construct(); 11 | $this->_removeButton('add'); 12 | $this->_addButton( 13 | 'full_report', 14 | array( 15 | 'label' => $this->__('View Summary'), 16 | 'onclick' => 'window.location=\''. $this->getUrl('*/*/index') .'\'', 17 | ) 18 | ); 19 | } 20 | } -------------------------------------------------------------------------------- /app/code/community/EW/UntranslatedStrings/Block/Adminhtml/Report/Grid.php: -------------------------------------------------------------------------------- 1 | setId('ew_untranslatedstrings_adminhtml_report_grid'); 8 | } 9 | 10 | protected function _prepareCollection() { 11 | /* var $collection EW_UntranslatedStrings_Model_Resource_String_Collection */ 12 | $collection = Mage::getResourceModel('ew_untranslatedstrings/string_collection'); 13 | $collection->joinStoreCode(); 14 | //$collection->setOrder('encounter_count', Varien_Db_Select::SQL_DESC); //causes problems with grid sorting 15 | 16 | $this->setCollection($collection); 17 | 18 | return parent::_prepareCollection(); 19 | } 20 | 21 | protected function _prepareColumns() { 22 | $this->addColumn('id', array( 23 | 'header' => $this->__('ID'), 24 | 'align' => 'left', 25 | 'width' => '75px', 26 | 'index' => 'id', 27 | 'type' => 'number' 28 | )); 29 | 30 | $this->addColumn('code', array( 31 | 'header' => $this->__('Store Code'), 32 | 'align' => 'left', 33 | 'width' => '75px', 34 | 'index' => 'code', 35 | )); 36 | 37 | $this->addColumn('untranslated_string', array( 38 | 'header' => $this->__('Untranslated String'), 39 | 'align' => 'left', 40 | 'index' => 'untranslated_string', 41 | )); 42 | 43 | $this->addColumn('translation_code', array( 44 | 'header' => $this->__('Translation Code'), 45 | 'align' => 'left', 46 | 'index' => 'translation_code', 47 | )); 48 | 49 | $this->addColumn('translation_module', array( 50 | 'header' => $this->__('Translation Module'), 51 | 'align' => 'left', 52 | 'index' => 'translation_module', 53 | )); 54 | 55 | 56 | $this->addColumn('locale', array( 57 | 'header' => $this->__('Locale'), 58 | 'align' => 'left', 59 | 'index' => 'locale', 60 | )); 61 | 62 | $this->addColumn('url_found', array( 63 | 'header' => $this->__('URL'), 64 | 'align' => 'left', 65 | 'index' => 'url_found', 66 | )); 67 | 68 | $this->addColumn('date_found', array( 69 | 'header' => $this->__('Date Found'), 70 | 'index' => 'date_found', 71 | 'width' => '175px', 72 | 'type' => 'datetime', 73 | )); 74 | 75 | $this->addColumn('encounter_count', array( 76 | 'header' => $this->__('Popularity'), 77 | 'align' => 'left', 78 | 'width' => '75px', 79 | 'index' => 'encounter_count', 80 | 'type' => 'number' 81 | )); 82 | 83 | $this->addExportType('*/*/exportCsv', $this->__('CSV')); 84 | 85 | return parent::_prepareColumns(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/code/community/EW/UntranslatedStrings/Block/Adminhtml/Summary.php: -------------------------------------------------------------------------------- 1 | _controller = 'Summary'; 7 | $this->_blockGroup = 'ew_untranslatedstrings'; 8 | $this->_controller = 'adminhtml_summary'; 9 | $this->_headerText = Mage::helper('ew_untranslatedstrings')->__('Untranslated Strings Summary'); 10 | parent::__construct(); 11 | $this->_removeButton('add'); 12 | $this->_addButton( 13 | 'full_report', 14 | array( 15 | 'label' => $this->__('View Full Report'), 16 | 'onclick' => 'window.location=\''. $this->getUrl('*/*/report') .'\'', 17 | ) 18 | ); 19 | } 20 | } -------------------------------------------------------------------------------- /app/code/community/EW/UntranslatedStrings/Block/Adminhtml/Summary/Grid.php: -------------------------------------------------------------------------------- 1 | setId('ew_untranslatedstrings_adminhtml_summary_grid'); 8 | } 9 | 10 | protected function _prepareCollection() { 11 | /* var $collection EW_UntranslatedStrings_Model_Resource_String_Collection */ 12 | $collection = Mage::getResourceModel('ew_untranslatedstrings/string_collection'); 13 | $collection->configureSummary(); 14 | 15 | $this->setCollection($collection); 16 | 17 | return parent::_prepareCollection(); 18 | } 19 | 20 | protected function _prepareColumns() { 21 | $this->addColumn('code', array( 22 | 'header' => $this->__('Store'), 23 | 'align' => 'left', 24 | 'index' => 'code', 25 | )); 26 | 27 | $this->addColumn('locale', array( 28 | 'header' => $this->__('Locale'), 29 | 'align' => 'left', 30 | 'index' => 'locale', 31 | )); 32 | 33 | $this->addColumn('string_count', array( 34 | 'header' => $this->__('Strings Count'), 35 | 'align' => 'left', 36 | 'width' => '75px', 37 | 'index' => 'string_count', 38 | 'type' => 'number', 39 | 'filter' => false, 40 | 'sortable' => false 41 | )); 42 | 43 | // $this->addColumn('top_strings', array( 44 | // 'header' => $this->__('Top Strings'), 45 | // 'align' => 'left', 46 | // 'index' => 'top_strings', 47 | // 'type' => 'wrapline' 48 | // )); 49 | 50 | 51 | $this->addColumn('action', 52 | array( 53 | 'header' => $this->__('Action'), 54 | 'width' => '200px', 55 | 'type' => 'action', 56 | 'getter' => 'getLocaleStore', 57 | 'actions' => array( 58 | array( 59 | 'caption' => $this->__('Purge'), 60 | 'url' => array( 61 | 'base'=>'*/*/purge' 62 | ), 63 | 'field' => 'locale_store', 64 | 'confirm' => $this->__( 65 | 'This will update the string log for this locale and remove any that are now translated. Are you sure?' 66 | ) 67 | ), 68 | array( 69 | 'caption' => $this->__('Truncate'), 70 | 'url' => array( 71 | 'base'=>'*/*/truncate' 72 | ), 73 | 'field' => 'locale_store', 74 | 'confirm' => $this->__( 75 | 'This delete all strings for this locale. Are you sure?' 76 | ) 77 | ) 78 | ), 79 | 'filter' => false, 80 | 'sortable' => false 81 | )); 82 | 83 | return parent::_prepareColumns(); 84 | } 85 | 86 | protected function _prepareMassaction() { 87 | $this->setMassactionIdField('locale_store'); 88 | $this->setMassactionIdFilter('locale_store'); 89 | $this->setMassactionIdFieldOnlyIndexValue(true); 90 | $this->getMassactionBlock()->setFormFieldName('locale_store'); 91 | 92 | $this->getMassactionBlock()->addItem('purge', array( 93 | 'label'=> $this->__('Purge'), 94 | 'url' => $this->getUrl( 95 | '*/*/massPurge' 96 | ), 97 | 'confirm' => $this->__( 98 | 'This will update the string log for these locale(s) and remove any that are now translated. Are you sure?' 99 | ) 100 | )); 101 | 102 | $this->getMassactionBlock()->addItem('truncate', array( 103 | 'label'=> $this->__('Truncate'), 104 | 'url' => $this->getUrl( 105 | '*/*/massTruncate' 106 | ), 107 | 'confirm' => $this->__('This will remove all untranslated string logs for the selected locale(s). Are you sure?') 108 | )); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/code/community/EW/UntranslatedStrings/Helper/Data.php: -------------------------------------------------------------------------------- 1 | getStore()->getId() == Mage_Catalog_Model_Abstract::DEFAULT_STORE_ID) { 27 | //we're in the admin, and configured to ignore it 28 | return false; 29 | } 30 | 31 | return $enabled; 32 | } 33 | 34 | /** 35 | * Get exclude regex patterns 36 | * 37 | * @return array 38 | */ 39 | public function getExcludePattens() { 40 | $patterns = trim(Mage::getStoreConfig(self::CONFIG_PATH_EXCLUDE_CODES)); 41 | 42 | if(empty($patterns)) { 43 | return array(); 44 | } 45 | 46 | return explode("\n", $patterns); 47 | } 48 | 49 | /** 50 | * Log matching key value translation pairs? 51 | * 52 | * @return bool 53 | */ 54 | public function logMatchingKeyValuePairs() { 55 | return (bool)Mage::getStoreConfig(self::CONFIG_PATH_MATCHING_KEY_VALUE_PAIR_ENABLED); 56 | } 57 | 58 | /** 59 | * Get translator prepared for given locale 60 | * 61 | * @param $locale 62 | * @param bool $allowMatchingKeyValuePairs - matching key / value pairs count as translations 63 | * @param null $storeIdContext 64 | * @param bool $forceRefresh 65 | * @return EW_UntranslatedStrings_Model_Core_Translate 66 | */ 67 | public function getTranslator($locale, $allowMatchingKeyValuePairs = null, $storeIdContext = null, $forceRefresh = false) { 68 | if(!isset($this->_translators[$locale])) { 69 | if(is_null($allowMatchingKeyValuePairs)) { 70 | // "allow" and "log" are opposite concepts 71 | $allowMatchingKeyValuePairs = !$this->logMatchingKeyValuePairs(); 72 | } 73 | 74 | /* @var $translate EW_UntranslatedStrings_Model_Core_Translate */ 75 | $translate = Mage::getModel('ew_untranslatedstrings/core_translate'); 76 | $translate->setConfig( 77 | array( 78 | Mage_Core_Model_Translate::CONFIG_KEY_LOCALE => $locale 79 | ) 80 | ); 81 | $translate->setLocale($locale); 82 | $translate->setAllowLooseDevModuleMode(true); //prevent native dev mode differences 83 | $translate->setAllowMatchingKeyValuePairs($allowMatchingKeyValuePairs); 84 | if(!is_null($storeIdContext)) { 85 | $translate->setThemeContext($storeIdContext); 86 | } 87 | $translate->init(Mage_Core_Model_Design_Package::DEFAULT_AREA, $forceRefresh); 88 | 89 | $this->_translators[$locale] = $translate; 90 | } 91 | 92 | return $this->_translators[$locale]; 93 | } 94 | 95 | /** 96 | * Does text/code have translation for given locale? 97 | * 98 | * @param $text 99 | * @param $code 100 | * @param $locale 101 | * @return bool 102 | */ 103 | public function isTranslated($text, $code, $locale) { 104 | /* @var $translate EW_UntranslatedStrings_Model_Core_Translate */ 105 | $translate = $this->getTranslator($locale); 106 | 107 | return $translate->hasTranslation($text, $code); 108 | } 109 | 110 | /** 111 | * Should check batch locales? 112 | * 113 | * @return bool 114 | */ 115 | public function getCheckBatchLocales() { 116 | return (bool)Mage::getStoreConfig(self::CONFIG_PATH_BATCH_LOCALES_ENABLED); 117 | } 118 | 119 | /** 120 | * Return array of locale codes to check 121 | * @return array 122 | */ 123 | public function getCheckLocales() { 124 | if($this->getCheckBatchLocales()) { 125 | return explode(',', Mage::getStoreConfig(self::CONFIG_PATH_BATCH_LOCALES)); 126 | } 127 | 128 | //not enabled, so stick to configured locale 129 | return array(Mage::app()->getLocale()->getLocaleCode()); 130 | } 131 | } -------------------------------------------------------------------------------- /app/code/community/EW/UntranslatedStrings/Model/Core/Translate.php: -------------------------------------------------------------------------------- 1 | _allowMatchingKeyValuePairs; 22 | } 23 | 24 | /** 25 | * Set if matching key / value translation pairs 26 | * allowed when loading translations. 27 | * 28 | * @param bool $allow 29 | */ 30 | public function setAllowMatchingKeyValuePairs($allow) { 31 | $this->_allowMatchingKeyValuePairs = (bool)$allow; 32 | } 33 | 34 | /** 35 | * Use native "Not allow use translation not related to module" 36 | * check when loading translations? 37 | * 38 | * @return bool 39 | */ 40 | public function getAllowLooseDevModuleMode() { 41 | return $this->_allowLooseDevModuleMode; 42 | } 43 | 44 | /** 45 | * Set if native "Not allow use translation not related to module" 46 | * behavior used when loading translations. 47 | * 48 | * @param bool $allow 49 | */ 50 | public function setAllowLooseDevModuleMode($allow) { 51 | $this->_allowLooseDevModuleMode = (bool)$allow; 52 | } 53 | 54 | /** 55 | * Get locales to check and store on local variable 56 | * 57 | * @return array 58 | */ 59 | protected function _getLocalesToCheck() { 60 | if(is_null($this->_localesToCheck)) { 61 | $this->_localesToCheck = Mage::helper('ew_untranslatedstrings')->getCheckLocales(); 62 | } 63 | 64 | return $this->_localesToCheck; 65 | } 66 | 67 | /** 68 | * If called before init(), override the theme context 69 | * from which translations will be loaded. 70 | * 71 | * @param int $storeId 72 | */ 73 | public function setThemeContext($storeId) { 74 | $this->_themeStore = $storeId; 75 | } 76 | 77 | public function hasTranslation($text, $code) { 78 | if (array_key_exists($code, $this->getData()) || array_key_exists($text, $this->getData())) { 79 | return true; 80 | } 81 | 82 | return false; 83 | } 84 | 85 | /** 86 | * Evaluate translated text and code and determine 87 | * if they are untranslated. 88 | * 89 | * @param string $text 90 | * @param string $code 91 | */ 92 | protected function _checkTranslatedString($text, $code) { 93 | Varien_Profiler::start(__CLASS__ . '::' . __FUNCTION__); 94 | Varien_Profiler::start(EW_UntranslatedStrings_Helper_Data::PROFILER_KEY); 95 | 96 | //loop locale(s) and find gaps 97 | $untranslatedPhrases = array(); 98 | foreach($this->_getLocalesToCheck() as $locale) { 99 | if(!Mage::helper('ew_untranslatedstrings')->isTranslated($text,$code,$locale)) { 100 | $untranslatedPhrases[] = array( 101 | 'text' => $text, 102 | 'code' => $code, 103 | 'locale' => $locale 104 | ); 105 | } 106 | } 107 | $this->_storeUntranslated($untranslatedPhrases); 108 | 109 | Varien_Profiler::stop(EW_UntranslatedStrings_Helper_Data::PROFILER_KEY); 110 | Varien_Profiler::stop(__CLASS__ . '::' . __FUNCTION__); 111 | } 112 | 113 | /** 114 | * Check for translation gap before returning 115 | * 116 | * @param string $text 117 | * @param string $code 118 | * @return string 119 | */ 120 | protected function _getTranslatedString($text, $code) 121 | { 122 | if(Mage::helper('ew_untranslatedstrings')->isEnabled()) { 123 | $this->_checkTranslatedString($text, $code); 124 | } 125 | 126 | return parent::_getTranslatedString($text, $code); 127 | } 128 | 129 | /** 130 | * Rewrite to allow optional key = value in data 131 | * as well as optionally disabling developer mode check 132 | * 133 | * @param array $data 134 | * @param string $scope 135 | * @return Mage_Core_Model_Translate 136 | */ 137 | protected function _addData($data, $scope, $forceReload=false) 138 | { 139 | foreach ($data as $key => $value) { 140 | // BEGIN EDIT: conditionally exclude matching key value pairs 141 | if(!$this->getAllowMatchingKeyValuePairs()) { 142 | if ($key === $value) { 143 | continue; 144 | } 145 | } 146 | // END EDIT 147 | $key = $this->_prepareDataString($key); 148 | $value = $this->_prepareDataString($value); 149 | if ($scope && isset($this->_dataScope[$key]) && !$forceReload ) { 150 | /** 151 | * Checking previos value 152 | */ 153 | $scopeKey = $this->_dataScope[$key] . self::SCOPE_SEPARATOR . $key; 154 | if (!isset($this->_data[$scopeKey])) { 155 | if (isset($this->_data[$key])) { 156 | $this->_data[$scopeKey] = $this->_data[$key]; 157 | /** 158 | * Not allow use translation not related to module 159 | */ 160 | if (Mage::getIsDeveloperMode()) { 161 | // BEGIN EDIT: conditionally exclude module mismatch translations 162 | if(!$this->getAllowLooseDevModuleMode()) { 163 | unset($this->_data[$key]); 164 | } 165 | // END EDIT 166 | } 167 | } 168 | } 169 | $scopeKey = $scope . self::SCOPE_SEPARATOR . $key; 170 | $this->_data[$scopeKey] = $value; 171 | } 172 | else { 173 | $this->_data[$key] = $value; 174 | $this->_dataScope[$key]= $scope; 175 | } 176 | } 177 | return $this; 178 | } 179 | 180 | /** 181 | * Scrub phrases against excluded phrase patterns 182 | * 183 | * @param array $phrases 184 | * @return array 185 | */ 186 | protected function _scrubExcludedPhrases(array $phrases) { 187 | /** @var $patterns array */ 188 | $patterns = Mage::helper('ew_untranslatedstrings')->getExcludePattens(); 189 | 190 | if(empty($patterns)) { //quick short circuit if feature not used 191 | return $phrases; 192 | } 193 | 194 | $scrubbedPhrases = array(); 195 | foreach($phrases as $phrase) { 196 | $excluded = false; 197 | 198 | foreach($patterns as $pattern) { 199 | if(preg_match('/' . $pattern . '/', $phrase['code'])) { 200 | $excluded = true; 201 | break; 202 | } 203 | } 204 | 205 | if(!$excluded) { 206 | $scrubbedPhrases[] = $phrase; 207 | } 208 | } 209 | 210 | return $scrubbedPhrases; 211 | } 212 | 213 | /** 214 | * Store phrases to be found and written later 215 | * 216 | * @param array $phrases 217 | */ 218 | protected function _storeUntranslated(array $phrases) { 219 | $phrases = $this->_scrubExcludedPhrases($phrases); 220 | 221 | foreach($phrases as $phrase) { 222 | $locale = $phrase['locale']; 223 | 224 | //get array of all locales from registry or create new 225 | $strings = array(); 226 | if(Mage::registry(self::REGISTRY_KEY)) { 227 | $strings = Mage::registry(self::REGISTRY_KEY); 228 | Mage::unregister(self::REGISTRY_KEY); //we're going to set it again in a minute 229 | } 230 | 231 | //get locale specific section of registry array 232 | $localeStrings = isset($strings[$locale]) ? $strings[$locale] : array(); 233 | 234 | $text = $phrase['text']; 235 | $code = $phrase['code']; 236 | 237 | $codeParts = explode(Mage_Core_Model_Translate::SCOPE_SEPARATOR, $code); 238 | $module = $codeParts[0]; 239 | 240 | //add new entry 241 | $localeStrings[] = array( 242 | 'code' => $code, 243 | 'module' => $module, 244 | 'text' => $text, 245 | 'store_id' => Mage::app()->getStore()->getId(), 246 | 'locale' => $locale, 247 | 'url' => Mage::helper('core/url')->getCurrentUrl() 248 | ); 249 | 250 | $strings[$locale] = $localeStrings; //update "big" array 251 | 252 | //whether new or just augmented, set registry key again 253 | Mage::register(self::REGISTRY_KEY, $strings); 254 | } 255 | } 256 | 257 | /** 258 | * Get theme translation file. If override store set, 259 | * get file from that store's theme. Otherwise, get current 260 | * design package's translation file. 261 | * 262 | * @return string 263 | */ 264 | protected function _getThemeTranslationFile() { 265 | if(!is_null($this->_themeStore)) { 266 | // Start store emulation process 267 | $appEmulation = Mage::getSingleton('core/app_emulation'); 268 | $initialEnvironmentInfo = $appEmulation->startEnvironmentEmulation($this->_themeStore); 269 | 270 | /* @var $design Mage_Core_Model_Design_Package */ 271 | $design = Mage::getModel('core/design_package'); 272 | $file = $design->getLocaleFileName('translate.csv'); 273 | 274 | // Stop store emulation process 275 | $appEmulation->stopEnvironmentEmulation($initialEnvironmentInfo); 276 | 277 | return $file; 278 | } 279 | 280 | //fallback to default behavior 281 | return Mage::getDesign()->getLocaleFileName('translate.csv'); 282 | } 283 | 284 | /** 285 | * Rewrite to allow theme to be specified 286 | * 287 | * @param bool $forceReload 288 | * @return Mage_Core_Model_Translate 289 | */ 290 | protected function _loadThemeTranslation($forceReload = false) 291 | { 292 | // BEGIN EDIT: call _getThemeTranslationFile() to get theme translate file path 293 | $file = $this->_getThemeTranslationFile(); 294 | // END EDIT 295 | $this->_addData($this->_getFileData($file), false, $forceReload); 296 | return $this; 297 | } 298 | } -------------------------------------------------------------------------------- /app/code/community/EW/UntranslatedStrings/Model/Observer.php: -------------------------------------------------------------------------------- 1 | writeUntranslatedStrings($localeStrings); 29 | } 30 | 31 | Varien_Profiler::stop(EW_UntranslatedStrings_Helper_Data::PROFILER_KEY); 32 | } 33 | } -------------------------------------------------------------------------------- /app/code/community/EW/UntranslatedStrings/Model/Resource/String.php: -------------------------------------------------------------------------------- 1 | _init('ew_untranslatedstrings/string', 'id'); 7 | } 8 | 9 | /** 10 | * Write set of untranslated strings. Expects array of format 11 | * array( 12 | * array( 13 | * 'code' => code, 14 | * 'module' => module, 15 | * 'text' => untranslated string, 16 | * 'store_id' => store ID, 17 | * 'locale' => locale, 18 | * 'url' => url found 19 | * ), 20 | * ... 21 | * ) 22 | * 23 | * @param array $strings 24 | */ 25 | public function writeUntranslatedStrings(array $strings) { 26 | $write = $this->_getWriteAdapter(); 27 | 28 | // map expected keys to database columns 29 | $columnMapping = array( 30 | 'code' => 'translation_code', 31 | 'module' => 'translation_module', 32 | 'text' => 'untranslated_string', 33 | 'store_id' => 'store_id', 34 | 'locale' => 'locale', 35 | 'url' => 'url_found' 36 | ); 37 | 38 | $insertValues = array(); 39 | 40 | foreach($strings as $string) { 41 | $insertValue = array( 42 | 'date_found' => Zend_Date::now()->toString(Zend_Date::ISO_8601) 43 | ); 44 | 45 | foreach($string as $key => $value) { 46 | $insertValue[ $columnMapping[$key] ] = $value; 47 | } 48 | 49 | $insertValues[] = $insertValue; 50 | } 51 | 52 | $write->insertOnDuplicate( 53 | $this->getMainTable(), 54 | $insertValues, 55 | array( 56 | 'encounter_count' => new Zend_Db_Expr( 57 | sprintf( 58 | '%s + 1', 59 | $write->quoteIdentifier('encounter_count') 60 | ) 61 | ) 62 | ) 63 | ); 64 | } 65 | 66 | public function getLocaleStrings($locale) { 67 | $read = $this->getReadConnection(); 68 | 69 | $select = $read->select(); 70 | 71 | $select->from(array('main_table' => $this->getMainTable())); 72 | $select->reset(Zend_Db_Select::COLUMNS); 73 | $select->columns( 74 | array( 75 | 'id', 76 | 'untranslated_string', 77 | 'translation_code', 78 | 'translation_module' 79 | ) 80 | ); 81 | $select->where('locale = ?', $locale); 82 | $select->distinct(true); 83 | 84 | $rawResults = $select->query()->fetchAll(); 85 | 86 | return $rawResults; 87 | } 88 | 89 | /** 90 | * Deletes records in one query for the given string IDs 91 | * 92 | * @param array $ids 93 | */ 94 | public function purgeStrings(array $ids) { 95 | $where = $this->_getWriteAdapter()->quoteInto('id in (?)', $ids); 96 | $this->_getWriteAdapter()->delete($this->getMainTable(), $where); 97 | } 98 | 99 | /** 100 | * Deletes all records for the given locale 101 | * 102 | * @param $locale 103 | */ 104 | public function truncateRecords($locale) { 105 | $where = $this->_getWriteAdapter()->quoteInto('locale = ?', $locale); 106 | $this->_getWriteAdapter()->delete($this->getMainTable(), $where); 107 | } 108 | } -------------------------------------------------------------------------------- /app/code/community/EW/UntranslatedStrings/Model/Resource/String/Collection.php: -------------------------------------------------------------------------------- 1 | _init('ew_untranslatedstrings/string'); 10 | } 11 | 12 | /** 13 | * Join store code 14 | */ 15 | public function joinStoreCode() { 16 | if(!$this->_joinedStoreCode) { 17 | $this->join( 18 | array('stores' => 'core/store'), 19 | '`stores`.`store_id` = `main_table`.`store_id`', 20 | array('stores.code') 21 | ); 22 | } 23 | $this->_joinedStoreCode = true; 24 | } 25 | 26 | /** 27 | * Account for group and having clauses, if any 28 | * 29 | * @return Varien_Db_Select 30 | */ 31 | public function getSelectCountSql() 32 | { 33 | if(!$this->_interferWithCountSql) { 34 | return parent::getSelectCountSql(); 35 | } 36 | 37 | $this->_renderFilters(); 38 | 39 | $countSelect = clone $this->getSelect(); 40 | $countSelect->reset(Zend_Db_Select::ORDER); 41 | $countSelect->reset(Zend_Db_Select::LIMIT_COUNT); 42 | $countSelect->reset(Zend_Db_Select::LIMIT_OFFSET); 43 | //$countSelect->reset(Zend_Db_Select::COLUMNS); 44 | 45 | //$countSelect->columns('COUNT(*)'); 46 | 47 | return $countSelect; 48 | } 49 | 50 | /** 51 | * Get collection size 52 | * 53 | * @return int 54 | */ 55 | public function getSize() 56 | { 57 | if(!$this->_interferWithCountSql) { 58 | return parent::getSize(); 59 | } 60 | 61 | if (is_null($this->_totalRecords)) { 62 | $sql = $this->getSelectCountSql(); 63 | $this->_totalRecords = count($this->getConnection()->fetchAll($sql, $this->_bindParams)); 64 | } 65 | return intval($this->_totalRecords); 66 | } 67 | 68 | /** 69 | * Account for group and having clauses, if any 70 | * 71 | * @return array 72 | */ 73 | public function getAllIds() 74 | { 75 | if(!$this->_interferWithCountSql) { 76 | return parent::getAllIds(); 77 | } 78 | 79 | $idsSelect = clone $this->getSelect(); 80 | $idsSelect->reset(Zend_Db_Select::ORDER); 81 | $idsSelect->reset(Zend_Db_Select::LIMIT_COUNT); 82 | $idsSelect->reset(Zend_Db_Select::LIMIT_OFFSET); 83 | //$idsSelect->reset(Zend_Db_Select::COLUMNS); 84 | 85 | $idsSelect->columns($this->getResource()->getIdFieldName(), 'main_table'); 86 | return $this->getConnection()->fetchCol($idsSelect); 87 | } 88 | 89 | /** 90 | * Configures collection to be summary of 91 | * untranslated strings by locale. 92 | */ 93 | public function configureSummary($topStringsCount = 10) { 94 | $this->_interferWithCountSql = true; 95 | 96 | $this->joinStoreCode(); 97 | 98 | $this->getSelect() 99 | ->reset(Zend_Db_Select::COLUMNS) 100 | ->columns( 101 | array( 102 | 'string_count' => new Zend_Db_Expr( 103 | sprintf( 104 | 'count(%s)', 105 | $this->getConnection()->quoteIdentifier('untranslated_string') 106 | ) 107 | ), 108 | 'locale', 109 | 'store_id', 110 | 'locale_store' => new Zend_Db_Expr('CONCAT(main_table.store_id,\'-\',locale)'), //convenience column -- useful for magento grids 111 | 'stores.code', 112 | 'top_strings' => new Zend_Db_Expr( 113 | sprintf( 114 | 'SUBSTRING_INDEX(GROUP_CONCAT(%s ORDER BY %s SEPARATOR \'\n\'), \'\n\', %d)', 115 | $this->getConnection()->quoteIdentifier('untranslated_string'), 116 | $this->getConnection()->quoteIdentifier('encounter_count'), 117 | $topStringsCount 118 | ) 119 | ) 120 | ) 121 | ); 122 | 123 | $this->getSelect() 124 | ->distinct(true) 125 | ->group(array('store_id', 'locale')) 126 | ->having('string_count > 0'); 127 | } 128 | } -------------------------------------------------------------------------------- /app/code/community/EW/UntranslatedStrings/Model/String.php: -------------------------------------------------------------------------------- 1 | _init('ew_untranslatedstrings/string'); 7 | } 8 | 9 | /** 10 | * Purge strings which ARE translated 11 | * 12 | * @param $locale 13 | * @param $storeId 14 | */ 15 | public function purgeTranslatedRecords($locale, $storeId) { 16 | $strings = $this->getResource()->getLocaleStrings($locale); 17 | 18 | /* @var $translate EW_UntranslatedStrings_Model_Core_Translate */ 19 | $translate = Mage::helper('ew_untranslatedstrings')->getTranslator( 20 | $locale, 21 | null, //allow config to determine if matching key / value allowed 22 | $storeId, //set translator to use store's theme 23 | true //disable cache when purging 24 | ); 25 | 26 | $purgeIds = array(); 27 | foreach($strings as $string) { 28 | $id = $string['id']; 29 | $text = $string['untranslated_string']; 30 | $code = $string['translation_code']; 31 | $module = $string['translation_module']; 32 | 33 | if($translate->hasTranslation($text,$code)) { 34 | $purgeIds[] = $id; 35 | } 36 | } 37 | 38 | if(empty($purgeIds)) { 39 | return; 40 | } 41 | 42 | $this->getResource()->purgeStrings($purgeIds); 43 | } 44 | } -------------------------------------------------------------------------------- /app/code/community/EW/UntranslatedStrings/controllers/Adminhtml/UntranslatedController.php: -------------------------------------------------------------------------------- 1 | loadLayout(); 10 | $this->renderLayout(); 11 | } 12 | 13 | /** 14 | * Full untranslated strings report 15 | */ 16 | public function reportAction() { 17 | $this->loadLayout(); 18 | $this->renderLayout(); 19 | } 20 | 21 | /** 22 | * Mass purges strings 23 | */ 24 | public function massPurgeAction() { 25 | $localeStores = $this->getRequest()->getParam('locale_store'); 26 | 27 | try { 28 | foreach($localeStores as $localeStore) { 29 | $parts = explode('-', $localeStore); 30 | 31 | Mage::getModel('ew_untranslatedstrings/string')->purgeTranslatedRecords($parts[1], $parts[0]); 32 | } 33 | 34 | Mage::getSingleton('adminhtml/session')->addSuccess( 35 | $this->__( 36 | '%d locales successfully purged.', 37 | count($localeStores) 38 | ) 39 | ); 40 | } catch(Exception $ex) { 41 | Mage::getSingleton('adminhtml/session')->addError( 42 | $this->__( 43 | 'Error purging locales: %s', 44 | $ex->getMessage() 45 | ) 46 | ); 47 | } 48 | 49 | $this->_redirect('*/*/index'); 50 | } 51 | 52 | /** 53 | * Purge single locale 54 | */ 55 | public function purgeAction() { 56 | $localeStore = $this->getRequest()->getParam('locale_store'); 57 | $parts = explode('-', $localeStore); 58 | 59 | try { 60 | if(empty($parts)) { 61 | throw new Mage_Adminhtml_Exception( 62 | $this->__( 63 | 'No locale supplied.' 64 | ) 65 | ); 66 | } 67 | 68 | Mage::getModel('ew_untranslatedstrings/string')->purgeTranslatedRecords($parts[1], $parts[0]); 69 | 70 | Mage::getSingleton('adminhtml/session')->addSuccess( 71 | $this->__( 72 | 'Locale "%s" successfully purged.', 73 | $parts[0] 74 | ) 75 | ); 76 | } catch(Exception $ex) { 77 | Mage::getSingleton('adminhtml/session')->addSuccess( 78 | $this->__( 79 | 'Error purging locale "%s".', 80 | $ex->getMessage() 81 | ) 82 | ); 83 | } 84 | 85 | $this->_redirect('*/*/index'); 86 | } 87 | 88 | /** 89 | * Truncate single locale 90 | */ 91 | public function truncateAction() { 92 | $localeStore = $this->getRequest()->getParam('locale_store'); 93 | $parts = explode('-', $localeStore); 94 | 95 | try { 96 | if(empty($parts)) { 97 | throw new Mage_Adminhtml_Exception( 98 | $this->__( 99 | 'No locale supplied.' 100 | ) 101 | ); 102 | } 103 | 104 | Mage::getResourceModel('ew_untranslatedstrings/string')->truncateRecords($parts[1], $parts[0]); 105 | 106 | Mage::getSingleton('adminhtml/session')->addSuccess( 107 | $this->__( 108 | 'Locale "%s" successfully truncated.', 109 | $parts[0] 110 | ) 111 | ); 112 | } catch(Exception $ex) { 113 | Mage::getSingleton('adminhtml/session')->addSuccess( 114 | $this->__( 115 | 'Error truncating locale "%s".', 116 | $ex->getMessage() 117 | ) 118 | ); 119 | } 120 | 121 | $this->_redirect('*/*/index'); 122 | } 123 | 124 | /** 125 | * Mass truncates strings 126 | */ 127 | public function massTruncateAction() { 128 | $localeStores = $this->getRequest()->getParam('locale_store'); 129 | 130 | try { 131 | foreach($localeStores as $localeStore) { 132 | $parts = explode('-', $localeStore); 133 | 134 | Mage::getResourceModel('ew_untranslatedstrings/string')->truncateRecords($parts[1], $parts[0]); 135 | } 136 | 137 | Mage::getSingleton('adminhtml/session')->addSuccess( 138 | $this->__( 139 | '%d locales successfully truncated.', 140 | count($localeStores) 141 | ) 142 | ); 143 | } catch(Exception $ex) { 144 | Mage::getSingleton('adminhtml/session')->addError( 145 | $this->__( 146 | 'Error truncating locales: %s', 147 | $ex->getMessage() 148 | ) 149 | ); 150 | } 151 | 152 | $this->_redirect('*/*/index'); 153 | } 154 | 155 | /** 156 | * Enforce ACL 157 | * 158 | * @return bool 159 | */ 160 | protected function _isAllowed() 161 | { 162 | switch ($this->getRequest()->getActionName()) { 163 | case 'index': 164 | return Mage::getSingleton('admin/session')->isAllowed('report/ew_untranslatedstrings/activity_summary'); 165 | break; 166 | case 'report': 167 | return Mage::getSingleton('admin/session')->isAllowed('report/ew_untranslatedstrings/activity_report'); 168 | break; 169 | default: 170 | return Mage::getSingleton('admin/session')->isAllowed('report/ew_untranslatedstrings'); 171 | break; 172 | } 173 | } 174 | 175 | /** 176 | * Export to CSV 177 | */ 178 | public function exportCsvAction() 179 | { 180 | $fileName = 'untranslated_strings_report.csv'; 181 | $content = $this->getLayout() 182 | ->createBlock('ew_untranslatedstrings/adminhtml_report_grid','adminhtml_report.grid') 183 | ->getCsv(); 184 | 185 | $this->_prepareDownloadResponse($fileName, $content); 186 | } 187 | } -------------------------------------------------------------------------------- /app/code/community/EW/UntranslatedStrings/etc/adminhtml.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Untranslated Strings 8 | 1000 9 | 10 | 11 | Untranslated Strings Summary 12 | adminhtml/untranslated/index 13 | 14 | 15 | Untranslated Strings Report 16 | adminhtml/untranslated/report 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Untranslated Strings 31 | 32 | 33 | Untranslated Strings Summary 34 | 35 | 36 | Untranslated Strings Report 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/code/community/EW/UntranslatedStrings/etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 1.0.1 6 | 7 | 8 | 9 | 10 | 11 | EW_UntranslatedStrings_Block 12 | 13 | 14 | 15 | 16 | EW_UntranslatedStrings_Helper 17 | 18 | 19 | 20 | 21 | EW_UntranslatedStrings_Model 22 | ew_untranslatedstrings_resource 23 | 24 | 25 | EW_UntranslatedStrings_Model_Resource 26 | 27 | 28 | ew_untranslatedstrings_strings
29 |
30 |
31 |
32 | 33 | 34 | 35 | EW_UntranslatedStrings_Model_Core_Translate 36 | 37 | 38 |
39 | 40 | 41 | 42 | EW_UntranslatedStrings 43 | Mage_Core_Model_Resource_Setup 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ew_untranslatedstrings/observer 53 | flushUntranslatedStrings 54 | 55 | 56 | 57 | 58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 | EW_UntranslatedStrings_Adminhtml 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | ew_untranslatedstrings.xml 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 0 86 | 1 87 | 1 88 | 91 | 0 92 | 93 | 94 | 95 |
-------------------------------------------------------------------------------- /app/code/community/EW/UntranslatedStrings/etc/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 1000 9 | 1 10 | 1 11 | 1 12 | 13 | 14 | 15 | 16 | If set to yes, untranslated strings will be logged to database. 17 | WARNING: this adds a small performance penalty if enabled. 18 | 19 | select 20 | adminhtml/system_config_source_yesno 21 | dev/translate/untranslated_strings_enabled 22 | 100 23 | 1 24 | 1 25 | 1 26 | 27 | 28 | 29 | 30 | This is a convenience setting to allow you to enable the module 31 | at the default level, but actually ignore strings found in the admin. If you 32 | are interested in collecting admin untranslated strings, set to No. 33 | 34 | select 35 | adminhtml/system_config_source_yesno 36 | dev/translate/untranslated_strings_ignore_admin 37 | 110 38 | 1 39 | 0 40 | 0 41 | 42 | 1 43 | 44 | 45 | 46 | 47 | 48 | Should we log strings where the untranslated and translated strings match 49 | exactly? This often indicates actual translation gaps, as modules will 50 | provide entries which are not actually translated. If, on the other hand, 51 | you're only interested in strings which have no representation at all in 52 | translation files, set this to No. 53 | NOTE: For obvious reasons, this will result in many false positives for 54 | English locales. 55 | 56 | select 57 | adminhtml/system_config_source_yesno 58 | dev/translate/untranslated_strings_enable_matching_key_value_pair 59 | 120 60 | 1 61 | 1 62 | 1 63 | 64 | 1 65 | 66 | 67 | 68 | 69 | 73 | 74 | textarea 75 | dev/translate/untranslated_strings_exclude 76 | 130 77 | 1 78 | 1 79 | 1 80 | 81 | 1 82 | 83 | 84 | 85 | 86 | 87 | If set to yes, you can select locale(s) to check when a string is translated, 88 | instead of just the store's configured locale. If set to no, only the store's 89 | configured locale is used. 90 | WARNING: this adds a greater performance penalty if enabled. 91 | 92 | select 93 | adminhtml/system_config_source_yesno 94 | dev/translate/untranslated_strings_batch_locales_enabled 95 | 200 96 | 1 97 | 1 98 | 1 99 | 100 | 1 101 | 102 | 103 | 104 | 105 | Instead of the store's configured locale, these locale(s) will be checked and logged. 106 | dev/translate/untranslated_strings_locales 107 | multiselect 108 | adminhtml/system_config_source_locale 109 | 210 110 | 1 111 | 1 112 | 1 113 | 114 | 1 115 | 1 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /app/code/community/EW/UntranslatedStrings/sql/ew_untranslatedstrings_setup/install-1.0.0.php: -------------------------------------------------------------------------------- 1 | startSetup(); 6 | 7 | $tableName = $installer->getTable('ew_untranslatedstrings/string'); 8 | 9 | $table = new Varien_Db_Ddl_Table(); 10 | $table->setName($tableName); 11 | 12 | $table->addColumn('id', Varien_Db_Ddl_Table::TYPE_INTEGER, 11, array('nullable' => false, 'identity' => true, 'primary' => true)); 13 | $table->addColumn('store_id', Varien_Db_Ddl_Table::TYPE_INTEGER, 11, array('nullable' => false)); 14 | $table->addColumn('untranslated_string', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array('nullable' => false)); 15 | $table->addColumn('translation_code', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array('nullable' => false)); 16 | $table->addColumn('translation_module', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array('nullable' => true)); 17 | $table->addColumn('locale', Varien_Db_Ddl_Table::TYPE_VARCHAR, 10, array('nullable' => true)); 18 | $table->addColumn('url_found', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255, array('nullable' => true)); 19 | $table->addColumn('date_found', Varien_Db_Ddl_Table::TYPE_DATETIME, null, array('nullable' => false)); 20 | 21 | $installer->getConnection()->createTable($table); 22 | 23 | $uniqueFields = array( 24 | 'store_id', 25 | 'untranslated_string', 26 | 'translation_code', 27 | 'locale' 28 | ); 29 | $installer->getConnection()->addIndex( 30 | $tableName, 31 | $installer->getIdxName( 32 | $tableName, 33 | $uniqueFields, 34 | Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE 35 | ), 36 | $uniqueFields, 37 | Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE 38 | ); 39 | 40 | $installer->endSetup(); -------------------------------------------------------------------------------- /app/code/community/EW/UntranslatedStrings/sql/ew_untranslatedstrings_setup/upgrade-1.0.0-1.0.1.php: -------------------------------------------------------------------------------- 1 | startSetup(); 6 | 7 | $installer->getConnection() 8 | ->addColumn( 9 | $installer->getTable('ew_untranslatedstrings/string'), 10 | 'encounter_count', 11 | array( 12 | 'type' => Varien_Db_Ddl_Table::TYPE_INTEGER, 13 | 'length' => 11, 14 | 'nullable' => false, 15 | 'default' => 0, 16 | 'comment' => 'Number of times string encountered' 17 | ) 18 | ); 19 | 20 | $installer->endSetup(); -------------------------------------------------------------------------------- /app/design/adminhtml/default/default/layout/ew_untranslatedstrings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/etc/modules/EW_UntranslatedStrings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | community 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ericthehacker/untranslatedstrings", 3 | "type": "magento-module", 4 | "suggest": { 5 | "magento-hackathon/magento-composer-installer": "*" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /modman: -------------------------------------------------------------------------------- 1 | app/code/community/EW/UntranslatedStrings app/code/community/EW/UntranslatedStrings 2 | app/etc/modules/EW_UntranslatedStrings.xml app/etc/modules/EW_UntranslatedStrings.xml 3 | app/design/adminhtml/default/default/layout/ew_untranslatedstrings.xml app/design/adminhtml/default/default/layout/ew_untranslatedstrings.xml 4 | --------------------------------------------------------------------------------