Expose some of the functionality of extract-localizable-strings.pl as a module
authormitz@apple.com <mitz@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 6 Mar 2015 18:47:28 +0000 (18:47 +0000)
committermitz@apple.com <mitz@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 6 Mar 2015 18:47:28 +0000 (18:47 +0000)
https://bugs.webkit.org/show_bug.cgi?id=142038

Reviewed by Sam Weinig.

* LocalizableStrings.pm: Copied from Source/WebCore/extract-localizable-strings.pl.
(setTreatWarningsAsErrors): Added this setter for the variable moved here.
(sawError): Added a getter.
(emitError): Added. Emits the error message and sets $sawError.
(unescapeHexSequence): Moved from extract-localizable-strings.pl and renamed to start with
a lowercase letter.
(keyCollisionCount): Added this getter.
(localizedCount): Added this getter.
(HandleUIString): Moved from extract-localizable-strings.pl and made it increment the
localized string count.
(writeStringsFile): Moved code from extract-localizable-strings.pl into this new subroutine.
(verifyStringsFile): Ditto.

* WebCore.xcodeproj/project.pbxproj: Added LocalizableStrings.pm to the Copy Scripts build
phase.

* extract-localizable-strings.pl:
(emitWarning): Moved to the module.
(UnescapeHexSequence): Ditto.
(HandleUIString): Ditto.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@181170 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Source/WebCore/ChangeLog
Source/WebCore/LocalizableStrings.pm [new file with mode: 0755]
Source/WebCore/WebCore.xcodeproj/project.pbxproj
Source/WebCore/extract-localizable-strings.pl

index ed52db0..c206d67 100644 (file)
@@ -1,3 +1,31 @@
+2015-03-06  Dan Bernstein  <mitz@apple.com>
+
+        Expose some of the functionality of extract-localizable-strings.pl as a module
+        https://bugs.webkit.org/show_bug.cgi?id=142038
+
+        Reviewed by Sam Weinig.
+
+        * LocalizableStrings.pm: Copied from Source/WebCore/extract-localizable-strings.pl.
+        (setTreatWarningsAsErrors): Added this setter for the variable moved here.
+        (sawError): Added a getter.
+        (emitError): Added. Emits the error message and sets $sawError.
+        (unescapeHexSequence): Moved from extract-localizable-strings.pl and renamed to start with
+        a lowercase letter.
+        (keyCollisionCount): Added this getter.
+        (localizedCount): Added this getter.
+        (HandleUIString): Moved from extract-localizable-strings.pl and made it increment the
+        localized string count.
+        (writeStringsFile): Moved code from extract-localizable-strings.pl into this new subroutine.
+        (verifyStringsFile): Ditto.
+
+        * WebCore.xcodeproj/project.pbxproj: Added LocalizableStrings.pm to the Copy Scripts build
+        phase.
+
+        * extract-localizable-strings.pl:
+        (emitWarning): Moved to the module.
+        (UnescapeHexSequence): Ditto.
+        (HandleUIString): Ditto.
+
 2015-03-06  Darin Adler  <darin@apple.com>
 
         Remove unused C++ DOM event handler attribute functions
diff --git a/Source/WebCore/LocalizableStrings.pm b/Source/WebCore/LocalizableStrings.pm
new file mode 100755 (executable)
index 0000000..f389c56
--- /dev/null
@@ -0,0 +1,223 @@
+# Copyright (C) 2006, 2007, 2009, 2010, 2013, 2015 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+# THE POSSIBILITY OF SUCH DAMAGE.
+
+use strict;
+
+my $treatWarningsAsErrors = 0;
+
+sub setTreatWarningsAsErrors($)
+{
+    ($treatWarningsAsErrors) = @_;
+}
+
+my $sawError = 0;
+
+sub sawError()
+{
+    return $sawError;
+}
+
+sub emitError($$$)
+{
+    my ($file, $line, $message) = @_;
+    print "$file:$line: $message\n";
+    $sawError = 1;
+}
+
+sub emitWarning($$$)
+{
+    my ($file, $line, $message) = @_;
+    my $prefix = $treatWarningsAsErrors ? "" : "warning: ";
+    print "$file:$line: $prefix$message\n";
+    $sawError = 1 if $treatWarningsAsErrors;
+}
+
+# Unescapes C language hexadecimal escape sequences.
+sub unescapeHexSequence($)
+{
+    my ($originalStr) = @_;
+
+    my $escapedStr = $originalStr;
+    my $unescapedStr = "";
+
+    for (;;) {
+        if ($escapedStr =~ s-^\\x([[:xdigit:]]+)--) {
+            if (256 <= hex($1)) {
+                print "Hexadecimal escape sequence out of range: \\x$1\n";
+                return undef;
+            }
+            $unescapedStr .= pack("H*", $1);
+        } elsif ($escapedStr =~ s-^(.)--) {
+            $unescapedStr .= $1;
+        } else {
+            return $unescapedStr;
+        }
+    }
+}
+
+my $keyCollisionCount = 0;
+
+sub keyCollisionCount()
+{
+    return $keyCollisionCount;
+}
+
+my $localizedCount = 0;
+
+sub localizedCount()
+{
+    return $localizedCount;
+}
+
+my %stringByKey;
+my %commentByKey;
+my %fileByKey;
+my %lineByKey;
+
+sub HandleUIString
+{
+    my ($string, $key, $comment, $file, $line) = @_;
+
+    $localizedCount++;
+
+    my $bad = 0;
+    $string = unescapeHexSequence($string);
+    if (!defined($string)) {
+        print "$file:$line: string has an illegal hexadecimal escape sequence\n";
+        $bad = 1;
+    }
+    $key = unescapeHexSequence($key);
+    if (!defined($key)) {
+        print "$file:$line: key has an illegal hexadecimal escape sequence\n";
+        $bad = 1;
+    }
+    $comment = unescapeHexSequence($comment);
+    if (!defined($comment)) {
+        print "$file:$line: comment has an illegal hexadecimal escape sequence\n";
+        $bad = 1;
+    }
+    if (grep { $_ == 0xFFFD } unpack "U*", $string) {
+        print "$file:$line: string for translation has illegal UTF-8 -- most likely a problem with the Text Encoding of the source file\n";
+        $bad = 1;
+    }
+    if ($string ne $key && grep { $_ == 0xFFFD } unpack "U*", $key) {
+        print "$file:$line: key has illegal UTF-8 -- most likely a problem with the Text Encoding of the source file\n";
+        $bad = 1;
+    }
+    if (grep { $_ == 0xFFFD } unpack "U*", $comment) {
+        print "$file:$line: comment for translation has illegal UTF-8 -- most likely a problem with the Text Encoding of the source file\n";
+        $bad = 1;
+    }
+    if ($bad) {
+        $sawError = 1;
+        return;
+    }
+    
+    if ($stringByKey{$key} && $stringByKey{$key} ne $string) {
+        emitWarning($file, $line, "encountered the same key, \"$key\", twice, with different strings");
+        emitWarning($fileByKey{$key}, $lineByKey{$key}, "previous occurrence");
+        $keyCollisionCount++;
+        return;
+    }
+    if ($commentByKey{$key} && $commentByKey{$key} ne $comment) {
+        emitWarning($file, $line, "encountered the same key, \"$key\", twice, with different comments");
+        emitWarning($fileByKey{$key}, $lineByKey{$key}, "previous occurrence");
+        $keyCollisionCount++;
+        return;
+    }
+
+    $fileByKey{$key} = $file;
+    $lineByKey{$key} = $line;
+    $stringByKey{$key} = $string;
+    $commentByKey{$key} = $comment;
+}
+
+sub writeStringsFile($)
+{
+    my ($file) = @_;
+
+    my $localizedStrings = "";
+
+    for my $key (sort keys %commentByKey) {
+        $localizedStrings .= "/* $commentByKey{$key} */\n\"$key\" = \"$stringByKey{$key}\";\n\n";
+    }
+
+    # Write out the strings file as UTF-8
+    open STRINGS, ">", $file or die;
+    print STRINGS $localizedStrings;
+    close STRINGS;
+}
+
+sub verifyStringsFile($)
+{
+    my ($file) = @_;
+
+    open STRINGS, $file or die;
+
+    my $lastComment;
+    my $line;
+    my $sawError;
+
+    while (<STRINGS>) {
+        chomp;
+
+        next if (/^\s*$/);
+
+        if (/^\/\* (.*) \*\/$/) {
+            $lastComment = $1;
+        } elsif (/^"((?:[^\\]|\\[^"])*)"\s*=\s*"((?:[^\\]|\\[^"])*)";$/) #
+        {
+            my $string = delete $stringByKey{$1};
+            if (!defined $string) {
+                print "$file:$.: unused key \"$1\"\n";
+                $sawError = 1;
+            } else {
+                if (!($string eq $2)) {
+                    print "$file:$.: unexpected value \"$2\" for key \"$1\"\n";
+                    print "$fileByKey{$1}:$lineByKey{$1}: expected value \"$string\" defined here\n";
+                    $sawError = 1;
+                }
+                if (!($lastComment eq $commentByKey{$1})) {
+                    print "$file:$.: unexpected comment /* $lastComment */ for key \"$1\"\n";
+                    print "$fileByKey{$1}:$lineByKey{$1}: expected comment /* $commentByKey{$1} */ defined here\n";
+                    $sawError = 1;
+                }
+            }
+        } else {
+            print "$file:$.: line with unexpected format: $_\n";
+            $sawError = 1;
+        }
+    }
+
+    for my $missing (keys %stringByKey) {
+        print "$fileByKey{$missing}:$lineByKey{$missing}: missing key \"$missing\"\n";
+        $sawError = 1;
+    }
+
+    if ($sawError) {
+        print "\n$file:0: file is not up to date.\n";
+        exit 1;
+    }
+}
+
+1;
index 1c3e99e..5140dca 100644 (file)
                37C238221098C84200EF9F72 /* ComplexTextControllerCoreText.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C238201098C84200EF9F72 /* ComplexTextControllerCoreText.mm */; };
                37C28A6810F659CC008C7813 /* TypesettingFeatures.h in Headers */ = {isa = PBXBuildFile; fileRef = 37C28A6710F659CC008C7813 /* TypesettingFeatures.h */; settings = {ATTRIBUTES = (Private, ); }; };
                37C61F0112095C87007A3C67 /* AtomicStringKeyedMRUCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 37C61F0012095C87007A3C67 /* AtomicStringKeyedMRUCache.h */; };
+               37D456FD1A9A50D8003330A1 /* LocalizableStrings.pm in Copy Scripts */ = {isa = PBXBuildFile; fileRef = 37D456FB1A9A50B6003330A1 /* LocalizableStrings.pm */; };
                37DDCD9413844FD50008B793 /* MIMEHeader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 37DDCD9213844FD50008B793 /* MIMEHeader.cpp */; };
                37DDCD9513844FD50008B793 /* MIMEHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 37DDCD9313844FD50008B793 /* MIMEHeader.h */; };
                37DDCD9E13844FFA0008B793 /* Archive.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 37DDCD9D13844FFA0008B793 /* Archive.cpp */; };
                        dstPath = PrivateHeaders/Scripts;
                        dstSubfolderSpec = 1;
                        files = (
+                               37D456FD1A9A50D8003330A1 /* LocalizableStrings.pm in Copy Scripts */,
                                3717D7E817ECC591003C276D /* extract-localizable-strings.pl in Copy Scripts */,
                        );
                        name = "Copy Scripts";
                37C238201098C84200EF9F72 /* ComplexTextControllerCoreText.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ComplexTextControllerCoreText.mm; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; };
                37C28A6710F659CC008C7813 /* TypesettingFeatures.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TypesettingFeatures.h; sourceTree = "<group>"; };
                37C61F0012095C87007A3C67 /* AtomicStringKeyedMRUCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AtomicStringKeyedMRUCache.h; sourceTree = "<group>"; };
+               37D456FB1A9A50B6003330A1 /* LocalizableStrings.pm */ = {isa = PBXFileReference; lastKnownFileType = text.script.perl; path = LocalizableStrings.pm; sourceTree = "<group>"; };
                37DDCD9213844FD50008B793 /* MIMEHeader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MIMEHeader.cpp; sourceTree = "<group>"; };
                37DDCD9313844FD50008B793 /* MIMEHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MIMEHeader.h; sourceTree = "<group>"; };
                37DDCD9D13844FFA0008B793 /* Archive.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Archive.cpp; sourceTree = "<group>"; };
                        isa = PBXGroup;
                        children = (
                                3717D7E517ECC3A6003C276D /* extract-localizable-strings.pl */,
+                               37D456FB1A9A50B6003330A1 /* LocalizableStrings.pm */,
                        );
                        name = Scripts;
                        sourceTree = "<group>";
index bdecd06..4dd19ac 100755 (executable)
 use strict;
 use File::Compare;
 use File::Copy;
+use FindBin;
 use Getopt::Long;
+use lib $FindBin::Bin;
+use LocalizableStrings;
 no warnings 'deprecated';
 
-sub UnescapeHexSequence($);
-
 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 );
 
 my $verify;
@@ -66,6 +67,8 @@ my %options = (
 
 GetOptions(%options);
 
+setTreatWarningsAsErrors($treatWarningsAsErrors);
+
 @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";
 
 -f $exceptionsFile or die "Couldn't find exceptions file $exceptionsFile\n" unless !defined $exceptionsFile;
@@ -84,24 +87,12 @@ if (@ARGV < 1) {
     }
 }
 
-my $sawError = 0;
-
-my $localizedCount = 0;
-my $keyCollisionCount = 0;
 my $notLocalizedCount = 0;
 my $NSLocalizeCount = 0;
 
 my %exception;
 my %usedException;
 
-sub emitWarning($$$)
-{
-    my ($file, $line, $message) = @_;
-    my $prefix = $treatWarningsAsErrors ? "" : "warning: ";
-    print "$file:$line: $prefix$message\n";
-    $sawError = 1 if $treatWarningsAsErrors;
-}
-
 if (defined $exceptionsFile && open EXCEPTIONS, $exceptionsFile) {
     while (<EXCEPTIONS>) {
         chomp;
@@ -165,8 +156,7 @@ for my $file (sort @files) {
             
             if ($token eq "\"") {
                 if ($expected and $expected ne "a quoted string") {
-                    print "$file:$.: found a quoted string but expected $expected\n";
-                    $sawError = 1;
+                    emitError($file, $., "found a quoted string but expected $expected");
                     $expected = "";
                 }
                 if (s-^(([^\\$token]|\\.)*?)$token--) {
@@ -177,8 +167,7 @@ for my $file (sort @files) {
                         $string .= $1;
                     }
                 } else {
-                    print "$file:$.: mismatched quotes\n";
-                    $sawError = 1;
+                    emitError($file, $., "mismatched quotes");
                     $_ = "";
                 }
                 next;
@@ -227,9 +216,8 @@ handleString:
             $previousToken = $token;
 
             if ($token =~ /^NSLocalized/ && $token !~ /NSLocalizedDescriptionKey/ && $token !~ /NSLocalizedStringFromTableInBundle/ && $token !~ /NSLocalizedFileSizeDescription/ && $token !~ /NSLocalizedDescriptionKey/ && $token !~ /NSLocalizedRecoverySuggestionErrorKey/) {
-                print "$file:$.: found a use of an NSLocalized macro ($token); not supported\n";
+                emitError($file, $., "found a use of an NSLocalized macro ($token); not supported");
                 $nestingLevel = 0 if !defined $nestingLevel;
-                $sawError = 1;
                 $NSLocalizeCount++;
             } elsif ($token eq "/*") {
                 if (!s-^.*?\*/--) {
@@ -240,14 +228,12 @@ handleString:
                 $_ = ""; # Discard the rest of the line
             } elsif ($token eq "'") {
                 if (!s-([^\\]|\\.)'--) { #' <-- that single quote makes the Project Builder editor less confused
-                    print "$file:$.: mismatched single quote\n";
-                    $sawError = 1;
+                    emitError($file, $., "mismatched single quote");
                     $_ = "";
                 }
             } else {
                 if ($expected and $expected ne $token) {
-                    print "$file:$.: found $token but expected $expected\n";
-                    $sawError = 1;
+                    emitError($file, $., "found $token but expected $expected");
                     $expected = "";
                 }
                 if ($token =~ /(WEB_)?UI_STRING(_KEY)?(_INTERNAL)?$/) {
@@ -269,7 +255,6 @@ handleString:
                         HandleUIString($UIString, $key, $comment, $file, $macroLine);
                         $macro = "";
                         $expected = "";
-                        $localizedCount++;
                     }
                 } elsif ($isDebugMacro{$token}) {
                     $nestingLevel = 0 if !defined $nestingLevel;
@@ -282,98 +267,13 @@ handleString:
     goto handleString if defined $string;
     
     if ($expected) {
-        print "$file: reached end of file but expected $expected\n";
-        $sawError = 1;
+        emitError($file, 0, "reached end of file but expected $expected");
     }
     
     close SOURCE;
 }
 
-# Unescapes C language hexadecimal escape sequences.
-sub UnescapeHexSequence($)
-{
-    my ($originalStr) = @_;
-
-    my $escapedStr = $originalStr;
-    my $unescapedStr = "";
-
-    for (;;) {
-        if ($escapedStr =~ s-^\\x([[:xdigit:]]+)--) {
-            if (256 <= hex($1)) {
-                print "Hexadecimal escape sequence out of range: \\x$1\n";
-                return undef;
-            }
-            $unescapedStr .= pack("H*", $1);
-        } elsif ($escapedStr =~ s-^(.)--) {
-            $unescapedStr .= $1;
-        } else {
-            return $unescapedStr;
-        }
-    }
-}
-
-my %stringByKey;
-my %commentByKey;
-my %fileByKey;
-my %lineByKey;
-
-sub HandleUIString
-{
-    my ($string, $key, $comment, $file, $line) = @_;
-
-    my $bad = 0;
-    $string = UnescapeHexSequence($string);
-    if (!defined($string)) {
-        print "$file:$line: string has an illegal hexadecimal escape sequence\n";
-        $bad = 1;
-    }
-    $key = UnescapeHexSequence($key);
-    if (!defined($key)) {
-        print "$file:$line: key has an illegal hexadecimal escape sequence\n";
-        $bad = 1;
-    }
-    $comment = UnescapeHexSequence($comment);
-    if (!defined($comment)) {
-        print "$file:$line: comment has an illegal hexadecimal escape sequence\n";
-        $bad = 1;
-    }
-    if (grep { $_ == 0xFFFD } unpack "U*", $string) {
-        print "$file:$line: string for translation has illegal UTF-8 -- most likely a problem with the Text Encoding of the source file\n";
-        $bad = 1;
-    }
-    if ($string ne $key && grep { $_ == 0xFFFD } unpack "U*", $key) {
-        print "$file:$line: key has illegal UTF-8 -- most likely a problem with the Text Encoding of the source file\n";
-        $bad = 1;
-    }
-    if (grep { $_ == 0xFFFD } unpack "U*", $comment) {
-        print "$file:$line: comment for translation has illegal UTF-8 -- most likely a problem with the Text Encoding of the source file\n";
-        $bad = 1;
-    }
-    if ($bad) {
-        $sawError = 1;
-        return;
-    }
-    
-    if ($stringByKey{$key} && $stringByKey{$key} ne $string) {
-        emitWarning($file, $line, "encountered the same key, \"$key\", twice, with different strings");
-        emitWarning($fileByKey{$key}, $lineByKey{$key}, "previous occurrence");
-        $keyCollisionCount++;
-        return;
-    }
-    if ($commentByKey{$key} && $commentByKey{$key} ne $comment) {
-        emitWarning($file, $line, "encountered the same key, \"$key\", twice, with different comments");
-        emitWarning($fileByKey{$key}, $lineByKey{$key}, "previous occurrence");
-        $keyCollisionCount++;
-        return;
-    }
-
-    $fileByKey{$key} = $file;
-    $lineByKey{$key} = $line;
-    $stringByKey{$key} = $string;
-    $commentByKey{$key} = $comment;
-}
-
-print "\n" if $sawError || $notLocalizedCount || $NSLocalizeCount;
+print "\n" if sawError() || $notLocalizedCount || $NSLocalizeCount;
 
 my @unusedExceptions = sort grep { !$usedException{$_} } keys %exception;
 if (@unusedExceptions) {
@@ -383,30 +283,21 @@ if (@unusedExceptions) {
     print "\n";
 }
 
-print "$localizedCount localizable strings\n" if $localizedCount;
-print "$keyCollisionCount key collisions\n" if $keyCollisionCount;
+print localizedCount() . " localizable strings\n" if localizedCount();
+print keyCollisionCount() . " key collisions\n" if keyCollisionCount();
 print "$notLocalizedCount strings not marked for localization\n" if $notLocalizedCount;
 print "$NSLocalizeCount uses of NSLocalize\n" if $NSLocalizeCount;
 print scalar(@unusedExceptions), " unused exceptions\n" if @unusedExceptions;
 
-if ($sawError) {
+if (sawError()) {
     print "\nErrors encountered. Exiting without writing to $fileToUpdate.\n";
     exit 1;
 }
 
-my $localizedStrings = "";
-
-for my $key (sort keys %commentByKey) {
-    $localizedStrings .= "/* $commentByKey{$key} */\n\"$key\" = \"$stringByKey{$key}\";\n\n";
-}
-
 if (-e "$fileToUpdate") {
     if (!$verify) {
-        # Write out the strings file as UTF-8
         my $temporaryFile = "$fileToUpdate.updated";
-        open STRINGS, ">", $temporaryFile or die;
-        print STRINGS $localizedStrings;
-        close STRINGS;
+        writeStringsFile($temporaryFile);
 
         # Avoid updating the target file's modification time if the contents have not changed.
         if (compare($temporaryFile, $fileToUpdate)) {
@@ -415,51 +306,7 @@ if (-e "$fileToUpdate") {
             unlink $temporaryFile;
         }
     } else {
-        open STRINGS, $fileToUpdate or die;
-
-        my $lastComment;
-        my $line;
-
-        while (<STRINGS>) {
-            chomp;
-
-            next if (/^\s*$/);
-
-            if (/^\/\* (.*) \*\/$/) {
-                $lastComment = $1;
-            } elsif (/^"((?:[^\\]|\\[^"])*)"\s*=\s*"((?:[^\\]|\\[^"])*)";$/) #
-            {
-                my $string = delete $stringByKey{$1};
-                if (!defined $string) {
-                    print "$fileToUpdate:$.: unused key \"$1\"\n";
-                    $sawError = 1;
-                } else {
-                    if (!($string eq $2)) {
-                        print "$fileToUpdate:$.: unexpected value \"$2\" for key \"$1\"\n";
-                        print "$fileByKey{$1}:$lineByKey{$1}: expected value \"$string\" defined here\n";
-                        $sawError = 1;
-                    }
-                    if (!($lastComment eq $commentByKey{$1})) {
-                        print "$fileToUpdate:$.: unexpected comment /* $lastComment */ for key \"$1\"\n";
-                        print "$fileByKey{$1}:$lineByKey{$1}: expected comment /* $commentByKey{$1} */ defined here\n";
-                        $sawError = 1;
-                    }
-                }
-            } else {
-                print "$fileToUpdate:$.: line with unexpected format: $_\n";
-                $sawError = 1;
-            }
-        }
-
-        for my $missing (keys %stringByKey) {
-            print "$fileByKey{$missing}:$lineByKey{$missing}: missing key \"$missing\"\n";
-            $sawError = 1;
-        }
-
-        if ($sawError) {
-            print "\n$fileToUpdate:0: file is not up to date.\n";
-            exit 1;
-        }
+        verifyStringsFile($fileToUpdate);
     }
 } else {
     print "error: $fileToUpdate does not exist\n";