blob: 822d293e8e08a8c8a68fc8d3ac4941a464ce5417 [file] [log] [blame]
mitz@apple.comf7dadb52013-09-20 20:13:11 +00001#!/usr/bin/perl -w
2
3# Copyright (C) 2006, 2007, 2009, 2010, 2013 Apple 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.
mjs@apple.com92047332014-03-15 04:08:27 +000014# 3. Neither the name of Apple Inc. ("Apple") nor the names of
mitz@apple.comf7dadb52013-09-20 20:13:11 +000015# 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
45use strict;
mrowe@apple.com35bae532015-02-11 23:14:30 +000046use File::Compare;
47use File::Copy;
mitz@apple.comb76add82015-03-06 18:47:28 +000048use FindBin;
mitz@apple.comf7dadb52013-09-20 20:13:11 +000049use Getopt::Long;
mitz@apple.comb76add82015-03-06 18:47:28 +000050use lib $FindBin::Bin;
51use LocalizableStrings;
mitz@apple.comf7dadb52013-09-20 20:13:11 +000052no warnings 'deprecated';
53
mitz@apple.comf7dadb52013-09-20 20:13:11 +000054my %isDebugMacro = ( ASSERT_WITH_MESSAGE => 1, LOG_ERROR => 1, ERROR => 1, NSURL_ERROR => 1, FATAL => 1, LOG => 1, LOG_WARNING => 1, UI_STRING_LOCALIZE_LATER => 1, UI_STRING_LOCALIZE_LATER_KEY => 1, LPCTSTR_UI_STRING_LOCALIZE_LATER => 1, UNLOCALIZED_STRING => 1, UNLOCALIZED_LPCTSTR => 1, dprintf => 1, NSException => 1, NSLog => 1, printf => 1 );
55
56my $verify;
57my $exceptionsFile;
58my @directoriesToSkip = ();
mitz@apple.com93bd8aa2014-12-25 22:45:31 +000059my $treatWarningsAsErrors;
mitz@apple.comf7dadb52013-09-20 20:13:11 +000060
61my %options = (
62 'verify' => \$verify,
63 'exceptions=s' => \$exceptionsFile,
64 'skip=s' => \@directoriesToSkip,
mitz@apple.com93bd8aa2014-12-25 22:45:31 +000065 'treat-warnings-as-errors' => \$treatWarningsAsErrors,
mitz@apple.comf7dadb52013-09-20 20:13:11 +000066);
67
68GetOptions(%options);
69
mitz@apple.comb76add82015-03-06 18:47:28 +000070setTreatWarningsAsErrors($treatWarningsAsErrors);
71
mitz@apple.com93bd8aa2014-12-25 22:45:31 +000072@ARGV >= 2 or die "Usage: extract-localizable-strings [--verify] [--treat-warnings-as-errors] [--exceptions <exceptions file>] <file to update> [--skip directory | directory]...\nDid you mean to run update-webkit-localizable-strings instead?\n";
mitz@apple.comf7dadb52013-09-20 20:13:11 +000073
74-f $exceptionsFile or die "Couldn't find exceptions file $exceptionsFile\n" unless !defined $exceptionsFile;
75
76my $fileToUpdate = shift @ARGV;
77-f $fileToUpdate or die "Couldn't find file to update $fileToUpdate\n";
78
79my $warnAboutUnlocalizedStrings = defined $exceptionsFile;
80
81my @directories = ();
82if (@ARGV < 1) {
83 push(@directories, ".");
84} else {
85 for my $dir (@ARGV) {
86 push @directories, $dir;
87 }
88}
89
mitz@apple.comf7dadb52013-09-20 20:13:11 +000090my $notLocalizedCount = 0;
91my $NSLocalizeCount = 0;
92
93my %exception;
94my %usedException;
95
96if (defined $exceptionsFile && open EXCEPTIONS, $exceptionsFile) {
97 while (<EXCEPTIONS>) {
98 chomp;
99 if (/^"([^\\"]|\\.)*"$/ or /^[-_\/\w\s.]+.(h|m|mm|c|cpp)$/ or /^[-_\/\w\s.]+.(h|m|mm|c|cpp):"([^\\"]|\\.)*"$/) {
100 if ($exception{$_}) {
mitz@apple.com93bd8aa2014-12-25 22:45:31 +0000101 emitWarning($exceptionsFile, $., "exception for $_ appears twice");
102 emitWarning($exceptionsFile, $exception{$_}, "first appearance");
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000103 } else {
104 $exception{$_} = $.;
105 }
106 } else {
mitz@apple.com93bd8aa2014-12-25 22:45:31 +0000107 emitWarning($exceptionsFile, $., "syntax error");
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000108 }
109 }
110 close EXCEPTIONS;
111}
112
113my $quotedDirectoriesString = '"' . join('" "', @directories) . '"';
114for my $dir (@directoriesToSkip) {
115 $quotedDirectoriesString .= ' -path "' . $dir . '" -prune -o';
116}
117
118my @files = ( split "\n", `find $quotedDirectoriesString \\( -name "*.h" -o -name "*.m" -o -name "*.mm" -o -name "*.c" -o -name "*.cpp" \\)` );
119
120for my $file (sort @files) {
121 next if $file =~ /\/\w+LocalizableStrings\w*\.h$/ || $file =~ /\/LocalizedStrings\.h$/;
122
123 $file =~ s-^./--;
124
125 open SOURCE, $file or die "can't open $file\n";
126
127 my $inComment = 0;
128
129 my $expected = "";
130 my $macroLine;
131 my $macro;
132 my $UIString;
133 my $key;
134 my $comment;
carlosgc@webkit.org6878dca2017-03-22 09:55:34 +0000135 my $mnemonic;
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000136
137 my $string;
138 my $stringLine;
139 my $nestingLevel;
140
141 my $previousToken = "";
142
143 while (<SOURCE>) {
144 chomp;
145
146 # Handle continued multi-line comment.
147 if ($inComment) {
148 next unless s-.*\*/--;
149 $inComment = 0;
150 }
151
152 next unless defined $nestingLevel or /(\"|\/\*)/;
153
154 # Handle all the tokens in the line.
155 while (s-^\s*([#\w]+|/\*|//|[^#\w/'"()\[\],]+|.)--) {
156 my $token = $1;
cdumez@apple.com01d59422017-01-06 18:55:35 +0000157
158 if ($token eq "@" and $expected and $expected eq "a quoted string") {
159 next;
160 }
161
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000162 if ($token eq "\"") {
163 if ($expected and $expected ne "a quoted string") {
mitz@apple.comb76add82015-03-06 18:47:28 +0000164 emitError($file, $., "found a quoted string but expected $expected");
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000165 $expected = "";
166 }
167 if (s-^(([^\\$token]|\\.)*?)$token--) {
168 if (!defined $string) {
169 $stringLine = $.;
170 $string = $1;
171 } else {
172 $string .= $1;
173 }
174 } else {
mitz@apple.comb76add82015-03-06 18:47:28 +0000175 emitError($file, $., "mismatched quotes");
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000176 $_ = "";
177 }
178 next;
179 }
180
181 if (defined $string) {
182handleString:
183 if ($expected) {
184 if (!defined $UIString) {
185 # FIXME: Validate UTF-8 here?
186 $UIString = $string;
187 $expected = ",";
188 } elsif (($macro =~ /(WEB_)?UI_STRING_KEY(_INTERNAL)?$/) and !defined $key) {
189 # FIXME: Validate UTF-8 here?
190 $key = $string;
191 $expected = ",";
carlosgc@webkit.org6878dca2017-03-22 09:55:34 +0000192 } elsif (($macro =~ /WEB_UI_STRING_WITH_MNEMONIC$/) and !defined $mnemonic) {
193 $mnemonic = $string;
194 $expected = ",";
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000195 } elsif (!defined $comment) {
196 # FIXME: Validate UTF-8 here?
197 $comment = $string;
198 $expected = ")";
199 }
200 } else {
201 if (defined $nestingLevel) {
202 # In a debug macro, no need to localize.
203 } elsif ($previousToken eq "#include" or $previousToken eq "#import") {
204 # File name, no need to localize.
205 } elsif ($previousToken eq "extern" and $string eq "C") {
206 # extern "C", no need to localize.
207 } elsif ($string eq "") {
208 # Empty string can sometimes be localized, but we need not complain if not.
209 } elsif ($exception{$file}) {
210 $usedException{$file} = 1;
211 } elsif ($exception{"\"$string\""}) {
212 $usedException{"\"$string\""} = 1;
213 } elsif ($exception{"$file:\"$string\""}) {
214 $usedException{"$file:\"$string\""} = 1;
215 } else {
mitz@apple.com93bd8aa2014-12-25 22:45:31 +0000216 emitWarning($file, $stringLine, "\"$string\" is not marked for localization") if $warnAboutUnlocalizedStrings;
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000217 $notLocalizedCount++;
218 }
219 }
220 $string = undef;
221 last if !defined $token;
222 }
223
224 $previousToken = $token;
225
226 if ($token =~ /^NSLocalized/ && $token !~ /NSLocalizedDescriptionKey/ && $token !~ /NSLocalizedStringFromTableInBundle/ && $token !~ /NSLocalizedFileSizeDescription/ && $token !~ /NSLocalizedDescriptionKey/ && $token !~ /NSLocalizedRecoverySuggestionErrorKey/) {
mitz@apple.comb76add82015-03-06 18:47:28 +0000227 emitError($file, $., "found a use of an NSLocalized macro ($token); not supported");
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000228 $nestingLevel = 0 if !defined $nestingLevel;
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000229 $NSLocalizeCount++;
230 } elsif ($token eq "/*") {
231 if (!s-^.*?\*/--) {
232 $_ = ""; # If the comment doesn't end, discard the result of the line and set flag
233 $inComment = 1;
234 }
235 } elsif ($token eq "//") {
236 $_ = ""; # Discard the rest of the line
237 } elsif ($token eq "'") {
238 if (!s-([^\\]|\\.)'--) { #' <-- that single quote makes the Project Builder editor less confused
mitz@apple.comb76add82015-03-06 18:47:28 +0000239 emitError($file, $., "mismatched single quote");
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000240 $_ = "";
241 }
242 } else {
243 if ($expected and $expected ne $token) {
mitz@apple.comb76add82015-03-06 18:47:28 +0000244 emitError($file, $., "found $token but expected $expected");
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000245 $expected = "";
246 }
carlosgc@webkit.org6878dca2017-03-22 09:55:34 +0000247 if (($token =~ /(WEB_)?UI_STRING(_KEY)?(_INTERNAL)?$/) || ($token =~ /WEB_UI_NSSTRING$/) || ($token =~ /WEB_UI_STRING_WITH_MNEMONIC$/) || ($token =~ /WEB_UI_CFSTRING$/)) {
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000248 $expected = "(";
249 $macro = $token;
250 $UIString = undef;
251 $key = undef;
252 $comment = undef;
carlosgc@webkit.org6878dca2017-03-22 09:55:34 +0000253 $mnemonic = undef;
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000254 $macroLine = $.;
255 } elsif ($token eq "(" or $token eq "[") {
256 ++$nestingLevel if defined $nestingLevel;
257 $expected = "a quoted string" if $expected;
258 } elsif ($token eq ",") {
259 $expected = "a quoted string" if $expected;
260 } elsif ($token eq ")" or $token eq "]") {
261 $nestingLevel = undef if defined $nestingLevel && !--$nestingLevel;
262 if ($expected) {
263 $key = $UIString if !defined $key;
264 HandleUIString($UIString, $key, $comment, $file, $macroLine);
265 $macro = "";
266 $expected = "";
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000267 }
268 } elsif ($isDebugMacro{$token}) {
269 $nestingLevel = 0 if !defined $nestingLevel;
270 }
271 }
272 }
273
274 }
275
276 goto handleString if defined $string;
277
278 if ($expected) {
mitz@apple.comb76add82015-03-06 18:47:28 +0000279 emitError($file, 0, "reached end of file but expected $expected");
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000280 }
281
282 close SOURCE;
283}
284
mitz@apple.comb76add82015-03-06 18:47:28 +0000285print "\n" if sawError() || $notLocalizedCount || $NSLocalizeCount;
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000286
287my @unusedExceptions = sort grep { !$usedException{$_} } keys %exception;
288if (@unusedExceptions) {
289 for my $unused (@unusedExceptions) {
mitz@apple.com93bd8aa2014-12-25 22:45:31 +0000290 emitWarning($exceptionsFile, $exception{$unused}, "exception $unused not used");
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000291 }
292 print "\n";
293}
294
mitz@apple.comb76add82015-03-06 18:47:28 +0000295print localizedCount() . " localizable strings\n" if localizedCount();
296print keyCollisionCount() . " key collisions\n" if keyCollisionCount();
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000297print "$notLocalizedCount strings not marked for localization\n" if $notLocalizedCount;
298print "$NSLocalizeCount uses of NSLocalize\n" if $NSLocalizeCount;
299print scalar(@unusedExceptions), " unused exceptions\n" if @unusedExceptions;
300
mitz@apple.comb76add82015-03-06 18:47:28 +0000301if (sawError()) {
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000302 print "\nErrors encountered. Exiting without writing to $fileToUpdate.\n";
303 exit 1;
304}
305
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000306if (-e "$fileToUpdate") {
307 if (!$verify) {
mrowe@apple.com35bae532015-02-11 23:14:30 +0000308 my $temporaryFile = "$fileToUpdate.updated";
mitz@apple.comb76add82015-03-06 18:47:28 +0000309 writeStringsFile($temporaryFile);
mrowe@apple.com35bae532015-02-11 23:14:30 +0000310
311 # Avoid updating the target file's modification time if the contents have not changed.
312 if (compare($temporaryFile, $fileToUpdate)) {
313 move($temporaryFile, $fileToUpdate);
314 } else {
315 unlink $temporaryFile;
316 }
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000317 } else {
mitz@apple.comb76add82015-03-06 18:47:28 +0000318 verifyStringsFile($fileToUpdate);
mitz@apple.comf7dadb52013-09-20 20:13:11 +0000319 }
320} else {
321 print "error: $fileToUpdate does not exist\n";
322 exit 1;
323}