// Copyright 2016 The Chromium Authors
// 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>
#import <stdio.h>
#import <map>
#import <set>
#import <string>
#import <string_view>
#import <utility>
#import <vector>
#import "base/apple/foundation_util.h"
#import "base/files/file_path.h"
#import "base/files/file_util.h"
#import "base/strings/sys_string_conversions.h"
#import "ios/chrome/tools/strings/grit_header_parsing.h"
#import "ui/base/resource/data_pack.h"
#import "ui/base/resource/resource_handle.h"
#import "ui/base/resource/resource_scale_factor.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::k100Percent));
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) {
std::optional<std::string_view> data = data_pack.GetStringPiece(resource_id);
if (!data.has_value()) {
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];
} else if (data_pack.GetTextEncodingType() == ui::DataPack::UTF16) {
return [[NSString alloc] initWithBytes:data->data()
length:data->length()
encoding:NSUTF16LittleEndianStringEncoding];
}
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;
}
std::vector<base::FilePath> headers;
for (NSString* header in header_list) {
headers.push_back(base::apple::NSStringToFilePath(
[root_header_dir stringByAppendingPathComponent:header]));
}
std::optional<ResourceMap> resource_map =
LoadResourcesFromGritHeaders(headers);
if (!resource_map) {
return nil;
}
NSMutableDictionary* resource_ids = [[NSMutableDictionary alloc] init];
for (const auto& pair : *resource_map) {
resource_ids[base::SysUTF8ToNSString(pair.first)] =
[NSNumber numberWithInt:pair.second];
}
return [resource_ids copy];
}
// 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;
}
int real_main(int argc, char* const argv[]) {
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);
default:
fprintf(stderr, "ERROR: bad command line arg: %c.n\n", ch);
exit(1);
}
}
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;
}
} // namespace
int main(int argc, char* const argv[]) {
@autoreleasepool {
return real_main(argc, argv);
}
}