Translate Text / GoogleTranslator.m

Full commit
//  GoogleTranslator.m
//  Translate Text
//  Created by Peter Hosey on 2008-06-22.
//  Copyright 2008 Peter Hosey. All rights reserved.

#import "GoogleTranslator.h"

#import "JSON/JSON.h"

#include <sys/sysctl.h>

@implementation GoogleTranslator

- (BOOL) canTranslateString:(NSString *)inputString error:(out NSError **)outError {
	//Google's Terms of Service limit translation inputs to 500 characters. Therefore, we reject any input string containing more than 500 characters.
	//We want to count characters, not UTF-16 code units. We do this by asking for what the length would be in UTF-32 without a BOM; that comes back in bytes, so we divide by 4 (bytes in 32 bits) to convert to characters.
	NSUInteger numCharacters = ([inputString lengthOfBytesUsingEncoding:NSUTF32BigEndianStringEncoding] / 4U);
	if (numCharacters > 500U) {
		NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
			NSLocalizedString(@"Input text too long", /*comment*/ @"Error message failure reasons"), NSLocalizedDescriptionKey,
			NSLocalizedString(@"Google refuses to translate text longer than 500 characters.", /*comment*/ @"Error message failure reasons"), NSLocalizedFailureReasonErrorKey,
			NSLocalizedString(@"You must cut down the text to have no more than 500 characters.", /*comment*/ @"Error message recovery suggestions"), NSLocalizedRecoverySuggestionErrorKey,
		*outError = [NSError errorWithDomain:TranslatorErrorDomain code:TranslatorErrorOriginalStringTooLong userInfo:userInfo];
		return NO;

	return YES;

- (NSString *) userAgentString {
	NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];

	NSString *appName = [infoDictionary objectForKey:(NSString *)kCFBundleNameKey];
	appName = [appName stringByReplacingOccurrencesOfString:@" " withString:@""];
	NSString *version = [infoDictionary objectForKey:@"CFBundleShortVersionString"];

	NSArray *components = [NSArray arrayWithObjects:

	NSString *processorArchitectureName = @"Unknown";
	int mib[] = { CTL_HW, HW_MACHINE };
	size_t miblen = sizeof(mib) / sizeof(mib[0]);
	char *buf = NULL;
	size_t buflen = 0U;

	if (sysctl(mib, miblen, buf, &buflen, /*newbuf*/ NULL, /*newlen*/ 0U) == 0U) {
		buf = malloc(buflen + 1U);
		if (buf) {
			if (sysctl(mib, miblen, buf, &buflen, /*newbuf*/ NULL, /*newlen*/ 0U) == 0U) {
				processorArchitectureName = [NSString stringWithUTF8String:buf];
	if ([processorArchitectureName isEqualToString:@"i386"])
		processorArchitectureName = @"Intel";
	else if ([processorArchitectureName isEqualToString:@"ppc"])
		processorArchitectureName = @"PPC";

	NSString *operatingSystemVersion = [[NSDictionary dictionaryWithContentsOfFile:@"/System/Library/CoreServices/SystemVersion.plist"] objectForKey:@"ProductVersion"];
	operatingSystemVersion = [operatingSystemVersion stringByReplacingOccurrencesOfString:@"." withString:@"_"];

	NSString *userLanguage = [[NSLocale preferredLanguages] objectAtIndex:0U];

	return [[components componentsJoinedByString:@"/"] stringByAppendingFormat:@" (Macintosh; N; %@ Mac OS X %@; %@)", processorArchitectureName, operatingSystemVersion, userLanguage];

- (NSString *) translateString:(NSString *)str
		  fromLanguageWithCode:(NSString *)fromLanguageCode
			toLanguageWithCode:(NSString *)toLanguageCode
		detectFromLanguageCode:(out NSString **)outFromLanguageCode
						 error:(out NSError **)outError
	if (!fromLanguageCode)
		fromLanguageCode = @"";
	if (![self canTranslateString:str error:outError])
		return nil;

	NSURL *URL = [NSURL URLWithString:[NSString stringWithFormat:
		[fromLanguageCode stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding],
		[toLanguageCode   stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding],
		[str              stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]

	NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
	[request setValue:@"" forHTTPHeaderField:@"Referer"];
	[request setValue:[self userAgentString] forHTTPHeaderField:@"User-Agent"];

	NSURLResponse *response = nil;
	NSData *JSONData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:outError];
	if (!JSONData)
		return nil;

	NSString *JSONResponse = [[[NSString alloc] initWithData:JSONData encoding:NSUTF8StringEncoding] autorelease];
	NSDictionary *responseDict = [JSONResponse JSONValue];
	NSNumber *statusNum = [responseDict objectForKey:@"responseStatus"];
	if (statusNum && [statusNum intValue] == 200) {
		NSDictionary *translationResult = [responseDict objectForKey:@"responseData"];

		NSString *detectedLanguage = [translationResult objectForKey:@"detectedSourceLanguage"];
		if (outFromLanguageCode && detectedLanguage)
			*outFromLanguageCode = [[detectedLanguage copy] autorelease];

		NSString *translatedString = [translationResult objectForKey:@"translatedText"];

		//That's entity-encoded, so we need to dereference the entity references.
		translatedString = [(NSString *)CFXMLCreateStringByUnescapingEntities(kCFAllocatorDefault, (CFStringRef)translatedString, /*entitiesDictionary*/ NULL) autorelease];

		return translatedString;
	} else {
		//We got an error!
		if (outError) {
			NSString *domain = TranslatorErrorDomain;
			NSUInteger code = TranslatorErrorUnknown;
			NSString *errorDesc = nil;
			NSString *failureReason = nil;
			NSString *recoverySuggestion = nil;

			//Check for various known cases.
			if ([[responseDict objectForKey:@"responseDetails"] isEqualToString:@"invalid translation language pair"]) {
				if ([fromLanguageCode length] == 0U) {
					code = TranslatorErrorAutomaticLanguageDetectionFailed;
					errorDesc = NSLocalizedString(@"Couldn't automatically detect input language", /*comment*/ @"Error message error descriptions");
					failureReason = NSLocalizedString(@"Input string probably not long enough", /*comment*/ @"Error message failure reasons");
					recoverySuggestion = NSLocalizedString(@"Try a longer input text.", /*comment*/ @"Error message recovery suggestions");
				} else {
					code = TranslatorErrorInvalidLanguage;
					errorDesc = NSLocalizedString(@"Can't translate from that language to that language", /*comment*/ @"Error message error descriptions");
					failureReason = NSLocalizedString(@"One or both languages is not supported by Google Language Tools", /*comment*/ @"Error message failure reasons");
					recoverySuggestion = NSLocalizedString(@"Google Language Tools only works with certain languages. For a current list of them, see <>.", /*comment*/ @"Error message recovery suggestions");

			//If any of these are missing, fill them in so we have *something* to display here.
			if (!errorDesc)
				errorDesc = NSLocalizedString(@"Unknown translation error", /*comment*/ @"Error message error descriptions");
			if (!recoverySuggestion)
				recoverySuggestion = NSLocalizedString(@"Please send email to the author of this program, making sure to include the input text, the language you were translating from, whether you were using auto-detection, the language you were translating to, and the means by which you began the translation (main window, script, or Services menu).", /*comment*/ @"Error message error descriptions");

			NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
				errorDesc, NSLocalizedDescriptionKey,
				recoverySuggestion, NSLocalizedRecoverySuggestionErrorKey,
				//This comes last because failureReason may be nil.
				failureReason, NSLocalizedFailureReasonErrorKey,

			*outError = [NSError errorWithDomain:domain code:code userInfo:userInfo];

		return nil;

- (NSString *) localizedTranslatorName {
	return NSLocalizedString(@"Google Language Tools", /*comment*/ @"Translator names");
- (NSURL *) translatorURL {
	return [NSURL URLWithString:@""];