Development

/branches/1.0/lib/i18n/sfMessageSource_XLIFF.class.php

You must first sign up to be able to contribute.

root/branches/1.0/lib/i18n/sfMessageSource_XLIFF.class.php

Revision 19777, 13.2 kB (checked in by fabien, 5 years ago)

[1.0, 1.1, 1.2, 1.3] fixed warnings when trying to load a non-valid XML file with the XLIFF i18n message source class (closes #6728)

  • Property svn:mime-type set to text/x-php
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id Rev Date
Line 
1 <?php
2
3 /**
4  * sfMessageSource_XLIFF class file.
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the BSD License.
8  *
9  * Copyright(c) 2004 by Qiang Xue. All rights reserved.
10  *
11  * To contact the author write to {@link mailto:qiang.xue@gmail.com Qiang Xue}
12  * The latest version of PRADO can be obtained from:
13  * {@link http://prado.sourceforge.net/}
14  *
15  * @author     Wei Zhuo <weizhuo[at]gmail[dot]com>
16  * @version    $Id$
17  * @package    symfony
18  * @subpackage i18n
19  */
20
21 /**
22  * sfMessageSource_XLIFF class.
23  *
24  * Using XML XLIFF format as the message source for translation.
25  * Details and example of XLIFF can be found in the following URLs.
26  *
27  * # http://www.opentag.com/xliff.htm
28  * # http://www-106.ibm.com/developerworks/xml/library/x-localis2/
29  *
30  * See the MessageSource::factory() method to instantiate this class.
31  *
32  * @author Xiang Wei Zhuo <weizhuo[at]gmail[dot]com>
33  * @version v1.0, last update on Fri Dec 24 16:18:44 EST 2004
34  * @package    symfony
35  * @subpackage i18n
36  */
37 class sfMessageSource_XLIFF extends sfMessageSource
38 {
39   /**
40    * Message data filename extension.
41    * @var string
42    */
43   protected $dataExt = '.xml';
44
45   /**
46    * Separator between culture name and source.
47    * @var string
48    */
49   protected $dataSeparator = '.';
50
51   /**
52    * Constructor.
53    *
54    * @param string the directory where the messages are stored.
55    * @see MessageSource::factory();
56    */
57   function __construct($source)
58   {
59     $this->source = (string) $source;
60   }
61
62   /**
63    * Loads the messages from a XLIFF file.
64    *
65    * @param string XLIFF file.
66    * @return array of messages.
67    */
68   protected function &loadData($filename)
69   {
70     libxml_use_internal_errors(true);
71     if (!$xml = simplexml_load_file($filename))
72     {
73       $error = false;
74
75       return $error;
76     }
77     libxml_use_internal_errors(false);
78
79     $translationUnit = $xml->xpath('//trans-unit');
80
81     $translations = array();
82
83     foreach ($translationUnit as $unit)
84     {
85       $source = (string) $unit->source;
86       $translations[$source][] = (string) $unit->target;
87       $translations[$source][]= (string) $unit['id'];
88       $translations[$source][]= (string) $unit->note;
89     }
90
91     return $translations;
92   }
93
94   /**
95    * Gets the last modified unix-time for this particular catalogue+variant.
96    * Just use the file modified time.
97    *
98    * @param string catalogue+variant
99    * @return int last modified in unix-time format.
100    */
101   protected function getLastModified($source)
102   {
103     return is_file($source) ? filemtime($source) : 0;
104   }
105
106   /**
107    * Gets the XLIFF file for a specific message catalogue and cultural variant.
108    *
109    * @param string message catalogue
110    * @return string full path to the XLIFF file.
111    */
112   protected function getSource($variant)
113   {
114     return $this->source.'/'.$variant;
115   }
116
117   /**
118    * Determines if the XLIFF file source is valid.
119    *
120    * @param string XLIFF file
121    * @return boolean true if valid, false otherwise.
122    */
123   protected function isValidSource($source)
124   {
125     return is_file($source);
126   }
127
128   /**
129    * Gets all the variants of a particular catalogue.
130    *
131    * @param string catalogue name
132    * @return array list of all variants for this catalogue.
133    */
134   protected function getCatalogueList($catalogue)
135   {
136     $variants = explode('_', $this->culture);
137     $source = $catalogue.$this->dataExt;
138
139     $catalogues = array($source);
140
141     $variant = null;
142
143     for ($i = 0, $max = count($variants); $i < $max; $i++)
144     {
145       if (strlen($variants[$i]) > 0)
146       {
147         $variant .= $variant ? '_'.$variants[$i] : $variants[$i];
148         $catalogues[] = $catalogue.$this->dataSeparator.$variant.$this->dataExt;
149       }
150     }
151
152     $byDir = $this->getCatalogueByDir($catalogue);
153     $catalogues = array_merge($byDir, array_reverse($catalogues));
154
155     return $catalogues;
156   }
157
158   /**
159    * Traverses through the directory structure to find the catalogues.
160    * This should only be called by getCatalogueList()
161    *
162    * @param string a particular catalogue.
163    * @return array a list of catalogues.
164    * @see getCatalogueList()
165    */
166   protected function getCatalogueByDir($catalogue)
167   {
168     $variants = explode('_', $this->culture);
169     $catalogues = array();
170
171     $variant = null;
172
173     for ($i = 0, $max = count($variants); $i < $max; $i++)
174     {
175       if (strlen($variants[$i]) > 0)
176       {
177         $variant .= $variant ? '_'.$variants[$i] : $variants[$i];
178         $catalogues[] = $variant.'/'.$catalogue.$this->dataExt;
179       }
180     }
181
182     return array_reverse($catalogues);
183   }
184
185   /**
186    * Returns a list of catalogue and its culture ID.
187    * E.g. array('messages', 'en_AU')
188    *
189    * @return array list of catalogues
190    * @see getCatalogues()
191    */
192   public function catalogues()
193   {
194     return $this->getCatalogues();
195   }
196
197   /**
198    * Returns a list of catalogue and its culture ID. This takes care
199    * of directory structures.
200    * E.g. array('messages', 'en_AU')
201    *
202    * @return array list of catalogues
203    */
204   protected function getCatalogues($dir = null, $variant = null)
205   {
206     $dir = $dir ? $dir : $this->source;
207     $files = scandir($dir);
208
209     $catalogue = array();
210
211     foreach ($files as $file)
212     {
213       if (is_dir($dir.'/'.$file) && preg_match('/^[a-z]{2}(_[A-Z]{2,3})?$/', $file))
214       {
215         $catalogue = array_merge($catalogue, $this->getCatalogues($dir.'/'.$file, $file));
216       }
217
218       $pos = strpos($file,$this->dataExt);
219       if ($pos > 0 && substr($file, -1 * strlen($this->dataExt)) == $this->dataExt)
220       {
221         $name = substr($file, 0, $pos);
222         $dot = strrpos($name, $this->dataSeparator);
223         $culture = $variant;
224         $cat = $name;
225         if (is_int($dot))
226         {
227           $culture = substr($name, $dot + 1,strlen($name));
228           $cat = substr($name, 0, $dot);
229         }
230         $details[0] = $cat;
231         $details[1] = $culture;
232
233         $catalogue[] = $details;
234       }
235     }
236     sort($catalogue);
237
238     return $catalogue;
239   }
240
241   /**
242    * Gets the variant for a catalogue depending on the current culture.
243    *
244    * @param string catalogue
245    * @return string the variant.
246    * @see save()
247    * @see update()
248    * @see delete()
249    */
250   protected function getVariants($catalogue = 'messages')
251   {
252     if (is_null($catalogue))
253     {
254       $catalogue = 'messages';
255     }
256
257     foreach ($this->getCatalogueList($catalogue) as $variant)
258     {
259       $file = $this->getSource($variant);
260       if (is_file($file))
261       {
262         return array($variant, $file);
263       }
264     }
265
266     return false;
267   }
268
269   /**
270    * Saves the list of untranslated blocks to the translation source.
271    * If the translation was not found, you should add those
272    * strings to the translation source via the <b>append()</b> method.
273    *
274    * @param string the catalogue to add to
275    * @return boolean true if saved successfuly, false otherwise.
276    */
277   public function save($catalogue = 'messages')
278   {
279     $messages = $this->untranslated;
280     if (count($messages) <= 0)
281     {
282       return false;
283     }
284
285     $variants = $this->getVariants($catalogue);
286     if ($variants)
287     {
288       list($variant, $filename) = $variants;
289     }
290     else
291     {
292       return false;
293     }
294
295     if (is_writable($filename) == false)
296     {
297       throw new sfException(sprintf("Unable to save to file %s, file must be writable.", $filename));
298     }
299
300     // create a new dom, import the existing xml
301     $dom = new DOMDocument();
302     $dom->load($filename);
303
304     // find the body element
305     $xpath = new DomXPath($dom);
306     $body = $xpath->query('//body')->item(0);
307
308     // find the biggest "id" used
309     $lastNodes = $xpath->query('//trans-unit[not(@id <= preceding-sibling::trans-unit/@id) and not(@id <= following-sibling::trans-unit/@id)]');
310     if (null !== $last = $lastNodes->item(0))
311     {
312       $count = intval($last->getAttribute('id'));
313     }
314     else
315     {
316       $count = 0;
317     }
318
319     // for each message add it to the XML file using DOM
320     foreach ($messages as $message)
321     {
322       $unit = $dom->createElement('trans-unit');
323       $unit->setAttribute('id', ++$count);
324
325       $source = $dom->createElement('source');
326       $source->appendChild($dom->createTextNode($message));
327       $target = $dom->createElement('target');
328       $target->appendChild($dom->createTextNode(''));
329
330       $unit->appendChild($dom->createTextNode("\n"));
331       $unit->appendChild($source);
332       $unit->appendChild($dom->createTextNode("\n"));
333       $unit->appendChild($target);
334       $unit->appendChild($dom->createTextNode("\n"));
335
336       $body->appendChild($dom->createTextNode("\n"));
337       $body->appendChild($unit);
338       $body->appendChild($dom->createTextNode("\n"));
339     }
340
341     $fileNode = $xpath->query('//file')->item(0);
342     $fileNode->setAttribute('date', @date('Y-m-d\TH:i:s\Z'));
343
344     // save it and clear the cache for this variant
345     $dom->save($filename);
346     if (!empty($this->cache))
347     {
348       $this->cache->clean($variant, $this->culture);
349     }
350
351     return true;
352   }
353
354   /**
355    * Updates the translation.
356    *
357    * @param string the source string.
358    * @param string the new translation string.
359    * @param string comments
360    * @param string the catalogue to save to.
361    * @return boolean true if translation was updated, false otherwise.
362    */
363   public function update($text, $target, $comments, $catalogue = 'messages')
364   {
365     $variants = $this->getVariants($catalogue);
366     if ($variants)
367     {
368       list($variant, $filename) = $variants;
369     }
370     else
371     {
372       return false;
373     }
374
375     if (is_writable($filename) == false)
376     {
377       throw new sfException(sprintf("Unable to update file %s, file must be writable.", $filename));
378     }
379
380     // create a new dom, import the existing xml
381     $dom = new DOMDocument();
382     $dom->load($filename);
383
384     // find the body element
385     $xpath = new DomXPath($dom);
386     $units = $xpath->query('//trans-unit');
387
388     // for each of the existin units
389     foreach ($units as $unit)
390     {
391       $found = false;
392       $targetted = false;
393       $commented = false;
394
395       //in each unit, need to find the source, target and comment nodes
396       //it will assume that the source is before the target.
397       foreach ($unit->childNodes as $node)
398       {
399         // source node
400         if ($node->nodeName == 'source' && $node->firstChild->wholeText == $text)
401         {
402           $found = true;
403         }
404
405         // found source, get the target and notes
406         if ($found)
407         {
408           // set the new translated string
409           if ($node->nodeName == 'target')
410           {
411             $node->nodeValue = $target;
412             $targetted = true;
413           }
414
415           // set the notes
416           if (!empty($comments) && $node->nodeName == 'note')
417           {
418             $node->nodeValue = $comments;
419             $commented = true;
420           }
421         }
422       }
423
424       // append a target
425       if ($found && !$targetted)
426       {
427         $targetNode = $dom->createElement('target');
428         $targetNode->appendChild($dom->createTextNode($target));
429         $unit->appendChild($targetNode);
430       }
431
432       // append a note
433       if ($found && !$commented && !empty($comments))
434       {
435         $commentsNode = $dom->createElement('note');
436         $commentsNode->appendChild($dom->createTextNode($comments));
437         $unit->appendChild($commentsNode);
438       }
439
440       // finished searching
441       if ($found)
442       {
443         break;
444       }
445     }
446
447     $fileNode = $xpath->query('//file')->item(0);
448     $fileNode->setAttribute('date', @date('Y-m-d\TH:i:s\Z'));
449
450     if ($dom->save($filename) > 0)
451     {
452       if (!empty($this->cache))
453       {
454         $this->cache->clean($variant, $this->culture);
455       }
456
457       return true;
458     }
459
460     return false;
461   }
462
463   /**
464    * Deletes a particular message from the specified catalogue.
465    *
466    * @param string the source message to delete.
467    * @param string the catalogue to delete from.
468    * @return boolean true if deleted, false otherwise.
469    */
470   public function delete($message, $catalogue='messages')
471   {
472     $variants = $this->getVariants($catalogue);
473     if ($variants)
474     {
475       list($variant, $filename) = $variants;
476     }
477     else
478     {
479       return false;
480     }
481
482     if (is_writable($filename) == false)
483     {
484       throw new sfException(sprintf("Unable to modify file %s, file must be writable.", $filename));
485     }
486
487     // create a new dom, import the existing xml
488     $dom = new DOMDocument();
489     $dom->load($filename);
490
491     // find the body element
492     $xpath = new DomXPath($dom);
493     $units = $xpath->query('//trans-unit');
494
495     // for each of the existin units
496     foreach ($units as $unit)
497     {
498       //in each unit, need to find the source, target and comment nodes
499       //it will assume that the source is before the target.
500       foreach ($unit->childNodes as $node)
501       {
502         // source node
503         if ($node->nodeName == 'source' && $node->firstChild->wholeText == $message)
504         {
505           // we found it, remove and save the xml file.
506           $unit->parentNode->removeChild($unit);
507
508           $fileNode = $xpath->query('//file')->item(0);
509           $fileNode->setAttribute('date', @date('Y-m-d\TH:i:s\Z'));
510
511           if ($dom->save($filename) > 0)
512           {
513             if (!empty($this->cache))
514             {
515               $this->cache->clean($variant, $this->culture);
516             }
517
518             return true;
519           }
520           else
521           {
522             return false;
523           }
524         }
525       }
526     }
527
528     return false;
529   }
530 }
531
Note: See TracBrowser for help on using the browser.