c353daee545cc3b1093d356d79ac10b2e7d58bd0
[WebKit-https.git] / WebKitTools / Scripts / extract-localizable-strings
1 #!/usr/bin/perl -w
2
3 # Copyright (C) 2006 Apple Computer, Inc.  All rights reserved.
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions
7 # are met:
8 #
9 # 1.  Redistributions of source code must retain the above copyright
10 #     notice, this list of conditions and the following disclaimer. 
11 # 2.  Redistributions in binary form must reproduce the above copyright
12 #     notice, this list of conditions and the following disclaimer in the
13 #     documentation and/or other materials provided with the distribution. 
14 # 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
15 #     its contributors may be used to endorse or promote products derived
16 #     from this software without specific prior written permission. 
17 #
18 # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
19 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
22 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29 # This script is like the genstrings tool (minus most of the options) with these differences.
30 #
31 #    1) It uses the names UI_STRING and UI_STRING_WITH_KEY for the macros, rather than the macros
32 #       from NSBundle.h, and doesn't support tables (although they would be easy to add).
33 #    2) It supports UTF-8 in key strings (and hence uses "" strings rather than @"" strings;
34 #       @"" strings only reliably support ASCII since they are decoded based on the system encoding
35 #       at runtime, so give different results on US and Japanese systems for example).
36 #    3) It looks for strings that are not marked for localization, using both macro names that are
37 #       known to be used for debugging in Intrigue source code and an exceptions file.
38 #    4) It finds the files to work on rather than taking them as parameters, and also uses a
39 #       hardcoded location for both the output file and the exceptions file.
40 #       It would have been nice to use the project to find the source files, but it's too hard to
41 #       locate source files after parsing a .pbxproj file.
42
43 # The exceptions file has a list of strings in quotes, filenames, and filename/string pairs separated by :.
44
45 use strict;
46
47 my $exceptionsFile = "English.lproj/StringsNotToBeLocalized.txt";
48 my $stringsFile = "English.lproj/Localizable.strings";
49 my %isDebugMacro = ( ASSERT_WITH_MESSAGE => 1, LOG_ERROR => 1, ERROR => 1, NSURL_ERROR => 1, FATAL => 1, LOG => 1, dprintf => 1, NSException => 1, NSLog => 1, printf => 1 );
50
51 die "usage: extract-localizable-strings\n" if @ARGV != 0;
52
53 my $sawError = 0;
54
55 my $localizedCount = 0;
56 my $keyCollisionCount = 0;
57 my $notLocalizedCount = 0;
58 my $NSLocalizeCount = 0;
59
60 my %exception;
61 my %usedException;
62
63 if (open EXCEPTIONS, $exceptionsFile) {
64     while (<EXCEPTIONS>) {
65         chomp;
66         if (/^"([^\\"]|\\.)*"$/ or /^[-_\/\w.]+.(h|m|mm)$/ or /^[-_\/\w.]+.(h|m|mm):"([^\\"]|\\.)*"$/) {
67             if ($exception{$_}) {
68                 print "$exceptionsFile:$.:exception for $_ appears twice\n";
69                 print "$exceptionsFile:$exception{$_}:first appearance\n";
70             } else {
71                 $exception{$_} = $.;
72             }
73         } else {
74             print "$exceptionsFile:$.:syntax error\n";
75         }
76     }
77     close EXCEPTIONS;
78 }
79
80 my @files = ( split "\n", `find . -name "*.h" -o -name "*.m" -o -name "*.mm"` );
81
82 for my $file (sort @files) {
83
84     next if $file =~ /\/WebLocalizableStrings\.h$/;
85     next if $file =~ /\/icu\//;
86
87     $file =~ s-^./--;
88
89     open SOURCE, $file or die "can't open $file\n";
90     
91     my $inComment = 0;
92     
93     my $expected = "";
94     my $macroLine;
95     my $macro;
96     my $UIString;
97     my $key;
98     my $comment;
99     
100     my $string;
101     my $stringLine;
102     my $nestingLevel;
103     
104     my $previousToken = "";
105
106     while (<SOURCE>) {
107         chomp;
108         
109         # Handle continued multi-line comment.
110         if ($inComment) {
111             next unless s-.*\*/--;
112             $inComment = 0;
113         }
114     
115         # Handle all the tokens in the line.
116         while (s-^\s*([#\w]+|/\*|//|[^#\w/'"()\[\],]+|.)--) {
117             my $token = $1;
118             
119             if ($token eq "\"") {
120                 if ($expected and $expected ne "a quoted string") {
121                     print "$file:$.:ERROR:found a quoted string but expected $expected\n";
122                     $sawError = 1;
123                     $expected = "";
124                 }
125                 if (s-^(([^\\$token]|\\.)*?)$token--) {
126                     if (!defined $string) {
127                         $stringLine = $.;
128                         $string = $1;
129                     } else {
130                         $string .= $1;
131                     }
132                 } else {
133                     print "$file:$.:ERROR:mismatched quotes\n";
134                     $sawError = 1;
135                     $_ = "";
136                 }
137                 next;
138             }
139             
140             if (defined $string) {
141 handleString:
142                 if ($expected) {
143                     if (!defined $UIString) {
144                         # FIXME: Validate UTF-8 here?
145                         $UIString = $string;
146                         $expected = ",";
147                     } elsif ($macro eq "UI_STRING_KEY" and !defined $key) {
148                         # FIXME: Validate UTF-8 here?
149                         $key = $string;
150                         $expected = ",";
151                     } elsif (!defined $comment) {
152                         # FIXME: Validate UTF-8 here?
153                         $comment = $string;
154                         $expected = ")";
155                     }
156                 } else {
157                     if (defined $nestingLevel) {
158                         # In a debug macro, no need to localize.
159                     } elsif ($previousToken eq "#include" or $previousToken eq "#import") {
160                         # File name, no need to localize.
161                     } elsif ($previousToken eq "extern" and $string eq "C") {
162                         # extern "C", no need to localize.
163                     } elsif ($string eq "") {
164                         # Empty string can sometimes be localized, but we need not complain if not.
165                     } elsif ($exception{$file}) {
166                         $usedException{$file} = 1;
167                     } elsif ($exception{"\"$string\""}) {
168                         $usedException{"\"$string\""} = 1;
169                     } elsif ($exception{"$file:\"$string\""}) {
170                         $usedException{"$file:\"$string\""} = 1;
171                     } else {
172                         print "$file:$stringLine:\"$string\" is not marked for localization\n";
173                         $notLocalizedCount++;
174                     }
175                 }
176                 $string = undef;
177                 last if !defined $token;
178             }
179             
180             $previousToken = $token;
181
182             if ($token =~ /^NSLocalized/ && $token !~ /NSLocalizedDescriptionKey/ && $token !~ /NSLocalizedStringFromTableInBundle/) {
183                 print "$file:$.:ERROR:found a use of an NSLocalized macro; not supported\n";
184                 $nestingLevel = 0 if !defined $nestingLevel;
185                 $sawError = 1;
186                 $NSLocalizeCount++;
187             } elsif ($token eq "/*") {
188                 if (!s-^.*?\*/--) {
189                     $_ = ""; # If the comment doesn't end, discard the result of the line and set flag
190                     $inComment = 1;
191                 }
192             } elsif ($token eq "//") {
193                 $_ = ""; # Discard the rest of the line
194             } elsif ($token eq "'") {
195                 if (!s-([^\\]|\\.)'--) { #' <-- that single quote makes the Project Builder editor less confused
196                     print "$file:$.:ERROR:mismatched single quote\n";
197                     $sawError = 1;
198                     $_ = "";
199                 }
200             } else {
201                 if ($expected and $expected ne $token) {
202                     print "$file:$.:ERROR:found $token but expected $expected\n";
203                     $sawError = 1;
204                     $expected = "";
205                 }
206                 if ($token eq "UI_STRING" or $token eq "UI_STRING_KEY") {
207                     $expected = "(";
208                     $macro = $token;
209                     $UIString = undef;
210                     $key = undef;
211                     $comment = undef;
212                     $macroLine = $.;
213                 } elsif ($token eq "(" or $token eq "[") {
214                     ++$nestingLevel if defined $nestingLevel;
215                     $expected = "a quoted string" if $expected;
216                 } elsif ($token eq ",") {
217                     $expected = "a quoted string" if $expected;
218                 } elsif ($token eq ")" or $token eq "]") {
219                     $nestingLevel = undef if defined $nestingLevel && !--$nestingLevel;
220                     if ($expected) {
221                         $key = $UIString if !defined $key;
222                         HandleUIString($UIString, $key, $comment, $file, $macroLine);
223                         $macro = "";
224                         $expected = "";
225                         $localizedCount++;
226                     }
227                 } elsif ($isDebugMacro{$token}) {
228                     $nestingLevel = 0 if !defined $nestingLevel;
229                 }
230             }
231         }
232             
233     }
234     
235     goto handleString if defined $string;
236     
237     if ($expected) {
238         print "$file:ERROR:reached end of file but expected $expected\n";
239         $sawError = 1;
240     }
241     
242     close SOURCE;
243 }
244
245 my %stringByKey;
246 my %commentByKey;
247 my %fileByKey;
248 my %lineByKey;
249
250 sub HandleUIString
251 {
252     my ($string, $key, $comment, $file, $line) = @_;
253
254     my $bad = 0;
255     if (grep { $_ == 0xFFFD } unpack "U*", $string) {
256         print "$file:$line:ERROR:string for translation has illegal UTF-8 -- most likely a problem with the Text Encoding of the source file\n";
257         $bad = 1;
258     }
259     if ($string ne $key && grep { $_ == 0xFFFD } unpack "U*", $key) {
260         print "$file:$line:ERROR:key has illegal UTF-8 -- most likely a problem with the Text Encoding of the source file\n";
261         $bad = 1;
262     }
263     if (grep { $_ == 0xFFFD } unpack "U*", $comment) {
264         print "$file:$line:ERROR:comment for translation has illegal UTF-8 -- most likely a problem with the Text Encoding of the source file\n";
265         $bad = 1;
266     }
267     if ($bad) {
268         $sawError = 1;
269         return;
270     }
271     
272     if ($stringByKey{$key} && $stringByKey{$key} ne $string) {
273         print "$file:$line:encountered the same key, \"$key\", twice, with different strings\n";
274         print "$fileByKey{$key}:$lineByKey{$key}:previous occurrence\n";
275         $keyCollisionCount++;
276         return;
277     }
278     if ($commentByKey{$key} && $commentByKey{$key} ne $comment) {
279         print "$file:$line:encountered the same key, \"$key\", twice, with different comments\n";
280         print "$fileByKey{$key}:$lineByKey{$key}:previous occurrence\n";
281         $keyCollisionCount++;
282         return;
283     }
284
285     $fileByKey{$key} = $file;
286     $lineByKey{$key} = $line;
287     $stringByKey{$key} = $string;
288     $commentByKey{$key} = $comment;
289 }
290
291 print "\n" if $sawError || $notLocalizedCount || $NSLocalizeCount;
292
293 my @unusedExceptions = sort grep { !$usedException{$_} } keys %exception;
294 if (@unusedExceptions) {
295     for my $unused (@unusedExceptions) {
296         print "$exceptionsFile:$exception{$unused}:exception $unused not used\n";
297     }
298     print "\n";
299 }
300
301 print "$localizedCount localizable strings\n" if $localizedCount;
302 print "$keyCollisionCount key collisions\n" if $keyCollisionCount;
303 print "$notLocalizedCount strings not marked for localization\n" if $notLocalizedCount;
304 print "$NSLocalizeCount uses of NSLocalize\n" if $NSLocalizeCount;
305 print scalar(@unusedExceptions), " unused exceptions\n" if @unusedExceptions;
306
307 if ($sawError) {
308     print "\nErrors encountered. Exiting without writing a $stringsFile file.\n";
309     exit 1;
310 }
311
312 my $localizedStrings = "";
313
314 for my $key (sort keys %commentByKey) {
315     $localizedStrings .= "/* $commentByKey{$key} */\n\"$key\" = \"$stringByKey{$key}\";\n\n";
316 }
317
318 # Write out the strings file in UTF-16 with a BOM.
319 open STRINGS, ">", $stringsFile or die;
320 utf8::decode($localizedStrings) if $^V ge chr(5).chr(8);
321 print STRINGS pack "n*", (0xFEFF, unpack "U*", $localizedStrings);
322 close STRINGS;