Saturday, October 29, 2011

African App Store

You have to admit, the Apple App Store completely revolutionized how people download and install mobile applications on their phones. The 70-30 model also empowered developers allowing them to easily reach a global market, with developers receiving 70% of the sales while Apple kept the other 30%.

Google, Microsoft and Blackberry were quick to follow with their own mobile application market places. However, we still haven't seen hyperlocal app stores, profiling apps created by developers in your own community. All apps get thrown under categories, but not regions.

I believe there is huge opportunity to incorporate geo-location into the market place, celebrating a 'shop local' mentality. Imagine searching for apps based on your location, 'show me apps developed within 100km of me' and allowing you easily contact that developer. This could spur a powerful synergy between developers and local companies looking to hire.

I also believe that region specific app stores, could celebrate locally developed applications. And rather than 30% going to large corporations like Google, Microsoft or Blackberry, that 30% could instead go to the community in which the app was developed. For example, you buy a mobile game created by a Nairobi developer, 70% goes to the developer and 30% goes to support local organizations like Map Kibera or Mathare Valley Group. Costumers would give back to their community, without even knowing it.

Supporting local charities was the original goal behind Apps4Good, social good organization that myself and some friends from Halifax started to give back to the community. Unfortunately the ongoing support and marketing proved a bigger challenge than actually developing the applications. And because Apps4Good is entirely volunteer based, we've struggled to sell the tens of thousands of apps like we had hoped. But the idea is still sound, and I believe there is huge potential to allow consumers to buy products they need, while giving back to their community in the process.

Thoughts? Ideas? I'd love to hear them :)

Tuesday, June 7, 2011

Applescript To Export And Import iOS Projects To CSV

Two weeks ago I wrote an Applescript to auto-translate an iOS project by extracting all phrases, translating them via Google Translate, and then re-importing the translations back into your project. Unfortunately since publishing the script, Google has announced they are deprecating the Translate API.

So, as an alternative to using Google Translate, I've updated the Applescript to also export to a CSV file so the phrases can be loaded into a Google Spreadsheet, and then re-imported from CSV file once the spreadsheet has been translated.
set EXPORT_TO_CSV to "Export To CSV"
set IMPORT_FROM_CSV to "Import From CSV"
set GOOGLE_TRANSLATE to "Google Translate"
set CLASSES_ONLY to "Classes Only"
set CLASSES_AND_NIBS to "Classes And NIBs"

set question to display dialog "Select Your Activity" buttons {EXPORT_TO_CSV, IMPORT_FROM_CSV, GOOGLE_TRANSLATE} default button 1
set answer to button returned of question
if answer is equal to EXPORT_TO_CSV then
exportToCSV(choose folder with prompt "Select Project Source Folder", ¬
choose folder with prompt "Select Translations Destination Folder", ¬
display dialog "Enter CSV Destination File" default answer "localizations.csv", ¬
display dialog "Enter CSV Language Codes" default answer "fr,es,de,nl,it,pt,pl,ru,sw,zh,ja,ko,ar,cs,el,hi", ¬
display dialog "Export Phrases From" buttons {CLASSES_ONLY, CLASSES_AND_NIBS} default button 1)
else if answer is equal to IMPORT_FROM_CSV then
importFromCSV(choose folder with prompt "Select Project Source Folder", ¬
choose folder with prompt "Select Translations Destination Folder", ¬
choose file with prompt "Enter CSV Source File" of type {"CSV"})
else if answer is equal to GOOGLE_TRANSLATE then
translateProject(choose folder with prompt "Select Project Source Folder", ¬
choose folder with prompt "Select Localization Destination Folder", ¬
display dialog "Enter Destination Language Code" default answer "", ¬
display dialog "Translate Phrases From" buttons {CLASSES_ONLY, CLASSES_AND_NIBS} default button 1)
end if

on importFromCSV(projectFolder, localizationFolder, importFile)
set englishFolder to createFolder(localizationFolder, "en.lproj")
set importLines to paragraphs of readFilePath(importFile as text)

set firstLine to replaceCharacters(item 1 of importLines, "file,key,en,", "")
set firstLine to replaceCharacters(firstLine, "\"file\",\"key\",\"en\",", "")
set firstLine to replaceCharacters(firstLine, "\"", "")

set languageCodes to itemsFromCSV(firstLine)
repeat with languageCode in languageCodes
repeat with englishFile in filesWithExtension(englishFolder, ".strings")
set languageFileName to getFileName(englishFile)
set languageFolder to createFolder(localizationFolder, (languageCode & ".lproj"))
set languageFile to writeFile(languageFolder, languageFileName, "", false)
end repeat
end repeat

set importLines to items 2 thru -1 of importLines
repeat with importLine in importLines
set importLine to replaceCharacters(importLine, "\"\"\"", "\"")
set lineItems to itemsFromCSV(importLine)
set lineItemsCount to count of lineItems
if lineItemsCount is greater than 3 then
set fileName to item 1 of lineItems
set englishKey to item 2 of lineItems
set englishText to item 3 of lineItems
set translatedItems to items 4 through lineItemsCount of lineItems

set languageCodeIndex to 1
repeat with translatedItem in translatedItems
if length of translatedItem is greater than 0 then
set languageCode to item languageCodeIndex of languageCodes
set languageFolder to createFolder(localizationFolder, (languageCode & ".lproj"))
set translatedLine to "\"" & englishKey & "\" = \"" & translatedItem & "\";"
writeFile(languageFolder, fileName, translatedLine & return, true)
end if
if (count of languageCodes) is equal to languageCodeIndex then
exit repeat
end if
set languageCodeIndex to languageCodeIndex + 1
end repeat
end if
end repeat
display dialog "Translations Imported From " & importFile
end importFromCSV

on exportToCSV(projectFolder, localizationFolder, outputFileDialog, languageCodesDialog, phrasesDialog)
set languageCodesString to languageCodesDialog's text returned
if length of languageCodesString is equal to 0 then
display dialog "Language Codes Required"
else
set phrasesAnswer to button returned of phrasesDialog
set languageCodes to itemsFromCSV(languageCodesString)
set outputFile to outputFileDialog's text returned

set headerLine to "file,key,en," & languageCodesString
writeFile(localizationFolder, outputFile, headerLine & return, false)

do shell script "find " & quoted form of (POSIX path of projectFolder) & " -name '*.m' " & ¬
" | xargs genstrings -o " & quoted form of (POSIX path of localizationFolder & "en.lproj")

set englishFolder to createFolder(localizationFolder, "en.lproj")

if phrasesAnswer is equal to "Classes And NIBs" then
set nibFiles to filesWithExtension(projectFolder, ".xib")
repeat with nibFile in nibFiles
set nibFile to parseNib(nibFile, englishFolder)
end repeat
end if

repeat with englishFile in filesWithExtension(englishFolder, ".strings")
set languageFileName to getFileName(englishFile)
set englishLines to paragraphs of readFile(englishFolder, languageFileName)
repeat with englishLine in englishLines
if length of englishLine is greater than 0 then
set englishKey to parseKey(englishLine)
if length of englishKey is greater than 0 then
set newLine to languageFileName
if "\"" is in englishKey then
set newLine to newLine & ",\"\"" & englishKey & "\"\""
else if "," is in englishKey then
set newLine to newLine & ",\"" & englishKey & "\""
else
set newLine to newLine & "," & englishKey
end if

set englishText to parseValue(englishLine)
if "\"" is in englishText then
set newLine to newLine & ",\"\"" & englishText & "\"\""
else if "," is in englishText then
set newLine to newLine & ",\"" & englishText & "\""
else
set newLine to newLine & "," & englishText
end if

repeat with languageCode in languageCodes
set languageFolder to createFolder(localizationFolder, (languageCode & ".lproj"))
set languageFilePath to (languageFolder as text) & languageFileName as text
if fileExists(languageFilePath) then
set fileLines to paragraphs of readFile(languageFolder, languageFileName)
set hasTranslations to false
repeat with fileLine in fileLines
if "\"" & englishKey & "\" =" is in fileLine then
set languageText to parseValue(fileLine)
if "\"" is in languageText then
set newLine to newLine & ",\"\"" & languageText & "\"\""
else if "," is in languageText then
set newLine to newLine & ",\"" & languageText & "\""
else
set newLine to newLine & "," & languageText
end if
set hasTranslations to true
exit repeat
end if
end repeat
if hasTranslations is false then
set newLine to newLine & ","
end if
else
set newLine to newLine & ","
end if
end repeat
writeFile(localizationFolder, outputFile, newLine & return, true)
end if
end if
end repeat
end repeat
display dialog "Translations Exported To " & localizationFolder & outputFile
end if
end exportToCSV

on translateProject(projectFolder, localizationFolder, languageCodeDialog, phrasesDialog)
set languageCode to languageCodeDialog's text returned
if length of languageCode is equal to 0 then
display dialog "Language Code Required"
else
set phrasesAnswer to button returned of phrasesDialog
set englishFolder to createFolder(localizationFolder, "en.lproj")

do shell script "find " & quoted form of (POSIX path of projectFolder) & " -name '*.m' " & ¬
" | xargs genstrings -o " & quoted form of (POSIX path of localizationFolder & "en.lproj")

set nibFiles to filesWithExtension(projectFolder, ".xib")
if phrasesAnswer is equal to "Classes And NIBs" then
repeat with nibFile in nibFiles
set nibFile to parseNib(nibFile, englishFolder)
end repeat
end if

set languageFolder to createFolder(localizationFolder, (languageCode & ".lproj"))

repeat with englishFile in filesWithExtension(englishFolder, ".strings")
set languageFileName to getFileName(englishFile)

set languageFilePath to (languageFolder as text) & languageFileName as text
if fileExists(languageFilePath) is false then
set languageFile to writeFile(languageFolder, languageFileName, "", false)
end if

set fileLines to paragraphs of readFile(englishFolder, languageFileName)
repeat with fileLine in fileLines
if length of fileLine is greater than 0 then
set englishKey to parseKey(fileLine)
set englishText to parseValue(fileLine)

if length of englishText is greater than 0 then
if fileContainsText(languageFolder, languageFileName, "\"" & englishKey & "\" = ") is false then
set encodedText to encodeText(englishText, true, true)

set responseText to GoogleTranslate(encodedText, languageCode)
set translatedText to parseJson(responseText, "translatedText")

repeat until length of translatedText is greater than 0
set errorMessage to parseJson(responseText, "responseDetails")
if length of errorMessage is greater than 0 then
display dialog "Oops!" & space & errorMessage & space & "(Wait a few minutes, then click OK to continue)"
else
display dialog "Oops!" & space & responseText & space & "(Wait a few minutes, then click OK to continue)"
end if

set responseText to GoogleTranslate(encodedText, languageCode)
set translatedText to parseJson(responseText, "translatedText")
end repeat

set translatedLine to "\"" & englishKey & "\" = \"" & (translatedText as Unicode text) & "\";" as Unicode text
writeFile(languageFolder, languageFileName, translatedLine & return, true)
end if
end if
end if
end repeat

end repeat

if phrasesAnswer is equal to "Classes And NIBs" then
repeat with nibFile in nibFiles
importNib(nibFile, languageFolder)
end repeat
end if

display dialog "Translation Complete"
end if
end translateProject

on itemsFromCSV(csvLine)
set csvItems to {}
if "\"" is in csvLine then
set csvItem to ""
set insideQuotes to false
set previousQuote to false
repeat with c in the characters of csvLine
if insideQuotes then
if c as text is equal to "\"" then
if previousQuote then
set csvItem to csvItem & "\""
set previousQuote to false
else
set previousQuote to true
end if
else if c as text is equal to "," then
if previousQuote then
copy csvItem to the end of csvItems
set csvItem to ""
set insideQuotes to false
set previousQuote to false
else
set csvItem to csvItem & c
set previousQuote to false
end if
else
set csvItem to csvItem & c
set previousQuote to false
end if
else
if c as text is equal to "\"" then
set insideQuotes to true
set previousQuote to true
else if c as text is equal to "," then
copy csvItem to the end of csvItems
set csvItem to ""
set previousQuote to false
else
set csvItem to csvItem & c
set previousQuote to false
end if
end if
end repeat
if length of csvItem is greater than 0 then
copy csvItem to the end of csvItems
set csvItem to ""
set previousQuote to false
end if
else
set AppleScript's text item delimiters to ","
set csvItems to text items of csvLine
set AppleScript's text item delimiters to ""
end if
return csvItems
end itemsFromCSV

on parseNib(filePath, targetFolder)
set fileName to getFileName(filePath)
set fileNameWithoutExtension to replaceCharacters(fileName, ".xib", "")
set stringsPath to POSIX path of targetFolder & fileNameWithoutExtension & ".strings"
set sourceNib to POSIX path of filePath
do shell script "ibtool --export-strings-file" & space & ¬
quoted form of stringsPath & space & ¬
quoted form of sourceNib
return stringsPath
end parseNib

on importNib(filePath, targetFolder)
if fileExists(filePath) then
set fileName to getFileName(filePath)
set fileNameWithoutExtension to replaceCharacters(fileName, ".xib", "")
set stringsPath to POSIX path of targetFolder & fileNameWithoutExtension & ".strings"
set targetNib to POSIX path of targetFolder & fileName
set sourceNib to POSIX path of filePath
do shell script "ibtool --strings-file" & space & ¬
quoted form of stringsPath & space & "--write" & space & ¬
quoted form of targetNib & space & ¬
quoted form of sourceNib
end if
end importNib

on getFileName(filePath)
set {name:fileName} to info for file filePath without size
return fileName
end getFileName

on filesWithExtension(fileFolder, fileExtension)
set fileList to {}
tell application "Finder"
set filePaths to every file of entire contents of folder fileFolder as list
repeat with filePath in filePaths
if (filePath as text) ends with fileExtension then
set end of fileList to filePath as text
end if
end repeat
end tell
return fileList
end filesWithExtension

on fileContainsText(fileFolder, fileName, textString)
set fileLines to paragraphs of readFile(fileFolder, fileName)
repeat with fileLine in fileLines
if textString is in fileLine then
return true
end if
end repeat
return false
end fileContainsText

on createFolder(parentFolder, folderName)
tell application "Finder"
if not (exists folder folderName of parentFolder) then
make new folder at parentFolder with properties {name:folderName}
end if
return folder folderName of parentFolder as text
end tell
end createFolder

on writeFile(fileFolder, fileName, textString, shouldAppend)
tell application "Finder"
set filePath to (fileFolder as text) & fileName as text
try
set fileAccess to open for access file filePath with write permission
if shouldAppend is false then
set eof of fileAccess to 0
write (ASCII character 254) & (ASCII character 255) to fileAccess starting at eof -- UTF-16 BOM
else
set fileDoesExist to exists file filePath
if fileDoesExist is false then
set eof of fileAccess to 0
write (ASCII character 254) & (ASCII character 255) to fileAccess starting at eof -- UTF-16 BOM
end if
end if
write textString to fileAccess starting at eof as Unicode text
close access fileAccess
on error
try
close access file filePath
end try
end try
return filePath as text
end tell
end writeFile

on readFile(fileFolder, fileName)
set filePath to (fileFolder as text) & fileName as text
return readFilePath(filePath)
end readFile

on readFilePath(filePath)
tell application "Finder"
try
set fileAccess to open for access file filePath
set textContents to read fileAccess as Unicode text
close access fileAccess
return textContents
on error
display dialog "Unable to read filepath " & filePath
try
close access file filePath
end try
end try
return ""
end tell
end readFilePath

on fileExists(filePath)
tell application "Finder"
try
if exists file filePath then
return true
else
return false
end if
on error
return false
end try
end tell
return false
end fileExists

on GoogleTranslate(textString, codeString)
return do shell script "/usr/bin/curl" & space & ¬
"'http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&langpair=en|" & codeString & "&q=" & quoted form of textString & "'"
end GoogleTranslate

on parseKey(textString)
if (textString does not start with "/*") then
set AppleScript's text item delimiters to "="
set textItems to text items of textString
set AppleScript's text item delimiters to ""

if (count of textItems) > 1 then
set textItem to item 1 of textItems

set AppleScript's text item delimiters to "\""
set textValue to text items of textItem
set AppleScript's text item delimiters to ""

if (count of textValue) > 1 then
return trimCharacters((item 2 of textValue), "\"") as Unicode text
end if
end if
end if
return ""
end parseKey

on parseValue(textString)
if (textString does not start with "/*") then
set AppleScript's text item delimiters to "="
set textItems to text items of textString
set AppleScript's text item delimiters to ""

if (count of textItems) > 1 then
set textItem to item 2 of textItems

set AppleScript's text item delimiters to "\""
set textValue to text items of textItem
set AppleScript's text item delimiters to ""

if (count of textValue) > 1 then
set valueString to trimCharacters((item 2 of textValue), ";")
set valueString to trimCharacters(valueString, "\"")
return valueString as Unicode text
end if
end if
end if
return ""
end parseValue

on parseJson(jsonString, jsonElement)
repeat with delimiter1 in {"\":\"", "\": \""}
set AppleScript's text item delimiters to "\"" & jsonElement & delimiter1
set jsonItems to text items of jsonString
set AppleScript's text item delimiters to ""

if (count of jsonItems) > 1 then
set jsonValue to item 2 of jsonItems

repeat with delimiter2 in {"\"}", "\","}
set AppleScript's text item delimiters to delimiter2
set jsonValues to text items of jsonValue
set AppleScript's text item delimiters to ""

if (count of jsonValues) > 1 then
return decodeHTML(decodeURL(item 1 of jsonValues as Unicode text))
end if
end repeat
end if
end repeat
return ""
end parseJson

on trimCharacters(textString, trimCharacter)
repeat until textString does not start with trimCharacter
set textString to text 2 thru -1 of textString
end repeat
repeat until textString does not end with trimCharacter
set textString to text 1 thru -2 of textString
end repeat
return textString
end trimCharacters

on replaceCharacters(textString, searchString, replacementString)
set AppleScript's text item delimiters to the searchString
set the itemList to every text item of textString
set AppleScript's text item delimiters to the replacementString
set textString to the itemList as string
set AppleScript's text item delimiters to ""
return textString
end replaceCharacters

on encodeText(textString, encode_URL_A, encode_URL_B)
set the standardCharacters to "abcdefghijklmnopqrstuvwxyz0123456789"
set the URL_A_chars to "$+!'/?;&@=#%><{}[]\"~`^\\|*" set the URL_B_chars to ".-_:" set the acceptableCharacters to the standardCharacters if encode_URL_A is false then set the acceptableCharacters to the acceptableCharacters & the URL_A_chars if encode_URL_B is false then set the acceptableCharacters to the acceptableCharacters & the URL_B_chars set the encodedText to "" repeat with charString in textString if charString is in the acceptableCharacters then set the encodedText to (the encodedText & charString) as string else set the encodedText to (the encodedText & encodeCharacter(charString)) as string end if end repeat return the encodedText end encodeText on encodeCharacter(charString) set the ASCII_num to (the ASCII number charString) set the hex_list to {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"} set x to item ((ASCII_num div 16) + 1) of the hex_list set y to item ((ASCII_num mod 16) + 1) of the hex_list return ("%" & x & y) as string end encodeCharacter on decodeCharacters(charString) copy charString to {indentifying_char, multiplier_char, remainder_char} set the hexList to "123456789ABCDEF" if the multiplier_char is in "ABCDEF" then set the multiplier_amt to the offset of the multiplier_char in the hexList else set the multiplier_amt to the multiplier_char as integer end if if the remainder_char is in "ABCDEF" then set the remainder_amt to the offset of the remainder_char in the hexList else set the remainder_amt to the remainder_char as integer end if set the ASCII_num to (multiplier_amt * 16) + remainder_amt return (ASCII character ASCII_num) end decodeCharacters on decodeURL(textString) set flag_A to false set flag_B to false set temp_char to "" set the characterList to {} repeat with charString in textString set charString to contents of charString if charString is "%" then set flag_A to true else if flag_A is true then set the temp_char to charString set flag_A to false set flag_B to true else if flag_B is true then set the end of characterList to my decodeCharacters(("%" & temp_char & charString) as string) set the temp_char to "" set flag_A to false set flag_B to false else set the end of characterList to charString end if end repeat return characterList as string end decodeURL on decodeHTML(textString) repeat with entities in {{"\\u0026", "&"}, ¬ {" ", " "}, {"!", "!"}, {""", "\""}, {"#", "#"}, {"$", "$"}, ¬ {"%", "%"}, {"&", "&"}, {"'", "'"}, {"(", "("}, {")", ")"}, ¬ {"*", "*"}, {"+", "+"}, {",", ","}, {"-", "-"}, {".", "."}, ¬ {"/", "/"}, {":", ":"}, {";", ";"}, {"<", "<"}, {"=", "="}, ¬ {">", ">"}, {"?", "?"}, {"@", "@"}, {"[", "["}, {"\", "\\"}, ¬
{"]", "]"}, {"^", "^"}, {"_", "_"}, {"`", "`"}, {"{", "{"}, ¬
{"|", "|"}, {"}", "}"}, {"~", "~"}, {" ", " "}, {"¡", "¡"}, ¬
{"¢", "¢"}, {"£", "£"}, {"¤", "¤"}, {"¥", "¥"}, {"¦", "¦"}, ¬
{"§", "§"}, {"¨", "¨"}, {"©", "©"}, {"ª", "ª"}, {"«", "«"}, ¬
{"®", "®"}, {"¯", "¯"}, {"°", "°"}, {"±", "±"}, {"²", "²"}, ¬
{"³", "³"}, {"´", "´"}, {"µ", "µ"}, {"¶", "¶"}, {"·", "·"}, ¬
{"¸", "¸"}, {"¹", "¹"}, {"º", "º"}, {"»", "»"}, {"¼", "¼"}, ¬
{"½", "½"}, {"¾", "¾"}, {"¿", "¿"}, {"À", "À"}, {"Á", "Á"}, ¬
{"Â", "Â"}, {"Ã", "Ã"}, {"Ä", "Ä"}, {"Å", "Å"}, {"Æ", "Æ"}, ¬
{"Ç", "Ç"}, {"È", "È"}, {"É", "É"}, {"Ê", "Ê"}, {"Ë", "Ë"}, ¬
{"Ì", "Ì"}, {"Í", "Í"}, {"Î", "Î"}, {"Ï", "Ï"}, {"Ð", "Ð"}, ¬
{"Ñ", "Ñ"}, {"Ò", "Ò"}, {"Ó", "Ó"}, {"Ô", "Ô"}, {"Õ", "Õ"}, ¬
{"Ö", "Ö"}, {"×", "×"}, {"Ø", "Ø"}, {"Ù", "Ù"}, {"Ú", "Ú"}, ¬
{"Û", "Û"}, {"Ü", "Ü"}, {"Ý", "Ý"}, {"Þ", "Þ"}, {"ß", "ß"}, ¬
{"à", "à"}, {"á", "á"}, {"â", "â"}, {"ã", "ã"}, {"ä", "ä"}, ¬
{"å", "å"}, {"æ", "æ"}, {"ç", "ç"}, {"è", "è"}, {"é", "é"}, ¬
{"ê", "ê"}, {"ë", "ë"}, {"ì", "ì"}, {"í", "í"}, {"î", "î"}, ¬
{"ï", "ï"}, {"ð", "ð"}, {"ñ", "ñ"}, {"ò", "ò"}, {"ó", "ó"}, ¬
{"ô", "ô"}, {"õ", "õ"}, {"ö", "ö"}, {"÷", "÷"}, {"ø", "ø"}, ¬
{"ù", "ù"}, {"ú", "ú"}, {"û", "û"}, {"ü", "ü"}, {"ý", "ý"}, ¬
{"þ", "þ"}, {"ÿ", "ÿ"}, {"Œ", "Œ"}, {"œ", "œ"}, {"Š", "Š"}, ¬
{"š", "š"}, {"Ÿ", "Ÿ"}, {"ƒ", "ƒ"}, {"–", "–"}, {"—", "—"}, ¬
{"‘", "‘"}, {"’", "’"}, {"‚", "‚"}, {"“", "“"}, {"”", "”"}, ¬
{"„", "„"}, {"†", "†"}, {"‡", "‡"}, {"•", "•"}, {"…", "…"}, ¬
{"‰", "‰"}, {"€", "€"}, {"™", "™"}}
set textString to replaceCharacters(textString, item 1 of entities, item 2 of entities)
end repeat
return textString as string
end decodeHTML

You can download the updated Applescript at TranslateiOS.scpt

Friday, May 27, 2011

Applescript To Google Translate iOS Projects

Translating your iOS projects can often be a painful process, so I wrote a little Applescript to help me translate the Ushahidi iOS app.

The script extracts phrases from your project with genstrings and NIB files with ibtool, calls the Google Translate service for each phrase, writes the translated text to file and then re-imports those phrases back into NIB files in the target language folder.

translateProject(choose folder with prompt "Select Project Source Folder", ¬
choose folder with prompt "Select Localization Destination Folder", ¬
display dialog "Enter Destination Language Code" default answer "")

on translateProject(projectFolder, localizationFolder, languageCodeDialog)
set languageCode to languageCodeDialog's text returned
if length of languageCode is equal to 0 then
display dialog "Language Code Required"
else
set englishFolder to createFolder(localizationFolder, "en.lproj")

do shell script "find " & quoted form of (POSIX path of projectFolder) & " -name '*.m' " & ¬
" | xargs genstrings -o " & quoted form of (POSIX path of localizationFolder & "en.lproj")

set nibFiles to filesWithExtension(projectFolder, ".xib")
repeat with nibFile in nibFiles
set nibFile to parseNib(nibFile, englishFolder)
end repeat

set languageFolder to createFolder(localizationFolder, (languageCode & ".lproj"))

repeat with englishFile in filesWithExtension(englishFolder, ".strings")
set languageFileName to getFileName(englishFile)

set languageFilePath to (languageFolder as text) & languageFileName as text
if fileExists(languageFilePath) is false then
set languageFile to writeFile(languageFolder, languageFileName, "", false)
end if

set fileLines to paragraphs of readFile(englishFolder, languageFileName)
repeat with fileLine in fileLines
if length of fileLine is greater than 0 then
set englishKey to parseKey(fileLine)
set englishText to parseValue(fileLine)

if length of englishText is greater than 0 then
if fileContainsText(languageFolder, languageFileName, "\"" & englishKey & "\" = ") is false then
set encodedText to encodeText(englishText, true, true)

set responseText to googleTranslate(encodedText, languageCode)
set translatedText to parseJson(responseText, "translatedText")

repeat until length of translatedText is greater than 0
set errorMessage to parseJson(responseText, "responseDetails")
if length of errorMessage is greater than 0 then
display dialog "Oops!" & space & errorMessage & space & "(Wait a few minutes, then click OK to continue)"
else
display dialog "Oops!" & space & responseText & space & "(Wait a few minutes, then click OK to continue)"
end if

set responseText to googleTranslate(encodedText, languageCode)
set translatedText to parseJson(responseText, "translatedText")
end repeat

set translatedLine to "\"" & englishKey & "\" = \"" & (translatedText as Unicode text) & "\";" as Unicode text
writeFile(languageFolder, languageFileName, translatedLine & return, true)
end if
end if
end if
end repeat

end repeat

repeat with nibFile in nibFiles
importNib(nibFile, languageFolder)
end repeat

display dialog "Translation Complete"
end if
end translateProject

on parseNib(filePath, targetFolder)
set fileName to getFileName(filePath)
set fileNameWithoutExtension to replaceCharacters(fileName, ".xib", "")
set stringsPath to POSIX path of targetFolder & fileNameWithoutExtension & ".strings"
set sourceNib to POSIX path of filePath
do shell script "ibtool --export-strings-file" & space & ¬
quoted form of stringsPath & space & ¬
quoted form of sourceNib
return stringsPath
end parseNib

on importNib(filePath, targetFolder)
if fileExists(filePath) then
set fileName to getFileName(filePath)
set fileNameWithoutExtension to replaceCharacters(fileName, ".xib", "")
set stringsPath to POSIX path of targetFolder & fileNameWithoutExtension & ".strings"
set targetNib to POSIX path of targetFolder & fileName
set sourceNib to POSIX path of filePath
do shell script "ibtool --strings-file" & space & ¬
quoted form of stringsPath & space & "--write" & space & ¬
quoted form of targetNib & space & ¬
quoted form of sourceNib
end if
end importNib

on getFileName(filePath)
set {name:fileName} to info for file filePath without size
return fileName
end getFileName

on filesWithExtension(fileFolder, fileExtension)
set fileList to {}
tell application "Finder"
set filePaths to every file of entire contents of folder fileFolder as list
repeat with filePath in filePaths
if (filePath as text) ends with fileExtension then
set end of fileList to filePath as text
end if
end repeat
end tell
return fileList
end filesWithExtension

on fileContainsText(fileFolder, fileName, textString)
set fileLines to paragraphs of readFile(fileFolder, fileName)
repeat with fileLine in fileLines
if textString is in fileLine then
return true
end if
end repeat
return false
end fileContainsText

on createFolder(parentFolder, folderName)
tell application "Finder"
if not (exists folder folderName of parentFolder) then
make new folder at parentFolder with properties {name:folderName}
end if
return folder folderName of parentFolder as text
end tell
end createFolder

on writeFile(fileFolder, fileName, textString, shouldAppend)
tell application "Finder"
set filePath to (fileFolder as text) & fileName as text
try
set fileAccess to open for access file filePath with write permission
if shouldAppend is false then
set eof of fileAccess to 0
write (ASCII character 254) & (ASCII character 255) to fileAccess starting at eof -- UTF-16 BOM
else
set fileDoesExist to exists file filePath
if fileDoesExist is false then
set eof of fileAccess to 0
write (ASCII character 254) & (ASCII character 255) to fileAccess starting at eof -- UTF-16 BOM
end if
end if
write textString to fileAccess starting at eof as Unicode text
close access fileAccess
on error
try
close access file filePath
end try
end try
return filePath as text
end tell
end writeFile

on readFile(fileFolder, fileName)
tell application "Finder"
set filePath to (fileFolder as text) & fileName as text
try
set fileAccess to open for access file filePath
set textContents to read fileAccess as Unicode text
close access fileAccess
return textContents
on error
try
close access file filePath
end try
end try
return ""
end tell
end readFile

on fileExists(filePath)
tell application "Finder"
try
if exists file filePath then
return true
else
return false
end if
on error
return false
end try
end tell
return false
end fileExists

on googleTranslate(textString, codeString)
return do shell script "/usr/bin/curl" & space & ¬
"'http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&langpair=en|" & codeString & "&q=" & quoted form of textString & "'"
end googleTranslate

on parseKey(textString)
if (textString does not start with "/*") then
set AppleScript's text item delimiters to "="
set textItems to text items of textString
set AppleScript's text item delimiters to ""

if (count of textItems) > 1 then
set textItem to item 1 of textItems

set AppleScript's text item delimiters to "\""
set textValue to text items of textItem
set AppleScript's text item delimiters to ""

if (count of textValue) > 1 then
return trimCharacters((item 2 of textValue), "\"") as Unicode text
end if
end if
end if
return ""
end parseKey

on parseValue(textString)
if (textString does not start with "/*") then
set AppleScript's text item delimiters to "="
set textItems to text items of textString
set AppleScript's text item delimiters to ""

if (count of textItems) > 1 then
set textItem to item 2 of textItems

set AppleScript's text item delimiters to "\""
set textValue to text items of textItem
set AppleScript's text item delimiters to ""

if (count of textValue) > 1 then
set valueString to trimCharacters((item 2 of textValue), ";")
set valueString to trimCharacters(valueString, "\"")
return valueString as Unicode text
end if
end if
end if
return ""
end parseValue

on parseJson(jsonString, jsonElement)
repeat with delimiter1 in {"\":\"", "\": \""}
set AppleScript's text item delimiters to "\"" & jsonElement & delimiter1
set jsonItems to text items of jsonString
set AppleScript's text item delimiters to ""

if (count of jsonItems) > 1 then
set jsonValue to item 2 of jsonItems

repeat with delimiter2 in {"\"}", "\","}
set AppleScript's text item delimiters to delimiter2
set jsonValues to text items of jsonValue
set AppleScript's text item delimiters to ""

if (count of jsonValues) > 1 then
return decodeHTML(decodeURL(item 1 of jsonValues as Unicode text))
end if
end repeat
end if
end repeat
return ""
end parseJson

on trimCharacters(textString, trimCharacter)
repeat until textString does not start with trimCharacter
set textString to text 2 thru -1 of textString
end repeat
repeat until textString does not end with trimCharacter
set textString to text 1 thru -2 of textString
end repeat
return textString
end trimCharacters

on replaceCharacters(textString, searchString, replacementString)
set AppleScript's text item delimiters to the searchString
set the itemList to every text item of textString
set AppleScript's text item delimiters to the replacementString
set textString to the itemList as string
set AppleScript's text item delimiters to ""
return textString
end replaceCharacters

on encodeText(textString, encode_URL_A, encode_URL_B)
set the standardCharacters to "abcdefghijklmnopqrstuvwxyz0123456789"
set the URL_A_chars to "$+!'/?;&@=#%><{}[]\"~`^\\|*" set the URL_B_chars to ".-_:" set the acceptableCharacters to the standardCharacters if encode_URL_A is false then set the acceptableCharacters to the acceptableCharacters & the URL_A_chars if encode_URL_B is false then set the acceptableCharacters to the acceptableCharacters & the URL_B_chars set the encodedText to "" repeat with charString in textString if charString is in the acceptableCharacters then set the encodedText to (the encodedText & charString) as string else set the encodedText to (the encodedText & encodeCharacter(charString)) as string end if end repeat return the encodedText end encodeText on encodeCharacter(charString) set the ASCII_num to (the ASCII number charString) set the hex_list to {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"} set x to item ((ASCII_num div 16) + 1) of the hex_list set y to item ((ASCII_num mod 16) + 1) of the hex_list return ("%" & x & y) as string end encodeCharacter on decodeCharacters(charString) copy charString to {indentifying_char, multiplier_char, remainder_char} set the hexList to "123456789ABCDEF" if the multiplier_char is in "ABCDEF" then set the multiplier_amt to the offset of the multiplier_char in the hexList else set the multiplier_amt to the multiplier_char as integer end if if the remainder_char is in "ABCDEF" then set the remainder_amt to the offset of the remainder_char in the hexList else set the remainder_amt to the remainder_char as integer end if set the ASCII_num to (multiplier_amt * 16) + remainder_amt return (ASCII character ASCII_num) end decodeCharacters on decodeURL(textString) set flag_A to false set flag_B to false set temp_char to "" set the characterList to {} repeat with charString in textString set charString to contents of charString if charString is "%" then set flag_A to true else if flag_A is true then set the temp_char to charString set flag_A to false set flag_B to true else if flag_B is true then set the end of characterList to my decodeCharacters(("%" & temp_char & charString) as string) set the temp_char to "" set flag_A to false set flag_B to false else set the end of characterList to charString end if end repeat return characterList as string end decodeURL on decodeHTML(textString) repeat with entities in {{"\\u0026", "&"}, ¬ {" ", " "}, {"!", "!"}, {""", "\""}, {"#", "#"}, {"$", "$"}, ¬ {"%", "%"}, {"&", "&"}, {"'", "'"}, {"(", "("}, {")", ")"}, ¬ {"*", "*"}, {"+", "+"}, {",", ","}, {"-", "-"}, {".", "."}, ¬ {"/", "/"}, {":", ":"}, {";", ";"}, {"<", "<"}, {"=", "="}, ¬ {">", ">"}, {"?", "?"}, {"@", "@"}, {"[", "["}, {"\", "\\"}, ¬
{"]", "]"}, {"^", "^"}, {"_", "_"}, {"`", "`"}, {"{", "{"}, ¬
{"|", "|"}, {"}", "}"}, {"~", "~"}, {" ", " "}, {"¡", "¡"}, ¬
{"¢", "¢"}, {"£", "£"}, {"¤", "¤"}, {"¥", "¥"}, {"¦", "¦"}, ¬
{"§", "§"}, {"¨", "¨"}, {"©", "©"}, {"ª", "ª"}, {"«", "«"}, ¬
{"®", "®"}, {"¯", "¯"}, {"°", "°"}, {"±", "±"}, {"²", "²"}, ¬
{"³", "³"}, {"´", "´"}, {"µ", "µ"}, {"¶", "¶"}, {"·", "·"}, ¬
{"¸", "¸"}, {"¹", "¹"}, {"º", "º"}, {"»", "»"}, {"¼", "¼"}, ¬
{"½", "½"}, {"¾", "¾"}, {"¿", "¿"}, {"À", "À"}, {"Á", "Á"}, ¬
{"Â", "Â"}, {"Ã", "Ã"}, {"Ä", "Ä"}, {"Å", "Å"}, {"Æ", "Æ"}, ¬
{"Ç", "Ç"}, {"È", "È"}, {"É", "É"}, {"Ê", "Ê"}, {"Ë", "Ë"}, ¬
{"Ì", "Ì"}, {"Í", "Í"}, {"Î", "Î"}, {"Ï", "Ï"}, {"Ð", "Ð"}, ¬
{"Ñ", "Ñ"}, {"Ò", "Ò"}, {"Ó", "Ó"}, {"Ô", "Ô"}, {"Õ", "Õ"}, ¬
{"Ö", "Ö"}, {"×", "×"}, {"Ø", "Ø"}, {"Ù", "Ù"}, {"Ú", "Ú"}, ¬
{"Û", "Û"}, {"Ü", "Ü"}, {"Ý", "Ý"}, {"Þ", "Þ"}, {"ß", "ß"}, ¬
{"à", "à"}, {"á", "á"}, {"â", "â"}, {"ã", "ã"}, {"ä", "ä"}, ¬
{"å", "å"}, {"æ", "æ"}, {"ç", "ç"}, {"è", "è"}, {"é", "é"}, ¬
{"ê", "ê"}, {"ë", "ë"}, {"ì", "ì"}, {"í", "í"}, {"î", "î"}, ¬
{"ï", "ï"}, {"ð", "ð"}, {"ñ", "ñ"}, {"ò", "ò"}, {"ó", "ó"}, ¬
{"ô", "ô"}, {"õ", "õ"}, {"ö", "ö"}, {"÷", "÷"}, {"ø", "ø"}, ¬
{"ù", "ù"}, {"ú", "ú"}, {"û", "û"}, {"ü", "ü"}, {"ý", "ý"}, ¬
{"þ", "þ"}, {"ÿ", "ÿ"}, {"Œ", "Œ"}, {"œ", "œ"}, {"Š", "Š"}, ¬
{"š", "š"}, {"Ÿ", "Ÿ"}, {"ƒ", "ƒ"}, {"–", "–"}, {"—", "—"}, ¬
{"‘", "‘"}, {"’", "’"}, {"‚", "‚"}, {"“", "“"}, {"”", "”"}, ¬
{"„", "„"}, {"†", "†"}, {"‡", "‡"}, {"•", "•"}, {"…", "…"}, ¬
{"‰", "‰"}, {"€", "€"}, {"™", "™"}}
set textString to replaceCharacters(textString, item 1 of entities, item 2 of entities)
end repeat
return textString as string
end decodeHTML

The script is still a work in progress, so any feedback is gladly welcome. You can download the Applescript at Translate.scpt

Friday, April 29, 2011

Logo Design

I'm currently in search for a logo for a new project I'm launching called Repurposed Labs, giving a new purpose to old computers. During this process, I've also come to realize the brilliance behind a well designed logo. I've always loved the simple yet meaningful design of FrontlineSMS, HopePhones, MobileActive, Ubuntu, SiGeneration and SwiftRiver.

FrontlineSMS, is open source software that uses SMS to empower it's users. Their logo is a speech bubble out of the word SMS, with ascii characters \0/ inside which represents raising your arms up in victory. The brilliance behind the logo is apparent in the sister projects, FrontlineSMS:Medic \+/, FrontlineSMS:Credit \$/, FrontlineSMS:Legal \=/, FrontlineSMS:Learn \?/ and FrontlineSMS:Radio \~/ which reuse the logo but change the message inside.

HopePhones, is an initiative to collect used cell phones so they can be repurposed by community healthcare workers in developing countries. Each old phone donated can help save a life in a developing country, thus the angel wings on the phone in their logo.

MobileActive, is a global network of people using mobile technology for social impact. Their new logo is pretty ingenius, could be interpreted as four people raising their arms in protest or four mobile devices with antennas.

Ubuntu, is an open source community that provides a free and easy-to-use operating system for the world. The word 'ubuntu' is a Southern African ethical ideology which means 'humanity towards others'. Their logo could be interpreted as six components fitting together or three people of different color united.

Social Innovation Generation (SiG) is a centre in Toronto whose goal is to use technology to solve serious social problems. Their logo looks like a dandelion flower with the seeds blowing in the wind, which matches their mission to help seed innovation.

SwiftRiver is an open source platform that helps users add context to realtime data. Their logo is one of my favorites; it naturally has a river shape, a visible letter 'S', as well as the thumbs up and thumbs down which is key functionality to the platform.

So what are your favorite logos? How do their design capture the organization's mission?

Sunday, April 3, 2011

The Mobile Web vs Mobile App Debate

The mobile web vs mobile app debate has been ongoing. Some argue that the mobile web is the future, others are in favor of mobile apps.

I personally find this debate somewhat tiresome, it's like asking; what's better, cars or trucks? The answer, they each serve a purpose.

From a functionality perspective, although HTML5 has made great strides to support offline content with hooks for geo-location and camera capability, it's still tough to rival a well designed native mobile app. Take Google's Gmail mobile web version, it has set the bar as to what's possible via a mobile web browser. Would I choose it over Apple's native Mail app on the iPhone? Not likely, in fact I'm writing this blog post as I fly from Frankfurt to Madrid by emailing myself a draft using the Mail app on my iPhone. As for Amazon's mobile web version, it's a good example of a well designed mobile interface, clean and easy to read with expandable content. In this case, it serves my purpose for comparing book prices while I'm out shopping.

From a company's perspective, with limited budget and time, you need to ask how you can best utilize your resources. If you want the widest reach and target the most potential users, perhaps mobile web is a good option. But again, that depends on what features you want to provide. Are you offering free or paid content? What is your target audience? How can you best deliver the desired functionality?

From a user's perspective, I'd actually like to have both options. I appreciate Gmail's mobile web version if I'm checking my email on a friends phone, but still prefer the native Mail app on my own device. It's hard to ignore how easy the App Store has made searching for and installing both free and paid mobile apps. Although searching Google in a mobile web browser is also pretty easy, I don't have the luxury of reading user reviews or viewing screenshots of your search results. You're also left with the uncertainty whether the webpage you visit will be mobile friendly or not. Mobile Safari does offer a rich user experience, but I would still prefer a mobile friendly page over having to pinch and zoom, especially if the site has a lot of unnecessary graphics slowing the page load.

Another option which is sort of the best of both worlds, is using a cross platform mobile framework like Titanium, PhoneGap or RhoMobile. Develop one app and publish it to multiple mobile platforms. It however, is not a silver bullet. Do you want a consistent user experience across all platforms? Or do you want the user experience to be familiar with the target platform? It's an important question to ask. For a company, it would be easier to provide technical support for a consistent design. But for a user, do you really want your iPhone app to look like a scaled Blackberry app? Will the user say, "wow I love how this app looks exactly the same on both my Blackberry and iPhone"? Or will they say, "this app looks strange and feels different than all my other apps"? There are also risks with cross platform frameworks. One, its common for them to be behind the native OS development cycle, so you may not be able to support the newest OS features. Two, what happens if that cross platform framework discontinues development? It could mean your next version of the app is a rebuild from scratch.

If you are just serving content, perhaps exposing the data in a consumable format may be enough. Even though every newspaper company is scrambling to release a mobile version of their site, I personally would rather read that content in my own RSS reader, rather than have a separate mobile app for the New York Times, Globe & Mail and Huffington Post.

So in the end, what's better, mobile web or mobile apps? That depends ;)

Sunday, December 12, 2010

Smartphone Comparison Chart

Smartphone compartion chart for the iPhone, Android, Blackberry and Palm.


Original Image Source

Tuesday, November 16, 2010

The Mobile Developer Journey

Great infographic on The Mobile Developer Journey.
The Mobile Developer Journey