/* * POConverter.m * * Created by Da Woon Jung on Wed March 09 2005. * Copyright (c) 2005 dwj. All rights reserved. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #import #define POCONV_DEFAULT_STRINGS_FILENAME @"po.strings" #define POCONV_DEBUG_LEVEL 1 BOOL gIgnoreFuzzy; /*! -n cmdline arg, YES = ignore fuzzy entries */ int gDebugLevel; /*! -v cmdline arg, 0 = no console messages */ NSAutoreleasePool *gPool; NSCharacterSet *gNewLineSet; NSCharacterSet *gDelimitSet; /*! Returns NO if line is a comment. range and result are modified. */ static BOOL readLine(NSString *string, NSRange *range, NSString **outString) { // Using -lineRangeForRange: because it reliably detects all sorts of line endings NSRange lineRange = [string lineRangeForRange: *range]; NSString *lineString = (NSNotFound != lineRange.location && lineRange.length > 0) ? [string substringWithRange: lineRange] : nil; range->location = NSMaxRange(lineRange); range->length = 0; *outString = lineString; // Detect comment lines return lineString ? ![lineString hasPrefix: @"#"] : YES; } /*! Scans data (from a po file) in ascii mode and gets the charset specification */ static NSStringEncoding getCharset(NSData *data) { NSString *string = [[[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding] autorelease]; if (nil == string) return 0; CFStringEncoding enc = kCFStringEncodingUTF8; NSString *line = nil; NSString *encName = nil; NSRange encRange; NSRange range = NSMakeRange(0, 0); BOOL notComment = YES; while (YES) { notComment = readLine(string, &range, &line); if (nil == line) break; if (notComment) { encRange = [line rangeOfString: @"Content-Type:"]; if (NSNotFound != encRange.location && encRange.length > 0) { encRange = [line rangeOfString: @"charset="]; if (NSNotFound != encRange.location && encRange.length > 0) { encName = [line substringWithRange: NSMakeRange(NSMaxRange(encRange), [line length] - NSMaxRange(encRange))]; if (encName) { encRange = [encName rangeOfString:@"\\n\"" options:NSBackwardsSearch]; if (NSNotFound != encRange.location && encRange.location > 0 && encRange.length > 0) encName = [encName substringToIndex: encRange.location]; // Some special cases if (NSOrderedSame == [encName caseInsensitiveCompare:@"gbk"]) enc = kCFStringEncodingGBK_95; else if (NSOrderedSame == [encName caseInsensitiveCompare:@"ISO_8859-15"]) enc = kCFStringEncodingISOLatin9; else enc = CFStringConvertIANACharSetNameToEncoding((CFStringRef)encName); } if (gDebugLevel) CFShow(line); break; } } } } return (kCFStringEncodingInvalidId == enc) ? NSUTF8StringEncoding : CFStringConvertEncodingToNSStringEncoding(enc); } static NSString *extractStringFromLine(NSString *line) { NSMutableString *result = [NSMutableString stringWithCapacity: [line length]]; NSString *aString; NSScanner *scanner = [NSScanner scannerWithString: line]; NSRange escRange; NSMutableString *escString = [NSMutableString string]; while (![scanner isAtEnd]) { aString = nil; [scanner scanUpToString:@"\"" intoString: nil]; if ([scanner scanString:@"\"" intoString: nil]) { while (YES) { // Make sure we don't skip leading spaces (but skip newlines) NSCharacterSet *origSkip = [scanner charactersToBeSkipped]; [scanner setCharactersToBeSkipped: gNewLineSet]; [scanner scanUpToCharactersFromSet: gDelimitSet intoString: &aString]; // Remove shorcut key ampersands // TODO: Recognize longer "(&x)" format used in // non-roman localizations while ([scanner scanString: @"&" intoString: nil]) { if (aString) [escString appendString: aString]; [scanner scanUpToCharactersFromSet: gDelimitSet intoString: &aString]; } // [scanner scanUpToString:@"\"" intoString: &aString]; [scanner setCharactersToBeSkipped: origSkip]; if (aString) { [escString appendString: aString]; escRange = [aString rangeOfString:@"\\" options:(NSBackwardsSearch|NSAnchoredSearch)]; if (NSNotFound != escRange.location && escRange.length > 0) { [scanner scanString:@"\"" intoString: &aString]; [escString appendString: aString]; aString = nil; continue; } } aString = nil; break; } if ([scanner scanString:@"\"" intoString: nil]) { if (escString) [result appendString: escString]; } } else break; } return result; } /*! Get all parts of a quoted string (possibly broken across multiple lines) and returns the concatenated result. */ static NSString *getQuotedString(NSString *fileString, NSRange *range, NSString *line) { NSMutableString *result = [NSMutableString stringWithString:extractStringFromLine(line)]; NSString *aString; NSRange origRange; // Loop until no more string parts found BOOL notComment = YES; while (YES) { origRange = *range; notComment = readLine(fileString, range, &line); if (nil == line || ([line length] == 0)) break; if (!notComment || [line hasPrefix: @"msgid"] || [line hasPrefix: @"msgstr"]) { // Back up the cursor *range = origRange; break; } aString = extractStringFromLine(line); [result appendString: aString]; } return ([result length] > 0) ? result : nil; } /*! Takes any comment line. If it is a flag line (starts with #, ) and contains the "fuzzy" flag, returns YES */ static BOOL isFuzzy(NSString *line) { NSRange flagRange = [line rangeOfString:@"#," options:NSAnchoredSearch]; if (NSNotFound != flagRange.location && flagRange.length > 0) { // Parse comma-delimited list of flags NSString *aString = [line substringWithRange: NSMakeRange(NSMaxRange(flagRange), [line length] - NSMaxRange(flagRange))]; if (aString) { NSArray *flags; NSEnumerator *flagEnum; NSScanner *scanner; flags = [aString componentsSeparatedByString: @","]; flagEnum = [flags objectEnumerator]; while ((aString = [flagEnum nextObject])) { scanner = [NSScanner scannerWithString: aString]; if ([scanner scanString:@"fuzzy" intoString: nil]) { // Check if fuzzy is the entire word if ([scanner isAtEnd]) return YES; } } } } return NO; } #ifdef TARGET_CELESTIA /*! Celestia: Returns YES if the line contains source file entries */ static BOOL isSource(NSString *line, BOOL *containsNonKde) { BOOL isSource = NO; NSRange range = [line rangeOfString:@"#:" options:NSAnchoredSearch]; if (NSNotFound != range.location && range.length > 0) { BOOL nonKdeFound = NO; BOOL kdeFound = NO; isSource = YES; NSString *aString = [line substringWithRange: NSMakeRange(NSMaxRange(range), [line length] - NSMaxRange(range))]; if (aString) { NSArray *sources; NSEnumerator *sourceEnum; sources = [aString componentsSeparatedByString: @" "]; sourceEnum = [sources objectEnumerator]; while ((aString = [sourceEnum nextObject])) { if (0 == [aString length]) continue; range = [aString rangeOfString: @"kde"]; if (NSNotFound == range.location) { nonKdeFound = YES; break; } else if (range.length > 0) kdeFound = YES; } } if (nonKdeFound || !kdeFound) *containsNonKde = YES; } return isSource; } #endif TARGET_CELESTIA /*! Does all the work of reading, converting, and writing. Returns the number of lines successfully converted. */ static long convertData(NSData *data, NSStringEncoding encoding, NSFileHandle *ofh) { long numConverted = 0; NSString *fileString = [[[NSString alloc] initWithData:data encoding:encoding] autorelease]; if (nil == fileString) return 0; BOOL notComment = YES; NSString *line; NSString **aString; NSRange range = NSMakeRange(0, 0); long fileStringLength = [fileString length]; BOOL acceptEntry = YES; #ifdef TARGET_CELESTIA BOOL parsingSources = NO; BOOL containsNonKde = NO; #endif NSString *msgid = nil; NSString *msgstr = nil; while (range.location < fileStringLength) { notComment = readLine(fileString, &range, &line); if (nil == line) break; if (notComment) { aString = nil; if ([line hasPrefix: @"msgid"]) aString = &msgid; else if ([line hasPrefix: @"msgstr"]) { if (nil == msgid) { if (gDebugLevel>2) CFShow(@"Ignoring msgstr without msgid (probably the po header)"); continue; } aString = &msgstr; } else { // Blank line cancels fuzzy flag acceptEntry = YES; } #ifdef TARGET_CELESTIA // if (parsingSources) // acceptEntry = containsNonKde; acceptEntry = YES; parsingSources = NO; containsNonKde = NO; #endif if (aString) { *aString = getQuotedString(fileString, &range, line); if (*aString == msgstr && msgid) { if (nil == *aString) { if (gDebugLevel>1) CFShow((CFStringRef)[NSString stringWithFormat:@"Ignoring msgid \"%@\" without msgstr", msgid]); continue; } if (acceptEntry) { NSString *ofhString = [NSString stringWithFormat:@"\"%@\" = \"%@\";\n", msgid, msgstr]; if (ofhString) { NSData *ofhData; if (numConverted > 0) { size_t ofhBufSize = [ofhString length]*sizeof(unichar); unichar *ofhBuf = malloc(ofhBufSize); [ofhString getCharacters: ofhBuf]; // gets utf-16 chars ofhData = [NSData dataWithBytesNoCopy:ofhBuf length:ofhBufSize]; } else { // Before overwriting an existing file, have to zero it if (ofh) [ofh truncateFileAtOffset: 0]; // Output bom for first entry ofhData = [ofhString dataUsingEncoding:NSUnicodeStringEncoding]; } [ofh writeData:ofhData]; } else if (gDebugLevel) CFShow(@"***Couldn't write data***"); ++numConverted; if (gDebugLevel>2 && *aString) CFShow(*aString); } else if (gDebugLevel) CFShow((CFStringRef)[NSString stringWithFormat:@"Ignoring entry \"%@\"", *aString]); msgid = nil; msgstr = nil; acceptEntry = YES; } } } else { #ifdef TARGET_CELESTIA // Celestia: filter out kde entries // This looks complicated, but is needed because // source entries (#:) may span multiple lines if (!parsingSources) { containsNonKde = NO; parsingSources = isSource(line, &containsNonKde); } else { BOOL tempKdeCheck = NO; if (isSource(line, &tempKdeCheck)) containsNonKde = containsNonKde || tempKdeCheck; } #endif // Filter out fuzzy entries if specified by the caller if (gIgnoreFuzzy && isFuzzy(line)) acceptEntry = NO; } } return numConverted; } static void usage() { printf("Poconverter converts a GNU gettext po file to Mac OS-native\n" "UTF-16 Localized.strings format.\n" "\n" "Usage: Poconverter [-nv] file.po [destination filename]\n" "\n" " -n Ignore entries tagged with fuzzy flag\n" " -s Silent mode (existing file overwritten without warning)\n" " -v Verbose output on stderr\n" "\n"); } static void ctrl_c_handler(int sig) { fprintf(stderr, "\nGot Ctrl-C, cleaning up...\n"); [gPool release]; exit(1); } int main(int argc, const char **argv) { NSAutoreleasePool *gPool = [[NSAutoreleasePool alloc] init]; // Catch ctrl-c so we can release the autorelease pool signal(SIGINT, ctrl_c_handler); gIgnoreFuzzy = NO; gDebugLevel = 0; BOOL isSilent = NO; BOOL isErr = NO; NSProcessInfo *theProcess = [NSProcessInfo processInfo]; NSArray *args = [theProcess arguments]; NSString *arg; NSEnumerator *argEnum = [args objectEnumerator]; [argEnum nextObject]; // advance past args[0] (the program full path) NSString *poFilename = nil; NSString *resultFilename = nil; NSFileHandle *ofh; do { if (argc < 2 || argc > 5) { usage(); break; } // Parse options long j; while ((arg = [argEnum nextObject]) && [arg hasPrefix: @"-"]) { if ([arg length] > 1) { for (j = 1; j < [arg length]; ++j) { switch ([arg characterAtIndex: j]) { case 'n': gIgnoreFuzzy = YES; break; case 's': isSilent = YES; break; case 'v': gDebugLevel = POCONV_DEBUG_LEVEL; break; default: isErr = YES; } if (isErr) break; } } else { isErr = YES; break; } } if (isErr) { usage(); break; } // Can't just have options, need filename(s) too if (nil == arg)//i == argc) { usage(); break; } poFilename = arg; // Check if po file exists and isn't a directory BOOL isDirectory; BOOL fileExists; NSFileManager *fm = [NSFileManager defaultManager]; if (![fm fileExistsAtPath:poFilename isDirectory:&isDirectory] || isDirectory) { usage(); break; } // Destination path - can be either a filename or a directory // If unspecified, defaults to stdout resultFilename = [argEnum nextObject]; if (resultFilename) { if ([argEnum nextObject]) { // Too many filenames usage(); break; } // If directory, append default filename fileExists = [fm fileExistsAtPath:resultFilename isDirectory:&isDirectory]; if (fileExists && isDirectory) { resultFilename = [resultFilename stringByAppendingPathComponent:POCONV_DEFAULT_STRINGS_FILENAME]; fileExists = [fm fileExistsAtPath: resultFilename]; } if (!fileExists) { [fm createFileAtPath:resultFilename contents:nil attributes:nil]; } if(!isSilent && fileExists) { printf("\nDestination file exists, overwrite (y/n)? [y] "); int confirm = getchar(); fflush(stdout); switch (confirm) { case 'y': case 'Y': case '\n': break; default: printf("Destination file not overwritten.\n\n"); [gPool release]; exit(0); } } ofh = [NSFileHandle fileHandleForWritingAtPath: resultFilename]; // Delay truncating file and wiping it out until we actually // get something to write to } else { // Output filename not specified, use stdout ofh = [NSFileHandle fileHandleWithStandardOutput]; } if (nil == ofh) { CFShow(@"***Could not write output. Please check if directory exists, and you have permissions.***"); [gPool release]; exit(1); } if (gDebugLevel) CFShow((CFStringRef)[NSString stringWithFormat:@"poFile='%@', result='%@', ignoreFuzzy=%d, debugLevel=%d, silent=%d", poFilename, (resultFilename ? resultFilename : @"stdout"), gIgnoreFuzzy, gDebugLevel, isSilent]); NSData *data = [NSData dataWithContentsOfMappedFile: poFilename]; NSMutableCharacterSet *newLineSet = [[[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy] autorelease]; [newLineSet formIntersectionWithCharacterSet: [[NSCharacterSet whitespaceCharacterSet] invertedSet]]; gNewLineSet = newLineSet; gDelimitSet = [NSCharacterSet characterSetWithCharactersInString: @"&\""]; // First detect the file encoding NSStringEncoding encoding = getCharset(data); if (gDebugLevel) CFShow((CFStringRef)[NSString stringWithFormat:@"NSStringEncoding = %#lx", encoding]); // Reread the file again, in the correct encoding long numConverted = 0; if (encoding) { numConverted = convertData(data, encoding, ofh); } if (gDebugLevel) CFShow((CFStringRef)[NSString stringWithFormat:@"%ld entries converted.", numConverted]); } while (NO); [gPool release]; return 0; }