Translation Management

Over the Air (Strings)

Update translations for iOS, Android, React Native and Flutter applications with a single click without releasing a new version to the App Store or Google Play. Updates for text labels are instantly pushed to mobile apps.

OTA_user_device_diagram.png

A new release must be created in order for the updated settings to be applied.

When a starting an application implementing the iOS, Android, React Native or Flutter SDK for the first time on a device, a unique and random device identifier is generated. This identifier tracks active users over a given period of time. It is not used for any other form or means of tracking and does not contain any user or device information.

The only limit regarding OTA is the amount of MAU (monthly active users), depending on selected pricing plan.

MAU is the number of devices from which translations are requested. Each device is assigned a random ID and the MAU is calculated from the number of different IDs assigned over the last 30 days.

OTA Distributions

Target platforms are defined within the distribution (i.e. iOS, Android, Flutter, React Native). Multiple distributions are possible but ideally there is one distribution per project. If using a distribution for iOS and Android, placeholders for the two formats are automatically converted.

Fallbacks

If language fallbacks are set in the language settings of the project the distribution is connected to, strings from the selected language will be displayed if the requested language exists but the key is not translated. If a country-specific language (e.g en-GB) is used, but is not part of the release, the system can fall back on a standard version (e.g. en) of that language if it exists in the project. If the language requested is not found at all, the default locale of the project can be served instead.

Create a distribution

To create a distribution, follow these steps:

  1. From the Over the air (OTA) box on the Integrations page, click Configure or the number of configurations if some already exist.

    The Over the air page opens and displays existing configurations.

  2. Click New distribution. The Add distribution windows opens.

  3. Provide a Name, which Project the distribution is associated with, required Languages, required Platforms and distribution specific fallback Settings.

    For Android distributions, the format option that encloses any translation including HTML tags in CDATA can be selected.

    Fallback options are prioritized as displayed in the list.

  4. Click Save.

    Distribution details are displayed with IDs required the SDKs. Details can be displayed again by clicking the distribution from the Over the air page.

OTA Releases

To update translations, create a new release within the distribution. The current state of the project is exported and made available to connected clients.

To create a release, follow these steps:

  1. From the Over the air page, click Add release beside the required distribution.

    The New release window opens.

  2. Provide a Description, required Platform, Branch, Locales and App versions.

  3. Click Save.

    The release is added to the list on the bottom of the distribution details page.

OTA Mobile SDK Reports

Integrating the appropriate SDK with Android, iOS, React Native and Flutter allows updating of translations with a single click, but also provides metrics to measure usage. The mobile SDK reports give valuable insight into active app users and their app languages. This set of reports can be accessed for each distribution and the data for the reports is updated twice a day.

Reports are provided for number of active users, overall requests, requests per language, requests per platform and for device languages not provided.

Reports for each distribution are accessed via the Reports.jpg icon on the Over the air page.

OTA Android SKD Installation

With the SDK, the app regularly checks for updated translations and downloads them in the background.

Regularly check the latest releases of the Android SDK, especially considering upgrades.

If translations are not being updated:

  • Ensure distribution id and environment secret are correct.

  • Ensure a release was created on for the current app version.

  • Reload the ViewController to make changes appear immediately.

If the wrong version of a translation is being used, ensure a release with the latest translations and the current app version is available and the versionName for the app set and are using the <major>.<minor>.<point>. format.

Requirements

  • The SDK requires at least appcompat version 1.2.0. If using an older version of appcompat, consider using SDK version 2.1.3

  • The library depends on AndroidX to support backward compatible UI elements such as the toolbar.

Include the SDK

Add a new repository to the root build.gradle:

allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}

Add the library as a dependency:

dependencies {
    implementation 'com.github.phrase:android-sdk:3.0.5'
    ...
}

Configuration

Initialize the SDK in the application class and add the distribution ID and environment secret. Classes inheriting from Application should overwrite attachBaseContext to enable translations outside of the activity context:

public class MainApplication extends Application {
  @Override
  public void onCreate() {
      super.onCreate();
      Phrase.setup(this, "DISTRIBUTION_ID", "ENVIRONMENT_TOKEN");
      Phrase.updateTranslations();
  }

  @Override
  protected void attachBaseContext(Context newBase) {
    super.attachBaseContext(Phrase.wrapApplicationContext(newBase));
  }  
}

Iinject the SDK in each activity, e.g. by creating a base activity which all other activities inherit from:

public class BaseActivity extends AppCompatActivity {
  @NonNull
  @Override
  public AppCompatDelegate getDelegate() {
    return Phrase.getDelegate(this, super.getDelegate());
  }
}

Translations can be used as usual in layouts:

<TextView android:text="@string/translation_key" />

And inside code:

TextView text = (TextView) findViewById(R.id.text_id);
text.setText(R.string.translation_key);

Change language

If not using the system language, a different language can be set in the setLocaleCode method. The language code (locale) must be present in a release.

Phrase.setLocaleCode("fr");
Phrase.updateTranslations();

Custom app version

The SDK uses the app version by default to return a release which matches the release constraints for the min and max version. The app version must use semantic versioning otherwise no translation update is returned. In case the app does not use semantic versioning it is possible to manually override the used app version.

Example:

Phrase.setAppVersion("3.2.4");

The version must be set before calling updateTranslations( ).

Set timeout

The default timeout for translation downloads is set to 10s.

The default can be changed with:

Phrase.setTimeout(10000); // Timeout in milliseconds

Update Callback

If the handling of successful translation updates is required, attach a callback handler:

Phrase.updateTranslations(new TranslationsSyncCallback() {
    @Override
    public void onSuccess(boolean translationsChanged) {
    }

    @Override
    public void onFailure() {
    }
});

Translation updates can also be manually triggered:

Phrase.updateTranslations(new TranslationsSyncCallback() {
    @Override
    public void onSuccess(boolean translationsChanged) { 
    }

    @Override
    public void onFailure() {
    }
});

Fallback

In case it is not possible to reach Phrase due to a missing network connection of the client or a service interruption, the SDK uses the bundled translations from the resource file. The regular updating of the bundled translations in the app is recommended. The SDK also caches translations locally on the device. If such a cache exists, it is used until the next translation update.

The SDK uses the most recent release for the translations. In case the versionName for the app is set, the most recent release that satisfies the version restrictions will be used.

Add a new language

Creating the new language in Phrase and create a new release. The SDK fetches the language when this is the device language of a user. Regularly adding a new strings.xml for new languages files when releasing a new app version is recommended or users will only see the fallback translations determined by Android at the first start of the app.

Data

The SDK communicates with the OTA service in order to check for updates and includes the following details with each request:

  • Device identifier (e.g. "F3AFCB10-80A2-84CB-94C0-27F5EF58876D". Unique for this app and therefore does not allow tracking a specific device.)

  • App version (e.g. "1.2.0")

  • Last update of the translation file (e.g. "1542187679")

  • SDK version (e.g. "1.0.0")

  • Locale (e.g. "de-DE")

  • File format (e.g. "strings")

  • Client (e.g. "ios")

  • Distribution ID (ID of the distribution)

  • Environment secret (to distinguish between development from production)

Auditing

The SDK is closed source and can not be viewed or modified. If is is an organization requirement, audits can be provided. Contact us for more details if required.

Example app

https://github.com/phrase/android-sdk-example

OTA Flutter SDK Installation

With the SDK, the app regularly checks for updated translations and downloads them in the background.

Requirements

This library depends on 0.17.0 version of Flutter's intl library.Follow their guide to add localizations support to the app.

Installation

Add Phrase to the pubspec.yaml:

dependencies:
  phrase: ^1.0.1 
  ...
  intl: ^0.17.0
  flutter_localizations:
    sdk: flutter
  ...

flutter:
  generate: true
  ...

Like nad intl library, code generation is used to process ARB files. Run this command to update:

flutter pub run phrase

I using build_runner:

flutter pub run build_runner watch

Usage

Initialize Phrase in the main.dart file:

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_gen/gen_l10n/phrase_localizations.dart';
import 'package:phrase/phrase.dart';

void main() {
  Phrase.setup("[DISTRIBUTION_ID]", "[ENVIRONMENT_ID]");
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      //..
      localizationsDelegates: PhraseLocalizations.localizationsDelegates,
      supportedLocales: PhraseLocalizations.supportedLocales,
    );
  }
}

Access messages with:

Text(AppLocalizations.of(context)!.helloWorld);

Customization

Update behavior

OTA translations are updated every time the app launches. To disable this:

Phrase.setup("[DISTRIBUTION_ID]", "[ENVIRONMENT_ID]", 
    checkForUpdates: false);

To update manually:

Phrase.updateTranslations(context).then((_) => print("Done!"));

Custom app version

The SDK uses the app version by default to return a release which matches the release constraints for the min and max version. The app version must use semantic versioning otherwise no translation update will be returned. In case app does not use semantic versioning, the app version can be manually overridden: it is possible to manually override the app version:

Phrase.setup("[DISTRIBUTION_ID]", "[ENVIRONMENT_ID]",
    customAppVersion: "1.2.3");

Example app

https://github.com/phrase/flutter_sdk_example

OTA iOS Installation

With the SDK, the app regularly checks for updated translations and downloads them in the background.

The SDK can be installed manually or via Swift Package Manager, Carthage or Cocoa Pods.

If translations are not being updated:

  • Ensure distribution id and environment secret are correct.

  • Ensure a release was created on for the current app version.

  • Reload the ViewController to make changes appear immediately.

If the wrong version of a translation is being used, ensure a release with the latest translations and the current app version is available.

Swift Package Manager

Add the public repository URL (https://github.com/phrase/ios-sdk/). Xcode automatically handles the rest of the installation.

Carthage

Add the following line into your Cartfile:

binary "https://raw.githubusercontent.com/phrase/ios-sdk/master/PhraseSDK.json" ~> 3.0.0

Run carthage update and add the PhraseApp.framework  to your project as desribed in the Carthage documentation.

Cocoa Pods

Add the following line into your Podfile:

pod 'PhraseSDK'

Run pod install. If new to CocoaPods, see their documentation.

Manual installation

Follow these steps:

  1. Download the latest release.

  2. Add PhraseSDK.framework in Xcode as the linked binary to the target.

  3. A script to strip the extra binaries needs to be run before you upload the app as the Apple store rejects apps including simulator binaries.

    Go to Build Phases and add a Run Script section by clicking the + symbol. Paste in this script:

    FRAMEWORK="PhraseSDK"
    FRAMEWORK_EXECUTABLE_PATH="${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/$FRAMEWORK.framework/$FRAMEWORK"
    EXTRACTED_ARCHS=()
    for ARCH in $ARCHS
    do
       lipo -extract "$ARCH" "$FRAMEWORK_EXECUTABLE_PATH" -o "$FRAMEWORK_EXECUTABLE_PATH-$ARCH"
       EXTRACTED_ARCHS+=("$FRAMEWORK_EXECUTABLE_PATH-$ARCH")
    done
    lipo -o "$FRAMEWORK_EXECUTABLE_PATH-merged" -create "${EXTRACTED_ARCHS[@]}"
    rm "${EXTRACTED_ARCHS[@]}"
    rm "$FRAMEWORK_EXECUTABLE_PATH"
    mv "$FRAMEWORK_EXECUTABLE_PATH-merged" "$FRAMEWORK_EXECUTABLE_PATH"

Configuration

  1. Import PhraseSDK:

    import PhraseSDK
  2. Initialize the SDK by calling the following code:

    Phrase.shared.setup(
      distributionID: <Distribution ID>, 
      environmentSecret: <Environment Secret>
    )
  3. To update localization files call Phrase.shared.updateTranslations().

    This method will raises an exception if SDK is not correctly setup.

Calling both functions within the AppDelegate in the applicationDidFinishLaunchingWithOptions method is recommended.

Objective-C

Integrate the SDK into the Objective-C application:

@import PhraseSDK;

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  [[Phrase shared] setDebugMode:true]; // Optional

  [[Phrase shared] setupWithDistributionID:@"Your Distribution ID"
                         environmentSecret:@"Your Environment Secret"];

  // OR:
  //
  //  [[Phrase shared] setupWithDistributionID:@"Your Distribution ID"
  //                         environmentSecret:@"Your Environment Secret"
  //                                   timeout:10];

  // Update translations using callback block:
  [[Phrase shared] updateTranslationsWithCompletionHandler:^(BOOL updated, NSError* error){
    NSLog(@"Updated: %@", updated ? @"true" : @"false");

    if (error) {
      NSLog(@"Domain: %@ Code: %ld Message: %@", error.domain, (long)error.code, error.localizedDescription);
    } else {
      NSLog(@"No error");
    }

    // Translate via bundle proxy:
    NSString *translation = NSLocalizedString(@"layouts.application.about", @"");
    NSLog(@"NSLocalizedString via bundle proxy: %@", translation);

    // OR:
    //
    // Translate using fallback method:
    NSString *otherTranslation = [[Phrase shared]
                                  localizedStringForKey:@"layouts.application.about" value:NULL table:NULL];
    NSLog(@"Phrase.shared localizedStringForKey: %@", otherTranslation);
  }];

  // OR:
  //
  // [[Phrase shared] updateTranslationsWithCompletionHandler:NULL]; // ignore result and errors (not recommended)

  // [...] Your other code
  
  return YES;
}

Disable Swizzling

To disable swizzling, set PhraseSDKMainBundleProxyDisabled to YES in the Info.plist file.

When swizzling is disabled, updated translations are no longer be displayed. The translation will still be synced if updateTranslations is called and can be accessed with the Phrase.localizedString() method.

App version handling

To determine which release should be returned the SDK requires a semantic version of the app so translations are updated.

The SDK attempts to get a semantic version the following way:

  • CFBundleShortVersionString is used if semantic.

  • If not, CFBundleVersion is used if semantic.

  • If both are not semantic, a combination of (CFBundleShortVersionString.CFBundleVersion) is used.

If CFBundleShortVersionString is missing or unable to be created with a semantic version together with CFBundleVersion, the SDK throws the PhraseSetupError.appVersionNotSemantic message.

Callbacks

Attach a callback handler to handle successful translation updates:

Phrase.shared.updateTranslations { result in
            switch result {
            case .success(let updated):
            case .failure:
            }
        }

Making updates visible

Updated translations are usually automatically visible to the user the next time app launches. To enforce new translations to be immediately visible, reload the ViewController on a successful callback:

func reloadRootViewController() {
  DispatchQueue.main.async {
    let storyboard = UIStoryboard.init(name: "Main", bundle: nil)
    let appDelegate = UIApplication.shared.delegate as! AppDelegate
    appDelegate.window?.rootViewController = storyboard.instantiateInitialViewController()
  }
}

Debug Mode

If further information is required, enable the debug mode to get additional logging of the PhraseSDK.framework into the console:

Phrase.shared.debugMode = true 

Set timeout for requests

Set a timeout for the requests against Phrase by calling:

Phrase.shared.setup(
  distributionID: <Distribution ID>, 
  environmentSecret: <Environment Secret>,
  timeout: <Double>
)

The default timeout is 10 seconds and connections taking longer than 10 seconds will be closed.

Provide manual language override

If not using the system language as the locale, a different locale can be set in the init call. The locale code needs to be present in a release from Phrase:

Phrase.shared.setup(
  distributionID: <Distribution ID>, 
  environmentSecret: <Environment Secret>,
  timeout: <Double>, // must be set when using localeOverride (default is 10 seconds)
  localeOverride: <String>
)

Fallback

In case new translations cannot be fetched from Phrase via the SDK, the latest translation files that the installation received are used. If the App never received new files from Phrase, it uses the compiled translation files of app. This prevents errors in case of any technical difficulties or networking errors. Keeping your translation files that are compiled into the app up to date with every release is recommended.

Data

The SDK communicates with the OTA service in order to check for updates and includes the following details with each request:

  • Device identifier (e.g. "F3AFCB10-80A2-84CB-94C0-27F5EF58876D". Unique for this app and therefore does not allow tracking a specific device.)

  • App version (e.g. "1.2.0")

  • Last update of the translation file (e.g. "1542187679")

  • SDK version (e.g. "1.0.0")

  • Locale (e.g. "de-DE")

  • File format (e.g. "strings")

  • Client (e.g. "ios")

  • Distribution ID (ID of the distribution)

  • Environment secret (to distinguish between development from production)

Auditing

The SDK is closed source and can not be viewed or modified. If is is an organization requirement, audits can be provided. Contact us for more details if required.

OTA React Native SDK Installation

With the SDK, the app regularly checks for updated translations and downloads them in the background.

The library for OTA translations only works with the react-i18next library.

To install the React Native SDK, follow these steps:

  1. Run this command:

    $ npm install react-native-phrase-sdk --save
  2. Initialize Phrase:

    import Phrase from "react-native-phrase-sdk";
    
    let phrase = new Phrase(
        "YOUR_DISTRIBUTION_ID",
        "YOUR_DEVELOPMENT_OR_PRODUCTION_SECRET",
        "YOUR_APP_VERSION",
        "i18next"
    );
  3. Create i18next backend based on instance:

    import resourcesToBackend from "i18next-resources-to-backend";
    
    const backendPhrase = resourcesToBackend((language, namespace, callback) => {
        phrase.requestTranslation(language)
            .then((remoteResources) => {
                callback(null, remoteResources);
            })
            .catch((error) => {
                callback(error, null);
            });
    });
    
    const backendFallback = resourcesToBackend(localResources)
  4. Initialize i18n with Phrase backend:

    i18n
      .use(ChainedBackend)
      .use(initReactI18next)
      .init({
        backend: {
            backends: [backendPhrase, backendFallback]
        }
        //...
      });

Sample i18next.js file

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import ChainedBackend from "i18next-chained-backend";
import resourcesToBackend from "i18next-resources-to-backend";
import translationEN from "./locales/en/translation.json";
import translationRU from "./locales/ru/translation.json";
import Phrase from "react-native-phrase-sdk";

const localResources = {
  en: {
    translation: translationEN,
  },
  ru: {
    translation: translationRU,
  },
};

let phrase = new Phrase(
    "YOUR_DISTRIBUTION_ID",
    "YOUR_ENVIRONMENT_ID",
    require('./package.json').version,
    "i18next"
);

const backendPhrase = resourcesToBackend((language, namespace, callback) => {
    phrase.requestTranslation(language)
        .then((remoteResources) => {
            callback(null, remoteResources);
        })
        .catch((error) => {
            callback(error, null);
        });
});

const backendFallback = resourcesToBackend(localResources)

i18n
  .use(ChainedBackend)
  .use(initReactI18next)
  .init({
    backend: {
        backends: [backendPhrase, backendFallback]
    },
    debug: true,
    lng: "en",
    fallbackLng: "en",
    interpolation: {
      escapeValue: false, // not needed for react as it escapes by default
    }
  });

Example app

https://github.com/phrase/react_native_sdk_example

Was this article helpful?

Sorry about that! In what way was it not helpful?

The article didn’t address my problem.
I couldn’t understand the article.
The feature doesn’t do what I need.
Other reason.

Note that feedback is provided anonymously so we aren't able to reply to questions.
If you'd like to ask a question, submit a request to our Support team.
Thank you for your feedback.