mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 1 | #!/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.com | 9204733 | 2014-03-15 04:08:27 +0000 | [diff] [blame] | 14 | # 3. Neither the name of Apple Inc. ("Apple") nor the names of |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 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; |
mrowe@apple.com | 35bae53 | 2015-02-11 23:14:30 +0000 | [diff] [blame] | 46 | use File::Compare; |
| 47 | use File::Copy; |
mitz@apple.com | b76add8 | 2015-03-06 18:47:28 +0000 | [diff] [blame] | 48 | use FindBin; |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 49 | use Getopt::Long; |
mitz@apple.com | b76add8 | 2015-03-06 18:47:28 +0000 | [diff] [blame] | 50 | use lib $FindBin::Bin; |
| 51 | use LocalizableStrings; |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 52 | no warnings 'deprecated'; |
| 53 | |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 54 | my %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 | |
| 56 | my $verify; |
| 57 | my $exceptionsFile; |
| 58 | my @directoriesToSkip = (); |
mitz@apple.com | 93bd8aa | 2014-12-25 22:45:31 +0000 | [diff] [blame] | 59 | my $treatWarningsAsErrors; |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 60 | |
| 61 | my %options = ( |
| 62 | 'verify' => \$verify, |
| 63 | 'exceptions=s' => \$exceptionsFile, |
| 64 | 'skip=s' => \@directoriesToSkip, |
mitz@apple.com | 93bd8aa | 2014-12-25 22:45:31 +0000 | [diff] [blame] | 65 | 'treat-warnings-as-errors' => \$treatWarningsAsErrors, |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 66 | ); |
| 67 | |
| 68 | GetOptions(%options); |
| 69 | |
mitz@apple.com | b76add8 | 2015-03-06 18:47:28 +0000 | [diff] [blame] | 70 | setTreatWarningsAsErrors($treatWarningsAsErrors); |
| 71 | |
mitz@apple.com | 93bd8aa | 2014-12-25 22:45:31 +0000 | [diff] [blame] | 72 | @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.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 73 | |
| 74 | -f $exceptionsFile or die "Couldn't find exceptions file $exceptionsFile\n" unless !defined $exceptionsFile; |
| 75 | |
| 76 | my $fileToUpdate = shift @ARGV; |
| 77 | -f $fileToUpdate or die "Couldn't find file to update $fileToUpdate\n"; |
| 78 | |
| 79 | my $warnAboutUnlocalizedStrings = defined $exceptionsFile; |
| 80 | |
| 81 | my @directories = (); |
| 82 | if (@ARGV < 1) { |
| 83 | push(@directories, "."); |
| 84 | } else { |
| 85 | for my $dir (@ARGV) { |
| 86 | push @directories, $dir; |
| 87 | } |
| 88 | } |
| 89 | |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 90 | my $notLocalizedCount = 0; |
| 91 | my $NSLocalizeCount = 0; |
| 92 | |
| 93 | my %exception; |
| 94 | my %usedException; |
| 95 | |
| 96 | if (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.com | 93bd8aa | 2014-12-25 22:45:31 +0000 | [diff] [blame] | 101 | emitWarning($exceptionsFile, $., "exception for $_ appears twice"); |
| 102 | emitWarning($exceptionsFile, $exception{$_}, "first appearance"); |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 103 | } else { |
| 104 | $exception{$_} = $.; |
| 105 | } |
| 106 | } else { |
mitz@apple.com | 93bd8aa | 2014-12-25 22:45:31 +0000 | [diff] [blame] | 107 | emitWarning($exceptionsFile, $., "syntax error"); |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 108 | } |
| 109 | } |
| 110 | close EXCEPTIONS; |
| 111 | } |
| 112 | |
| 113 | my $quotedDirectoriesString = '"' . join('" "', @directories) . '"'; |
| 114 | for my $dir (@directoriesToSkip) { |
| 115 | $quotedDirectoriesString .= ' -path "' . $dir . '" -prune -o'; |
| 116 | } |
| 117 | |
| 118 | my @files = ( split "\n", `find $quotedDirectoriesString \\( -name "*.h" -o -name "*.m" -o -name "*.mm" -o -name "*.c" -o -name "*.cpp" \\)` ); |
| 119 | |
| 120 | for 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.org | 6878dca | 2017-03-22 09:55:34 +0000 | [diff] [blame] | 135 | my $mnemonic; |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 136 | |
| 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.com | 01d5942 | 2017-01-06 18:55:35 +0000 | [diff] [blame] | 157 | |
| 158 | if ($token eq "@" and $expected and $expected eq "a quoted string") { |
| 159 | next; |
| 160 | } |
| 161 | |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 162 | if ($token eq "\"") { |
| 163 | if ($expected and $expected ne "a quoted string") { |
mitz@apple.com | b76add8 | 2015-03-06 18:47:28 +0000 | [diff] [blame] | 164 | emitError($file, $., "found a quoted string but expected $expected"); |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 165 | $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.com | b76add8 | 2015-03-06 18:47:28 +0000 | [diff] [blame] | 175 | emitError($file, $., "mismatched quotes"); |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 176 | $_ = ""; |
| 177 | } |
| 178 | next; |
| 179 | } |
| 180 | |
| 181 | if (defined $string) { |
| 182 | handleString: |
| 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.org | 6878dca | 2017-03-22 09:55:34 +0000 | [diff] [blame] | 192 | } elsif (($macro =~ /WEB_UI_STRING_WITH_MNEMONIC$/) and !defined $mnemonic) { |
| 193 | $mnemonic = $string; |
| 194 | $expected = ","; |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 195 | } 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.com | 93bd8aa | 2014-12-25 22:45:31 +0000 | [diff] [blame] | 216 | emitWarning($file, $stringLine, "\"$string\" is not marked for localization") if $warnAboutUnlocalizedStrings; |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 217 | $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.com | b76add8 | 2015-03-06 18:47:28 +0000 | [diff] [blame] | 227 | emitError($file, $., "found a use of an NSLocalized macro ($token); not supported"); |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 228 | $nestingLevel = 0 if !defined $nestingLevel; |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 229 | $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.com | b76add8 | 2015-03-06 18:47:28 +0000 | [diff] [blame] | 239 | emitError($file, $., "mismatched single quote"); |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 240 | $_ = ""; |
| 241 | } |
| 242 | } else { |
| 243 | if ($expected and $expected ne $token) { |
mitz@apple.com | b76add8 | 2015-03-06 18:47:28 +0000 | [diff] [blame] | 244 | emitError($file, $., "found $token but expected $expected"); |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 245 | $expected = ""; |
| 246 | } |
carlosgc@webkit.org | 6878dca | 2017-03-22 09:55:34 +0000 | [diff] [blame] | 247 | if (($token =~ /(WEB_)?UI_STRING(_KEY)?(_INTERNAL)?$/) || ($token =~ /WEB_UI_NSSTRING$/) || ($token =~ /WEB_UI_STRING_WITH_MNEMONIC$/) || ($token =~ /WEB_UI_CFSTRING$/)) { |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 248 | $expected = "("; |
| 249 | $macro = $token; |
| 250 | $UIString = undef; |
| 251 | $key = undef; |
| 252 | $comment = undef; |
carlosgc@webkit.org | 6878dca | 2017-03-22 09:55:34 +0000 | [diff] [blame] | 253 | $mnemonic = undef; |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 254 | $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.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 267 | } |
| 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.com | b76add8 | 2015-03-06 18:47:28 +0000 | [diff] [blame] | 279 | emitError($file, 0, "reached end of file but expected $expected"); |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 280 | } |
| 281 | |
| 282 | close SOURCE; |
| 283 | } |
| 284 | |
mitz@apple.com | b76add8 | 2015-03-06 18:47:28 +0000 | [diff] [blame] | 285 | print "\n" if sawError() || $notLocalizedCount || $NSLocalizeCount; |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 286 | |
| 287 | my @unusedExceptions = sort grep { !$usedException{$_} } keys %exception; |
| 288 | if (@unusedExceptions) { |
| 289 | for my $unused (@unusedExceptions) { |
mitz@apple.com | 93bd8aa | 2014-12-25 22:45:31 +0000 | [diff] [blame] | 290 | emitWarning($exceptionsFile, $exception{$unused}, "exception $unused not used"); |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 291 | } |
| 292 | print "\n"; |
| 293 | } |
| 294 | |
mitz@apple.com | b76add8 | 2015-03-06 18:47:28 +0000 | [diff] [blame] | 295 | print localizedCount() . " localizable strings\n" if localizedCount(); |
| 296 | print keyCollisionCount() . " key collisions\n" if keyCollisionCount(); |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 297 | print "$notLocalizedCount strings not marked for localization\n" if $notLocalizedCount; |
| 298 | print "$NSLocalizeCount uses of NSLocalize\n" if $NSLocalizeCount; |
| 299 | print scalar(@unusedExceptions), " unused exceptions\n" if @unusedExceptions; |
| 300 | |
mitz@apple.com | b76add8 | 2015-03-06 18:47:28 +0000 | [diff] [blame] | 301 | if (sawError()) { |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 302 | print "\nErrors encountered. Exiting without writing to $fileToUpdate.\n"; |
| 303 | exit 1; |
| 304 | } |
| 305 | |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 306 | if (-e "$fileToUpdate") { |
| 307 | if (!$verify) { |
mrowe@apple.com | 35bae53 | 2015-02-11 23:14:30 +0000 | [diff] [blame] | 308 | my $temporaryFile = "$fileToUpdate.updated"; |
mitz@apple.com | b76add8 | 2015-03-06 18:47:28 +0000 | [diff] [blame] | 309 | writeStringsFile($temporaryFile); |
mrowe@apple.com | 35bae53 | 2015-02-11 23:14:30 +0000 | [diff] [blame] | 310 | |
| 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.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 317 | } else { |
mitz@apple.com | b76add8 | 2015-03-06 18:47:28 +0000 | [diff] [blame] | 318 | verifyStringsFile($fileToUpdate); |
mitz@apple.com | f7dadb5 | 2013-09-20 20:13:11 +0000 | [diff] [blame] | 319 | } |
| 320 | } else { |
| 321 | print "error: $fileToUpdate does not exist\n"; |
| 322 | exit 1; |
| 323 | } |