PageRenderTime 49ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/core/Translate/Writer.php

https://github.com/CodeYellowBV/piwik
PHP | 383 lines | 172 code | 59 blank | 152 comment | 22 complexity | b77c96fe5e2c95345f9bf3a26447037b MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. *
  9. */
  10. namespace Piwik\Translate;
  11. use Exception;
  12. use Piwik\Filesystem;
  13. use Piwik\Piwik;
  14. use Piwik\Translate\Filter\FilterAbstract;
  15. use Piwik\Translate\Validate\ValidateAbstract;
  16. /**
  17. * Writes clean translations to file
  18. *
  19. */
  20. class Writer
  21. {
  22. /**
  23. * current language to write files for
  24. *
  25. * @var string
  26. */
  27. protected $language = '';
  28. /**
  29. * Name of a plugin (if set in contructor)
  30. *
  31. * @var string|null
  32. */
  33. protected $pluginName = null;
  34. /**
  35. * translations to write to file
  36. *
  37. * @var array
  38. */
  39. protected $translations = array();
  40. /**
  41. * Validators to check translations with
  42. *
  43. * @var ValidateAbstract[]
  44. */
  45. protected $validators = array();
  46. /**
  47. * Message why validation failed
  48. *
  49. * @var string|null
  50. */
  51. protected $validationMessage = null;
  52. /**
  53. * Filters to to apply to translations
  54. *
  55. * @var FilterAbstract[]
  56. */
  57. protected $filters = array();
  58. /**
  59. * Messages which filter changed the data
  60. *
  61. * @var array
  62. */
  63. protected $filterMessages = array();
  64. const UNFILTERED = 'unfiltered';
  65. const FILTERED = 'filtered';
  66. protected $currentState = self::UNFILTERED;
  67. /**
  68. * If $pluginName is given, Writer will be initialized for the given plugin if it exists
  69. * Otherwise it will be initialized for core translations
  70. *
  71. * @param string $language ISO 639-1 alpha-2 language code
  72. * @param string $pluginName optional plugin name
  73. * @throws \Exception
  74. */
  75. public function __construct($language, $pluginName = null)
  76. {
  77. $this->setLanguage($language);
  78. if (!empty($pluginName)) {
  79. $installedPlugins = \Piwik\Plugin\Manager::getInstance()->readPluginsDirectory();
  80. if (!in_array($pluginName, $installedPlugins)) {
  81. throw new Exception(Piwik::translate('General_ExceptionLanguageFileNotFound', array($pluginName)));
  82. }
  83. $this->pluginName = $pluginName;
  84. }
  85. }
  86. /**
  87. * @param string $language ISO 639-1 alpha-2 language code
  88. *
  89. * @throws \Exception
  90. */
  91. public function setLanguage($language)
  92. {
  93. if (!preg_match('/^([a-z]{2,3}(-[a-z]{2,3})?)$/i', $language)) {
  94. throw new Exception(Piwik::translate('General_ExceptionLanguageFileNotFound', array($language)));
  95. }
  96. $this->language = strtolower($language);
  97. }
  98. /**
  99. * @return string ISO 639-1 alpha-2 language code
  100. */
  101. public function getLanguage()
  102. {
  103. return $this->language;
  104. }
  105. /**
  106. * Returns if there are translations available or not
  107. * @return bool
  108. */
  109. public function hasTranslations()
  110. {
  111. return !empty($this->translations);
  112. }
  113. /**
  114. * Set the translations to write (and cleans them)
  115. *
  116. * @param $translations
  117. */
  118. public function setTranslations($translations)
  119. {
  120. $this->currentState = self::UNFILTERED;
  121. $this->translations = $translations;
  122. $this->applyFilters();
  123. }
  124. /**
  125. * Get translations from file
  126. *
  127. * @param string $lang ISO 639-1 alpha-2 language code
  128. * @throws Exception
  129. * @return array Array of translations ( plugin => ( key => translated string ) )
  130. */
  131. public function getTranslations($lang)
  132. {
  133. $path = $this->getTranslationPathBaseDirectory('lang', $lang);
  134. if (!is_readable($path)) {
  135. return array();
  136. }
  137. $data = file_get_contents($path);
  138. $translations = json_decode($data, true);
  139. return $translations;
  140. }
  141. /**
  142. * Returns the temporary path for translations
  143. *
  144. * @return string
  145. */
  146. public function getTemporaryTranslationPath()
  147. {
  148. return $this->getTranslationPathBaseDirectory('tmp');
  149. }
  150. /**
  151. * Returns the path to translation files
  152. *
  153. * @return string
  154. */
  155. public function getTranslationPath()
  156. {
  157. return $this->getTranslationPathBaseDirectory('lang');
  158. }
  159. /**
  160. * Get translation file path based on given params
  161. *
  162. * @param string $base Optional base directory (either 'lang' or 'tmp')
  163. * @param string|null $lang forced language
  164. * @throws \Exception
  165. * @return string path
  166. */
  167. protected function getTranslationPathBaseDirectory($base, $lang = null)
  168. {
  169. if (empty($lang)) {
  170. $lang = $this->getLanguage();
  171. }
  172. if (!empty($this->pluginName)) {
  173. if ($base == 'tmp') {
  174. return sprintf('%s/tmp/plugins/%s/lang/%s.json', PIWIK_INCLUDE_PATH, $this->pluginName, $lang);
  175. } else {
  176. return sprintf('%s/plugins/%s/lang/%s.json', PIWIK_INCLUDE_PATH, $this->pluginName, $lang);
  177. }
  178. }
  179. return sprintf('%s/%s/%s.json', PIWIK_INCLUDE_PATH, $base, $lang);
  180. }
  181. /**
  182. * Converts translations to a string that can be written to a file
  183. *
  184. * @return string
  185. */
  186. public function __toString()
  187. {
  188. /*
  189. * Use JSON_UNESCAPED_UNICODE and JSON_PRETTY_PRINT for PHP >= 5.4
  190. */
  191. $options = 0;
  192. if (defined('JSON_UNESCAPED_UNICODE')) {
  193. $options |= JSON_UNESCAPED_UNICODE;
  194. }
  195. if (defined('JSON_PRETTY_PRINT')) {
  196. $options |= JSON_PRETTY_PRINT;
  197. }
  198. return json_encode($this->translations, $options);
  199. }
  200. /**
  201. * Save translations to file; translations should already be cleaned.
  202. *
  203. * @throws \Exception
  204. * @return bool|int False if failure, or number of bytes written
  205. */
  206. public function save()
  207. {
  208. $this->applyFilters();
  209. if (!$this->hasTranslations() || !$this->isValid()) {
  210. throw new Exception('unable to save empty or invalid translations');
  211. }
  212. $path = $this->getTranslationPath();
  213. Filesystem::mkdir(dirname($path));
  214. return file_put_contents($path, $this->__toString());
  215. }
  216. /**
  217. * Save translations to temporary file; translations should already be cleansed.
  218. *
  219. * @throws \Exception
  220. * @return bool|int False if failure, or number of bytes written
  221. */
  222. public function saveTemporary()
  223. {
  224. $this->applyFilters();
  225. if (!$this->hasTranslations() || !$this->isValid()) {
  226. throw new Exception('unable to save empty or invalid translations');
  227. }
  228. $path = $this->getTemporaryTranslationPath();
  229. Filesystem::mkdir(dirname($path));
  230. return file_put_contents($path, $this->__toString());
  231. }
  232. /**
  233. * Adds an validator to check before saving
  234. *
  235. * @param ValidateAbstract $validator
  236. */
  237. public function addValidator(ValidateAbstract $validator)
  238. {
  239. $this->validators[] = $validator;
  240. }
  241. /**
  242. * Returns if translations are valid to save or not
  243. *
  244. * @return bool
  245. */
  246. public function isValid()
  247. {
  248. $this->applyFilters();
  249. $this->validationMessage = null;
  250. foreach ($this->validators AS $validator) {
  251. if (!$validator->isValid($this->translations)) {
  252. $this->validationMessage = $validator->getMessage();
  253. return false;
  254. }
  255. }
  256. return true;
  257. }
  258. /**
  259. * Returns last validation message
  260. *
  261. * @return null|string
  262. */
  263. public function getValidationMessage()
  264. {
  265. return $this->validationMessage;
  266. }
  267. /**
  268. * Returns if the were translations removed while cleaning
  269. *
  270. * @return bool
  271. */
  272. public function wasFiltered()
  273. {
  274. return !empty($this->filterMessages);
  275. }
  276. /**
  277. * Returns the cleaning errors
  278. *
  279. * @return array
  280. */
  281. public function getFilterMessages()
  282. {
  283. return $this->filterMessages;
  284. }
  285. /**
  286. * @param FilterAbstract $filter
  287. */
  288. public function addFilter(FilterAbstract $filter)
  289. {
  290. $this->filters[] = $filter;
  291. }
  292. /**
  293. * @throws \Exception
  294. *
  295. * @return bool error state
  296. */
  297. protected function applyFilters()
  298. {
  299. // skip if already cleaned
  300. if ($this->currentState == self::FILTERED) {
  301. return $this->wasFiltered();
  302. }
  303. $this->filterMessages = array();
  304. // skip if not translations available
  305. if (!$this->hasTranslations()) {
  306. $this->currentState = self::FILTERED;
  307. return false;
  308. }
  309. $cleanedTranslations = $this->translations;
  310. foreach ($this->filters AS $filter) {
  311. $cleanedTranslations = $filter->filter($cleanedTranslations);
  312. $filteredData = $filter->getFilteredData();
  313. if (!empty($filteredData)) {
  314. $this->filterMessages[] = get_class($filter) . " changed: " . var_export($filteredData, 1);
  315. }
  316. }
  317. $this->currentState = self::FILTERED;
  318. if ($cleanedTranslations != $this->translations) {
  319. $this->filterMessages[] = 'translations have been cleaned';
  320. }
  321. $this->translations = $cleanedTranslations;
  322. return $this->wasFiltered();
  323. }
  324. }