When we use an English app, many people will immediately check whether there is a Corresponding Chinese version. This shows how important it is to display the most user-friendly language text in your app. For a significant number of apps, if you can localize the text displayed in the UI, you’re basically done localizing the app. In this article, we’ll explore how to localize display text in iOS development. The Demo of this article is written with SwiftUI.

The original post was posted on my blog wwww.fatbobman.com

How text localization works

As a programmer, if you were to consider designing a logic to localize the original text for different languages, I think most people would consider a dictionary (key-value pair) solution. Apple does the same thing. By creating multiple dictionaries for different languages, the system can easily find the localized text (value) corresponding to the original text (key). Such as:

//en 
"hello" = "Hello";
Copy the code
//zh
"hello" = "Hello";
Copy the code

This set of methods is the main text localization method adopted in this paper.

System at the time of the compiled code, will be able to localize the operation of the text has carried on the tag, when the app runs in a different language environment (e.g., French), the system will try as far as possible from French text keys to replace the contents of the file to find out the corresponding, if can’t find will continue to find in accordance with the order of the language preference list. For some types, such as LocalizedStringKey, this is done automatically, but for strings, which are most commonly used in code, you need to do this explicitly.

Fortunately, most of SwiftUI’s controls (some of which are currently buggy) prefer the LocalizedStringKey constructor for text types, which greatly relieves developers of manual processing.

Add language

The ability to develop internationally is a must-have feature for modern programming languages and development environments. When we create a project in Xcode, by default the app is developed only for its corresponding Development Language.

So we must first let the project know that we are going to localize the project and choose the appropriate language.

In Project Navigation, click Project and select Info to add languages in Localizations.

Click the + sign to select the language we will add.

Here we are simply telling the project that we will probably localize the languages in the list. But how to localize, localize those files and resources, we need to set them up separately.

When you enable Use Base Internationalization, Xcode will modify your project folder structure. The xib and StoreyBoard files will be moved to the base.lproj folder, and the string elements will be extracted to the project locale folder. This option is for development using Storyboard, so don’t worry if you use SwiftUI.

For the UIKit framework, Xcode will let you choosestoryboardAs used in this articleThe Demo projectFor all SwiftUI architecture, thereforeDon’tHere’s the picture.

Create a text string file

In apple’s development environment, the file type corresponding to the string file (text key-value pair file) we mentioned above is strings. We can create multiple string files in an app, and some string files with names have special meanings.

  • Localizable.strings

    UI default corresponding string file. Unless the name of the string file is specified, the app gets the corresponding localized text content from Localizable. Strings

  • InfoPlist.strings

    String file corresponding to info.plist. It is usually used for localization of app names, permissions warning prompts and other content.

In Project Navigation, we select New file

Select Strings File for the File type and name it Localizable. Strings

The Localizable. Strings file is not localized. Currently, there is only one file in your project, in which text key pairs are defined. Button to select the language file to generate Localizable. Strings (language list for adding language Settings to the project).

Check both languages on the right

Localizable. Strins in the left Project Navigation will become the following state:

English and Chinese are currently empty files, so we can now create the corresponding text key-value pairs here.

You can download the Demo project here

Actual combat 1: The list name of the Chinese bill form

In this section we try to provide the corresponding Localized Chinese text for ITEM, QUANTITY, UNIT PRICE, and AMOUNT.

Following the key/value pair declaration rule above, we add the following to the Localizable.Strings(Chinses) file:

"ITEM" = "Kind";
"QUANTITY" = "The number";
"UNIT PRICE" = "The price";
"AMOUNT" = "Total";
Copy the code

Open the TableView and add the localization environment configuration in the preview

 TableView()
            .environmentObject(Order.sampleOrder)
            .previewLayout(.sizeThatFits)
            .environment(\.locale,Locale(identifier: "zh"))
Copy the code

What changes do we see from the Preview area at this point? Nothing has changed!

The reason is that the keys we set up in the string file are problematic. The corresponding code in TableView for the ITEM we see in app rendering is as follows:

 HStack{
            Text("Item")
                .frame(maxWidth:.infinity)
            Text("Quantity")
                .frame(maxWidth:.infinity)
            Text("Unit Price")
                .frame(maxWidth:.infinity)
            Text("Amount")
                .frame(maxWidth:.infinity)
        }
        .foregroundStyle(.primary)
        .textCase(.uppercase) // Convert to uppercase
Copy the code

Text will use Item as the lookup Key, but we defined it as Item, so we didn’t find the corresponding value. Note: Keys in string files are small capitalization sensitive.

Modify the Chinese file as follows:

"Item" = "Kind";
"Quantity" = "The number";
"Unit Price" = "The price";
"Amount" = "Total";
Copy the code

At this point, in the preview window, we can see the result of The Localization:

Congratulations, you’ve got most of text localization covered by now.

I don’t know if you noticed, so farEnglishThe file was empty,ChineseWe only have localized text for four of the files. Anything we don’t set, the app will display the original text that we set in the code.

When defining in a string file, it is easy to make two mistakes: 1) enter Chinese punctuation incorrectly, and 2) forget the semicolon behind it.

Actual combat 2: The Chinese payment button

In this section we try to combine the text “Pay for 4 drinks” with Chinese culture.

This button is defined in the ButtonGroupView as follows:

 Button {
      showPayResult.toggle()
    } label: {
      Text("Pay for \(order.totalQuantity) drinks")}Copy the code

Pay for \(order.totalquantity) drinks how to set the corresponding key in Localizable. Strings file?

For localized Strings that use string interpolation, we need to use string format specifiers, and apple’s official documentation provides a detailed comparison.

In the code, order.totalQuantity corresponds to Int (Swift Int corresponds to Int64 on 64-bit systems), so we need to replace it with % LLD in the key-value pair. Make the following definitions in the Chinese file:

"Pay for %lld drinks" = "Pay for % LLD cup drinks";
Copy the code

So we get what we want. As you try to add or decrease the number of drinks, the number of drinks in the text changes.

Select the correct format specifier for your interpolation, as %d in the example above will be treated as another key and cannot be converted.

Actual combat 3: The program name of the Chinese App

In Xcode projects, we usually configure certain system parameters, such as Bundle identifier, Bundle name, etc., in the info. plist file. If we need to localize some of these configurations, we can use the infoplist.strings mentioned above

We create a string file named Infoplist. strings using the same steps used to create the Localizable. Strings file (don’t forget to localize the created file to make sure both Chinese and English are checked).

Add the following contents to the Chinese and English files of Infolist. strings respectively:

//chinese
"CFBundleDisplayName" = Fat Toot Toot.;
//english
"CFBundleDisplayName" = "FatbobBar";
Copy the code

At this point, install the APP on the simulator or real machine, and the name of the app will display the corresponding text in different languages.

In the last two versions of Xcode, it is not possible to set info.plist directly, usually viewing or modifying the value in Target’s Info

The configuration we need to localize doesn’t have to be in info or info.plist, as long as we localize the key-value pairs in InfoPlist. Strings, the app will take precedence. Normally we’ll do this in InfoPlist. Strings and in addition to the name of the app CFBundleDisplayName, There are CFBundleName, CFBundleShortVersionString, NSHumanReadableCopyright and various application description system permissions, Such as NSAppleMusicUsageDescription, NSCameraUsageDescription, etc. See the official documentation for more information about the info.plist parameter

Practice 4: Localize drink names

Add the following to the Localizable(Chinese) string file

"Orange Juice" = "Orange juice";
"Tea" = "Tea";
"Coffee" = "Coffee";
"Coke" = Happy Water;
"Sprite" = "Heart to heart.";
Copy the code

Check out the definition of beverageModel/Drink.swiftcode

View the preview by setting local environment variables, either changing the emulator language to Chinese, or changing App Lanuguage to Chinese in Scheme.

We didn’t get the results we expected when we implemented the app. The name of the drink did not become Chinese. We can find out why by looking at drink. swift: Text does not treat a String as LocalizedStringKey if it is already specified.

Previously in ItemRowView, we displayed the drink name with the following code:

Text(item.drink.name)
          .padding(.leading,20)
          .frame(maxWidth:.infinity,alignment: .leading)
Copy the code

The name of a Drink is defined as follows in Drink

struct Drink:Identifiable.Hashable.Comparable{
    let id = UUID(a)let name:String / / type String
    let price:Double
    let calories:Double
Copy the code

So the easiest way to do this is to modify the ItemRowView code

Text(LocalizedStringKey(item.drink.name))
         .padding(.leading,20)
         .frame(maxWidth:.infinity,alignment: .leading)
Copy the code

In some cases, we can only get String data, and we may often do similar conversions

Run it again and you will see that the drink name in the table has been changed to the correct Chinese display

Also modify the code in ItemListView:

/ / will be
Button(drink.name)
/ / to
Button(LocalizedStringKey(drink.name)) 
Copy the code

The list of drinks added is now displayed properly:

The modified code can normally display the Chinese name of the beverage.

The above method is a good way to solve problems in most cases, but it is not suitable to rely solely on Export Localizations… Generate a project to localize key-value pairs. Currently Xcode15 does not output text constructed using LocalizdStringKey.

In order to more accurately sort the localized text, we can also make a further change to Drink’s comparison function:

/ / will be
lhs.name < rhs.name
/ / to
NSLocalizedString(lhs.name,comment: "") < NSLocalizedString(rhs.name,comment: "")
Copy the code

NSLocalizedString can get the corresponding text value from the given text key

Will the InfoView

var list:String {
        order.list.map(\.drink.name).joined(separator: "")}Copy the code

To:

order.list.map{NSLocalizedString($0.drink.name, comment: "")}.joined(separator: "")
Copy the code

Can’t we just do itDrinkthenameIs defined asLocalizedStringKeyType?

Because the LocalizedStringKey does not support Identifiable, Hashable, Comparable agreement, also did not provide any official LocalizedStringKey converted into a String. Therefore, if we want to define name as LocalizedStringKey, we need to use some special means (through Mirror, which we won’t cover in this article).

Create a string dictionary file

Some problems that don’t exist in Chinese are big problems in other languages. Typical examples are complex numbers. If your app is only in English and needs to deal with fewer nouns, you can probably kill the plural rule in code. Such as:

if cups < = 1 {
  cupstring = "cup"
}
else {
  cupstring = = "cups"
}
Copy the code

However, this is not good for code maintenance, and it is not flexible for some languages with complex complex complex rules (such as Russian, Arabic, etc.).

To solve the problem of defining rules for complex numbers in different languages, Apple provides an alternative to strings: stringdict dictionary files.

It is a property list file with the extension of the.stringsdict file, and is treated exactly the same as editing any other property list (such as info.plist).

.stringsDict was originally developed to deal with complex numbers, but over the years it has been added to display different text for different values (often used for screen size variations) and for specific platforms (iPhone, iPad, MAC, TVOs).

In the figure above, we have formulated the use separatelyNSStringLocalizedFormatKeyThe complex number rule,NSStringVariableWidthRuleTypeVariable width rules as wellNSStringDeviceSpecificRuleTypeDevice-specific content rules

.stringdict has a root node called the Strings Dictionary, under which all our rules are built. We need to first build a Dictionary for each rule. In the figure above, the keys corresponding to the three rules are device % LLD, GDP, and Book % LLD cups. When the program encounters text content that meets these three key definitions, it uses its corresponding rules to generate the correct localized content.

So even though it looks a little different than strings, it actually has the same internal logic.

  • We can make any number of rules in it.
  • The default corresponding string dictionary file nameLocalizable.stringsdict.
  • .stringdictThe execution priority of the.stringsFor example, we are right in both filesGDPIf this parameter is defined, it will only be used.stringdictCorresponding content

Lay down rules for complex numbers

  • The meaning of quantitative categories depends on the language, and not all languages have the same category.

    For example, English only uses the one and other categories for plural forms. Arabic has different plural forms for categories zero, one, two, few, many, and other. Although Russian also uses the many category, the rules in the number many category are different from the Rules in Arabic.

  • All categories except other are optional.

    However, if you do not provide rules for all specific language categories, your text may not be syntactically correct. Instead, if you provide a rule for a category that the language does not use, it is ignored and the other format string is used.

  • In a zero, one, two, what the things, other use NSStringFormatValueTypeKey format specifier in the format string is optional. For example, when the number is 1, the one cup is returned, and the corresponding % LLD does not need to be included

How to define complex rules in each language please see the Official UNICODE documentation

Variable width rule

Unlike complex and device rules, the system does not automatically match the return value and requires the user to explicitly annotate it when defining localized text, such as:

let gdp = (NSLocalizedString("GDP",comment: "") as NSString).variantFittingPresentationWidth(25)
Text(gdp) // Return GDP(Billon Dollor)
let gdp = (NSLocalizedString("GDP",comment: "") as NSString).variantFittingPresentationWidth(100)
Text(gdp) // Return anything you want to talk about
Copy the code

When there are no identical numbers, the nearest one is returned.

Its usage scenarios, I feel, are not irreplaceable. After all, there is more participation in the code.

Device specific rule

Currently, the following devices are supported: AppleTV, Apple Watch, iPad, iPhone, iPod, and MAC

The user does not need to intervene in the code, and the system will return the corresponding content based on the user’s hardware device

Actual Combat 5: Reset the payment button

Complete payment buttons with complex rules.

The code for the payment button is in ButtonView:

Button {
     showPayResult.toggle()
   } label: {
      Text("Pay for \(order.totalQuantity) drinks")}Copy the code

We need to set “Pay for \(order. TotalQuantity) drinks”.

Start by creating the Localizable. Stringsdict file

For English, we need to set zero, one, and other. Set the following parameters in English:

In Chinese, just set zero and other

Adjust the order quantity, and the button will return the corresponding localized text according to the different language and different order quantity

In Actual Combat 2, we set key-value pairs for Pay for % LLD drinks in Localizable. Strings, but since.dict has a higher priority, NSStringPluralRuleType is preferred.

Actual Combat 6: Poke me or point me

The add drink button displays different content depending on the device.

For example, we can display TAP on iPhone and iPad, Select on AppleTV, and Click on MAC

Add in Chinese

Add in English

Formatter Formats the output

It’s not enough to just localize the display label. There are also a lot of numbers, dates, currencies, units of measure, names, and so on that need to be localized.

Apple has invested significant resources to provide developers with a complete solution, Formatter.

This year (2021), Apple will make further updates to Formatter that not only improve the ease of calling from Swift, but also allow developers to create bi-directional format conversions through the new FormatStyle protocol.

Formatter is too much to cover in a single article. A few examples in the Demo will give you a basic idea.

Actual Combat 7: Date, currency, percentage

The date of

Text(order.date,style: .date) // Displays the year, month and day
Text(order.date.formatted(.dateTime.weekday())) // Display the week
Copy the code

In the Demo we localized the date display in two ways.

  • Text itself supports formatted output of dates, although this is not very customizable.

  • The new FormatStyle is used to chain the output:

    Order.date.formatted (.datetime.weekday ()) will display only the day of the week

currency

  • Create the NumberFormatter
      private func currencyFormatter(a) -> NumberFormatter {
          let formatter = NumberFormatter()
          formatter.numberStyle = .currency
          formatter.maximumFractionDigits = 2
          if locale.identifier ! = "zh_CN" {
              formatter.locale = Locale(identifier: "en-us")}return formatter
      }
Copy the code

The Demo only provides the prices of two currencies. If the region of the system is not set in mainland China, the currency will be set to USD.

  • Apply Formatter to Text
Text(NSNumber(value: item.amount),formatter:currencyFormatter() )
Copy the code

Since Formatter can only be used with NSObject in Text, you need to convert Double to NSNumber.

FormatStyle currently provides too few configurable items for Currency.

The percentage

 Text(order.tax.formatted(.percent))
Copy the code

Use formatStyle directly.

Practice 8: Units of measurement, sequence

calories

Use MeasureMent to define units of energy. A MeasureMent object represents a quantity and unit of MeasureMent. Measurement types provide a programming interface for converting measurements into different units and for calculating the sum or difference between two measurements.

init(name: String.price: Double.calories: Double) {
        self.name = String.localizedStringWithFormat(NSLocalizedString(name, comment: name))
        self.price = price
        self.calories = Measurement<UnitEnergy>(value:calories,unit: .calories) // Set the raw data to calorie
    }
Copy the code

The measurement object can also perform data calculation:

    var totalCalories:Measurement<UnitEnergy>{
        items.keys.map{ drink in
            drink.calories * Double(items[drink] ?? 0)
        }.reduce(Measurement<UnitEnergy>(value: 0, unit: .calories), +)}Copy the code

Create a Formatter describing MeasureMent

    var measureFormatter:MeasurementFormatter{
        let formatter = MeasurementFormatter()
        formatter.unitStyle = .medium
        return formatter
    }
Copy the code

Displayed in SwiftUI

Text(order.totalCalories,formatter: measureFormatter)
Copy the code

The sequence

Create hyphenation patterns (punctuation, and or, etc.) that fit different language conventions.

    var list:String {
        order.list.map{NSLocalizedString($0.drink.name, comment: "")}.formatted(.list(type: .and))
    }
Copy the code

other

Use tabName to specify a particular named string file

Multiple string files can be created, and when the file name is not Localizabl, we need to specify the file name, such as Other.strings

Text("Item",tableName: "Other")
Copy the code

TableName also works with.dict

Specify string files in other bundles

If your app uses other bundles that contain multilingual resources, you can specify string files from other bundles

import MultiLanguPackage // ML
Text("some text",bundle:ML.self)
Copy the code

In a Package containing multilingual resources, you can specify bundles using the following code

Text("some text",bundle:Self.self) 
Copy the code

Markdown symbol support

Apple announced at WWDC 2021 that some markdown symbols can be used directly in Text. Such as:

Text("**Hello** *\(year)*")
Copy the code

We can also use the markdown symbol in string files

"**Hello** *%lld*" = "** hello ** *% LLD *";Copy the code

In addition, the new AttributedString type brings more creativity to the text.

conclusion

This article was originally part of my series on localization for iOS, but was never finished due to a lot of trivia.

Other topics, such as resource localization, localization debugging, localization preview, localization file editing, Formatter in-depth study, will be discussed together in the future.

I hope this article has been helpful to you.

The original article was posted on my blog www.fatbobman.com

My public number: [Swift Notepad for elbow]

Show Core Data in your app in Spotlight

SheetKit — SwiftUI Modal View Extension Library

Create apps that share data with multiple iCloud users