sfMessageSource_XLIFF.class.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. <?php
  2. /**
  3. * sfMessageSource_XLIFF class file.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the BSD License.
  7. *
  8. * Copyright(c) 2004 by Qiang Xue. All rights reserved.
  9. *
  10. * To contact the author write to {@link mailto:qiang.xue@gmail.com Qiang Xue}
  11. * The latest version of PRADO can be obtained from:
  12. * {@link http://prado.sourceforge.net/}
  13. *
  14. * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
  15. * @version $Id: sfMessageSource_XLIFF.class.php 13419 2008-11-27 13:15:14Z fabien $
  16. * @package symfony
  17. * @subpackage i18n
  18. */
  19. /**
  20. * sfMessageSource_XLIFF class.
  21. *
  22. * Using XML XLIFF format as the message source for translation.
  23. * Details and example of XLIFF can be found in the following URLs.
  24. *
  25. * # http://www.opentag.com/xliff.htm
  26. * # http://www-106.ibm.com/developerworks/xml/library/x-localis2/
  27. *
  28. * See the MessageSource::factory() method to instantiate this class.
  29. *
  30. * @author Xiang Wei Zhuo <weizhuo[at]gmail[dot]com>
  31. * @version v1.0, last update on Fri Dec 24 16:18:44 EST 2004
  32. * @package symfony
  33. * @subpackage i18n
  34. */
  35. class sfMessageSource_XLIFF extends sfMessageSource_File
  36. {
  37. /**
  38. * Message data filename extension.
  39. * @var string
  40. */
  41. protected $dataExt = '.xml';
  42. /**
  43. * Loads the messages from a XLIFF file.
  44. *
  45. * @param string $filename XLIFF file.
  46. * @return array of messages.
  47. */
  48. public function &loadData($filename)
  49. {
  50. $XML = simplexml_load_file($filename);
  51. if (!$XML)
  52. {
  53. return false;
  54. }
  55. $translationUnit = $XML->xpath('//trans-unit');
  56. $translations = array();
  57. foreach ($translationUnit as $unit)
  58. {
  59. $source = (string) $unit->source;
  60. $translations[$source][] = (string) $unit->target;
  61. $translations[$source][] = (string) $unit['id'];
  62. $translations[$source][] = (string) $unit->note;
  63. }
  64. return $translations;
  65. }
  66. /**
  67. * Creates and returns a new DOMDocument instance
  68. *
  69. * @param string $xml XML string
  70. *
  71. * @return DOMDocument
  72. */
  73. protected function createDOMDocument($xml = null)
  74. {
  75. $domimp = new DOMImplementation();
  76. $doctype = $domimp->createDocumentType('xliff', '-//XLIFF//DTD XLIFF//EN', 'http://www.oasis-open.org/committees/xliff/documents/xliff.dtd');
  77. $dom = $domimp->createDocument('', '', $doctype);
  78. $dom->formatOutput = true;
  79. $dom->preserveWhiteSpace = false;
  80. if (!is_null($xml) && is_string($xml))
  81. {
  82. // Add header for XML with UTF-8
  83. if (!preg_match('/<\?xml/', $xml))
  84. {
  85. $xml = '<?xml version="1.0" encoding="UTF-8"?>'."\n".$xml;
  86. }
  87. $dom->loadXML($xml);
  88. }
  89. return $dom;
  90. }
  91. /**
  92. * Gets the variant for a catalogue depending on the current culture.
  93. *
  94. * @param string $catalogue catalogue
  95. * @return string the variant.
  96. * @see save()
  97. * @see update()
  98. * @see delete()
  99. */
  100. protected function getVariants($catalogue = 'messages')
  101. {
  102. if (is_null($catalogue))
  103. {
  104. $catalogue = 'messages';
  105. }
  106. foreach ($this->getCatalogueList($catalogue) as $variant)
  107. {
  108. $file = $this->getSource($variant);
  109. if (is_file($file))
  110. {
  111. return array($variant, $file);
  112. }
  113. }
  114. return false;
  115. }
  116. /**
  117. * Saves the list of untranslated blocks to the translation source.
  118. * If the translation was not found, you should add those
  119. * strings to the translation source via the <b>append()</b> method.
  120. *
  121. * @param string $catalogue the catalogue to add to
  122. * @return boolean true if saved successfuly, false otherwise.
  123. */
  124. public function save($catalogue = 'messages')
  125. {
  126. $messages = $this->untranslated;
  127. if (count($messages) <= 0)
  128. {
  129. return false;
  130. }
  131. $variants = $this->getVariants($catalogue);
  132. if ($variants)
  133. {
  134. list($variant, $filename) = $variants;
  135. }
  136. else
  137. {
  138. list($variant, $filename) = $this->createMessageTemplate($catalogue);
  139. }
  140. if (is_writable($filename) == false)
  141. {
  142. throw new sfException(sprintf("Unable to save to file %s, file must be writable.", $filename));
  143. }
  144. // create a new dom, import the existing xml
  145. $dom = $this->createDOMDocument();
  146. @$dom->load($filename);
  147. // find the body element
  148. $xpath = new DomXPath($dom);
  149. $body = $xpath->query('//body')->item(0);
  150. if (is_null($body))
  151. {
  152. //create and try again
  153. $this->createMessageTemplate($catalogue);
  154. $dom->load($filename);
  155. $xpath = new DomXPath($dom);
  156. $body = $xpath->query('//body')->item(0);
  157. }
  158. // find the biggest "id" used
  159. $lastNodes = $xpath->query('//trans-unit[not(@id <= preceding-sibling::trans-unit/@id) and not(@id <= following-sibling::trans-unit/@id)]');
  160. if (null !== $last = $lastNodes->item(0))
  161. {
  162. $count = intval($last->getAttribute('id'));
  163. }
  164. else
  165. {
  166. $count = 0;
  167. }
  168. // for each message add it to the XML file using DOM
  169. foreach ($messages as $message)
  170. {
  171. $unit = $dom->createElement('trans-unit');
  172. $unit->setAttribute('id', ++$count);
  173. $source = $dom->createElement('source');
  174. $source->appendChild($dom->createTextNode($message));
  175. $target = $dom->createElement('target');
  176. $target->appendChild($dom->createTextNode(''));
  177. $unit->appendChild($source);
  178. $unit->appendChild($target);
  179. $body->appendChild($unit);
  180. }
  181. $fileNode = $xpath->query('//file')->item(0);
  182. $fileNode->setAttribute('date', @date('Y-m-d\TH:i:s\Z'));
  183. $dom = $this->createDOMDocument($dom->saveXML());
  184. // save it and clear the cache for this variant
  185. $dom->save($filename);
  186. if ($this->cache)
  187. {
  188. $this->cache->remove($variant.':'.$this->culture);
  189. }
  190. return true;
  191. }
  192. /**
  193. * Updates the translation.
  194. *
  195. * @param string $text the source string.
  196. * @param string $target the new translation string.
  197. * @param string $comments comments
  198. * @param string $catalogue the catalogue to save to.
  199. * @return boolean true if translation was updated, false otherwise.
  200. */
  201. public function update($text, $target, $comments, $catalogue = 'messages')
  202. {
  203. $variants = $this->getVariants($catalogue);
  204. if ($variants)
  205. {
  206. list($variant, $filename) = $variants;
  207. }
  208. else
  209. {
  210. return false;
  211. }
  212. if (is_writable($filename) == false)
  213. {
  214. throw new sfException(sprintf("Unable to update file %s, file must be writable.", $filename));
  215. }
  216. // create a new dom, import the existing xml
  217. $dom = $this->createDOMDocument();
  218. $dom->load($filename);
  219. // find the body element
  220. $xpath = new DomXPath($dom);
  221. $units = $xpath->query('//trans-unit');
  222. // for each of the existin units
  223. foreach ($units as $unit)
  224. {
  225. $found = false;
  226. $targetted = false;
  227. $commented = false;
  228. //in each unit, need to find the source, target and comment nodes
  229. //it will assume that the source is before the target.
  230. foreach ($unit->childNodes as $node)
  231. {
  232. // source node
  233. if ($node->nodeName == 'source' && $node->firstChild->wholeText == $text)
  234. {
  235. $found = true;
  236. }
  237. // found source, get the target and notes
  238. if ($found)
  239. {
  240. // set the new translated string
  241. if ($node->nodeName == 'target')
  242. {
  243. $node->nodeValue = $target;
  244. $targetted = true;
  245. }
  246. // set the notes
  247. if (!empty($comments) && $node->nodeName == 'note')
  248. {
  249. $node->nodeValue = $comments;
  250. $commented = true;
  251. }
  252. }
  253. }
  254. // append a target
  255. if ($found && !$targetted)
  256. {
  257. $targetNode = $dom->createElement('target');
  258. $targetNode->appendChild($dom->createTextNode($target));
  259. $unit->appendChild($targetNode);
  260. }
  261. // append a note
  262. if ($found && !$commented && !empty($comments))
  263. {
  264. $commentsNode = $dom->createElement('note');
  265. $commentsNode->appendChild($dom->createTextNode($comments));
  266. $unit->appendChild($commentsNode);
  267. }
  268. // finished searching
  269. if ($found)
  270. {
  271. break;
  272. }
  273. }
  274. $fileNode = $xpath->query('//file')->item(0);
  275. $fileNode->setAttribute('date', @date('Y-m-d\TH:i:s\Z'));
  276. if ($dom->save($filename) > 0)
  277. {
  278. if ($this->cache)
  279. {
  280. $this->cache->remove($variant.':'.$this->culture);
  281. }
  282. return true;
  283. }
  284. return false;
  285. }
  286. /**
  287. * Deletes a particular message from the specified catalogue.
  288. *
  289. * @param string $message the source message to delete.
  290. * @param string $catalogue the catalogue to delete from.
  291. * @return boolean true if deleted, false otherwise.
  292. */
  293. public function delete($message, $catalogue='messages')
  294. {
  295. $variants = $this->getVariants($catalogue);
  296. if ($variants)
  297. {
  298. list($variant, $filename) = $variants;
  299. }
  300. else
  301. {
  302. return false;
  303. }
  304. if (is_writable($filename) == false)
  305. {
  306. throw new sfException(sprintf("Unable to modify file %s, file must be writable.", $filename));
  307. }
  308. // create a new dom, import the existing xml
  309. $dom = $this->createDOMDocument();
  310. $dom->load($filename);
  311. // find the body element
  312. $xpath = new DomXPath($dom);
  313. $units = $xpath->query('//trans-unit');
  314. // for each of the existin units
  315. foreach ($units as $unit)
  316. {
  317. //in each unit, need to find the source, target and comment nodes
  318. //it will assume that the source is before the target.
  319. foreach ($unit->childNodes as $node)
  320. {
  321. // source node
  322. if ($node->nodeName == 'source' && $node->firstChild->wholeText == $message)
  323. {
  324. // we found it, remove and save the xml file.
  325. $unit->parentNode->removeChild($unit);
  326. $fileNode = $xpath->query('//file')->item(0);
  327. $fileNode->setAttribute('date', @date('Y-m-d\TH:i:s\Z'));
  328. if ($dom->save($filename) > 0)
  329. {
  330. if (!empty($this->cache))
  331. {
  332. $this->cache->remove($variant.':'.$this->culture);
  333. }
  334. return true;
  335. }
  336. else
  337. {
  338. return false;
  339. }
  340. }
  341. }
  342. }
  343. return false;
  344. }
  345. protected function createMessageTemplate($catalogue)
  346. {
  347. if (is_null($catalogue))
  348. {
  349. $catalogue = 'messages';
  350. }
  351. $variants = $this->getCatalogueList($catalogue);
  352. $variant = array_shift($variants);
  353. $file = $this->getSource($variant);
  354. $dir = dirname($file);
  355. if (!is_dir($dir))
  356. {
  357. @mkdir($dir);
  358. @chmod($dir, 0777);
  359. }
  360. if (!is_dir($dir))
  361. {
  362. throw new sfException(sprintf("Unable to create directory %s.", $dir));
  363. }
  364. $dom = $this->createDOMDocument($this->getTemplate($catalogue));
  365. file_put_contents($file, $dom->saveXML());
  366. chmod($file, 0777);
  367. return array($variant, $file);
  368. }
  369. protected function getTemplate($catalogue)
  370. {
  371. $date = date('c');
  372. return <<<EOD
  373. <?xml version="1.0" encoding="UTF-8"?>
  374. <!DOCTYPE xliff PUBLIC "-//XLIFF//DTD XLIFF//EN" "http://www.oasis-open.org/committees/xliff/documents/xliff.dtd" >
  375. <xliff version="1.0">
  376. <file source-language="EN" target-language="{$this->culture}" datatype="plaintext" original="$catalogue" date="$date" product-name="$catalogue">
  377. <header />
  378. <body>
  379. </body>
  380. </file>
  381. </xliff>
  382. EOD;
  383. }
  384. }