| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490 |
- #!/usr/bin/perl
- # The MIT License
- #
- # Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number
- # of other of contributors
- #
- # Permission is hereby granted, free of charge, to any person obtaining a copy
- # of this software and associated documentation files (the "Software"), to deal
- # in the Software without restriction, including without limitation the rights
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- # copies of the Software, and to permit persons to whom the Software is
- # furnished to do so, subject to the following conditions:
- #
- # The above copyright notice and this permission notice shall be included in
- # all copies or substantial portions of the Software.
- #
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- # THE SOFTWARE.
- # Author: Manuel Carrasco
- # Date: 20 Mar 2010
- # Perl script to generate missing translation keys and missing properties files,
- # to remove unused keys, and to convert utf8 properties files to iso or ascii.
- #
- # 1.- It recursively looks for files in a folder, and analyzes them to extract
- # the keys being used in the application.
- # 2.- If --add=true, it generates the appropriate file for the desired language
- # and adds these keys to it, adding the english text as a reference. If the
- # properties file already exists the script update it with the new keys.
- # 3.- When --remove=true and there are unused keys in our file, the script
- # removes them.
- # 4.- If an editor is passed as argument, the script edits each modified file
- # after adding new keys.
- # 5.- Finally, when --toiso=true or --toascii=true, the script is able to
- # convert utf-8 properties files to iso or unicode hex representation is
- # ascii.
- #
- # Note, while the migration to Jenkins this file will report the keys which
- # should point to Jenkins instead of the old name.
- use warnings;
- use strict;
- use File::Basename;
- use File::Find;
- use File::Path;
- use Getopt::Long;
- my (
- $lang, $editor, $dir, $toiso, $toascii, $add,
- $remove, $reuse, $counter, $target, $help, $debug
- ) = (undef, undef, "./", 0, 0, 0, 0, 0, 0, "./", 0, 0);
- GetOptions(
- 'help' => \$help,
- 'lang=s' => \$lang,
- 'editor' => \$editor,
- 'dir=s' => \$dir,
- 'to-iso' => \$toiso,
- 'to-ascii' => \$toascii,
- 'add' => \$add,
- 'remove' => \$remove,
- 'reuse' => \$reuse,
- 'counter' => \$counter,
- 'target=s' => \$target,
- 'debug' => \$debug
- ) or die("Error in command line arguments\n");
- if ($help) {
- usage();
- exit(0);
- }
- $add = 1 if ($editor);
- if ($toiso && $toascii) {
- warn "You can't have --to-iso and --to-ascii options at the same time\n";
- usage();
- exit(1);
- }
- my ($tfiles, $tkeys, $tmissing, $tunused, $tempty, $tsame, $tnojenkins,
- $countervalue)
- = (0, 0, 0, 0, 0, 0, 0, 1);
- ## language parameter is mandatory and shouldn't be 'en'
- unless (($lang) and ($lang ne 'en')) {
- usage();
- exit(1);
- }
- print "Searching for files ...\n";
- ## look for Message.properties and *.jelly files in the provided folder
- my @files = findTranslatableFiles($dir);
- print 'Found ', scalar(@files), ' files', "\n";
- ## load a cache with keys already translated to utilize in the case the same key
- ## is used
- my %cache = loadAllTranslatedKeys($reuse, $lang) if ($reuse && -e $reuse);
- ## process each file
- foreach (@files) {
- $tfiles++;
- processFile($_);
- }
- ## print statistics
- my $tdone = $tkeys - $tmissing - $tunused - $tempty - $tsame - $tnojenkins;
- my $pdone = 100;
- my $pmissing = 0;
- my $punused = 0;
- my $pempty = 0;
- my $psame = 0;
- my $pnojenkins = 0;
- if ($tkeys != 0) {
- $pdone = $tdone / $tkeys * 100;
- $pmissing = $tmissing / $tkeys * 100;
- $punused = $tunused / $tkeys * 100;
- $pempty = $tempty / $tkeys * 100;
- $psame = $tsame / $tkeys * 100;
- $pnojenkins = $tnojenkins / $tkeys * 100;
- }
- my @formatParameters = (
- $tfiles, $tkeys, $tdone, $pdone, $tmissing,
- $pmissing, $tunused, $punused, $tempty, $pempty,
- $tsame, $psame, $tnojenkins, $pnojenkins
- );
- printf
- "\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",
- (@formatParameters);
- ## end
- exit();
- ### This is the main method with is run for each file
- sub processFile {
- # ofile -> output file in the current language,
- # efile -> english file
- my $file = shift;
- my ($ofile, $efile) = ($file, $file);
- $ofile =~ s/$dir/$target/;
- $ofile =~ s/(\.jelly)|(\.properties)/_$lang.properties/;
- $efile =~ s/(\.jelly)/.properties/;
- # keys -> Hash of keys used in jelly or Message.properties files
- # ekeys -> Hash of key/values in English
- # okeys -> Hash of key/values in the desired language which are already
- # present in the file
- my (%keys, %okeys, %ekeys);
- # Read .jelly or Message.properties files, and fill a hash with the keys
- # found
- if ($file =~ m/.jelly$/) {
- %keys = loadJellyFile($file);
- %ekeys = loadPropertiesFile($efile);
- } else {
- %keys = %ekeys = loadPropertiesFile($file);
- }
- # load keys already present in the desired locale
- %okeys = loadPropertiesFile($ofile);
- # calculate missing keys in the file
- my $missing = "";
- foreach (keys %keys) {
- $tkeys++;
- if (!defined($okeys{$_})) {
- $_ .= "=" . $cache{$_} if (defined($cache{$_}));
- $missing .= ($add ? " Adding " : " Missing") . " -> $_\n";
- $tmissing++;
- } elsif ($okeys{$_} eq '') {
- $missing .= " Empty -> $_\n";
- $tempty++;
- }
- }
- # calculate old keys in the file which are currently unused
- my $unused = "";
- foreach (keys %okeys) {
- if (!defined $keys{$_}) {
- $unused .= " Unused -> $_\n";
- $tunused++;
- }
- }
- # calculate keys which have the same value in English
- my $same = "";
- foreach (keys %okeys) {
- if ($okeys{$_} && $ekeys{$_} && $okeys{$_} eq $ekeys{$_}) {
- $same .= " Same -> $_\n";
- $tsame++;
- }
- }
- my $nj = "";
- foreach (keys %okeys) {
- if ($okeys{$_} && $okeys{$_} =~ /Hudson/) {
- $nj .= " Non Jenkins -> $_ -> $okeys{$_}\n";
- $tnojenkins++;
- }
- }
- # Show Alerts
- print "\nFile: $ofile\n$missing$unused$same$nj"
- if ($missing ne "" || $unused ne '' || $same ne '' || $nj ne '');
- # write new keys in our file adding the English translation as a reference
- if ($add && $missing ne "") {
- printLicense($ofile) unless (-f $ofile);
- open(F, ">>$ofile");
- foreach (keys %keys) {
- if (!$okeys{$_}) {
- if (!defined($okeys{$_})) {
- print F "$_=";
- if (defined($cache{$_})) {
- print F $cache{$_} . "\n";
- } else {
- if ($counter) {
- # add unique value for each added translation
- print F "---TranslateMe " . $countervalue . "--- " .
- ($ekeys{$_} ? $ekeys{$_} : $_) . "\n";
- } else {
- print F "\n";
- }
- }
- $countervalue++;
- }
- }
- }
- close(F);
- }
- # open the editor if the user has specified it and there are changes to
- # manage
- system("$editor $ofile")
- if ($editor && $add && ($missing ne "" || $same ne "" || $nj ne ''));
- # write new keys in our file adding the English translation as a reference
- removeUnusedKeys($ofile, %keys) if ($remove && $unused ne "");
- # convert the language file to ISO or ASCII which are
- # the charsets which Jenkins supports right now
- convert($ofile, $toiso, $toascii) if (-f $ofile);
- }
- # Create a hash with all keys which exist and have an unique value
- sub loadAllTranslatedKeys {
- my ($dir, $lang, %ret) = @_;
- my @files = findTranslatableFiles($dir);
- foreach (@files) {
- s/(\.jelly)|(\.properties)/_$lang.properties/;
- next unless (-f $_);
- my (%h, $k, $v) = loadPropertiesFile($_);
- while (($k, $v) = each(%h)) {
- $ret{$k} = "" if (defined($ret{$k}) && $v ne $ret{$k});
- $ret{$k} = $v unless defined($ret{$k});
- }
- }
- return %ret;
- }
- # Look for Message.properties and *.jelly files
- sub findTranslatableFiles {
- my $dir = shift;
- die "Folder doesn't exist: $dir\n" unless (-e $dir);
- my @ret;
- find(
- sub {
- my $file = $File::Find::name;
- push(@ret, $file)
- if ($file !~ m#(/src/test/)|(/target/)|(\.svn)#
- && $file =~ /(Messages.properties)$|(.*\.jelly)$/);
- },
- $dir
- );
- return @ret;
- }
- # Fill a hash with key/1 pairs from a .jelly file
- sub loadJellyFile {
- my $file = shift;
- my %ret;
- open(F, $file) || die $! . " " . $file;
- while (<F>) {
- next if (!/\$\{.*?\%([^\(]+?).*\}/);
- my $line = $_;
- while ($line =~ /^.*?\$\{\%([^\(\}]+)(.*)$/
- || $line =~ /^.*?\$\{.*?['"]\%([^\(\}\"\']+)(.*)$/)
- {
- $line = $2;
- my $word = $1;
- $word =~ s/\(.+$//g;
- $word =~ s/'+/''/g;
- $word =~ s/ /\\ /g;
- $word =~ s/\>/>/g;
- $word =~ s/\</</g;
- $word =~ s/\&/&/g;
- $word =~ s/([#:=])/\\$1/g;
- $ret{$word} = 1;
- }
- }
- close(F);
- return %ret;
- }
- # Fill a hash with key/value pairs from a .properties file
- sub loadPropertiesFile {
- my $file = shift;
- my %ret;
- print "Trying to load $file... " if ($debug);
- unless (-f $file) {
- print "file does not exist, skipping.\n" if ($debug);
- return %ret;
- }
- print "done.\n" if ($debug);
- my $skip_comment = qr/^#/;
- open(my $in, '<', $file) or die "Cannot read $file: $!\n";
- my ($cont, $key, $val) = (0, undef, undef);
- while (<$in>) {
- chomp;
- next if $_ eq '';
- next if $_ =~ $skip_comment;
- print 'Line: ', $_, "\n" if ($debug);
- s/[\r\n]+//;
- $ret{$key} .= "\n$1" if ($cont && /\s*(.*)[\\\s]*$/);
- if (/^([^#\s].*?[^\\])=(.*)[\s\\]*$/) {
- ($key, $val) = (trim($1), trim($2));
- $ret{$key} = $val;
- }
- $cont = (/\\\s*$/) ? 1 : 0;
- }
- close($in);
- # TODO: Use of uninitialized value $_ in pattern match (m//) at
- # ./translation-tool.pl line 345.
- $ret{$key} .= "\n$1" if ($cont && /\s*(.*)[\\\s]*$/);
- return %ret;
- }
- # remove unused keys from a file
- sub removeUnusedKeys {
- my ($ofile, %keys) = @_;
- print "Removing unused keys from: $ofile\n";
- my $back = $ofile . "~~";
- if (rename($ofile, $back) && open(FI, $back) && open(FO, ">$ofile")) {
- my $cont = 0;
- while (<FI>) {
- if (!$cont) {
- if (/^([^#\s].*?[^\\])=(.*)[\s\\]*$/) {
- if (!$keys{$1}) {
- $cont = (/\\\s*$/) ? 1 : 0;
- next;
- }
- }
- print FO $_;
- } elsif ($cont && !/\\\s*$/) {
- $cont = 0;
- }
- }
- close(FI);
- close(FO);
- unlink($back);
- }
- }
- # convert a UTF-8 file to either ISO-8859 or ASCII
- sub convert {
- my ($ofile, $toiso, $toascii) = @_;
- if (isUtf8($ofile) && ($toiso || $toascii)) {
- print "\nConverting file $ofile to " .
- ($toiso ? "ISO-8859" : "ASCII") . "\n";
- my $back = $ofile . "~~";
- if (rename($ofile, $back) && open(FI, $back) && open(FO, ">$ofile")) {
- while (<FI>) {
- if ($toiso) {
- s/([\xC2\xC3])([\x80-\xBF])/chr(ord($1)<<6&0xC0|ord($2)&0x3F)/eg;
- } else {
- s/([\xC0-\xDF])([\x80-\xBF])/sprintf('\\u%04x',
- unpack("c",$1)<<6&0x07C0|unpack("c",$2)&0x003F)/ge;
- s/([\xE0-\xEF])([\x80-\xBF])([\x80-\xBF])/sprintf('\\u%04x',
- unpack("c",$1)<<12&0xF000|unpack("c",$2)<<6&0x0FC0|unpack("c",$3)&0x003F)/ge;
- s/([\xF0-\xF7])([\x80-\xBF])([\x80-\xBF])([\x80-\xBF])/sprintf('\\u%04x',
- unpack("c",$1)<<18&0x1C0000|unpack("c",$2)<<12&0x3F000|
- unpack("c",$3)<<6&0x0FC0|unpack("c",$4)&0x003F)/ge;
- }
- print FO "$_";
- }
- close(FI);
- close(FO);
- unlink($back);
- }
- }
- }
- # Return true if the file has any UTF-8 character
- sub isUtf8 {
- my $file = shift;
- if (open(F, $file)) {
- while (<F>) {
- if (/([\xC2\xC3])([\x80-\xBF])/) {
- close(F);
- return 1;
- }
- }
- close(F);
- }
- return 0;
- }
- # print MIT license in new files
- # Note: the license is read from the head of this file
- my $license;
- sub printLicense {
- my $file = shift;
- if (!$license && open(F, $0)) {
- $license = "";
- my $on = 0;
- while (<F>) {
- $on = 1 if (!$on && /The MIT/);
- last if ($on && (/^$/ || /^[^#]/));
- $license .= $_ if ($on);
- }
- close(F);
- }
- if ($license && $license ne "") {
- my $dirname = dirname($file);
- unless (-d $dirname) {
- mkpath($dirname);
- }
- open(F, ">" . $file) || die $!;
- print F "$license\n";
- close(F);
- }
- }
- # trim function to remove whitespace from the start and end of the string
- sub trim($) {
- my $string = shift;
- $string =~ s/^\s+//;
- $string =~ s/\s+$//;
- return $string;
- }
- ### Usage
- sub usage {
- print "
- Translation Tool for Jenkins
- Usage: $0 --lang=xx [options] [dir]
- dir: -> source folder for searching files (default is the current directory)
- options:
- --help -> print this help message and terminates the program with exit code 0.
- --lang=xx -> language code to use (it is mandatory and it has to be different to English)
- --to-iso -> optional, enables files in UTF-8 convertion to ISO-8859 if present
- --to-ascii -> optional, convert files in UTF-8 to ASCII using the native2ascii command if present
- --add -> optional, generate new files and add new keys to existing files if present
- --remove -> optional, remove unused key/value pair for existing files if present
- --editor=command -> command to run over each updated file, implies --add if present
- --reuse=folder -> optional, load a cache with keys already translated in the folder provided in
- order to utilize them when the same key appears if present
- --counter -> optional, to each translated key, unique value is added to easily identify match missing translation
- with value in source code if present
- --target=folder -> optional, target folder for writing files
- --debug -> optional, print debugging messages to STDOUT when they are available
- Examples:
- - Look for Spanish files with incomplete keys in the 'main' folder,
- edit them with gedit, and finally convert them to ISO-8859
- $0 --lang=es --editor=gedit --to-iso main
- - Convert all Japanese files in the current folder encoded with UTF-8 to ASCII
- $0 --lang=ja --to-ascii .
- - Remove all orphaned keys from German files which are in the current file
- $0 --lang=de --remove .
- ";
- }
|