| // Copyright 2016 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 a localized string files needed for iOS app bundles. |
| // Arguments: |
| // -p dir_to_data_pak |
| // -o output_dir |
| // -c config_file |
| // -I header_root_dir |
| // and a list of locales. |
| // |
| // Example: |
| // generate_localizable_strings \ |
| // -p "${SHARED_INTERMEDIATE_DIR}/repack_ios/repack" \ |
| // -o "${INTERMEDIATE_DIR}/app_infoplist_strings" \ |
| // -c "<(DEPTH)/ios/chrome/localizable_strings_config.plist" \ |
| // -I "${SHARED_INTERMEDIATE_DIR}" \ |
| // ar ca cs da de el en-GB en-US es fi fr he hr hu id it ja ko ms nb nl pl \ |
| // pt pt-PT ro ru sk sv th tr uk vi zh-CN zh-TW |
| |
| #import <Foundation/Foundation.h> |
| |
| #include <stdio.h> |
| #include <map> |
| #include <set> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/mac/scoped_nsautorelease_pool.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "ui/base/resource/data_pack.h" |
| #include "ui/base/resource/resource_handle.h" |
| |
| namespace { |
| |
| // Load the packed resource data pack for |locale| from |packed_data_pack_dir|. |
| // If loading fails, null is returned. |
| std::unique_ptr<ui::DataPack> LoadResourceDataPack( |
| NSString* packed_data_pack_dir, |
| NSString* locale_name) { |
| std::unique_ptr<ui::DataPack> resource_data_pack; |
| NSString* resource_path = |
| [NSString stringWithFormat:@"%@/%@.lproj/locale.pak", |
| packed_data_pack_dir, locale_name]; |
| |
| if (!resource_path) |
| return resource_data_pack; |
| |
| // FilePath may contain components that references parent directory |
| // (".."). DataPack disallows paths with ".." for security reasons. |
| base::FilePath resources_pak_path([resource_path fileSystemRepresentation]); |
| resources_pak_path = base::MakeAbsoluteFilePath(resources_pak_path); |
| if (!base::PathExists(resources_pak_path)) |
| return resource_data_pack; |
| |
| resource_data_pack.reset(new ui::DataPack(ui::SCALE_FACTOR_100P)); |
| if (!resource_data_pack->LoadFromPath(resources_pak_path)) |
| resource_data_pack.reset(); |
| |
| return resource_data_pack; |
| } |
| |
| // Create a |NSString| from the string with |resource_id| from |data_pack|. |
| // Return nil if none is found. |
| NSString* GetStringFromDataPack(const ui::DataPack& data_pack, |
| uint16_t resource_id) { |
| base::StringPiece data; |
| if (!data_pack.GetStringPiece(resource_id, &data)) |
| return nil; |
| |
| // Data pack encodes strings as either UTF8 or UTF16. |
| if (data_pack.GetTextEncodingType() == ui::DataPack::UTF8) { |
| return [[[NSString alloc] initWithBytes:data.data() |
| length:data.length() |
| encoding:NSUTF8StringEncoding] autorelease]; |
| } else if (data_pack.GetTextEncodingType() == ui::DataPack::UTF16) { |
| return [[[NSString alloc] initWithBytes:data.data() |
| length:data.length() |
| encoding:NSUTF16LittleEndianStringEncoding] |
| autorelease]; |
| } |
| return nil; |
| } |
| |
| // Generates a NSDictionary mapping string IDs to localized strings. The |
| // dictionary can be written as a Property List (only contains types that |
| // are valid in Propery Lists). |
| NSDictionary* GenerateLocalizableStringsDictionary( |
| const ui::DataPack& data_pack, |
| const char* locale, |
| NSArray* resources, |
| NSDictionary* resources_ids) { |
| NSMutableDictionary* dictionary = [NSMutableDictionary dictionary]; |
| for (id resource : resources) { |
| NSString* resource_name = nil; |
| NSString* resource_output_name = nil; |
| if ([resource isKindOfClass:[NSString class]]) { |
| resource_name = resource; |
| resource_output_name = resource; |
| } else if ([resource isKindOfClass:[NSDictionary class]]) { |
| resource_name = [resource objectForKey:@"input"]; |
| resource_output_name = [resource objectForKey:@"output"]; |
| if (!resource_name || !resource_output_name) { |
| fprintf( |
| stderr, |
| "ERROR: resources must be given in <string> or <dict> format.\n"); |
| return nil; |
| } |
| } else { |
| fprintf(stderr, |
| "ERROR: resources must be given in <string> or <dict> format.\n"); |
| return nil; |
| } |
| NSInteger resource_id = |
| [[resources_ids objectForKey:resource_name] integerValue]; |
| NSString* string = GetStringFromDataPack(data_pack, resource_id); |
| if (string) { |
| [dictionary setObject:string forKey:resource_output_name]; |
| } else { |
| fprintf(stderr, "ERROR: fail to load string '%s' for locale '%s'\n", |
| base::SysNSStringToUTF8(resource_name).c_str(), locale); |
| return nil; |
| } |
| } |
| |
| return dictionary; |
| } |
| |
| NSDictionary* LoadResourcesListFromHeaders(NSArray* header_list, |
| NSString* root_header_dir) { |
| if (![header_list count]) { |
| fprintf(stderr, "ERROR: No header file in the config.\n"); |
| return nil; |
| } |
| NSMutableDictionary* resources_ids = |
| [[[NSMutableDictionary alloc] init] autorelease]; |
| for (NSString* header : header_list) { |
| NSString* header_file = |
| [root_header_dir stringByAppendingPathComponent:header]; |
| if (![[NSFileManager defaultManager] isReadableFileAtPath:header_file]) { |
| fprintf(stderr, "ERROR: header file %s not readable.\n", |
| base::SysNSStringToUTF8(header_file).c_str()); |
| return nil; |
| } |
| NSString* header_content = |
| [NSString stringWithContentsOfFile:header_file |
| encoding:NSASCIIStringEncoding |
| error:nil]; |
| if (!header_content) { |
| fprintf(stderr, "ERROR: header file %s contains non-ASCII chars.\n", |
| base::SysNSStringToUTF8(header_file).c_str()); |
| return nil; |
| } |
| NSCharacterSet* separator = [NSCharacterSet newlineCharacterSet]; |
| NSArray* defines = |
| [header_content componentsSeparatedByCharactersInSet:separator]; |
| for (NSString* define : defines) { |
| if (![define hasPrefix:@"#define "]) { |
| continue; |
| } |
| NSArray* define_string_id = [define componentsSeparatedByString:@" "]; |
| if ([define_string_id count] != 3) { |
| fprintf(stderr, "ERROR: header %s contains invalid entry: %s.\n", |
| base::SysNSStringToUTF8(header_file).c_str(), |
| base::SysNSStringToUTF8(define).c_str()); |
| return nil; |
| } |
| NSString* string_name = [define_string_id objectAtIndex:1]; |
| NSInteger string_id = [[define_string_id objectAtIndex:2] integerValue]; |
| if (!string_id) { |
| fprintf(stderr, "ERROR: header %s contains invalid entry: %s.\n", |
| base::SysNSStringToUTF8(header_file).c_str(), |
| base::SysNSStringToUTF8(define).c_str()); |
| return nil; |
| } |
| if ([resources_ids valueForKey:string_name]) { |
| fprintf(stderr, "ERROR: duplicate entry for key %s.\n", |
| base::SysNSStringToUTF8(string_name).c_str()); |
| return nil; |
| } |
| [resources_ids setValue:[NSNumber numberWithInteger:string_id] |
| forKey:string_name]; |
| } |
| } |
| return resources_ids; |
| } |
| |
| // Save |dictionary| as a Property List file (in binary1 encoding) |
| // with |locale| to |output_dir|/|locale|.lproj/|output_filename|. |
| bool SavePropertyList(NSDictionary* dictionary, |
| NSString* locale, |
| NSString* output_dir, |
| NSString* output_filename) { |
| // Compute the path to the output directory with locale. |
| NSString* output_path = [output_dir |
| stringByAppendingPathComponent:[NSString |
| stringWithFormat:@"%@.lproj", locale]]; |
| |
| // Prepare the directory. |
| NSFileManager* file_manager = [NSFileManager defaultManager]; |
| if (![file_manager fileExistsAtPath:output_path] && |
| ![file_manager createDirectoryAtPath:output_path |
| withIntermediateDirectories:YES |
| attributes:nil |
| error:nil]) { |
| fprintf(stderr, "ERROR: '%s' didn't exist or failed to create it\n", |
| base::SysNSStringToUTF8(output_path).c_str()); |
| return false; |
| } |
| |
| // Convert to property list in binary format. |
| NSError* error = nil; |
| NSData* data = [NSPropertyListSerialization |
| dataWithPropertyList:dictionary |
| format:NSPropertyListBinaryFormat_v1_0 |
| options:0 |
| error:&error]; |
| if (!data) { |
| fprintf(stderr, "ERROR: conversion to property list failed: %s\n", |
| base::SysNSStringToUTF8([error localizedDescription]).c_str()); |
| return false; |
| } |
| |
| // Save the strings to the disk. |
| output_path = [output_path stringByAppendingPathComponent:output_filename]; |
| if (![data writeToFile:output_path atomically:YES]) { |
| fprintf(stderr, "ERROR: Failed to write out '%s'\n", |
| base::SysNSStringToUTF8(output_filename).c_str()); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| } // namespace |
| |
| int main(int argc, char* const argv[]) { |
| base::mac::ScopedNSAutoreleasePool autorelease_pool; |
| |
| NSString* output_dir = nil; |
| NSString* data_pack_dir = nil; |
| NSString* root_header_dir = nil; |
| NSString* config_file = nil; |
| |
| int ch; |
| while ((ch = getopt(argc, argv, "c:I:p:o:h")) != -1) { |
| switch (ch) { |
| case 'c': |
| config_file = base::SysUTF8ToNSString(optarg); |
| break; |
| case 'I': |
| root_header_dir = base::SysUTF8ToNSString(optarg); |
| break; |
| case 'p': |
| data_pack_dir = base::SysUTF8ToNSString(optarg); |
| break; |
| case 'o': |
| output_dir = base::SysUTF8ToNSString(optarg); |
| break; |
| case 'h': |
| fprintf(stdout, |
| "usage: generate_localizable_strings -p data_pack_dir " |
| "-o output_dir -c config_file -I input_root " |
| "locale [locale...]\n" |
| "\n" |
| "Generate localized string files specified in |config_file|\n" |
| "for all specified locales in output_dir from packed data\n" |
| "packs in data_pack_dir.\n"); |
| exit(0); |
| break; |
| default: |
| fprintf(stderr, "ERROR: bad command line arg: %c.n\n", ch); |
| exit(1); |
| break; |
| } |
| } |
| |
| if (!config_file) { |
| fprintf(stderr, "ERROR: missing config file.\n"); |
| exit(1); |
| } |
| |
| if (!root_header_dir) { |
| fprintf(stderr, "ERROR: missing root header dir.\n"); |
| exit(1); |
| } |
| |
| if (!output_dir) { |
| fprintf(stderr, "ERROR: missing output directory.\n"); |
| exit(1); |
| } |
| |
| if (!data_pack_dir) { |
| fprintf(stderr, "ERROR: missing data pack directory.\n"); |
| exit(1); |
| } |
| |
| if (optind == argc) { |
| fprintf(stderr, "ERROR: missing locale list.\n"); |
| exit(1); |
| } |
| |
| NSDictionary* config = |
| [NSDictionary dictionaryWithContentsOfFile:config_file]; |
| |
| NSDictionary* resources_ids = LoadResourcesListFromHeaders( |
| [config objectForKey:@"headers"], root_header_dir); |
| |
| if (!resources_ids) { |
| exit(1); |
| } |
| |
| NSMutableArray* locales = [NSMutableArray arrayWithCapacity:(argc - optind)]; |
| for (int i = optind; i < argc; ++i) { |
| // In order 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* locale = [NSString stringWithUTF8String:argv[i]]; |
| if ([locale isEqualToString:@"en-US"]) { |
| locale = @"en"; |
| } else { |
| locale = |
| [locale stringByReplacingOccurrencesOfString:@"-" withString:@"_"]; |
| } |
| [locales addObject:locale]; |
| } |
| |
| NSArray* outputs = [config objectForKey:@"outputs"]; |
| |
| if (![outputs count]) { |
| fprintf(stderr, "ERROR: No output in config file\n"); |
| exit(1); |
| } |
| |
| for (NSString* locale in locales) { |
| std::unique_ptr<ui::DataPack> data_pack = |
| LoadResourceDataPack(data_pack_dir, locale); |
| if (!data_pack) { |
| fprintf(stderr, "ERROR: Failed to load branded pak for language: %s\n", |
| base::SysNSStringToUTF8(locale).c_str()); |
| exit(1); |
| } |
| |
| for (NSDictionary* output : [config objectForKey:@"outputs"]) { |
| NSString* output_name = [output objectForKey:@"name"]; |
| if (!output_name) { |
| fprintf(stderr, "ERROR: Output without name.\n"); |
| exit(1); |
| } |
| NSArray* output_strings = [output objectForKey:@"strings"]; |
| if (![output_strings count]) { |
| fprintf(stderr, "ERROR: Output without strings: %s.\n", |
| base::SysNSStringToUTF8(output_name).c_str()); |
| exit(1); |
| } |
| |
| NSDictionary* dictionary = GenerateLocalizableStringsDictionary( |
| *data_pack, base::SysNSStringToUTF8(locale).c_str(), output_strings, |
| resources_ids); |
| if (dictionary) { |
| SavePropertyList(dictionary, locale, output_dir, output_name); |
| } else { |
| fprintf(stderr, "ERROR: Unable to create %s.\n", |
| base::SysNSStringToUTF8(output_name).c_str()); |
| exit(1); |
| } |
| } |
| } |
| return 0; |
| } |