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.
Over-the-Air (OTA) also integrates with the i18Next and Rails platforms, allowing users to update and manage localized content in web applications without requiring a redeployment.
A new release must be created in order for the updated settings to be applied. All content included in a release must be available in Phrase Strings, which streams translations directly to the application. If the content is not present, translations will not be delivered.
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 unique 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. Every interaction with Phrase servers is considered a request.
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:
-
iOS
-
Android
-
Flutter
-
i18next
-
Rails
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.
-
In the General tab, provide a , which the distribution is associated with, required , and required .
-
For Android distributions, click on the Android tab to select the format option that encloses any translation including HTML tags in CDATA, if required.
-
After selecting the project to associate with the distribution, the Scheduling tab becomes available. If required, use this tab to set up OTA scheduled releases in the distribution.
-
-
In the Fallback languages tab, select distribution specific fallback settings as required. Fallback options are prioritized as displayed in the list.
-
Optionally, click on the Translations tab to select the option to use the latest reviewed version of translations.
-
Click Save.
Distribution details are displayed with IDs required by 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:
-
From the Over the air page, click Add release beside the required distribution.
The
window opens. -
Provide a
, required , , and .If necessary, enter tags to include only keys with specific tags in the release.
-
Click Save.
The release is added to the list on the bottom of the distribution details page.
To set up recurring schedules for releasing the distribution, follow these steps:
-
From the
page, click on the cog wheelicon beside the required distribution.
The
window is displayed. -
Select the Schedule release tab and click .
Release scheduling options are displayed.
Note
Scheduling options are also available upon creation of a new distribution.
-
From the
dropdown, choose the release frequency by selecting one of the available options:-
-
Select the desired weekdays for the scheduled releases.
-
-
Provide a time and relevant
. -
If necessary, select
, and for the scheduled releases.-
The
field is displayed only if branching is enabled in the project. Selecting a branch updates the list of locales and languages.
-
-
Optionally, specify the application versions in the
and fields.Leave blank to apply the schedule release to all versions of the app.
-
Click Save.
The distribution is updated with release scheduling information.
Disabling the schedule stops the automatic release of the distribution, but the configured settings are saved.
Integrating the appropriate mobile SDK or Web library allows updating of translations with a single click, but also provides metrics to measure usage. The mobile SDK and Web library 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://maven.download.phrase.com" } } }
Add the library as a dependency:
dependencies { implementation "com.phrase.android:ota-sdk:3.5.0" ... }
Jetpack Compose support
To enable Jetpack Compose support for OTA translations, follow these steps:
-
Add the library
implementation "com.phrase.android:ota-sdk-compose:3.5.0"
to the rootbuild.gradle
. -
Wrap the Jetpack Compose code in
Phrase { ... }
.
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);
Some libraries do not support automatically unwrapping the context and expect a specific class. In this case context wrapping in Jetpack Compose components can be disabled with:
Phrase(contextWrapping = false) { Text( text = phraseString(R.string.test) ) }
Configurations for log levels:
-
Java
PhraseLog.setLogLevel(Severity.Debug);
-
Kotlin
PhraseLog.logLevel = Severity.Verbose
-
Other supported logging options
-
None
-
Error
-
Warning
-
Info
-
Debug
-
Verbose
-
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 triggered manually. Newly fetched translations are displayed upon the next application launch.
To make the latest translations immediately available, use the method Phrase.applyPendingUpdates()
. This can be combined with listening for translation updates:
Phrase.updateTranslations(new TranslationsSyncCallback() { @Override public void onSuccess(boolean translationsChanged) { if(translationsChanged) { Phrase.applyPendingUpdates() // Custom logic to refresh UI } } @Override public void onFailure() { } });
The UI does not display translations automatically and must be recreated.
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.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 it is an organization requirement, audits can be provided. Contact us for more details if required.
Custom View Support
Custom views can be translated using styled attributes. Since TypedArray
does not allow overwriting the resources, slight changes in the custom view are required:
-
Kotlin example
Before:
context.obtainStyledAttributes(attrs, R.styleable.CustomView, defStyleAttr, 0).use { text = it.getText(R.styleable.CustomView_android_text) }
After:
context.obtainStyledAttributes(attrs, R.styleable.CustomView, defStyleAttr, 0).use { text = it.getTextWithPhrase(R.styleable.CustomView_android_text) }
-
Java example
Before:
final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CustomView, defStyleAttr, 0); try { setText(ta.getText(R.styleable.CustomView_android_text)); } finally { ta.recycle(); }
After:
final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CustomView, defStyleAttr, 0); try { setText(PhraseTypedArray.getTextWithPhrase(ta, R.styleable.CustomView_android_text)); } finally { ta.recycle(); }
Example app
With the SDK, the app regularly checks for updated translations and downloads them in the background.
Requirements
This library depends on 0.18.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: ^2.5.1 ... intl: ^0.18.0 flutter_localizations: sdk: flutter ... flutter: generate: true ...
Like in the intl
library, code generation is used to process ARB files. Run this command to update:
flutter pub run phrase
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.
Mac Catalyst is also supported.
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.updateTranslation()
.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 updateTranslation
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.
Disable translation for multiple tables
To prevent OTA from translating any tables in the main iOS bundle other than the default Localizable
table, set the following option:
Phrase.shared.configuration.ignoreOtherTables = true
Callbacks
Attach a callback handler to handle successful translation updates:
Phrase.shared.updateTranslation { result in switch result { case .success(let updated): case .failure: } }
Debug mode
If further information is required, enable the debug mode to get additional logging of the PhraseSDK.framework
into the console:
Phrase.shared.configuration.debugMode = true
Set timeout for requests
Set a timeout for the requests against Phrase by calling:
Phrase.shared.configuration.timeout = TimeInterval(20)
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.configuration.localeOverride = "en-US"
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 it 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" );
The file format can be either
i18next
(as in the example above) ori18next_4
, which results in the i18next v4 format file.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.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 } });
Example app
The i18next-phrase-backend library implements an example backend for i18next which retrieves the translations from OTA releases.
To create a new release, the
platform needs to be set for the distribution and be part of a release.Usage
To initialize Phrase:
import i18n from "i18next"; import { I18nextPhraseBackend } from "@phrase/i18next-backend"; i18n .use(I18nextPhraseBackend) .init({ fallbackLng: 'en', backend: { distribution: 'DISTRIBUTION_ID', secret: 'YOUR_ENVIRONMENT_SECRET', appVersion: '1.0.0', } });
Caching
The library is caching translations and will not check for new translations for 5 minutes. Caching can be configured by setting the cacheExpirationTime
option in the backend configuration for testing purposes.
i18n .use(I18nextPhraseBackend) .init({ fallbackLng: 'en', backend: { distribution: 'DISTRIBUTION_ID', secret: 'YOUR_ENVIRONMENT_SECRET', appVersion: '1.0.0', cacheExpirationTime: 60 * 5, // time in seconds } });
Note
It is recommended to use at least 5 minutes caching time in production environments.
Example app
The phrase-ota-i18n library implements an example backend for the I18n gem which retrieves the translations from OTA releases. With the library, the application regularly checks for updated translations and downloads them in the background.
The library is valid for any Ruby application using the i18n gem.
To create a new release, the
platform needs to be set for the distribution and be part of a release.Installation
-
Add the following line to the application's Gemfile:
gem 'phrase-ota-i18n'
-
Execute:
$ bundle install
-
Alternatively, install it as:
$ gem install phrase-ota-i18n
Usage
To generate the configuration:
bundle exec rails generate phrase_ota:install --distribution-id <DISTRIBUTION_ID> --secret-token <SECRET>
Debugging
The gem has a debug
configuration which can be enabled in the initializer by setting:
config.debug = true