/* StringsFile Copyright (C) 2002 Free Software Foundation, Inc. Written by: Alexander Malmberg Created: 2002 This file is part of the GNUstep Project 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 3 of the License, or (at your option) any later version. You should have received a copy of the GNU General Public License along with this program; see the file COPYINGv3. If not, write to the Free Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #import "common.h" #import "Foundation/NSString.h" #import "Foundation/NSArray.h" #import "Foundation/NSFileManager.h" #import "Foundation/NSDate.h" #include "StringsFile.h" #include "StringsEntry.h" #include "SourceEntry.h" #include "make_strings.h" static NSString *parse_string(NSString **ptr) { NSString *str = *ptr; NSString *ret; int i,c; unichar ch; c = [str length]; for (i = 0; i < c; i++) { ch = [str characterAtIndex: i]; if (ch == '\\') { if (i == c) { fprintf(stderr,"parse error, \\ without second character\n"); exit(1); } i++; } if (ch == '\"') break; } if (i == c) { fprintf(stderr,"parse error, unterminated string\n"); exit(1); } ret = [str substringToIndex: i]; str = [str substringFromIndex: i+1]; *ptr = str; return ret; } #define DUMMY @"" @implementation StringsFile - (BOOL) isTranslated: (NSString *)key { return [keys_translated containsObject: key]; } - (void) addTranslated: (NSString *)key { if (![self isTranslated: key]) [keys_translated addObject: key]; } - (BOOL) isMatched: (NSString *)key { return [keys_matched containsObject: key]; } - (void) addMatched: (NSString *)key { if (![self isMatched: key]) [keys_matched addObject: key]; } - init { self = [super init]; strings = [[NSMutableArray alloc] init]; keys_translated = [[NSMutableArray alloc] init]; keys_matched = [[NSMutableArray alloc] init]; return self; } - (void) dealloc { DESTROY(global_comment); DESTROY(strings); [super dealloc]; } - initWithFile: (NSString *)filename { NSString *str; self = [self init]; str = [NSString stringWithContentsOfFile: filename]; if (!str) return self; { StringsEntry *se = nil; NSMutableArray *update_list = [[NSMutableArray alloc] init]; NSArray *lines; NSString *l; NSUInteger i,c,pos; NSMutableDictionary *dummy_entries = [[NSMutableDictionary alloc] init]; NSMutableString *user_comment = [[NSMutableString alloc] init]; NSString *key,*trans; /* this is a bit yucky, but it works */ lines = [str componentsSeparatedByString: @"/*"]; c = [lines count]; for (i = 0; i < c; i++) { l = [lines objectAtIndex: i]; /* First entry has everything before the first comment and needs to be handled specially. */ if (i) { /* Parse special comments. */ if ([l hasPrefix: @"**"]) { /* It's one of our banners, just ignore it. If it's the first one we put all comments so far in the global user comment. */ if (user_comment) { if (![user_comment isEqual: @""]) global_comment = [user_comment copy]; DESTROY(user_comment); } } else if ([l hasPrefix: @" File: "]) { se = [[StringsEntry alloc] init]; [se addFlag: FLAG_UNMATCHED]; /* TODO: ? */ [update_list addObject: se]; [se release]; l = [l substringFromIndex: 7]; pos = [l rangeOfString: @":"].location; [se setFile: [l substringToIndex: pos]]; l = [l substringFromIndex: pos+1]; [se setLine: [l intValue]]; } else if ([l hasPrefix: @" Flag: untranslated */"]) [se addFlag: FLAG_UNTRANSLATED]; else if ([l hasPrefix: @" Flag: unmatched */"]) [se addFlag: FLAG_UNMATCHED]; /* this is essentially a noop */ else if ([l hasPrefix: @" Comment: "]) { l = [l substringFromIndex: 10]; pos = [l rangeOfString: @" */"].location; if (pos == NSNotFound) continue; [se setComment: [l substringToIndex: pos]]; } else { pos = [l rangeOfString: @"*/"].location; if (pos == NSNotFound) { fprintf(stderr,"parse error in '%s', missing '*\'\n", [filename cString]); pos = [l length]; } if ([user_comment length]) [user_comment appendString: @"\n"]; [user_comment appendString: [l substringToIndex: pos]]; } pos = [l rangeOfString: @"*/"].location; if (pos == NSNotFound) continue; l = [l substringFromIndex: pos+2]; } while (1) { pos = [l rangeOfString: @"\""].location; if (pos == NSNotFound) break; l = [l substringFromIndex: pos+1]; key = parse_string(&l); pos = [l rangeOfString: @"="].location; if (pos == NSNotFound) { fprintf(stderr,"parse error in '%s', expecting '='\n", [filename cString]); exit(1); } l = [l substringFromIndex: pos+1]; pos = [l rangeOfString: @"\""].location; if (pos == NSNotFound) { fprintf(stderr,"parse error in '%s', expecting second string\n", [filename cString]); exit(1); } l = [l substringFromIndex: pos+1]; trans = parse_string(&l); pos = [l rangeOfString: @";"].location; if (pos == NSNotFound) { fprintf(stderr,"parse error in '%s', expecting ';'\n", [filename cString]); exit(1); } l = [l substringFromIndex: pos+1]; if (![update_list count]) { /* we're probably parsing a file not created by us */ if (![dummy_entries objectForKey: key]) { se = [[StringsEntry alloc] init]; [se setFile: DUMMY]; [se setFlags: FLAG_UNMATCHED]; [update_list addObject: se]; [se release]; [dummy_entries setObject: se forKey: key]; } } [update_list makeObjectsPerformSelector: @selector(setKey:) withObject: key]; [update_list makeObjectsPerformSelector: @selector(setTranslated:) withObject: trans]; { int i,c = [update_list count]; for (i = 0; i < c; i++) { // printf("%4i : %@\n",i,[update_list objectAtIndex: i]); se = [update_list objectAtIndex: i]; if (!([se flags] & FLAG_UNTRANSLATED)) { [self addTranslated: key]; break; } } } [strings addObjectsFromArray: update_list]; [update_list removeAllObjects]; se = nil; } } DESTROY(user_comment); DESTROY(dummy_entries); DESTROY(update_list); } return self; } - (void) _writeTo: (NSMutableString *)str entryHead: (StringsEntry *)se { if ([se file]) { [str appendString: @"/* File: "]; [str appendString: [se file]]; [str appendString: [NSString stringWithFormat: @":%i */\n",[se line]]]; } if ([se comment]) { [str appendString: @"/* Comment: "]; [str appendString: [se comment]]; [str appendString: @" */\n"]; } } - (void) _writeTo: (NSMutableString *)str entryFlags: (StringsEntry *)se { int flags = [se flags]; if (!flags) return; if (flags & FLAG_UNMATCHED) [str appendString: @"/* Flag: unmatched */\n"]; else if (flags & FLAG_UNTRANSLATED) [str appendString: @"/* Flag: untranslated */\n"]; else { fprintf(stderr,"unknown flag %08x\n",flags); } } - (void) _writeTo: (NSMutableString *)str entryKey: (StringsEntry *)se { [str appendString: @"\""]; [str appendString: [se key]]; /* Try to write a nice readable output ... key and value with whitrespace * around the '=' charactger, or wrap to a new line with a two space indent. */ if ([[se key] length] + [[se translated] length] < 70) [str appendString: @"\" = \""]; else [str appendString: @"\"\n = \""]; [str appendString: [se translated]]; [str appendString: @"\";\n"]; } - (void) _writeTo: (NSMutableString *)str manyEntries: (NSMutableArray *)list { int i,c; StringsEntry *tr,*cur; [list sortUsingSelector: @selector(compareFileLine:)]; c = [list count]; if (!c) return; cur = tr = nil; for (i = 0; i < c; i++) { cur = [list objectAtIndex: i]; [self _writeTo: str entryHead: cur]; if ([cur flags]) [self _writeTo: str entryFlags: cur]; if (!([cur flags] & FLAG_UNTRANSLATED)) tr = cur; } if (tr) [self _writeTo: str entryKey: tr]; else [self _writeTo: str entryKey: cur]; } - (BOOL) _shouldIgnore: (StringsEntry *)se { if (([se flags] & (FLAG_UNMATCHED|FLAG_UNTRANSLATED)) == (FLAG_UNMATCHED|FLAG_UNTRANSLATED)) return YES; if (aggressive_import && [[se file] isEqual: DUMMY] && [self isMatched: [se key]]) return YES; if (aggressive_remove && ([se flags] & FLAG_UNMATCHED) && [self isMatched: [se key]]) return YES; return NO; } - (BOOL) writeToFile: (NSString *)filename { unsigned int i,c; BOOL result; NSMutableString *str = [[NSMutableString alloc] initWithCapacity: 32*1024]; StringsEntry *se; NSMutableArray *strings_left = [strings mutableCopy]; NSMutableArray *str_list = [[NSMutableArray alloc] init]; NSMutableArray *dup_list = [[NSMutableArray alloc] init]; NSMutableArray *un_list = [[NSMutableArray alloc] init]; StringsEntry *cur,*c2; int single_file,wrote_banner,unflags; int un_count = 0; if (global_comment && ![global_comment isEqual: @""]) { [str appendString: @"/*"]; [str appendString: global_comment]; [str appendString: @"*/\n\n"]; } [str appendString: [NSString stringWithFormat: @"/***\n" @"%@\n" @"updated by make_strings %@\n" @"add comments above this one\n" @"***/\n", filename,[NSDate dateWithTimeIntervalSinceNow: 0]]]; wrote_banner = 0; /* First, output all keys that appear in multiple places (unless all appearances are in one file and none are marked unmatched or untranslated). Collect unmatched or untranslated single entries in un_list and matched translated (single/multiple in one file) entries in str_list. */ while ([strings_left count]) { cur = [strings_left objectAtIndex: 0]; if ([self _shouldIgnore: cur]) { [strings_left removeObjectAtIndex: 0]; continue; } [dup_list addObject: cur]; [strings_left removeObjectAtIndex: 0]; single_file = 1; unflags = [cur flags]; for (i = 0; i < [strings_left count]; i++) { c2 = [strings_left objectAtIndex: i]; if ([self _shouldIgnore: c2]) { [strings_left removeObjectAtIndex: i]; i--; continue; } if ([[cur key] isEqual: [c2 key]]) { unflags |= [c2 flags]; [dup_list addObject: c2]; [strings_left removeObjectAtIndex: i]; if (single_file) if (![[cur file] isEqual: [c2 file]]) single_file = 0; i--; } } if (single_file && !unflags) { [str_list addObjectsFromArray: dup_list]; [dup_list removeAllObjects]; continue; } if ([dup_list count] == 1) { [un_list addObjectsFromArray: dup_list]; [dup_list removeAllObjects]; continue; } if (unflags) un_count += [dup_list count]; if (!wrote_banner) { [str appendString: @"\n\n/*** Keys found in multiple places ***/\n"]; wrote_banner = 1; } [str appendString: @"\n"]; [self _writeTo: str manyEntries: dup_list]; [dup_list removeAllObjects]; } DESTROY(strings_left); /* Now output all single unmatched or untranslated entries. Order by line and file so key changes and movements are easy to spot and fix. */ if ([un_list count]) { [str appendString: @"\n\n/*** Unmatched/untranslated keys ***/\n"]; [un_list sortUsingSelector: @selector(compareFileLine:)]; c = [un_list count]; un_count += c; for (i = 0; i < c; i++) { se = [un_list objectAtIndex: i]; [str appendString: @"\n"]; [self _writeTo: str entryHead: se]; [self _writeTo: str entryFlags: se]; [self _writeTo: str entryKey: se]; } } /* Finally, output all matched and translated entries ordered by file. The translator should never have to touch these strings (unless there are typos or something). */ if ([str_list count]) { NSString *last_filename = nil; [str_list sortUsingSelector: @selector(compareFileKeyComment:)]; c = [str_list count]; for (i = 0; i < c; i++) { se = [str_list objectAtIndex: i]; if (!last_filename || ![last_filename isEqual: [se file]]) { last_filename = [se file]; [str appendString: [NSString stringWithFormat: @"\n\n/*** Strings from %@ ***/\n", last_filename]]; } [self _writeTo: str entryHead: se]; if (i == c - 1 || ![[se key] isEqual: [[str_list objectAtIndex: i + 1] key]]) [self _writeTo: str entryKey: se]; } } DESTROY(str_list); DESTROY(dup_list); { NSString *backupname = [filename stringByAppendingString: @"~"]; [[NSFileManager defaultManager] removeFileAtPath: backupname handler: nil]; [[NSFileManager defaultManager] movePath: filename toPath: backupname handler: nil]; result = [str writeToFile: filename atomically: YES]; if (!result) fprintf(stderr,"Error saving '%s'!\n",[filename cString]); } DESTROY(str); DESTROY(un_list); if (un_count) fprintf(stderr,"'%s': %i untranslated or unmatched messages\n", [filename cString],un_count); return result; } - (void) addSourceEntry: (SourceEntry *)e { /* First try to find a match among our unmatched strings. We consider two entries to match if they have the same key, file and comment. This could be extended, but the risk of errors increases. */ int i,c; StringsEntry *se; c = [strings count]; [self addMatched: [e key]]; /* Look for exact matches. If we find an exact match (same file, key, and comment) we mark the StringsEntry matched and don't add the SourceEntry. */ for (i = 0; i < c; i++) { se = [strings objectAtIndex: i]; if (!([se flags] & FLAG_UNMATCHED)) continue; if (![[se key] isEqual: [e key]]) continue; if (([se flags] & FLAG_UNMATCHED) && [[se file] isEqual: [e file]]) { if ((![se comment] && ![e comment]) || ([[se comment] isEqual: [e comment]])) { [se setFlags: [se flags] & ~FLAG_UNMATCHED]; [se setLine: [e line]]; return; } } } if (aggressive_match) { /* If aggressive match is enabled, we try to find an existing and translated StringsEntry. If we find we add a new StringsEntry from the SourceEntry with the same translation and marked as translated. */ for (i = 0; i < c; i++) { se = [strings objectAtIndex: i]; if ([se flags] & FLAG_UNTRANSLATED) continue; if (![[se key] isEqual: [e key]]) continue; { StringsEntry *se2 = [StringsEntry stringsEntryFromSourceEntry: e]; [se2 setFlags: 0]; [se2 setTranslated: [se translated]]; [strings addObject: se2]; return; } } } /* No match, add a new, untranslated StringsEntry. */ [strings addObject: [StringsEntry stringsEntryFromSourceEntry: e]]; } @end