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

Debug toolbar