Today I was [too many] years old…

…when I learned how to localise strings containing plural values using string dictionaries (.stringsdict) in Xcode.

I am working on a terminology app teachers could use to write basic terminologies about the courses they teach. Then they could share those to the students to aid in learning the course topics. Student can also add new terms to the terminology if they wish on their own devices. And even create their own terms and terminologies. Users can also share the terminologies to others.

Anyways, I wanted to localise this at least to Finnish and English. I was supervising three localisation related student software engineering projects this Spring, so I guess that also motivated me to learn more.

Anyways, I already knew from before how to do simple localisation, but not how to localise singular and plural forms of text using string dictionaries. Finally I decided to take a shot at it, and here is the result, in Finnish first. The localised text is on the left side list, on the second gray text row of each list item:

Screenshot of a app view with localised text in Finnish, the number of elements pluralised.
The list on the left is localised from English to Finnish, using string dictionaries to pluralise the count of elements (gray text on second line of list items). The string dictionary XML is visible in the background in Xcode editor.

Below is the English version. Note that the actual contents of the app has been written in Finnish, so the main text in first line of the list is of course Finnish – only the descriptive text (“Category has 40 terms” or “No terms in this category”) is localised. And pluralised, which was the main goal here.

Screenshot of a app view with localised text in English, the number of elements pluralised.
English localisation of the same view.

This was bit of a fight (as usual), since the documents are there but no single document shows in one place a) how to specify the string dictionary and b) how to use it from code in SwiftUI. I managed to write the XML file using Apple documentation, but couldn’t find a usage sample there.

So I searched the internet and browsed Stack overflow. Then it came to my mind that surely I have some sample app or open source app on my machine where string dictionaries are used. So I searched from the directory under which I have all the source code:

find ./ -name "*.stringsdict" -print

And found that the NetNewsWire RSS reader app (highly recommended!) that I cloned from GitHub some time ago uses it. Then I dug into the source code and saw a sample from there. This and some stack overflow discussions helped me to finish the job.

So here is a sample of the string dictionary (Finnish version):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
   <dict>
      <key>term(s) in category</key>
         <dict>
            <key>NSStringLocalizedFormatKey</key>
            <string>%#@terms@</string>
            <key>terms</key>
            <dict>
               <key>NSStringFormatSpecTypeKey</key>
               <string>NSStringPluralRuleType</string>
               <key>NSStringFormatValueTypeKey</key>
               <string>lld</string>
               <key>one</key>
               <string>Kategoriassa on yksi termi</string>
               <key>other</key>
               <string>Kategoriassa on %lld termiä</string>
               <key>zero</key>
               <string>Ei termejä tässä kategoriassa</string>
            </dict>
        </dict>
    </dict>
</plist>

And this is how to use it from code — the actual beef of the whole thing. All the complexity above is to make the code simple, even though you would add support for tens of different languages to the app.

First the actual Text element in SwiftUI view that shows the localised and pluralised text:

   Text(categorySubTitle())
       .foregroundColor(.secondary)

And the function categorySubTitle() that does the formatting:

private func categorySubTitle() -> String {
   let format = NSLocalizedString("term(s) in category", comment: "");
   return String.localizedStringWithFormat(format, category.termCount)
}

Simple — when you finally get the pieces together.

Hopefully I’ll get the app finished by Fall to be used by students and staff who find if useful. As a bonus, this will be a topic for a new demonstration in a GUI programming course I am participating as an assistant teacher next Spring.

There’s one potential obstacle though — seeing the new SwiftData and other new stuff coming out this week from WWDC2023 are soooo tempting….

But if I start working using those betas, I’d have to redo lots of stuff. Though since I am already using Core Data, that shouldn’t take too long. There is also the issue that then the apps (this works on macOS, iOS and iPadOS, sharing data via iCloud) would require macOS 14 Sonoma and iOS 17, and that might leave out many users who cannot upgrade or haven’t upgraded by the time courses begin… And I’d also pay some time to refactor the Java version too, that also needs some attention.

Oh well. But now some pizza ? and red wine ?!

Xcode quick action scripts to jump to GitHub or Bitbucket repository

Stumbled upon this handy little macOS quick action script project in GitHub which support opening the code selected in Xcode in a browser so that the selected code file in GitHub repository is opened and selected lines are highlighted in the browser. Or you could copy the link from Xcode to GitHub hosted project, while from Xcode, and share the link to someone by email or whatever.

Image from the original repo by wojteklu

Neat, wanted to try it out and cloned the project and installed the action scripts. That was easy, just double click on the scripts and they are installed.

I tried this with a project that is actually hosted in Bitbucket.org. Obviously it didn’t work out well. I forked the project, cloned it and copied and opened the action scripts in Automator app, edited the scripts to fit Bitbucket and now I have another pair of scripts which do work also with Bitbucket hosted repositories!

The changes to the scripts were quite easy to implement. Just change .com to .org, and instead of GitHub’s “blob” path, use the Bitbucket’s “src” path for the code files. And instead of addressing code lines the GitHub way (#L54-L58), use the Bitbucket’s way (#lines-33:35). Here’s an example link to a GitHub repo…

https://github.com/anttijuu/PluginDllDemo/blob/0f7b83f7ada5613cb0561a63a8c7d09f95f46144/EasyCryptoReversePlugin/EasyCryptoPlugin.cpp#L54-L58

… and here’s an example of a Bitbucket link (note: these links to BitBucket project ohar2014 are no longer valid since I have chopped the project into separate components in separate repositories):

https://bitbucket.org/anttijuu/ohar2014/src/e6fd26ef3d142d3da0cbe5969e6f037e465cede5/BaseLayer/ConfigurationDataItem.cpp#lines-33:35

The long code in both of the links is the current commit id of the version I’m viewing in Xcode. And here’s the relevant parts of my new shiny Action script source code…

on run {input, parameters}
	tell application "Xcode"
		set activeDocument to document 1 whose name ends with (word -1 of (get name of window 1))
		set activeDocumentPath to path of activeDocument
		set currentLines to selected paragraph range of activeDocument
		set startLine to get item 1 of currentLines
		set endLine to get item 2 of currentLines
	end tell
	
	set repositoryUrl to do shell script "cd $(dirname " & activeDocumentPath & "); git remote -v | grep fetch | awk '{print $2}' | sed 's/git@/https:\\/\\//' | sed 's/org:/org\\//' | sed 's/\\.git//'"
	
	set pathToRepositoryDirLocation to do shell script "cd $(dirname " & activeDocumentPath & "); git rev-parse --show-toplevel "
	
	set activeFilePathInRepository to replace_text(activeDocumentPath, pathToRepositoryDirLocation, "")
	
	set currentCommit to do shell script "cd $(dirname " & activeDocumentPath & "); git rev-parse HEAD"
	
	set bitbucketUrl to repositoryUrl & "/src/" & currentCommit & activeFilePathInRepository & "#lines-" & startLine & ":" & endLine
	
	set the clipboard to bitbucketUrl
	
	return input
end run

…and the script itself installed and visible in the Xcode menu!

Action scripts installed for Bitbucket.

I also made a pull request to the original project, in case he wants to merge my contribution into it.