| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Helper tool that is built and run during a build to pull strings from |
| // the GRD files and generate the InfoPlist.strings files needed for |
| // Mac OS X app bundles. |
| |
| #import <Foundation/Foundation.h> |
| #include <stdint.h> |
| #include <stdio.h> |
| #include <unistd.h> |
| |
| #include <memory> |
| |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/i18n/icu_util.h" |
| #include "base/i18n/message_formatter.h" |
| #include "base/mac/scoped_nsautorelease_pool.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/time/time.h" |
| #include "chrome/grit/chromium_strings.h" |
| #include "ui/base/resource/data_pack.h" |
| |
| namespace { |
| |
| NSString* ApplicationVersionString(const char* version_file_path) { |
| NSError* error = nil; |
| NSString* path_string = [NSString stringWithUTF8String:version_file_path]; |
| NSString* version_file = |
| [NSString stringWithContentsOfFile:path_string |
| encoding:NSUTF8StringEncoding |
| error:&error]; |
| if (!version_file || error) { |
| fprintf(stderr, "Failed to load version file: %s\n", |
| [[error description] UTF8String]); |
| return nil; |
| } |
| |
| int major = 0, minor = 0, build = 0, patch = 0; |
| NSScanner* scanner = [NSScanner scannerWithString:version_file]; |
| if ([scanner scanString:@"MAJOR=" intoString:nil] && |
| [scanner scanInt:&major] && |
| [scanner scanString:@"MINOR=" intoString:nil] && |
| [scanner scanInt:&minor] && |
| [scanner scanString:@"BUILD=" intoString:nil] && |
| [scanner scanInt:&build] && |
| [scanner scanString:@"PATCH=" intoString:nil] && |
| [scanner scanInt:&patch]) { |
| return [NSString stringWithFormat:@"%d.%d.%d.%d", |
| major, minor, build, patch]; |
| } |
| fprintf(stderr, "Failed to parse version file\n"); |
| return nil; |
| } |
| |
| ui::DataPack* LoadResourceDataPack(const char* dir_path, |
| const char* branding_strings_name, |
| const char* locale_name) { |
| ui::DataPack* resource_pack = NULL; |
| |
| NSString* resource_path = [NSString stringWithFormat:@"%s/%s_%s.pak", |
| dir_path, branding_strings_name, locale_name]; |
| if (resource_path) { |
| base::FilePath resources_pak_path([resource_path fileSystemRepresentation]); |
| resources_pak_path = base::MakeAbsoluteFilePath(resources_pak_path); |
| resource_pack = new ui::DataPack(ui::SCALE_FACTOR_100P); |
| bool success = resource_pack->LoadFromPath(resources_pak_path); |
| if (!success) { |
| delete resource_pack; |
| resource_pack = NULL; |
| } |
| } |
| |
| return resource_pack; |
| } |
| |
| NSString* LoadStringFromDataPack(ui::DataPack* data_pack, |
| const char* data_pack_lang, |
| uint32_t resource_id, |
| const char* resource_id_str) { |
| NSString* result = nil; |
| base::StringPiece data; |
| if (data_pack->GetStringPiece(resource_id, &data)) { |
| // Data pack encodes strings as either UTF8 or UTF16. |
| if (data_pack->GetTextEncodingType() == ui::DataPack::UTF8) { |
| result = |
| [[[NSString alloc] initWithBytes:data.data() |
| length:data.length() |
| encoding:NSUTF8StringEncoding] |
| autorelease]; |
| } else if (data_pack->GetTextEncodingType() == ui::DataPack::UTF16) { |
| result = |
| [[[NSString alloc] initWithBytes:data.data() |
| length:data.length() |
| encoding:NSUTF16LittleEndianStringEncoding] |
| autorelease]; |
| } else { |
| fprintf(stderr, "ERROR: requested string %s from binary data pack\n", |
| resource_id_str); |
| exit(1); |
| } |
| } |
| if (!result) { |
| fprintf(stderr, "ERROR: failed to load string %s for lang %s\n", |
| resource_id_str, data_pack_lang); |
| exit(1); |
| } |
| return result; |
| } |
| |
| // Escape quotes, newlines, etc so there are no errors when the strings file |
| // is parsed. |
| NSString* EscapeForStringsFileValue(NSString* str) { |
| NSMutableString* worker = [NSMutableString stringWithString:str]; |
| |
| // Since this is a build tool, we don't really worry about making this |
| // the most efficient code. |
| |
| // Backslash first since we need to do it before we put in all the others |
| [worker replaceOccurrencesOfString:@"\\" |
| withString:@"\\\\" |
| options:NSLiteralSearch |
| range:NSMakeRange(0, [worker length])]; |
| // Now the rest of them. |
| [worker replaceOccurrencesOfString:@"\n" |
| withString:@"\\n" |
| options:NSLiteralSearch |
| range:NSMakeRange(0, [worker length])]; |
| [worker replaceOccurrencesOfString:@"\r" |
| withString:@"\\r" |
| options:NSLiteralSearch |
| range:NSMakeRange(0, [worker length])]; |
| [worker replaceOccurrencesOfString:@"\t" |
| withString:@"\\t" |
| options:NSLiteralSearch |
| range:NSMakeRange(0, [worker length])]; |
| [worker replaceOccurrencesOfString:@"\"" |
| withString:@"\\\"" |
| options:NSLiteralSearch |
| range:NSMakeRange(0, [worker length])]; |
| |
| return [[worker copy] autorelease]; |
| } |
| |
| // The valid types for the -t arg |
| const char kAppType_Main[] = "main"; // Main app |
| const char kAppType_Helper[] = "helper"; // Helper app |
| |
| } // namespace |
| |
| int main(int argc, char* const argv[]) { |
| base::mac::ScopedNSAutoreleasePool autorelease_pool; |
| |
| const char* version_file_path = NULL; |
| const char* grit_output_dir = NULL; |
| const char* branding_strings_name = NULL; |
| const char* output_dir = NULL; |
| const char* app_type = kAppType_Main; |
| |
| // Process the args |
| int ch; |
| while ((ch = getopt(argc, argv, "t:v:g:b:o:")) != -1) { |
| switch (ch) { |
| case 't': |
| app_type = optarg; |
| break; |
| case 'v': |
| version_file_path = optarg; |
| break; |
| case 'g': |
| grit_output_dir = optarg; |
| break; |
| case 'b': |
| branding_strings_name = optarg; |
| break; |
| case 'o': |
| output_dir = optarg; |
| break; |
| default: |
| fprintf(stderr, "ERROR: bad command line arg\n"); |
| exit(1); |
| break; |
| } |
| } |
| argc -= optind; |
| argv += optind; |
| |
| #define CHECK_ARG(a, b) \ |
| do { \ |
| if ((a)) { \ |
| fprintf(stderr, "ERROR: " b "\n"); \ |
| exit(1); \ |
| } \ |
| } while (false) |
| |
| // Check our args |
| CHECK_ARG(!version_file_path, "Missing VERSION file path"); |
| CHECK_ARG(!grit_output_dir, "Missing grit output dir path"); |
| CHECK_ARG(!output_dir, "Missing path to write InfoPlist.strings files"); |
| CHECK_ARG(!branding_strings_name, "Missing branding strings file name"); |
| CHECK_ARG(argc == 0, "Missing language list"); |
| CHECK_ARG((strcmp(app_type, kAppType_Main) != 0 && |
| strcmp(app_type, kAppType_Helper) != 0), |
| "Unknown app type"); |
| |
| char* const* lang_list = argv; |
| int lang_list_count = argc; |
| |
| base::i18n::InitializeICU(); |
| |
| // Parse the version file and build our string |
| NSString* version_string = ApplicationVersionString(version_file_path); |
| if (!version_string) { |
| fprintf(stderr, "ERROR: failed to get a version string"); |
| exit(1); |
| } |
| |
| NSFileManager* fm = [NSFileManager defaultManager]; |
| |
| for (int loop = 0; loop < lang_list_count; ++loop) { |
| const char* cur_lang = lang_list[loop]; |
| |
| // Open the branded string pak file |
| std::unique_ptr<ui::DataPack> branded_data_pack( |
| LoadResourceDataPack(grit_output_dir, branding_strings_name, cur_lang)); |
| if (branded_data_pack.get() == NULL) { |
| fprintf(stderr, "ERROR: Failed to load branded pak for language: %s\n", |
| cur_lang); |
| exit(1); |
| } |
| |
| uint32_t name_id = IDS_PRODUCT_NAME; |
| const char* name_id_str = "IDS_PRODUCT_NAME"; |
| uint32_t short_name_id = IDS_APP_MENU_PRODUCT_NAME; |
| const char* short_name_id_str = "IDS_APP_MENU_PRODUCT_NAME"; |
| if (strcmp(app_type, kAppType_Helper) == 0) { |
| name_id = IDS_HELPER_NAME; |
| name_id_str = "IDS_HELPER_NAME"; |
| short_name_id = IDS_SHORT_HELPER_NAME; |
| short_name_id_str = "IDS_SHORT_HELPER_NAME"; |
| } |
| |
| // Fetch the strings |
| NSString* name = |
| LoadStringFromDataPack(branded_data_pack.get(), cur_lang, |
| name_id, name_id_str); |
| NSString* short_name = |
| LoadStringFromDataPack(branded_data_pack.get(), cur_lang, |
| short_name_id, short_name_id_str); |
| NSString* copyright_format = |
| LoadStringFromDataPack(branded_data_pack.get(), cur_lang, |
| IDS_ABOUT_VERSION_COPYRIGHT, |
| "IDS_ABOUT_VERSION_COPYRIGHT"); |
| NSString* address_book_prompt_description = |
| LoadStringFromDataPack(branded_data_pack.get(), cur_lang, |
| IDS_AUTOFILL_ADDRESS_BOOK_PROMPT_DESCRIPTION, |
| "IDS_AUTOFILL_ADDRESS_BOOK_PROMPT_DESCRIPTION"); |
| |
| NSString* copyright = base::SysUTF16ToNSString( |
| base::i18n::MessageFormatter::FormatWithNumberedArgs( |
| base::SysNSStringToUTF16(copyright_format), base::Time::Now())); |
| |
| // For now, assume this is ok for all languages. If we need to, this could |
| // be moved into generated_resources.grd and fetched. |
| NSString *get_info = [NSString stringWithFormat:@"%@ %@, %@", |
| name, version_string, copyright]; |
| |
| // Generate the InfoPlist.strings file contents |
| NSString* strings_file_contents_string = |
| [NSString stringWithFormat: |
| @"CFBundleDisplayName = \"%@\";\n" |
| @"CFBundleGetInfoString = \"%@\";\n" |
| @"CFBundleName = \"%@\";\n" |
| @"NSContactsUsageDescription = \"%@\";\n" |
| @"NSHumanReadableCopyright = \"%@\";\n", |
| EscapeForStringsFileValue(name), |
| EscapeForStringsFileValue(get_info), |
| EscapeForStringsFileValue(short_name), |
| EscapeForStringsFileValue(address_book_prompt_description), |
| EscapeForStringsFileValue(copyright)]; |
| |
| // We set up Xcode projects expecting strings files to be UTF8, so make |
| // sure we write the data in that form. When Xcode copies them it will |
| // put them final runtime encoding. |
| NSData* strings_file_contents_utf8 = |
| [strings_file_contents_string dataUsingEncoding:NSUTF8StringEncoding]; |
| |
| if ([strings_file_contents_utf8 length] == 0) { |
| fprintf(stderr, "ERROR: failed to get the utf8 encoding of the strings " |
| "file for language: %s\n", cur_lang); |
| exit(1); |
| } |
| |
| // For Cocoa to find the locale at runtime, it needs to use '_' instead of |
| // '-' (http://crbug.com/20441). Also, 'en-US' should be represented |
| // simply as 'en' (http://crbug.com/19165, http://crbug.com/25578). |
| NSString* cur_lang_ns = [NSString stringWithUTF8String:cur_lang]; |
| if ([cur_lang_ns isEqualToString:@"en-US"]) { |
| cur_lang_ns = @"en"; |
| } |
| cur_lang_ns = [cur_lang_ns stringByReplacingOccurrencesOfString:@"-" |
| withString:@"_"]; |
| // Make sure the lproj we write to exists |
| NSString *lproj_name = [NSString stringWithFormat:@"%@.lproj", cur_lang_ns]; |
| NSString *output_path = |
| [[NSString stringWithUTF8String:output_dir] |
| stringByAppendingPathComponent:lproj_name]; |
| NSError* error = nil; |
| if (![fm fileExistsAtPath:output_path] && |
| ![fm createDirectoryAtPath:output_path |
| withIntermediateDirectories:YES |
| attributes:nil |
| error:&error]) { |
| fprintf(stderr, "ERROR: '%s' didn't exist or we failed to create it\n", |
| [output_path UTF8String]); |
| exit(1); |
| } |
| |
| // Write out the file |
| output_path = |
| [output_path stringByAppendingPathComponent:@"InfoPlist.strings"]; |
| if (![strings_file_contents_utf8 writeToFile:output_path |
| atomically:YES]) { |
| fprintf(stderr, "ERROR: Failed to write out '%s'\n", |
| [output_path UTF8String]); |
| exit(1); |
| } |
| } |
| return 0; |
| } |