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.

A new release must be created in order for the updated settings to be applied.
When 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.
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)
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:
-
From the Configure or the number of configurations if some already exist.
box on the page, clickThe
page opens and displays existing configurations. -
Click New distribution. The windows opens.
-
Provide a
, which the distribution is associated with, required , required and distribution specific fallback .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.
-
Click Save.
Distribution details are displayed with IDs required the SDKs. Details can be displayed again by clicking the distribution from the
page.
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:
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 icon on the page.
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() { } });
Configure US data center
Phrase US data center is also supported. The US data center can be configured by calling:
Phrase.setHost("https://ota.us.app.phrase.com")
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.
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
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");
Configure US data center
Phrase US data center is also supported. The US data center can be selected by passing the relevant API hostname parameter in the SDK configuration:
Phrase.setup("[DISTRIBUTION_ID]", "[ENVIRONMENT_ID]", host: PhraseHost.us);
Example app
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:
-
Download the latest release.
-
Add
PhraseSDK.framework
in Xcode as the linked binary to the target. -
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
and add a 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
-
Import PhraseSDK:
import PhraseSDK
-
Initialize the SDK by calling the following code:
Phrase.shared.setup( distributionID: <Distribution ID>, environmentSecret: <Environment Secret> )
-
To update localization files call
Phrase.shared.updateTranslations()
.This method will raises an exception if SDK is not correctly setup.
To configure OTA to use the US data center, set the host before calling
PhraseApp.shared.updateTranslation()
withPhrase.shared.configuration.apiHost = .us
.
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.
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.
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:
-
Run this command:
$ npm install react-native-phrase-sdk --save
-
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" );
Phrase US data center is also supported. To use the React Native SDK with the US data center, pass the relevant host during initialization:
import Phrase from "react-native-phrase-sdk"; let phrase = new Phrase( "YOUR_DISTRIBUTION_ID", "YOUR_DEVELOPMENT_OR_PRODUCTION_SECRET", "YOUR_APP_VERSION", "i18next", host="https://ota.us.app.phrase.com" );
-
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)
-
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 } });