Software localization

iOS VoiceOver Internationalization

Learn how to deliver a superior user experience for global customers with special needs—with this iOS VoiceOver internationalization guide.
Software localization blog category featured image | Phrase

Accessibility in iOS is a set of features and tools provided by Apple to create a strong user experience for everyone. The toolbox includes captioning and audio description, display customization (increase the font size, reduce motion, etc.), speech, VoiceOver, and more.

For an internationalized app, supporting accessibility features could be a great way to extend your app to new users. However, missing a step into this journey could push your users away from your app—that's why it's crucial that accessibility features are aligned with the internationalization 'level' of your app.

This article will be focused on internationalizing VoiceOver, a screen-reading feature in iOS that can read through the screen to help users with visual impairment interact with your app. Shall we start?

Enable VoiceOver

What we'll be doing in this tutorial is extending an existing 'Log in' view to support VoiceOver and make sure the interacting objects read are also localized. The 'Log In' view is based on a simple UIViewController, which contains a title, two text fields, and two buttons.

Internationalization accessibility iOS basics  | Phrase

Our very first step is to enable VoiceOver on the device. We’ll need to go to the 'Settings' app and enable 'VoiceOver' under the 'Accessibility' menu.

VoiceOver Accessibility iOS | Phrase

Unfortunately, VoiceOver isn’t available on a simulator at the moment, and only a physical device can support it.

From there, any single-tap gesture will focus on the selected UI component and read its accessibility information. A double-tap gesture will confirm the interaction. Single-swipe left or right will move the focus to the next/previous events.

Having covered the basics of VoiceOver, it's time to try it out, and see what happens when used on the 'Log In' page.

Setting Accessibility Attributes

When landing on the 'Log In' view with VoiceOver enabled, we can test the information read by Apple’s feature. While it gives us minimal information, it isn’t really user-friendly.

Those portions of information rely on the accessibility attributes of the selected view. You might be familiar with the accessibility identifier that is often used for user interface testing of UIKit views. VoiceOver relies on other attributes to describe those components:

  • accessibility label—giving the default information of the component (for UILabel, it’s often the text within),
  • accessibility traits—describing the state of the component,
  • accessibility frame—will give you the position of the component’s frame,
  • accessibility hint—can capture extra information to give more context (quite useful since we have two 'Log In' views, a label and a button),
  • accessibility value—describing the value of the element (useful for user’s input for a slider or text field).

Now that we know where the pieces of information come from, we can set them up accordingly and eventually test them.

A great tool to inspect those attributes is the Accessibility Inspector (available with Xcode).

Internationalization accessibility iOS | Phrase

When reading the 'Log In' and 'Forgot Password' buttons, the accessibility attributes rely on the title of the button. The second label could make little sense to someone who can't see the button, let alone tap it. Let’s use the accessibilityLabel and accessibilityHint properties to correct this.

lazy var forgotPasswordButton: UIButton = {

    let button = UIButton()

    button.setTitle("Forgot password? Tap here to reset", for: .normal)

    button.tintColor = .secondaryLabel

    button.setTitleColor(.secondaryLabel, for: .normal)

    button.titleLabel?.font = .systemFont(ofSize: 12)

    button.accessibilityLabel = "Forgot password"

    button.accessibilityHint = "Assist you to reset your password and create a new one"

    return button

}()

Much better, right? We can apply the same logic to other views as well.

lazy var emailField: UITextField = {

    let textField = UITextField()

    textField.placeholder = "E-mail"

    textField.textColor = .secondaryLabel

    textField.font = .systemFont(ofSize: 18)

    textField.keyboardType = .emailAddress

    textField.backgroundColor = .secondarySystemBackground

    textField.accessibilityLabel = "E-mail"

    textField.accessibilityHint = "Email filled is required to login"

    return textField

}()

🗒 Note » When applying similar accessibility attributes to the UITextField, the accessibilityValue seems always overwritten by the value of the text field or its current placeholder. This is why it’s also important to set an accessibilityLabel as well so users can still get the context of the field.

So far so good, we’ve managed to improve the usability of our view by taking advantage of accessibility features, but how to extend to internationalization? That’s our next step.

iOS VoiceOver Internationalization in Action

Before diving deeper into the code part, it’s important to prepare our project for internationalization. We won't cover this in-depth here, but you can get all the details on iOS i18n in our iOS Tutorial on Internationalization and Localization.

For this example, I’ll add French as a supported language to my project. We’ll use this as a base for the accessibility attributes later on.

Adding French as a supported language to our project | Phrase

I’ll also add a string file to my project and include a version in French and English to support both languages.

Adding a string file to the project and including a version in French and English | Phrase

The project is now fully set up so we can finally internationalize our attributes.

Internationalizing Accessibility Attributes

Just like for any required translation, we’ll create keys and values in our Localizable.strings file to support different versions and languages. Let’s get it started with content for buttons and labels!

"E-mail" = "E-mail";

"Log In" = "Log In";

"Password" = "Password";

"Forgot password? Tap here to reset" = "Forgot password? Tap here to reset";

For accessibility attributes, if you want to dissociate them in the rest of the translations (and you might want to do so over time), I'd suggest either create a dedicated file for it or just separate it from the rest via a comment for a smaller project.

One last thing to pay attention to is a naming convention for the key used to identify the value. As a naming convention, I’ve used the a11n prefix to make sure I get the context right when I’ll be looking for it.

// Accessibility

"a11n_email" = "E-mail";

"a11n_email_hint" = "Email filled is required to login";

"a11n_password" = "Password";

"a11n_password_hint" = "Password filled is required to login";

"a11n_login" = "Login";

"a11n_login_hint" = "Log in with the provided email and password to our awesome app";

"a11n_forgot_password" = "Forgot password";

"a11n_forgot_password_hint" = "Assist you to reset your password and create a new one";

I’ll do the same in the French version of the file so we can try both later on.

// Accessibility

"a11n_email" = "E-mail";

"a11n_email_hint" = "Une adresse email est requise pour se connecter";

"a11n_password" = "Mot de passe";

"a11n_password_hint" = "Un mot de passe est requis pour se connecter";

"a11n_login" = "Se connecter";

"a11n_login_hint" = "Se connecter avec une adresse email et un mot de passe à notre super application mobile";

"a11n_forgot_password" = "Mot de passe oublié";

"a11n_forgot_password_hint" = "Aide à réinitialiser votre mot passe en créant un nouveau";

All our copies and accessibility attributes are ready to use, the last part is to update the implementation to reflect those values.

Accessibility Meets Internationalization

Back to our UIViewController, we can update the implementation to use localized accessibility attributes moving forward.

One way is to directly use NSLocalizedString(_ key: String, comment: String) in our code.

lazy var forgotPasswordButton: UIButton = {

    let button = UIButton()

    button.setTitle(NSLocalizedString("Forgot password? Tap here to reset", comment: ""), for: .normal)

    button.tintColor = .secondaryLabel

    button.setTitleColor(.secondaryLabel, for: .normal)

    button.titleLabel?.font = .systemFont(ofSize: 12)

    button.accessibilityLabel = NSLocalizedString("a11n_forgot_password", comment: "")

    button.accessibilityHint = NSLocalizedString("a11n_forgot_password_hint", comment: "")

    return button

}()

Great, now the accessibility attributes are based on the device's locale and can support French or English.

However, this implementation doesn’t allow for reusability and discoverability throughout our project. Let’s try to make small improvements for better readability.

Reusability and Discoverability

Adding a simple enum can easily improve the developer experience in our case. From our previous code, and based on the naming convention, we can define a standard to access our attributes.

enum Accessibility: String {

    case email

    case password

    case login

    case forgotPassowrd = "forgot_password"

    var label: String {

        NSLocalizedString("a11n_\(self.rawValue)", comment: "")

    }

    var hint: String {

        NSLocalizedString("a11n_\(self.rawValue)_hint", comment: "")

    }

}

With this enum, I define the key of the translation, apply the naming convention, and can look for available translation through label or hint. We can now finally tidy up the code!

// old code

button.accessibilityLabel = NSLocalizedString("a11n_forgot_password", comment: "")

button.accessibilityHint = NSLocalizedString("a11n_forgot_password_hint", comment: "")

// new code

button.accessibilityLabel = Accessibility.forgotPassowrd.label

button.accessibilityHint = Accessibility.forgotPassowrd.hint

Wrapping Up Our iOS VoiceOver i18n tutorial

Good job! We’ve internationalized accessibility attributes, improved the reusability of localized copy, and found a nice way to make it discoverable through autocompletion. The last step is to run the app in a different language to make sure everything works as expected. Taking it to a higher level, we’ve extended our existing app to make it more accessible and prepared it for localization—all at the same time—significantly improving the user experience at different levels.

Last but not least, if you want to take your i18n game to a higher level, check out Phrase. Fast, lean, and reliable, Phrase is a software localization platform featuring a robust CLI and API, GitHub, Bitbucket, and GitLab sync, machine learning translations, and a great web console for translators. Let Phrase do the heavy lifting in your i18n/l10n process, and stay focused on the creative code you love. Check out all of Phrase's products, and sign up for a free 14-day trial!