translation-tool.pl 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. #!/usr/bin/perl
  2. # The MIT License
  3. #
  4. # Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number
  5. # of other of contributors
  6. #
  7. # Permission is hereby granted, free of charge, to any person obtaining a copy
  8. # of this software and associated documentation files (the "Software"), to deal
  9. # in the Software without restriction, including without limitation the rights
  10. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. # copies of the Software, and to permit persons to whom the Software is
  12. # furnished to do so, subject to the following conditions:
  13. #
  14. # The above copyright notice and this permission notice shall be included in
  15. # all copies or substantial portions of the Software.
  16. #
  17. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. # THE SOFTWARE.
  24. # Author: Manuel Carrasco
  25. # Date: 20 Mar 2010
  26. # Perl script to generate missing translation keys and missing properties files,
  27. # to remove unused keys, and to convert utf8 properties files to iso or ascii.
  28. #
  29. # 1.- It recursively looks for files in a folder, and analyzes them to extract
  30. # the keys being used in the application.
  31. # 2.- If --add=true, it generates the appropriate file for the desired language
  32. # and adds these keys to it, adding the english text as a reference. If the
  33. # properties file already exists the script update it with the new keys.
  34. # 3.- When --remove=true and there are unused keys in our file, the script
  35. # removes them.
  36. # 4.- If an editor is passed as argument, the script edits each modified file
  37. # after adding new keys.
  38. # 5.- Finally, when --toiso=true or --toascii=true, the script is able to
  39. # convert utf-8 properties files to iso or unicode hex representation is
  40. # ascii.
  41. #
  42. # Note, while the migration to Jenkins this file will report the keys which
  43. # should point to Jenkins instead of the old name.
  44. use warnings;
  45. use strict;
  46. use File::Basename;
  47. use File::Find;
  48. use File::Path;
  49. use Getopt::Long;
  50. my (
  51. $lang, $editor, $dir, $toiso, $toascii, $add,
  52. $remove, $reuse, $counter, $target, $help, $debug
  53. ) = (undef, undef, "./", 0, 0, 0, 0, 0, 0, "./", 0, 0);
  54. GetOptions(
  55. 'help' => \$help,
  56. 'lang=s' => \$lang,
  57. 'editor' => \$editor,
  58. 'dir=s' => \$dir,
  59. 'to-iso' => \$toiso,
  60. 'to-ascii' => \$toascii,
  61. 'add' => \$add,
  62. 'remove' => \$remove,
  63. 'reuse' => \$reuse,
  64. 'counter' => \$counter,
  65. 'target=s' => \$target,
  66. 'debug' => \$debug
  67. ) or die("Error in command line arguments\n");
  68. if ($help) {
  69. usage();
  70. exit(0);
  71. }
  72. $add = 1 if ($editor);
  73. if ($toiso && $toascii) {
  74. warn "You can't have --to-iso and --to-ascii options at the same time\n";
  75. usage();
  76. exit(1);
  77. }
  78. my ($tfiles, $tkeys, $tmissing, $tunused, $tempty, $tsame, $tnojenkins,
  79. $countervalue)
  80. = (0, 0, 0, 0, 0, 0, 0, 1);
  81. ## language parameter is mandatory and shouldn't be 'en'
  82. unless (($lang) and ($lang ne 'en')) {
  83. usage();
  84. exit(1);
  85. }
  86. print "Searching for files ...\n";
  87. ## look for Message.properties and *.jelly files in the provided folder
  88. my @files = findTranslatableFiles($dir);
  89. print 'Found ', scalar(@files), ' files', "\n";
  90. ## load a cache with keys already translated to utilize in the case the same key
  91. ## is used
  92. my %cache = loadAllTranslatedKeys($reuse, $lang) if ($reuse && -e $reuse);
  93. ## process each file
  94. foreach (@files) {
  95. $tfiles++;
  96. processFile($_);
  97. }
  98. ## print statistics
  99. my $tdone = $tkeys - $tmissing - $tunused - $tempty - $tsame - $tnojenkins;
  100. my $pdone = 100;
  101. my $pmissing = 0;
  102. my $punused = 0;
  103. my $pempty = 0;
  104. my $psame = 0;
  105. my $pnojenkins = 0;
  106. if ($tkeys != 0) {
  107. $pdone = $tdone / $tkeys * 100;
  108. $pmissing = $tmissing / $tkeys * 100;
  109. $punused = $tunused / $tkeys * 100;
  110. $pempty = $tempty / $tkeys * 100;
  111. $psame = $tsame / $tkeys * 100;
  112. $pnojenkins = $tnojenkins / $tkeys * 100;
  113. }
  114. my @formatParameters = (
  115. $tfiles, $tkeys, $tdone, $pdone, $tmissing,
  116. $pmissing, $tunused, $punused, $tempty, $pempty,
  117. $tsame, $psame, $tnojenkins, $pnojenkins
  118. );
  119. printf
  120. "\nTOTAL: Files: %d Keys: %d Done: %d(%.2f%%)\n Missing: %d(%.2f%%) Orphan: %d(%.2f%%) Empty: %d(%.2f%%) Same: %d(%.2f%%) NoJenkins: %d(%.2f%%)\n\n",
  121. (@formatParameters);
  122. ## end
  123. exit();
  124. ### This is the main method with is run for each file
  125. sub processFile {
  126. # ofile -> output file in the current language,
  127. # efile -> english file
  128. my $file = shift;
  129. my ($ofile, $efile) = ($file, $file);
  130. $ofile =~ s/$dir/$target/;
  131. $ofile =~ s/(\.jelly)|(\.properties)/_$lang.properties/;
  132. $efile =~ s/(\.jelly)/.properties/;
  133. # keys -> Hash of keys used in jelly or Message.properties files
  134. # ekeys -> Hash of key/values in English
  135. # okeys -> Hash of key/values in the desired language which are already
  136. # present in the file
  137. my (%keys, %okeys, %ekeys);
  138. # Read .jelly or Message.properties files, and fill a hash with the keys
  139. # found
  140. if ($file =~ m/.jelly$/) {
  141. %keys = loadJellyFile($file);
  142. %ekeys = loadPropertiesFile($efile);
  143. } else {
  144. %keys = %ekeys = loadPropertiesFile($file);
  145. }
  146. # load keys already present in the desired locale
  147. %okeys = loadPropertiesFile($ofile);
  148. # calculate missing keys in the file
  149. my $missing = "";
  150. foreach (keys %keys) {
  151. $tkeys++;
  152. if (!defined($okeys{$_})) {
  153. $_ .= "=" . $cache{$_} if (defined($cache{$_}));
  154. $missing .= ($add ? " Adding " : " Missing") . " -> $_\n";
  155. $tmissing++;
  156. } elsif ($okeys{$_} eq '') {
  157. $missing .= " Empty -> $_\n";
  158. $tempty++;
  159. }
  160. }
  161. # calculate old keys in the file which are currently unused
  162. my $unused = "";
  163. foreach (keys %okeys) {
  164. if (!defined $keys{$_}) {
  165. $unused .= " Unused -> $_\n";
  166. $tunused++;
  167. }
  168. }
  169. # calculate keys which have the same value in English
  170. my $same = "";
  171. foreach (keys %okeys) {
  172. if ($okeys{$_} && $ekeys{$_} && $okeys{$_} eq $ekeys{$_}) {
  173. $same .= " Same -> $_\n";
  174. $tsame++;
  175. }
  176. }
  177. my $nj = "";
  178. foreach (keys %okeys) {
  179. if ($okeys{$_} && $okeys{$_} =~ /Hudson/) {
  180. $nj .= " Non Jenkins -> $_ -> $okeys{$_}\n";
  181. $tnojenkins++;
  182. }
  183. }
  184. # Show Alerts
  185. print "\nFile: $ofile\n$missing$unused$same$nj"
  186. if ($missing ne "" || $unused ne '' || $same ne '' || $nj ne '');
  187. # write new keys in our file adding the English translation as a reference
  188. if ($add && $missing ne "") {
  189. printLicense($ofile) unless (-f $ofile);
  190. open(F, ">>$ofile");
  191. foreach (keys %keys) {
  192. if (!$okeys{$_}) {
  193. if (!defined($okeys{$_})) {
  194. print F "$_=";
  195. if (defined($cache{$_})) {
  196. print F $cache{$_} . "\n";
  197. } else {
  198. if ($counter) {
  199. # add unique value for each added translation
  200. print F "---TranslateMe " . $countervalue . "--- " .
  201. ($ekeys{$_} ? $ekeys{$_} : $_) . "\n";
  202. } else {
  203. print F "\n";
  204. }
  205. }
  206. $countervalue++;
  207. }
  208. }
  209. }
  210. close(F);
  211. }
  212. # open the editor if the user has specified it and there are changes to
  213. # manage
  214. system("$editor $ofile")
  215. if ($editor && $add && ($missing ne "" || $same ne "" || $nj ne ''));
  216. # write new keys in our file adding the English translation as a reference
  217. removeUnusedKeys($ofile, %keys) if ($remove && $unused ne "");
  218. # convert the language file to ISO or ASCII which are
  219. # the charsets which Jenkins supports right now
  220. convert($ofile, $toiso, $toascii) if (-f $ofile);
  221. }
  222. # Create a hash with all keys which exist and have an unique value
  223. sub loadAllTranslatedKeys {
  224. my ($dir, $lang, %ret) = @_;
  225. my @files = findTranslatableFiles($dir);
  226. foreach (@files) {
  227. s/(\.jelly)|(\.properties)/_$lang.properties/;
  228. next unless (-f $_);
  229. my (%h, $k, $v) = loadPropertiesFile($_);
  230. while (($k, $v) = each(%h)) {
  231. $ret{$k} = "" if (defined($ret{$k}) && $v ne $ret{$k});
  232. $ret{$k} = $v unless defined($ret{$k});
  233. }
  234. }
  235. return %ret;
  236. }
  237. # Look for Message.properties and *.jelly files
  238. sub findTranslatableFiles {
  239. my $dir = shift;
  240. die "Folder doesn't exist: $dir\n" unless (-e $dir);
  241. my @ret;
  242. find(
  243. sub {
  244. my $file = $File::Find::name;
  245. push(@ret, $file)
  246. if ($file !~ m#(/src/test/)|(/target/)|(\.svn)#
  247. && $file =~ /(Messages.properties)$|(.*\.jelly)$/);
  248. },
  249. $dir
  250. );
  251. return @ret;
  252. }
  253. # Fill a hash with key/1 pairs from a .jelly file
  254. sub loadJellyFile {
  255. my $file = shift;
  256. my %ret;
  257. open(F, $file) || die $! . " " . $file;
  258. while (<F>) {
  259. next if (!/\$\{.*?\%([^\(]+?).*\}/);
  260. my $line = $_;
  261. while ($line =~ /^.*?\$\{\%([^\(\}]+)(.*)$/
  262. || $line =~ /^.*?\$\{.*?['"]\%([^\(\}\"\']+)(.*)$/)
  263. {
  264. $line = $2;
  265. my $word = $1;
  266. $word =~ s/\(.+$//g;
  267. $word =~ s/'+/''/g;
  268. $word =~ s/ /\\ /g;
  269. $word =~ s/\&gt;/>/g;
  270. $word =~ s/\&lt;/</g;
  271. $word =~ s/\&amp;/&/g;
  272. $word =~ s/([#:=])/\\$1/g;
  273. $ret{$word} = 1;
  274. }
  275. }
  276. close(F);
  277. return %ret;
  278. }
  279. # Fill a hash with key/value pairs from a .properties file
  280. sub loadPropertiesFile {
  281. my $file = shift;
  282. my %ret;
  283. print "Trying to load $file... " if ($debug);
  284. unless (-f $file) {
  285. print "file does not exist, skipping.\n" if ($debug);
  286. return %ret;
  287. }
  288. print "done.\n" if ($debug);
  289. my $skip_comment = qr/^#/;
  290. open(my $in, '<', $file) or die "Cannot read $file: $!\n";
  291. my ($cont, $key, $val) = (0, undef, undef);
  292. while (<$in>) {
  293. chomp;
  294. next if $_ eq '';
  295. next if $_ =~ $skip_comment;
  296. print 'Line: ', $_, "\n" if ($debug);
  297. s/[\r\n]+//;
  298. $ret{$key} .= "\n$1" if ($cont && /\s*(.*)[\\\s]*$/);
  299. if (/^([^#\s].*?[^\\])=(.*)[\s\\]*$/) {
  300. ($key, $val) = (trim($1), trim($2));
  301. $ret{$key} = $val;
  302. }
  303. $cont = (/\\\s*$/) ? 1 : 0;
  304. }
  305. close($in);
  306. # TODO: Use of uninitialized value $_ in pattern match (m//) at
  307. # ./translation-tool.pl line 345.
  308. $ret{$key} .= "\n$1" if ($cont && /\s*(.*)[\\\s]*$/);
  309. return %ret;
  310. }
  311. # remove unused keys from a file
  312. sub removeUnusedKeys {
  313. my ($ofile, %keys) = @_;
  314. print "Removing unused keys from: $ofile\n";
  315. my $back = $ofile . "~~";
  316. if (rename($ofile, $back) && open(FI, $back) && open(FO, ">$ofile")) {
  317. my $cont = 0;
  318. while (<FI>) {
  319. if (!$cont) {
  320. if (/^([^#\s].*?[^\\])=(.*)[\s\\]*$/) {
  321. if (!$keys{$1}) {
  322. $cont = (/\\\s*$/) ? 1 : 0;
  323. next;
  324. }
  325. }
  326. print FO $_;
  327. } elsif ($cont && !/\\\s*$/) {
  328. $cont = 0;
  329. }
  330. }
  331. close(FI);
  332. close(FO);
  333. unlink($back);
  334. }
  335. }
  336. # convert a UTF-8 file to either ISO-8859 or ASCII
  337. sub convert {
  338. my ($ofile, $toiso, $toascii) = @_;
  339. if (isUtf8($ofile) && ($toiso || $toascii)) {
  340. print "\nConverting file $ofile to " .
  341. ($toiso ? "ISO-8859" : "ASCII") . "\n";
  342. my $back = $ofile . "~~";
  343. if (rename($ofile, $back) && open(FI, $back) && open(FO, ">$ofile")) {
  344. while (<FI>) {
  345. if ($toiso) {
  346. s/([\xC2\xC3])([\x80-\xBF])/chr(ord($1)<<6&0xC0|ord($2)&0x3F)/eg;
  347. } else {
  348. s/([\xC0-\xDF])([\x80-\xBF])/sprintf('\\u%04x',
  349. unpack("c",$1)<<6&0x07C0|unpack("c",$2)&0x003F)/ge;
  350. s/([\xE0-\xEF])([\x80-\xBF])([\x80-\xBF])/sprintf('\\u%04x',
  351. unpack("c",$1)<<12&0xF000|unpack("c",$2)<<6&0x0FC0|unpack("c",$3)&0x003F)/ge;
  352. s/([\xF0-\xF7])([\x80-\xBF])([\x80-\xBF])([\x80-\xBF])/sprintf('\\u%04x',
  353. unpack("c",$1)<<18&0x1C0000|unpack("c",$2)<<12&0x3F000|
  354. unpack("c",$3)<<6&0x0FC0|unpack("c",$4)&0x003F)/ge;
  355. }
  356. print FO "$_";
  357. }
  358. close(FI);
  359. close(FO);
  360. unlink($back);
  361. }
  362. }
  363. }
  364. # Return true if the file has any UTF-8 character
  365. sub isUtf8 {
  366. my $file = shift;
  367. if (open(F, $file)) {
  368. while (<F>) {
  369. if (/([\xC2\xC3])([\x80-\xBF])/) {
  370. close(F);
  371. return 1;
  372. }
  373. }
  374. close(F);
  375. }
  376. return 0;
  377. }
  378. # print MIT license in new files
  379. # Note: the license is read from the head of this file
  380. my $license;
  381. sub printLicense {
  382. my $file = shift;
  383. if (!$license && open(F, $0)) {
  384. $license = "";
  385. my $on = 0;
  386. while (<F>) {
  387. $on = 1 if (!$on && /The MIT/);
  388. last if ($on && (/^$/ || /^[^#]/));
  389. $license .= $_ if ($on);
  390. }
  391. close(F);
  392. }
  393. if ($license && $license ne "") {
  394. my $dirname = dirname($file);
  395. unless (-d $dirname) {
  396. mkpath($dirname);
  397. }
  398. open(F, ">" . $file) || die $!;
  399. print F "$license\n";
  400. close(F);
  401. }
  402. }
  403. # trim function to remove whitespace from the start and end of the string
  404. sub trim($) {
  405. my $string = shift;
  406. $string =~ s/^\s+//;
  407. $string =~ s/\s+$//;
  408. return $string;
  409. }
  410. ### Usage
  411. sub usage {
  412. print "
  413. Translation Tool for Jenkins
  414. Usage: $0 --lang=xx [options] [dir]
  415. dir: -> source folder for searching files (default is the current directory)
  416. options:
  417. --help -> print this help message and terminates the program with exit code 0.
  418. --lang=xx -> language code to use (it is mandatory and it has to be different to English)
  419. --to-iso -> optional, enables files in UTF-8 convertion to ISO-8859 if present
  420. --to-ascii -> optional, convert files in UTF-8 to ASCII using the native2ascii command if present
  421. --add -> optional, generate new files and add new keys to existing files if present
  422. --remove -> optional, remove unused key/value pair for existing files if present
  423. --editor=command -> command to run over each updated file, implies --add if present
  424. --reuse=folder -> optional, load a cache with keys already translated in the folder provided in
  425. order to utilize them when the same key appears if present
  426. --counter -> optional, to each translated key, unique value is added to easily identify match missing translation
  427. with value in source code if present
  428. --target=folder -> optional, target folder for writing files
  429. --debug -> optional, print debugging messages to STDOUT when they are available
  430. Examples:
  431. - Look for Spanish files with incomplete keys in the 'main' folder,
  432. edit them with gedit, and finally convert them to ISO-8859
  433. $0 --lang=es --editor=gedit --to-iso main
  434. - Convert all Japanese files in the current folder encoded with UTF-8 to ASCII
  435. $0 --lang=ja --to-ascii .
  436. - Remove all orphaned keys from German files which are in the current file
  437. $0 --lang=de --remove .
  438. ";
  439. }