GUI scripting often relies on referring to interface elements by name. Those names vary in different locales, and as a consequence, such scripts often don’t travel well.
The solution to this used to work reasonably well. Widgets had a US English name, and the localized string
command could be used to find the localized equivalent. This was done by looking up .strings
files, which typically used the original US English names as keys to the translations. The translation only had to be done on other systems.
In recent years Apple has introduced a slightly different method of handling localization. It makes it a bit easier for developers in some areas, but harder for scripters. In this scheme, the keys or base names of most widgets are usually machine-generated and meaningless, rather than simply the US English version. When an app is launched, all versions substitute the localized names that appear.
To make GUI scripts that travel, instead of passing the US English version of a name to the localized string
command, you have to pass the now largely meaningless key. That makes for pretty ugly and unhelpful code, plus a tedious process of looking up strings files to find them. And they can change, even when the label that appears when you run the app remains the same.
Fortunately, the issue can be scripted around. The code below has a main handler that takes a string, the language it is in, the language you want it in, the app or framework it is found in, and the name of the .strings
file. It also contains some simpler handlers based on this. The simplest, localizedStringForEnglish:inBundle:
, can be used the way localized string
used to be used.
The script works by looking up the string you pass in the .strings
file for its language, gets the key used for it, then uses that key to look up the value in the destination language’s .strings
file. The contents of the .strings
files is cached in a property, to provide a modest performance boost when performing multiple translations. it would be sensible to store it as a script library.
The process is not foolproof: it’s possible that multiple items in unrelated dialog use the same translation in one language but not another, and the script just uses the first it finds. But it should cope with most cases.
Take 2:
use AppleScript version "2.5" -- macOS 10.11 or later
use framework "Foundation"
use scripting additions
property dictCache : missing value -- we cache strings dictionaries here to speed multiple look-ups a bit
-- use this if you have the English name and it is in the standard Localized.strings file
on localizedStringForEnglish:enString inBundle:fileOrNSURL
set langID to current application's NSLocale's currentLocale()'s localeIdentifier()
return my localizedStringFor:enString fromTable:"" inBundle:fileOrNSURL destLang:langID sourceLang:"en"
end localizedStringForEnglish:inBundle:
-- use this if you want to pass a non-English name and it is in the standard Localized.strings file; langID should contain the ISO locale identifier for the string (eg, "en_GB", "fr", "fr_FR")
on localizedStringFor:baseString inBundle:fileOrNSURL sourceLang:langID
return my localizedStringFor:baseString fromTable:"" inBundle:fileOrNSURL destLang:"" sourceLang:langID
end localizedStringFor:inBundle:sourceLang:
-- use this when writing scripts if you just want to find out the English translation on a non-English system and the value is in the standard Localized.strings file
on enStringFor:baseString inBundle:fileOrNSURL
set langID to current application's NSLocale's currentLocale()'s localeIdentifier()
return my localizedStringFor:baseString fromTable:"" inBundle:fileOrNSURL destLang:"" sourceLang:langID
end enStringFor:inBundle:
-- use this if you also want to specify a particular .strings file. A destLangCode of "" means the current locale, and a stringsFileName of "" means the default Localizable.strings file
on localizedStringFor:baseString fromTable:stringsFileName inBundle:fileOrNSURL destLang:destLangCode sourceLang:sourceLangCode
if stringsFileName is "" then set stringsFileName to "Localizable"
set theBundle to current application's NSBundle's bundleWithURL:fileOrNSURL
set sourceLangString to current application's NSString's stringWithString:sourceLangCode
-- make key to search for cached values
set cacheKey to current application's NSString's stringWithFormat_("%@_%@_%@", theBundle's bundleIdentifier(), stringsFileName, sourceLangCode)
if my dictCache is missing value then set my dictCache to current application's NSMutableDictionary's dictionary()
-- get source strings values
set sourceDict to dictCache's objectForKey:cacheKey
if sourceDict = missing value then
set sourceURL to theBundle's URLForResource:stringsFileName withExtension:"strings" subdirectory:"" localization:sourceLangString
if sourceURL is missing value and (sourceLangString's containsString:"_") as boolean then
-- try stripping off country-specific part
set sourceLangString to sourceLangString's substringToIndex:2
set sourceURL to theBundle's URLForResource:stringsFileName withExtension:"strings" subdirectory:"" localization:sourceLangString
end if
if sourceURL is missing value then
-- try long name for localization
set sourceLangString to (current application's NSLocale's localeWithLocaleIdentifier:"en")'s localizedStringForLocaleIdentifier:sourceLangString
set sourceURL to theBundle's URLForResource:stringsFileName withExtension:"strings" subdirectory:"" localization:sourceLangString
end if
if sourceURL is missing value then error "No " & sourceLangCode & " localization found"
set theData to current application's NSData's alloc()'s initWithContentsOfURL:sourceURL
if theData is missing value then error "No " & sourceLangCode & " localization found"
set sourceDict to (current application's NSPropertyListSerialization's propertyListWithData:theData options:0 format:0 |error|:(missing value))
dictCache's setObject:sourceDict forKey:cacheKey
end if
-- make key to search for cached values
set cacheKey to current application's NSString's stringWithFormat_("%@_%@_%@", theBundle's bundleIdentifier(), stringsFileName, destLangCode)
-- get dest strings values
set destDict to dictCache's objectForKey:cacheKey
if destDict = missing value then
if destLangCode is "" then
set destLangString to current application's NSLocale's currentLocale()'s localeIdentifier()
else
set destLangString to current application's NSString's stringWithString:destLangCode
end if
set localURL to theBundle's URLForResource:stringsFileName withExtension:"strings" subdirectory:"" localization:destLangString
if localURL is missing value and (destLangString's containsString:"_") as boolean then
-- try stripping off country-specific part
set destLangString to destLangString's substringToIndex:2
set localURL to theBundle's URLForResource:stringsFileName withExtension:"strings" subdirectory:"" localization:destLangString
end if
if localURL is missing value then
-- try long name for localization
set destLangString to (current application's NSLocale's localeWithLocaleIdentifier:"en")'s localizedStringForLocaleIdentifier:destLangString
set localURL to theBundle's URLForResource:stringsFileName withExtension:"strings" subdirectory:"" localization:destLangString
end if
if localURL is missing value then error "No " & destLangCode & " localization found"
set theData to current application's NSData's alloc()'s initWithContentsOfURL:localURL
if theData is missing value then error "No " & destLangCode & " localization found"
set destDict to (current application's NSPropertyListSerialization's propertyListWithData:theData options:0 format:0 |error|:(missing value))
dictCache's setObject:destDict forKey:cacheKey
end if
-- convert from source to dest
set destValue to destDict's objectForKey:((sourceDict's allKeysForObject:baseString)'s firstObject())
if destValue is not missing value then set destValue to destValue as text
return destValue
end localizedStringFor:fromTable:inBundle:destLang:sourceLang:
-- example:
set theURL to path to application "Finder"
set deComments to my localizedStringFor:"Comments" fromTable:"" inBundle:theURL destLang:"de" sourceLang:"en"
set localComments to my localizedStringForEnglish:"Comments" inBundle:theURL
set enComments to my localizedStringFor:deComments fromTable:"" inBundle:theURL destLang:"en" sourceLang:"de"
return {deComments, localComments, enComments}
--> {"Kommentare", "Comments", "Comments"}