How to format localised strings in Swift? - ios

I am learning to localise my app to Simplified Chinese. I am following this tutorial on how to do this.
Because the tutorial is based on Obj-C, formatted strings can be written like this:
"Yesterday you sold %# apps" = "Ayer le vendió %# aplicaciones";
"You like?" = "~Es bueno?~";
But I am using Swift. And in Swift I don't think you can use %# to indicate that there is something to be placed there. We have string interpolation right?
My app is kind of related to maths. And I want to display which input(s) is used to compute the result in a detailed label of a table view cell. For example
--------------
1234.5678
From x, y <---- Here is the detailed label
--------------
Here, From x, y means "The result is computed from x and y". I want to translate this to Chinese:
从 x, y 得出
Before, I can just use this:
"From \(someVariable)"
with the strings file:
"From" = "从 得出";
And this is how I would use it in code
"\(NSLocalizedString("From", comment: "")) \(someVariable)"
But if this were used in the Chinese version, the final string will be like this:
"从 得出 x, y"
I mean I can put the 从 and 得出 in two different entries in the strings file. But is there a better way to do it?

You can use %# in Swift's String(format:...), it can be substituted
by a Swift String or any instance of a NSObject subclass.
For example, if the Localizable.strings file contains the definition
"From %#, %#" = "从 %#, %# 得出";
then
let x = 1.2
let y = 2.4
let text = String(format: NSLocalizedString("From %#, %#", comment: ""), "\(x)", "\(y)")
// Or alternatively:
let text = String(format: NSLocalizedString("From %#, %#", comment: ""), NSNumber(double: x), NSNumber(double: y))
produces "从 1.2, 2.4 得出". Another option would be to use the
%f format for double floating point numbers:
"From %f, %f" = "从 %f, %f 得出";
with
let text = String(format: NSLocalizedString("From %f, %f", comment: ""), x, y)
See Niklas' answer
for an even better solution which localizes the number representation
as well.

From WWDC 2017:
let format = NSLocalizedString("%d popular languages", comment:"Number of popular languages")
label.text = String.localizedStringWithFormat(format, popularLanguages.count)

Swift localize a string
One more simple example
let changeable = "something"
let result = String(format: NSLocalizedString("stringId", comment: ""), arguments: [changeable]) // Hello World and something
localizable.strings with
"stringId" = "Hello World and %#";
comment parameter doesn't have effect on result and is used for translators and by genstrings code-gen as comment

New in iOS 15 and macOS Monterey you can use the new refined method for String.
String(localized: "From \(x), \(y)", comment: "The result is computed from x and y")
They did a lot of updates in 2021 for localization with Xcode. Check this video from WWDC21 for more info.
https://developer.apple.com/videos/play/wwdc2021/10221/

In objective C, if we want to get strings added at runtime as below
John Appleseed is the name
YourLocalizable.strings
"theStringToDisplay" = "%# is the name";
ViewController.m
NSString *username = #"John Appleseed";
NSString *messageBeforeFormat = NSLocalizedStringFromTable(#"theStringToDisplay", #"YourLocalizable", nil);
NSString *messageAfterFormat = [NSString stringWithFormat:messageBeforeFormat, username ];
self.yourLabel.text = messageAfterFormat;
Further explanation in this nice post
https://www.oneskyapp.com/academy/learn-ios-localization/2-format-strings-plurals/

Related

Localizable.strings key-value pair

I want to localize "1 of 9", and 1 and 9 are int parameters, my code is as below
context = [NSString stringWithFormat:NSLocalizedString(#"%d of %d",
"This text will be used to show page number on the screen"),
currentPageIndex + 1, pageCount];
And the generated Localizable.strings show like that
/* This text will be used to show page number on the screen */
"%d of %d" = "%1$d of %2$d";
I think the thing on the left of "=" is key and the the thing on the right of "=" is value, but I want the key looks like "show_page_number" not included the format "%d", how can I do? I try to replace "%d of %d" with "show_page_number", but it does not work. Any advice?
NSLocalizedString() replaced key with value at runtime .So you can use anystring as key & it will be replaced as "%1$d of %2$d" at runtime.
Add string in Localizable file :
"show_page_number" = "%1$d of %2$d";
& in code use that key
context = [NSString stringWithFormat:NSLocalizedString(#"show_page_number", "This text will be used to show page number on the screen"), currentPageIndex + 1, pageCount];
Add localizable.string file in xcode project.
If you want full control over the key and its initial value you need to use NSLocalizedStringWithDefaultValue instead of NSLocalizedString (which uses the key as the initial value).
Change your code to:
NSString *format = NSLocalizedStringWithDefaultValue(#"show_page_number", #"Localizable", NSBundle.mainBundle, #"%1$d of %2$d", #"This text will be used to show page number on the screen")
context = [NSString stringWithFormat:format, currentPageIndex + 1, pageCount];
When you run genstrings, you will get:
/* This text will be used to show page number on the screen */
"show_page_number" = "%1$d of %2$d";

How should I localise with multiple parameters?

Let's say I have the following string:
[NSString stringWithFormat:#"Booked for %# at %#", colleagueName, time];
And I realise I've forgotten to localise that string, so I replace it:
[NSString stringWithFormat:NSLocalizedString(#"bookings.bookedFor", "Booked for user at time"), colleagueName, time];
Now when doing translations, I find that the language X needs the parameters the other way round; something closer to:
<time> for booking of <colleague> is done.
What is the best way to address the fact that now I need the second parameter of my formatted string to be time and the third to be colleagueName please?
As is often the case, my colleague found the solution almost as soon as I had asked on here! Apparently Objective-C has positional arguments
The positions are 1-indexed so %1$# refers to the first argument.
NSString *firstParam = #"1st";
NSString *secondParam = #"2nd";
NSLog(#"First %1$# Second: %2$#", firstParam, secondParam);
NSLog(#"Second %2$# First: %1$#", firstParam, secondParam);
This prints:
First 1st Second: 2nd
Second 2nd First: 1st
You can try like this:
NSString * language = [[NSLocale preferredLanguages] objectAtIndex:0];
if ([language isEqualToString:#"X"]) {//X is language code like "fr","de"
[NSString stringWithFormat:NSLocalizedString(#"bookings.bookedFor", "Booked for user at time"), time, colleagueName];
} else {
[NSString stringWithFormat:NSLocalizedString(#"bookings.bookedFor", "Booked for user at time"), colleagueName,time];
}

In Swift, how to insert formatted Int value into String?

This seems like a super basic question, but I just can't seem to find the answer anywhere :-( I am able to do this in Objective C, but I am getting stuck in Swift.
What I need to do:
Take an Integer value
Format it into a localized string
Inject the value into another string using the stringWithFormat equivalent method (since the other string is localized as well, which is not shown in simplified examples below)
How it's easily done in Objective C -- this works:
// points is of type NSNumber *
NSNumberFormatter *formatter = [NSNumberFormatter new];
formatter.locale = [NSLocale currentLocale];
formatter.numberStyle = NSNumberFormatterDecimalStyle;
NSString *ptsString = [formatter stringFromNumber:points];
NSString *message = [NSString stringWithFormat:#"You've earned %# points", ptsString];
My best attempt at doing this in Swift -- compiler error on last line:
// points is of type Int
let formatter = NSNumberFormatter()
formatter.locale = NSLocale.currentLocale()
formatter.numberStyle = NSNumberFormatterStyle.DecimalStyle
let ptsString = formatter.stringFromNumber(points)!
let message = String(format: "You've earned %# points", arguments: ptsString)
I'm getting the following error in Xcode on that last line:
"Cannot convert value of type 'String' to expected argument type '[CVarArgType]'"
(In my actual code, the message into which I want to insert the points value is itself localized as well, but I have simplified this example, as I'm getting the exact same error in both cases.)
What am I missing here..?
Thanks so much for any help,
Erik
You need to wrap the arguments in a collection. Like this:
let message = String(format: "You've earned %# points", arguments: [ptsString])
You can also use this method:
let message = "You've earned \(ptsString) points"
Additionally you can create an extension method to do this:
extension String {
func format(parameters: CVarArgType...) -> String {
return String(format: self, arguments: parameters)
}
}
Now you can do this:
let message = "You've earned %# points".format("test")
let message2params = "You've earned %# points %#".format("test1", "test2")
Sometimes, you need a little more control - so if you need to have leading zeros, you could use 'stringWithFormat' just like in objective-C
let ptsString = String(format: "%02d", points)

Compare NSString to ameliorate OCR

I'm working on an application in objective C (osX) in which I use an 'OCR' - I use 'Tesseract' which is really good. The problem is, there is some expressions that are not well recognized. In order to get a good result, I would try to make some string comparison and changes.
In other words, my algorithm would look like this :
myNewString = #"tept String";
if ( myNewString isEqualAt80%orMoreToString : #"test String" )
{
myNewString = #"test String";
}
But how could I test if my 'NSString' is at minimum equal at 80% or more than another string ?
Thank you for you help !

What characters are allowed in a iOS file name?

I'm looking for a way to make sure a string can be used as a file name under iOS. I'm currently in the section of the code that deletes incompatible characters. I'm wondering if I'm doing it right.
NSString *filename = #"A file name";
fileName = [fileName stringByTrimmingCharactersInSet: [NSCharacterSet controlCharacterSet]];
fileName = [fileName stringByTrimmingCharactersInSet: [NSCharacterSet newlineCharacterSet]];
I'm also wondering if there's already a method that validates a string as a file name.
Thank you for your advice!
Use RegEx:
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"[^a-zA-Z0-9_]+" options:0 error:nil];
filename = [regex stringByReplacingMatchesInString:filename options:0 range:NSMakeRange(0, filename.length) withTemplate:#"-"];
I find this to be cleaner and probably much more performant. This is based on Angel Naydenov's solution, but first constructing Character set with all invalid characters and then calling components(separatedBy:) just once.
Swift 3 & 4
var invalidCharacters = CharacterSet(charactersIn: ":/")
invalidCharacters.formUnion(.newlines)
invalidCharacters.formUnion(.illegalCharacters)
invalidCharacters.formUnion(.controlCharacters)
let newFilename = originalFilename
.components(separatedBy: invalidCharacters)
.joined(separator: "")
Swift 2
let invalidCharacters = NSMutableCharacterSet(charactersInString: ":/")
invalidCharacters.formUnionWithCharacterSet(NSCharacterSet.newlineCharacterSet())
invalidCharacters.formUnionWithCharacterSet(NSCharacterSet.illegalCharacterSet())
invalidCharacters.formUnionWithCharacterSet(NSCharacterSet.controlCharacterSet())
let filename = originalFilename
.componentsSeparatedByCharactersInSet(invalidCharacters)
.joinWithSeparator("")
First of all, you're using the wrong method. Trimming the string will only remove characters in the beginning and the end of the string.
What you're looking for is something more like:
fileName = [fileName stringByReplacingOccurrencesOfString:#"/" withString:#"_"];
However, that's a suboptimal solution, since you'll have to do that for every character you want to exclude, so maybe you want to keep looking or write you're own method for manipulating the string.
iOS is UNIX based and as such I suppose it supports almost any characters in filenames. UNIX allows white spaces, <, >, |, \, :, (, ), &, ;, as well as wildcards such as ? and *, to be quoted or escaped using \ symbol. However I wouldn't use any of those characters in my filenames. In fact, I would restrict the characters in my filenames to 'a'-'z', '0'-'9', '_' and '.'.
As I did not see a list with allowed characters in this question but the question wanted a list with such characters I am adding a bit more details on this topic.
First we need to know what is the file system that iOS devices use. Using multiple online sources this seems to be HFSX which is the HFS+ case sensitive version. And including one link here for reference: https://apple.stackexchange.com/questions/83671/what-filesystem-does-ios-use
Now that we know what the file system is we can look for what characters are not allowed. And these seem to be: colon (:) and slash (/). Here is a link for reference: http://www.comentum.com/File-Systems-HFS-FAT-UFS.html
Having this information and what others have written in this thread my personal preference for removing not allowed characters from file names is the following Swift code:
filename = "-".join(filename.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet()))
filename = "-".join(filename.componentsSeparatedByCharactersInSet(NSCharacterSet.illegalCharacterSet()))
filename = "-".join(filename.componentsSeparatedByCharactersInSet(NSCharacterSet.controlCharacterSet()))
filename = "-".join(filename.componentsSeparatedByString(":"))
filename = "-".join(filename.componentsSeparatedByString("/"))
The reason I am not preferring the RegEx approach is that it seems too restrictive to me. I do not want to restrict my users only to Latin characters. They may as well wish to use some Chinese, Cyrillic or whatever else they like.
Happy coding!
I've had to save remote files locally with filenames containing other characters than basic alpha-numeric characters. I use the method below to strip out potential invalid characters, ensuring it's a valid filename for the filesystem when generating a NSURL using URLWithString:
filename = [[filename componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] componentsJoinedByString:#"" ];
filename = [[filename componentsSeparatedByCharactersInSet:[NSCharacterSet illegalCharacterSet]] componentsJoinedByString:#"" ];
filename = [[filename componentsSeparatedByCharactersInSet:[NSCharacterSet symbolCharacterSet]] componentsJoinedByString:#"" ];
fileURLString = [NSTemporaryDirectory() stringByAppendingPathComponent:filename];
fileURL = [NSURL URLWithString:fileURLString];
You may also want to test for collision errors first using:
[[NSFileManager defaultManager] fileExistsAtPath:[fileURL absoluteString]]
This String extension (Swift 4.2) will help convert an invalid iOS file name to a valid iOS file name.
extension String {
func convertToValidFileName() -> String {
let invalidFileNameCharactersRegex = "[^a-zA-Z0-9_]+"
let fullRange = startIndex..<endIndex
let validName = replacingOccurrences(of: invalidFileNameCharactersRegex,
with: "-",
options: .regularExpression,
range: fullRange)
return validName
}
}
For example
"name.name?/!!23$$#1asd".convertToValudFileName() // "name-name-23-1asd"
"!Hello.312,^%-0//\r\r".convertToValidFileName() // "-Hello-312-0-"
"/foo/bar/pop?soda=yes|please".convertToValidFileName() // "-foo-bar-pop-soda-yes-please"
I'm pretty happy with this solution:
NSString *testString = #"This*is::/legal.😀,?縦書き 123";
NSString *result = [[[testString componentsSeparatedByCharactersInSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"length > 0"]] componentsJoinedByString:#"-"];
Output:
"This-is-legal-縦書き-123"
What is this sorcery?
Let me break it up into multiple lines so it's clear what's going on:
NSString *testString = #"This*is::/legal.😀,?縦書き 123";
// Get a character set for everything that's NOT alphanumeric.
NSCharacterSet *nonAlphanumericCharacterSet = [[NSCharacterSet alphanumericCharacterSet] invertedSet];
// Split the string on each non-alphanumeric character, thus removing them.
NSArray *cleanedUpComponentsWithBlanks = [testString componentsSeparatedByCharactersInSet:nonAlphanumericCharacterSet];
// Filter out empty strings ("length" is a KVO-compliant property that the predicate can call on each NSString in the array).
NSArray *cleanedUpComponentsWithoutBlanks = [cleanedUpComponentsWithBlanks filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"length > 0"]];
// Put the components back together and join them with a "-".
NSString *result = [cleanedUpComponentsWithoutBlanks componentsJoinedByString:#"-"];
Enjoy!
Swift 4 Version
Added by john-pang on 2021-09-01 with Swift version:
let testString = "This*is::/legal.😀,?縦書き 123"
// Get a character set for everything that's NOT alphanumeric.
let nonAlphanumericCharacterSet = CharacterSet.alphanumerics.inverted
// Split the string on each non-alphanumeric character, thus removing them.
let cleanedUpComponentsWithBlanks = testString.components(separatedBy: nonAlphanumericCharacterSet)
// Filter out empty strings ("length" is a KVO-compliant property that the predicate can call on each NSString in the array).
let cleanedUpComponentsWithoutBlanks = cleanedUpComponentsWithBlanks.filter { $0.length > 0 }
// Put the components back together and join them with a "-".
let result = cleanedUpComponentsWithoutBlanks.joined(separator: "_")
I came up with the following solution. Works nice so far.
import Foundation
extension String {
func removeUnsupportedCharactersForFileName() -> String {
var cleanString = self
["?", "/", "\\", "*"].forEach {
cleanString = cleanString.replacingOccurrences(of: $0, with: "-")
}
return cleanString
}
}
let a = "***???foo.png"
let validString = a.removeUnsupportedCharactersForFileName()
Base on Marian Answers, here is a string extension to remove any unwanted characters.
extension String {
func stripCharacters() -> String {
var invalidCharacters = CharacterSet(charactersIn: ":/")
invalidCharacters.formUnion(.newlines)
invalidCharacters.formUnion(.illegalCharacters)
invalidCharacters.formUnion(.controlCharacters)
let newString = self
.components(separatedBy: invalidCharacters)
.joined(separator: "_")
return newString
}
}
Example:
let fileName = "Man(lop23/45"
let newFileName = fileName.stripCharacters()
print(newFileName)
Swift 5 extension:
I wanted to remove emojis as well and in windows \ is also an invalid character. So I added symbols charset and backslash \ as well.
extension String {
var validFilename: String {
let invalidCharsets = CharacterSet(charactersIn: ":/\\")
.union(.illegalCharacters)
.union(.controlCharacters)
.union(.symbols)
.union(.newlines)
return self.components(separatedBy: invalidCharsets).joined()
}
}

Resources