| #include "SourcePos.h" |
| #include "ValuesFile.h" |
| #include "XLIFFFile.h" |
| #include "Perforce.h" |
| #include "merge_res_and_xliff.h" |
| #include "localize.h" |
| #include "file_utils.h" |
| #include "res_check.h" |
| #include "xmb.h" |
| |
| #include <host/pseudolocalize.h> |
| |
| #include <stdlib.h> |
| #include <stdarg.h> |
| #include <sstream> |
| #include <stdio.h> |
| #include <string.h> |
| #include <stdlib.h> |
| |
| using namespace std; |
| |
| FILE* g_logFile = NULL; |
| |
| int test(); |
| |
| int |
| read_settings(const string& filename, map<string,Settings>* result, const string& rootDir) |
| { |
| XMLNode* root = NodeHandler::ParseFile(filename, XMLNode::PRETTY); |
| if (root == NULL) { |
| SourcePos(filename, -1).Error("Error reading file."); |
| return 1; |
| } |
| |
| // <configuration> |
| vector<XMLNode*> configNodes = root->GetElementsByName("", "configuration"); |
| const size_t I = configNodes.size(); |
| for (size_t i=0; i<I; i++) { |
| const XMLNode* configNode = configNodes[i]; |
| |
| Settings settings; |
| settings.id = configNode->GetAttribute("", "id", ""); |
| if (settings.id == "") { |
| configNode->Position().Error("<configuration> needs an id attribute."); |
| delete root; |
| return 1; |
| } |
| |
| settings.oldVersion = configNode->GetAttribute("", "old-cl", ""); |
| |
| settings.currentVersion = configNode->GetAttribute("", "new-cl", ""); |
| if (settings.currentVersion == "") { |
| configNode->Position().Error("<configuration> needs a new-cl attribute."); |
| delete root; |
| return 1; |
| } |
| |
| // <app> |
| vector<XMLNode*> appNodes = configNode->GetElementsByName("", "app"); |
| |
| const size_t J = appNodes.size(); |
| for (size_t j=0; j<J; j++) { |
| const XMLNode* appNode = appNodes[j]; |
| |
| string dir = appNode->GetAttribute("", "dir", ""); |
| if (dir == "") { |
| appNode->Position().Error("<app> needs a dir attribute."); |
| delete root; |
| return 1; |
| } |
| |
| settings.apps.push_back(dir); |
| } |
| |
| // <reject> |
| vector<XMLNode*> rejectNodes = configNode->GetElementsByName("", "reject"); |
| |
| const size_t K = rejectNodes.size(); |
| for (size_t k=0; k<K; k++) { |
| const XMLNode* rejectNode = rejectNodes[k]; |
| |
| Reject reject; |
| |
| reject.file = rejectNode->GetAttribute("", "file", ""); |
| if (reject.file == "") { |
| rejectNode->Position().Error("<reject> needs a file attribute."); |
| delete root; |
| return 1; |
| } |
| string f = reject.file; |
| reject.file = rootDir; |
| reject.file += '/'; |
| reject.file += f; |
| |
| reject.name = rejectNode->GetAttribute("", "name", ""); |
| if (reject.name == "") { |
| rejectNode->Position().Error("<reject> needs a name attribute."); |
| delete root; |
| return 1; |
| } |
| |
| reject.comment = trim_string(rejectNode->CollapseTextContents()); |
| |
| settings.reject.push_back(reject); |
| } |
| |
| (*result)[settings.id] = settings; |
| } |
| |
| delete root; |
| return 0; |
| } |
| |
| |
| static void |
| ValuesFile_to_XLIFFFile(const ValuesFile* values, XLIFFFile* xliff, const string& englishFilename) |
| { |
| const set<StringResource>& strings = values->GetStrings(); |
| for (set<StringResource>::const_iterator it=strings.begin(); it!=strings.end(); it++) { |
| StringResource res = *it; |
| res.file = englishFilename; |
| xliff->AddStringResource(res); |
| } |
| } |
| |
| static bool |
| contains_reject(const Settings& settings, const string& file, const TransUnit& tu) |
| { |
| const string name = tu.id; |
| const vector<Reject>& reject = settings.reject; |
| const size_t I = reject.size(); |
| for (size_t i=0; i<I; i++) { |
| const Reject& r = reject[i]; |
| if (r.file == file && r.name == name) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * If it's been rejected, then we keep whatever info we have. |
| * |
| * Implements this truth table: |
| * |
| * S AT AS Keep |
| * ----------------------- |
| * 0 0 0 0 (this case can't happen) |
| * 0 0 1 0 (it was there, never translated, and removed) |
| * 0 1 0 0 (somehow it got translated, but it was removed) |
| * 0 1 1 0 (it was removed after having been translated) |
| * |
| * 1 0 0 1 (it was just added) |
| * 1 0 1 1 (it was added, has been changed, but it never got translated) |
| * 1 1 0 1 (somehow it got translated, but we don't know based on what) |
| * 1 1 1 0/1 (it's in both. 0 if S=AS b/c there's no need to retranslate if they're |
| * the same. 1 if S!=AS because S changed, so it should be retranslated) |
| * |
| * The first four are cases where, whatever happened in the past, the string isn't there |
| * now, so it shouldn't be in the XLIFF file. |
| * |
| * For cases 4 and 5, the string has never been translated, so get it translated. |
| * |
| * For case 6, it's unclear where the translated version came from, so we're conservative |
| * and send it back for them to have another shot at. |
| * |
| * For case 7, we have some data. We have two choices. We could rely on the translator's |
| * translation memory or tools to notice that the strings haven't changed, and populate the |
| * <target> field themselves. Or if the string hasn't changed since last time, we can just |
| * not even tell them about it. As the project nears the end, it will be convenient to see |
| * the xliff files reducing in size, so we pick the latter. Obviously, if the string has |
| * changed, then we need to get it retranslated. |
| */ |
| bool |
| keep_this_trans_unit(const string& file, const TransUnit& unit, void* cookie) |
| { |
| const Settings* settings = reinterpret_cast<const Settings*>(cookie); |
| |
| if (contains_reject(*settings, file, unit)) { |
| return true; |
| } |
| |
| if (unit.source.id == "") { |
| return false; |
| } |
| if (unit.altTarget.id == "" || unit.altSource.id == "") { |
| return true; |
| } |
| return unit.source.value->ContentsToString(XLIFF_NAMESPACES) |
| != unit.altSource.value->ContentsToString(XLIFF_NAMESPACES); |
| } |
| |
| int |
| validate_config(const string& settingsFile, const map<string,Settings>& settings, |
| const string& config) |
| { |
| if (settings.find(config) == settings.end()) { |
| SourcePos(settingsFile, -1).Error("settings file does not contain setting: %s\n", |
| config.c_str()); |
| return 1; |
| } |
| return 0; |
| } |
| |
| int |
| validate_configs(const string& settingsFile, const map<string,Settings>& settings, |
| const vector<string>& configs) |
| { |
| int err = 0; |
| for (size_t i=0; i<configs.size(); i++) { |
| string config = configs[i]; |
| err |= validate_config(settingsFile, settings, config); |
| } |
| return err; |
| } |
| |
| int |
| select_files(vector<string> *resFiles, const string& config, |
| const map<string,Settings>& settings, const string& rootDir) |
| { |
| int err; |
| vector<vector<string> > allResFiles; |
| vector<string> configs; |
| configs.push_back(config); |
| err = select_files(&allResFiles, configs, settings, rootDir); |
| if (err == 0) { |
| *resFiles = allResFiles[0]; |
| } |
| return err; |
| } |
| |
| int |
| select_files(vector<vector<string> > *allResFiles, const vector<string>& configs, |
| const map<string,Settings>& settings, const string& rootDir) |
| { |
| int err; |
| printf("Selecting files..."); |
| fflush(stdout); |
| |
| for (size_t i=0; i<configs.size(); i++) { |
| const string& config = configs[i]; |
| const Settings& setting = settings.find(config)->second; |
| |
| vector<string> resFiles; |
| err = Perforce::GetResourceFileNames(setting.currentVersion, rootDir, |
| setting.apps, &resFiles, true); |
| if (err != 0) { |
| fprintf(stderr, "error with perforce. bailing\n"); |
| return err; |
| } |
| |
| allResFiles->push_back(resFiles); |
| } |
| return 0; |
| } |
| |
| static int |
| do_export(const string& settingsFile, const string& rootDir, const string& outDir, |
| const string& targetLocale, const vector<string>& configs) |
| { |
| bool success = true; |
| int err; |
| |
| if (false) { |
| printf("settingsFile=%s\n", settingsFile.c_str()); |
| printf("rootDir=%s\n", rootDir.c_str()); |
| printf("outDir=%s\n", outDir.c_str()); |
| for (size_t i=0; i<configs.size(); i++) { |
| printf("config[%zd]=%s\n", i, configs[i].c_str()); |
| } |
| } |
| |
| map<string,Settings> settings; |
| err = read_settings(settingsFile, &settings, rootDir); |
| if (err != 0) { |
| return err; |
| } |
| |
| err = validate_configs(settingsFile, settings, configs); |
| if (err != 0) { |
| return err; |
| } |
| |
| vector<vector<string> > allResFiles; |
| err = select_files(&allResFiles, configs, settings, rootDir); |
| if (err != 0) { |
| return err; |
| } |
| |
| size_t totalFileCount = 0; |
| for (size_t i=0; i<allResFiles.size(); i++) { |
| totalFileCount += allResFiles[i].size(); |
| } |
| totalFileCount *= 3; // we try all 3 versions of the file |
| |
| size_t fileProgress = 0; |
| vector<Stats> stats; |
| vector<pair<string,XLIFFFile*> > xliffs; |
| |
| for (size_t i=0; i<configs.size(); i++) { |
| const string& config = configs[i]; |
| const Settings& setting = settings[config]; |
| |
| if (false) { |
| fprintf(stderr, "Configuration: %s (%zd of %zd)\n", config.c_str(), i+1, |
| configs.size()); |
| fprintf(stderr, " Old CL: %s\n", setting.oldVersion.c_str()); |
| fprintf(stderr, " Current CL: %s\n", setting.currentVersion.c_str()); |
| } |
| |
| Configuration english; |
| english.locale = "en_US"; |
| Configuration translated; |
| translated.locale = targetLocale; |
| XLIFFFile* xliff = XLIFFFile::Create(english, translated, setting.currentVersion); |
| |
| const vector<string>& resFiles = allResFiles[i]; |
| const size_t J = resFiles.size(); |
| for (size_t j=0; j<J; j++) { |
| string resFile = resFiles[j]; |
| |
| // parse the files into a ValuesFile |
| // pull out the strings and add them to the XLIFFFile |
| |
| // current file |
| print_file_status(++fileProgress, totalFileCount); |
| ValuesFile* currentFile = get_values_file(resFile, english, CURRENT_VERSION, |
| setting.currentVersion, true); |
| if (currentFile != NULL) { |
| ValuesFile_to_XLIFFFile(currentFile, xliff, resFile); |
| //printf("currentFile=[%s]\n", currentFile->ToString().c_str()); |
| } else { |
| fprintf(stderr, "error reading file %s@%s\n", resFile.c_str(), |
| setting.currentVersion.c_str()); |
| success = false; |
| } |
| |
| // old file |
| print_file_status(++fileProgress, totalFileCount); |
| ValuesFile* oldFile = get_values_file(resFile, english, OLD_VERSION, |
| setting.oldVersion, false); |
| if (oldFile != NULL) { |
| ValuesFile_to_XLIFFFile(oldFile, xliff, resFile); |
| //printf("oldFile=[%s]\n", oldFile->ToString().c_str()); |
| } |
| |
| // translated version |
| // (get the head of the tree for the most recent translation, but it's considered |
| // the old one because the "current" one hasn't been made yet, and this goes into |
| // the <alt-trans> tag if necessary |
| print_file_status(++fileProgress, totalFileCount); |
| string transFilename = translated_file_name(resFile, targetLocale); |
| ValuesFile* transFile = get_values_file(transFilename, translated, OLD_VERSION, |
| setting.currentVersion, false); |
| if (transFile != NULL) { |
| ValuesFile_to_XLIFFFile(transFile, xliff, resFile); |
| } |
| |
| delete currentFile; |
| delete oldFile; |
| delete transFile; |
| } |
| |
| Stats beforeFilterStats = xliff->GetStats(config); |
| |
| // run through the XLIFFFile and strip out TransUnits that have identical |
| // old and current source values and are not in the reject list, or just |
| // old values and no source values |
| xliff->Filter(keep_this_trans_unit, (void*)&setting); |
| |
| Stats afterFilterStats = xliff->GetStats(config); |
| afterFilterStats.totalStrings = beforeFilterStats.totalStrings; |
| |
| // add the reject comments |
| for (vector<Reject>::const_iterator reject = setting.reject.begin(); |
| reject != setting.reject.end(); reject++) { |
| TransUnit* tu = xliff->EditTransUnit(reject->file, reject->name); |
| tu->rejectComment = reject->comment; |
| } |
| |
| // config-locale-current_cl.xliff |
| stringstream filename; |
| if (outDir != "") { |
| filename << outDir << '/'; |
| } |
| filename << config << '-' << targetLocale << '-' << setting.currentVersion << ".xliff"; |
| xliffs.push_back(pair<string,XLIFFFile*>(filename.str(), xliff)); |
| |
| stats.push_back(afterFilterStats); |
| } |
| |
| // today is a good day to die |
| if (!success || SourcePos::HasErrors()) { |
| return 1; |
| } |
| |
| // write the XLIFF files |
| printf("\nWriting %zd file%s...\n", xliffs.size(), xliffs.size() == 1 ? "" : "s"); |
| for (vector<pair<string,XLIFFFile*> >::iterator it = xliffs.begin(); it != xliffs.end(); it++) { |
| const string& filename = it->first; |
| XLIFFFile* xliff = it->second; |
| string text = xliff->ToString(); |
| write_to_file(filename, text); |
| } |
| |
| // the stats |
| printf("\n" |
| " to without total\n" |
| " config files translate comments strings\n" |
| "-----------------------------------------------------------------------\n"); |
| Stats totals; |
| totals.config = "total"; |
| totals.files = 0; |
| totals.toBeTranslated = 0; |
| totals.noComments = 0; |
| totals.totalStrings = 0; |
| for (vector<Stats>::iterator it=stats.begin(); it!=stats.end(); it++) { |
| string cfg = it->config; |
| if (cfg.length() > 20) { |
| cfg.resize(20); |
| } |
| printf(" %-20s %-9zd %-9zd %-9zd %-19zd\n", cfg.c_str(), it->files, |
| it->toBeTranslated, it->noComments, it->totalStrings); |
| totals.files += it->files; |
| totals.toBeTranslated += it->toBeTranslated; |
| totals.noComments += it->noComments; |
| totals.totalStrings += it->totalStrings; |
| } |
| if (stats.size() > 1) { |
| printf("-----------------------------------------------------------------------\n" |
| " %-20s %-9zd %-9zd %-9zd %-19zd\n", totals.config.c_str(), totals.files, |
| totals.toBeTranslated, totals.noComments, totals.totalStrings); |
| } |
| printf("\n"); |
| return 0; |
| } |
| |
| struct PseudolocalizeSettings { |
| XLIFFFile* xliff; |
| bool expand; |
| }; |
| |
| |
| string |
| pseudolocalize_string(const string& source, const PseudolocalizeSettings* settings) |
| { |
| return pseudolocalize_string(source); |
| } |
| |
| static XMLNode* |
| pseudolocalize_xml_node(const XMLNode* source, const PseudolocalizeSettings* settings) |
| { |
| if (source->Type() == XMLNode::TEXT) { |
| return XMLNode::NewText(source->Position(), pseudolocalize_string(source->Text(), settings), |
| source->Pretty()); |
| } else { |
| XMLNode* target; |
| if (source->Namespace() == XLIFF_XMLNS && source->Name() == "g") { |
| // XXX don't translate these |
| target = XMLNode::NewElement(source->Position(), source->Namespace(), |
| source->Name(), source->Attributes(), source->Pretty()); |
| } else { |
| target = XMLNode::NewElement(source->Position(), source->Namespace(), |
| source->Name(), source->Attributes(), source->Pretty()); |
| } |
| |
| const vector<XMLNode*>& children = source->Children(); |
| const size_t I = children.size(); |
| for (size_t i=0; i<I; i++) { |
| target->EditChildren().push_back(pseudolocalize_xml_node(children[i], settings)); |
| } |
| |
| return target; |
| } |
| } |
| |
| void |
| pseudolocalize_trans_unit(const string&file, TransUnit* unit, void* cookie) |
| { |
| const PseudolocalizeSettings* settings = (PseudolocalizeSettings*)cookie; |
| |
| const StringResource& source = unit->source; |
| StringResource* target = &unit->target; |
| *target = source; |
| |
| target->config = settings->xliff->TargetConfig(); |
| |
| delete target->value; |
| target->value = pseudolocalize_xml_node(source.value, settings); |
| } |
| |
| int |
| pseudolocalize_xliff(XLIFFFile* xliff, bool expand) |
| { |
| PseudolocalizeSettings settings; |
| |
| settings.xliff = xliff; |
| settings.expand = expand; |
| xliff->Map(pseudolocalize_trans_unit, &settings); |
| return 0; |
| } |
| |
| static int |
| do_pseudo(const string& infile, const string& outfile, bool expand) |
| { |
| int err; |
| |
| XLIFFFile* xliff = XLIFFFile::Parse(infile); |
| if (xliff == NULL) { |
| return 1; |
| } |
| |
| pseudolocalize_xliff(xliff, expand); |
| |
| err = write_to_file(outfile, xliff->ToString()); |
| |
| delete xliff; |
| |
| return err; |
| } |
| |
| void |
| log_printf(const char *fmt, ...) |
| { |
| int ret; |
| va_list ap; |
| |
| if (g_logFile != NULL) { |
| va_start(ap, fmt); |
| ret = vfprintf(g_logFile, fmt, ap); |
| va_end(ap); |
| fflush(g_logFile); |
| } |
| } |
| |
| void |
| close_log_file() |
| { |
| if (g_logFile != NULL) { |
| fclose(g_logFile); |
| } |
| } |
| |
| void |
| open_log_file(const char* file) |
| { |
| g_logFile = fopen(file, "w"); |
| printf("log file: %s -- %p\n", file, g_logFile); |
| atexit(close_log_file); |
| } |
| |
| static int |
| usage() |
| { |
| fprintf(stderr, |
| "usage: localize export OPTIONS CONFIGS...\n" |
| " REQUIRED OPTIONS\n" |
| " --settings SETTINGS The settings file to use. See CONFIGS below.\n" |
| " --root TREE_ROOT The location in Perforce of the files. e.g. //device\n" |
| " --target LOCALE The target locale. See LOCALES below.\n" |
| "\n" |
| " OPTIONAL OPTIONS\n" |
| " --out DIR Directory to put the output files. Defaults to the\n" |
| " current directory if not supplied. Files are\n" |
| " named as follows:\n" |
| " CONFIG-LOCALE-CURRENT_CL.xliff\n" |
| "\n" |
| "\n" |
| "usage: localize import XLIFF_FILE...\n" |
| "\n" |
| "Import a translated XLIFF file back into the tree.\n" |
| "\n" |
| "\n" |
| "usage: localize xlb XMB_FILE VALUES_FILES...\n" |
| "\n" |
| "Read resource files from the tree file and write the corresponding XLB file\n" |
| "\n" |
| "Supply all of the android resource files (values files) to export after that.\n" |
| "\n" |
| "\n" |
| "\n" |
| "CONFIGS\n" |
| "\n" |
| "LOCALES\n" |
| "Locales are specified in the form en_US They will be processed correctly\n" |
| "to locate the resouce files in the tree.\n" |
| "\n" |
| "\n" |
| "usage: localize pseudo OPTIONS INFILE [OUTFILE]\n" |
| " OPTIONAL OPTIONS\n" |
| " --big Pad strings so they get longer.\n" |
| "\n" |
| "Read INFILE, an XLIFF file, and output a pseudotranslated version of that file. If\n" |
| "OUTFILE is specified, the results are written there; otherwise, the results are\n" |
| "written back to INFILE.\n" |
| "\n" |
| "\n" |
| "usage: localize rescheck FILES...\n" |
| "\n" |
| "Reads the base strings and prints warnings about bad resources from the given files.\n" |
| "\n"); |
| return 1; |
| } |
| |
| int |
| main(int argc, const char** argv) |
| { |
| //open_log_file("log.txt"); |
| //g_logFile = stdout; |
| |
| if (argc == 2 && 0 == strcmp(argv[1], "--test")) { |
| return test(); |
| } |
| |
| if (argc < 2) { |
| return usage(); |
| } |
| |
| int index = 1; |
| |
| if (0 == strcmp("export", argv[index])) { |
| string settingsFile; |
| string rootDir; |
| string outDir; |
| string baseLocale = "en"; |
| string targetLocale; |
| string language, region; |
| vector<string> configs; |
| |
| index++; |
| while (index < argc) { |
| if (0 == strcmp("--settings", argv[index])) { |
| settingsFile = argv[index+1]; |
| index += 2; |
| } |
| else if (0 == strcmp("--root", argv[index])) { |
| rootDir = argv[index+1]; |
| index += 2; |
| } |
| else if (0 == strcmp("--out", argv[index])) { |
| outDir = argv[index+1]; |
| index += 2; |
| } |
| else if (0 == strcmp("--target", argv[index])) { |
| targetLocale = argv[index+1]; |
| index += 2; |
| } |
| else if (argv[index][0] == '-') { |
| fprintf(stderr, "unknown argument %s\n", argv[index]); |
| return usage(); |
| } |
| else { |
| break; |
| } |
| } |
| for (; index<argc; index++) { |
| configs.push_back(argv[index]); |
| } |
| |
| if (settingsFile == "" || rootDir == "" || configs.size() == 0 || targetLocale == "") { |
| return usage(); |
| } |
| if (!split_locale(targetLocale, &language, ®ion)) { |
| fprintf(stderr, "illegal --target locale: '%s'\n", targetLocale.c_str()); |
| return usage(); |
| } |
| |
| |
| return do_export(settingsFile, rootDir, outDir, targetLocale, configs); |
| } |
| else if (0 == strcmp("import", argv[index])) { |
| vector<string> xliffFilenames; |
| |
| index++; |
| for (; index<argc; index++) { |
| xliffFilenames.push_back(argv[index]); |
| } |
| |
| return do_merge(xliffFilenames); |
| } |
| else if (0 == strcmp("xlb", argv[index])) { |
| string outfile; |
| vector<string> resFiles; |
| |
| index++; |
| if (argc < index+1) { |
| return usage(); |
| } |
| |
| outfile = argv[index]; |
| |
| index++; |
| for (; index<argc; index++) { |
| resFiles.push_back(argv[index]); |
| } |
| |
| return do_xlb_export(outfile, resFiles); |
| } |
| else if (0 == strcmp("pseudo", argv[index])) { |
| string infile; |
| string outfile; |
| bool big = false; |
| |
| index++; |
| while (index < argc) { |
| if (0 == strcmp("--big", argv[index])) { |
| big = true; |
| index += 1; |
| } |
| else if (argv[index][0] == '-') { |
| fprintf(stderr, "unknown argument %s\n", argv[index]); |
| return usage(); |
| } |
| else { |
| break; |
| } |
| } |
| |
| if (index == argc-1) { |
| infile = argv[index]; |
| outfile = argv[index]; |
| } |
| else if (index == argc-2) { |
| infile = argv[index]; |
| outfile = argv[index+1]; |
| } |
| else { |
| fprintf(stderr, "unknown argument %s\n", argv[index]); |
| return usage(); |
| } |
| |
| return do_pseudo(infile, outfile, big); |
| } |
| else if (0 == strcmp("rescheck", argv[index])) { |
| vector<string> files; |
| |
| index++; |
| while (index < argc) { |
| if (argv[index][0] == '-') { |
| fprintf(stderr, "unknown argument %s\n", argv[index]); |
| return usage(); |
| } |
| else { |
| break; |
| } |
| } |
| for (; index<argc; index++) { |
| files.push_back(argv[index]); |
| } |
| |
| if (files.size() == 0) { |
| return usage(); |
| } |
| |
| return do_rescheck(files); |
| } |
| else { |
| return usage(); |
| } |
| |
| if (SourcePos::HasErrors()) { |
| SourcePos::PrintErrors(stderr); |
| return 1; |
| } |
| |
| return 0; |
| } |
| |