How to localise CL applications in macOS (Xcode)? - swift

I am having trouble localising a command-line application I've created in Xcode. Here's what I did:
Used NSLocalizedString on all strings to be translated
Add base localisation, and the target localisation under Project settings
Add Strings file, check both English, and target language checkboxes
Create strings to be localised via genstring (both English and target language) and localised them as necessary
When I build the application, I still get all the text in English. What could be the issue here? My system is set to my target language, and when I try to add a scheme with the target language, I get the following error:
Error: 2 unexpected arguments: '(language_code)', 'NO'
This error is only present if I use swift-argument-parser for the application. For plain applications, it's still not possible to get the localised version.
Xcode 11.3, macOS10.15.3

Usually when you build a Cocoa application (with NSApplicationMain), the framework will take care of loading the appropriate localized strings from the main bundle. The framework will parse UserDefaults overrides from command line arguments, so you/Xcode can supply locale settings via flags like -AppleLanguages and -AppleLocale .
When you write a command-line utility, by default there is no bundle, no startup logic that performs localization, and no parsing of UserDefaults. Setting the target language from the scheme just adds the arguments (which is why you get an error from swift-argument-parser). However, you can load resources from a bundle yourself.
// Get the language from UserDefaults (to support scheme settings, will also need to parse command line arguments)
let language = UserDefaults.standard.stringArray(forKey: "AppleLanguages")?.first ?? "en"
// In addition to the command-line binary, you'll also need a bundle with the translations somewhere
let path = "/path/to/\(language).lproj"
let lproj = Bundle(path: path)
let localized = lproj?.localizedStringForKey("key", value: nil, table: nil)
Note that Bundle.main for an unbundled command line utility will just be the parent directory of wherever the binary is. So you can also use NSLocalizedString if your lproj files are built/installed in the appropriate place, but this would be unusual for utilities installed to binary directories like /usr/local/bin.

Related

Cannot localize my Swift 5 framework. Localization import does nothing

I develop a Swift 5 framework that runs on iOS 12 and above for iPhone and iPad.
I added French localization in addition to English, but the project info tab shows there's no localized files for both languages as shown on the image below:
When I run Xcode Editor|Export for localization I get the bundled files (including the fr.xliff file). I’ve edited fr.xliff with Xcode to translate some strings, and imported it back. But nothing changed in the app.
Btw, I can’t see any of the *.strings files in the project files hierarchy, which sounds weird.
[Update]
So I manually added Localizable.Strings file, and added English and French.
When I import xliff files, the Localizable.strings files remains empty. No apparent issue pops up.
When I put the key=value list in the string file, the NSLocalizedString() does not return the right localised version of the string (it always return the key).
Help appreciated.
Found what's wrong: I had to use the full (extended) version of NSLocalizedString() in order to make the function call get the translation from my framework Localizable.strings files (rather than the client application), as follows:
NSLocalizedString("Continue", tableName: tableName, bundle: bundle, comment: "Continue in the button title of UIAladdinOnboardingViewController")
Where:
tableName: a let global that hold the name of my localisable string file (in my case "Aladdin.Localizable"). I did this to avoid potential collision of .strings files (as my framework name is Aladdin);
bundle: a let global in my the framework name space to designates the framework model bundle to pick the string from. I defined it like this: let bundle = Bundle(for: ExtendedAppDelegate.self) where ExtendedAppDelegate is one of my framework custom class.
Side note of importance:
Btw, don’t put your interpolating string as the key parameter of the NSLocalizedString call, otherwise it’s gonna be interpolated at runtime, leading to an interpolated string that won't exist in your string file.
Don’t do that:
let s = NSLocalizedString("{Start your \(introductoryPeriod) free trial.}", tableName: tableName, bundle: bundle, comment: "in price display")
Do this:
let s = String(format: NSLocalizedString("{Start your %# free trial.}", tableName: tableName, bundle: bundle, comment: "{Start your \\(introductoryPeriod) free trial.}"), introductoryPeriod)
With the corresponding Aladdin.Localizable.strings (en):
/* {Start your \(introductoryPeriod) free trial.} (in price display) */
"{Start your %# free trial.}" = "{Start your %# free trial.}";
To generate the strings:
I used the gestrings from the terminal as follows:
$genstrings -o en.lproj/ *.swift
$genstrings -o fr.lproj/ *.swift
And so on for every target languages you have.
Finally, do the translation straight in the string files.
in Localization you can Localize your string directly from Storyboard.
after adding your language..click on Main.storyboard -> go to identity and type...
you can see your localize languages string...Image of your Main.strings -> select them select all Languages..
after that you can see main.strings(French) and main.strings(English) in your Navigation panel...
change your strings on prefer languages like...
Image of how to change your strings in English
Image of how to change your strings in French
now, change Your Simulator Language from Edit schema -> Run -> Your language(French or English).
it's working..

Swift NSLocalizedString practical use

I couldn't understand and find any sources that showed me the practical use of NSLocalizedString. Even Apple documentation..
NSLocalizedString(<#key: String#>, tableName: <#String?#>,
bundle: <#NSBundle#>, value: <#String#>, comment: <#String#>)
Ok so lets say I created this in a file, how do I write this for different languages?
Do I create a localized version of the whole file like: en.thisfile.swift , es.thisfile.swift, fr.thisfile.swift?
Or do I create new languages in different bundles or tables?
NSLocalizedString("hello", tableName: "en", value: "hello", comment: "wtf is this anyways?")
NSLocalizedString("hello", tableName: "fr", value: "bonjour", comment: "wtf is this anyways?")
NSLocalizedString("hello", tableName: "es", value: "hola", comment: "wtf is this anyways?")
And then how do I use it in a random file after setting it up?
println("dat nslocalizedstring I created, which will automatically know what to write??")
The table is the name of the file that defines the string, for example, table "ErrorMessages" will define a file called ErrorMessages.strings.
If no table is specified, the file will be the default Localizable.strings. The format of the file is a list of the following:
/* This is displayed when the entity is not found. */
"MSG_object_not_found" = "Object not found. Please, try again.";
Where the first line is the comment, the value on the left side is the key that identifies the string and the value on the right side is the actual localized value.
Usually, you can use the English value as the key (the 2 parameter version of NSLocalizedString). The language name (en, fr, es) is not part of the method call.
All of this is there to make translation simple. Note that most translators won't understand or even see the application, that's why it's important to have a good comment to provide a context for the message. There are examples of funny translation all over the internet which were created because the context was missing and the translators didn't know what they were translating.
Basically, your code will contain the texts in your main language and then you will generate the .strings files from your code using genstring tool (See this for more info). Then you will give the files to your translator, add the translated files to your application (every language in a different folder) and the application will select the correct files at runtime depending on the language selected by the user.

How to reliably detect os/platform in Go

Here's what I'm currently using, which I think gets the job done, but there's got to be a better way:
func isWindows() bool {
return os.PathSeparator == '\\' && os.PathListSeparator == ';'
}
As you can see, in my case all I need to know is how to detect windows but I'd like to know the way to detect any platform/os.
Play:
http://play.golang.org/p/r4lYWDJDxL
Detection at compile time
If you're doing this to have different implementations depending on the OS, it is more useful to
have separate files with the implementation of that feature and add build tags to each
of the files. This is used in many places in the standard library, for example in the os package.
These so-called "Build constraints" or "Build tags" are explained here.
Say you have the constant PATH_SEPARATOR and you want that platform-dependent, you
would make two files, one for Windows and one for the (UNIX) rest:
/project/path_windows.go
/project/path_unix.go
The code of these files would then be:
path_windows.go
// +build windows
package project
const PATH_SEPARATOR = '\\'
path_unix.go
// +build !windows
package project
const PATH_SEPARATOR = '/'
You can now access PATH_SEPARATOR in your code and have it platform dependant.
Detection at runtime
If you want to determine the operating system at runtime, use the runtime.GOOS
variable:
if runtime.GOOS == "windows" {
fmt.Println("Hello from Windows")
}
While this is compiled into the runtime and therefore ignores the environment,
you can nevertheless be relatively certain that the value is correct.
The reason for this is that every platform that is worth distinguishing needs
rebuilding due to different executable formats and thus has a new GOOS value.
Have you looked at the runtime package? It has a GOOS const: http://golang.org/pkg/runtime/#pkg-constants
It's 2022 and the correct answer for go 1.18+ is:
At runtime you want:
if runtime.GOOS == "windows" {
// windows specific code here...
}
If you need to determine the filesystem path separator character
Use: os.PathSeparator
Examples:
c:\program files
/usr/local/bin
If you need the Path List separator as used by the PATH environment variable
Use: os.PathListSeparator
Examples:
/usr/local/bin:/usr/local:
"C:\windows";"c:\windows\system32";
Since this is an older question and answer I have found another solution.
You could simply use the constants defined in the os package. This const returns a rune so you would need to use string conversion also.
string(os.PathSeparator)
string(os.PathListSeparator)
Example: https://play.golang.org/p/g6jnF7W5_pJ
I just stumbled on this looking for something else and noticed the age of this post so I'll add a more updated addition. If you're just trying to handle the correct filepath I would use filepath.Join(). Its takes all of the guesswork out of os issues. If there is more you need, other than just filepath, using the runtime constants (runtime.GOOS & runtime.GOARCH) are the way to go: playground example
I tested in Go 1.17.1 which really worked for me.
package main
import (
"fmt"
"runtime"
)
func main(){
fmt.Println(runtime.GOOS)
}
Output:
darwin
With regards to detecting the platform, you can use Distribution Detector project to detect the Linux distribution being run.
The first answer from #nemo is the most apropiate, i just wanted to point out that if you are currently a user of gopls language server the build tags may not work as intended.
There's no solution or workaround up to now, the most you can do is change your editor's lsp configs (vscode, neovim, emacs, etc) to select a build tag in order to being able to edit the files with that tag without errors.
Editing files with another tag will not work, and trying to select multiple tags fails as well.
This is the current progress of the issue github#go/x/tools/gopls

iOS how to localize files in for a module

I have a class that implements the "Take photo / Choose from library" that we all know and love. It is here https://github.com/fulldecent/FDTake This is included in my other projects via git submodule and that works fine.
Now I need to translate the text in that class to Chinese so it is "拍照 / 选照片" or something like that. Is there a good way to put translations in there so everyone can use them?
Localization is typically handled by the NSLocalizedString(<#key#>, <#comment#>) macro. In the source your replace all hard coded string with the macro. For example:
[self.buttonTitles addObject:#"Hi"]; // hard coded greeting
with
[self.theLabel setText:(NSLocalizedString(#"theKey", #"Hi"))];
Then genstrings (from inside a Terminal) is used to scan the implementations file (*.m) and to write its output to the language project folder (here: en.lproj)
$ genstrings -o en.lproj/ *.m
In the directory en.lproj/ a file called Localizable.strings. Its contents will be:
/* Hi */
"theKey" = "theKey";
The comment /* Hi */ is taken from our source code. The string Hi should be displayed to the (English speaking) user. So we need to edit the string on the right hand side of the equals sign and make it the greeting, = "theKey" has to become = "Hi":
/* Hi */
"theKey" = "Hi!";
So far so good
All this is fine if there are only a few strings or when there is no intend to ever modify the strings. The moment gestrings is run again it will overwrite the modifications and you effectively lose the work done in Localizable.strings.
An idea could be to write the output of genstrings to a different location. But then you will have to manually merge the changes. Once the Localizable.strings file grows it becomes a nightmare trying to keep source code and Localizable.strings in sync. So lets try to avoid that.
A big help comes from using NSLocalizedStringWithDefaultValue(<#key#>, <#tbl#>, <#bundle#>, <#val#>, <#comment#>). This macro will allow to set a default value in the Localizable.strings file and in addition, it will make the need for the initial edit of the value field go away.
Putting it together:
[self.theLabel setText:NSLocalizedStringWithDefaultValue(#"theKey2", #"Localizable", [NSBundle mainBundle], #"Hi!", #"informal greeting"))];
After running the genstrings command as used above there is now a small different in Localizable.strings
/* informal greeting */
"theKey2" = "Hi!";
Apart from the comment now telling the translator that we want an informal greeting, the “Hi!” is already present in the value filed. There is no need to go to the Localizable.strings file, search for the correct line, modify the field form “theKey” to “Hi!”. genstrings did that for us based on the default value supplied with NSLocalizedStringWithDefaultValue.
Add the file Localizable.strings to the xcode project.
Doing the translations
After changing the source code, for a new language from inside the Xcode first add a localization to Localizable.strings. Xcode will generate a copy of Localizable.strings under a subfolder based on the original Localizable.strings.
I personally don't speak Chinese, but German. So if to add German localization my translation would go under de.lproj/Localizable.strings, italian under it.lproj/ and so on.
Edit the new Localizable.strings as needed:
(German)
/* informal greeting */
"theKey2" = "Hallo!";
(Italian)
/* informal greeting */
"theKey2" = "Ciao!";
and then build and run.
********* begin edit
Bundle it up
The above considers a "standard" xcode project. You are asking about cereateing a module, therefore allowing your code to become an addition to a project. I suggest you create a bundle with the localizations. When somebody will include your code into their project the localizations remain separate.
Full documentation about bundles is here.
A project that uses bundles for localizations is QuincyKit (there probably are more, that one was the first that came to mind)
So when placing the localization into a bundle other than the mainBundle the [NSBundle mainBundle] in the line below has to change
[self.theLabel setText:NSLocalizedStringWithDefaultValue(#"theKey2", #"Localizable", [NSBundle mainBundle], #"Hi!", #"informal greeting"))];
Instead of getting the strings from the mainBundle, obtain a reference to your own module. The docs suggest:
NSBundle* myBundle = [NSBundle bundleForClass:[self class]];
So the line becomes:
[self.theLabel setText:NSLocalizedStringWithDefaultValue(#"theKey2", #"Localizable", myBundle, #"Hi!", #"informal greeting"))];
********* end edit
PS: : my original text can be seen here
I think a bundle (as Olaf suggests) would work great, but another way with less overhead to ensure that your localizable string resources don't interfere with another project in the same solution (causing a weird problem to people reusing your component in another localized project) is to change your localizable.strings file name to a unique file name. This means that where you used NSLocalizedString you now need to use its variant NSLocalizedStringFromTable (Apple documentation), where tableName is the same name as your strings file (without the .strings extension). You can define your own macro so that's it's just a straight replacement of NSLocalizedString with, say FDTakeLocalizedString.
A file name collision is much less likely to happen with XIBs files or storyboards than for the localizable.strings file. But in both cases, if you use a prefixed naming convention (say FDTake.strings and FDTake-Main.xib), it will eliminate the risk and can only help.

How to add an extra plist property using CMake?

I'm trying to add the item
<key>UIStatusBarHidden</key><true/>
to my plist that's auto-generated by CMake. For certain keys, it appears there are pre-defined ways to add an item; for example:
set(MACOSX_BUNDLE_ICON_FILE ${ICON})
But I can't find a way to add an arbitrary property.
I tried using the MACOSX_BUNDLE_INFO_PLIST target property as follows: I'd like the resulting plist to be identical to the old one, except with the new property I want, so I just copied the auto-generated plist and set that as my template. But the plist uses some Xcode variables, which also look like ${foo}, and CMake grumbles about this:
Syntax error in cmake code when
parsing string
<string>com.bedaire.${PRODUCT_NAME:identifier}</string>
syntax error, unexpected cal_SYMBOL,
expecting } (47)
Policy CMP0010 is not set: Bad
variable reference syntax is an error.
Run "cmake --help-policy CMP0010"
for policy details. Use the
cmake_policy command to set the
policy and suppress this warning. This
warning is for project developers.
Use -Wno-dev to suppress it.
In any case, I'm not even sure that this is the right thing to do. I can't find a good example or any good documentation about this. Ideally, I'd just let CMake generate everything as before, and just add a single extra line. What can I do?
Have you looked into copying the relevant *.plist.in file in /opt/local/share/cmake-2.8/Modules (such as MacOSXBundleInfo.plist.in), editing it to put <key>UIStatusBarHidden</key><true/> (or #VAR_TO_REPLACE_BY_CMAKE#), and adding the directory of the edited version in the CMAKE_MODULE_PATH?
If you have CMake installed as an app bundle, then the location of that file is /Applications/CMake.app/Contents/share/cmake-N.N/Modules
You can add your values using # and pass #ONLY to configure_file.
Unfortunately there is no simple way to add custom line to generated file.