StringPacks is a library to store translation strings in a more efficient binary format for Android applications, so that it reduces the Android APK size.
Check out our tech talk on StringPacks from DroidCon SF 2019 to know more about the motivation, architecture and prospect of the StringPacks project.
- Python 3 - The StringPacks python scripts are written in Python 3.
- minSdkVersion 15 - The library default min sdk version is 15, but it should work for lower SDK versions.
- Git - The script uses
git ls-filesto look up files. - Android development environment
- Gradle Build System
- Copy the scripts/ and pack.gradle from
library/to the root directory of your Android project. - Move either Java or Kotlin version of
StringPackIdsfile from templates/ directory to your project source code directory.- Edit package information of the file.
- Move template config.json to your Android application project directory.
- Replace
{app}to be your application project directory name. - Choose one of the two (mutually exclusive):
- Point
pack_ids_class_file_pathto the path where you put theStringPackIdsfile. - Configure
resource_config_settingto generate the necessary aapt config file.config_file_pathfile path for aapt2 config to set stable idsource_file_pathfor file path of generated Java sourcestring_offseta hex string for string id offset (usually "0x7f120000")plurals_offseta hex string for plural id offset (usually "0x7f100000")package_namefor package name.
- Point
- Replace
- Make following changes to your Android project's
build.gradle.allprojects { repositories { ... mavenCentral() } ... } // Replace `{path_to_config.json}` with the path to your `config.json` file ext { stringPacksConfigFile = "$rootDir/{path_to_config.json}" }- Replace
{path_to_config.json}with the path to yourconfig.jsonfile
- Replace
- Make following changes to your Android application's
build.gradleapply from: "$rootDir/pack.gradle" dependencies { ... ... implementation 'com.whatsapp.stringpacks:stringpacks:0.3.1' } - To remove old
.packfiles from the device's internal storage, on every app upgrade, add MyPackageReplacedReceiver.java and PackFileDeletionService.java to yourAndroidManifest.xml<uses-permission android:name="android.permission.WAKE_LOCK"/> <application ...> ... <receiver android:name="com.whatsapp.stringpacks.receiver.MyPackageReplacedReceiver"> <intent-filter> <action android:name="android.intent.action.MY_PACKAGE_REPLACED" /> </intent-filter> </receiver> <service android:name="com.whatsapp.stringpacks.service.PackFileDeletionService" android:permission="android.permission.BIND_JOB_SERVICE" /> </application>
Note: If you want to delete old
.packfiles, from internal storage, at some other time instead of app upgrade, callStringPacks.cleanupOldPackFiles(getApplicationContext())whenever you want. You don't have to include MyPackageReplacedReceiver.java or PackFileDeletionService.java in yourAndroidManifest.xml
You now have StringPacks available in your Android project.
There are a few steps to walk through before you can really use packed strings in your application. But don't worry, most of them only need to be done once.
Since the translated strings are moved to our special binary format (.pack files), your application needs a way to read those strings during runtime. The library provides a wrapper class for Context and Resources to help with that.
You need to add the following code to all subclasses of your Context class (like Activity and Service) to ensure the strings are read from .pack files instead of Android system resources.
// Java
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(StringPackContext.wrap(base));
}// Kotlin
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(StringPackContext.wrap(base))
}If all of the following conditions meet, you need to override getResources() function also in your Activity
- App's
minSdkVersionis < 17 - You have a dependency on
androidx.appcompat:appcompat:1.2.0 - Your Activity extends from
AppCompatActivity
// Java
private @Nullable StringPackResources stringPackResources;
@Override
public Resources getResources() {
if (stringPackResources == null) {
stringPackResources = StringPackResources.wrap(super.getResources());
}
return stringPackResources;
}// Kotlin
private @Nullable var stringPackResources:Resources? = null
override fun getResources(): Resources? {
if (stringPackResources == null) {
stringPackResources = StringPackResources.wrap(super.getResources())
}
return stringPackResources
}Your Android application also needs to use a custom Application, which needs to include the following code to ensure the strings are read from .pack files.
// Java
@Override
protected void attachBaseContext(Context base) {
StringPackIds.registerStringPackIds();
StringPacks.getInstance().setUp(base);
super.attachBaseContext(base);
}
private @Nullable StringPackResources stringPackResources;
@Override
public Resources getResources() {
if (stringPackResources == null) {
stringPackResources = StringPackResources.wrap(super.getResources());
}
return stringPackResources;
}// Kotlin
override fun attachBaseContext(base: Context?) {
registerStringPackIds()
StringPacks.getInstance().setUp(base)
super.attachBaseContext(base)
}
private @Nullable var stringPackResources:Resources? = null
override fun getResources(): Resources? {
if (stringPackResources == null) {
stringPackResources = StringPackResources.wrap(super.getResources())
}
return stringPackResources
}You only need to do this each time you add a new context component. You don't need to do this for each component if you add them to a base class.
You can map multiple regions into a single .pack file using pack_id_mapping in config.json. For example
pack_id_mapping = {
"es-rMX": "es",
"es-rES": "es"
}Here, translations in "es", "es-MX" and "es-ES" locales would be packed into strings_es.pack file.
If you are supporting any of the following features, you need to implement StringPacksLocaleMetaDataProvider.java and register the provider in your custom Application class
- Packing translations for multiple locales (for example,
es-MX,es) in to one.packfile, or - Fallback feature, or
- Supporting region specific locales
// Java
@Nullable private final StringPacksLocaleMetaDataProvider metaData = new LocaleMetaDataProviderImpl();
@Override
protected void attachBaseContext(Context base) {
StringPackIds.registerStringPackIds();
StringPacks.registerStringPackLocaleMetaData(metaData);
StringPacks.getInstance().setUp(base);
super.attachBaseContext(base);
}// Kotlin
private @Nullable var metaData:StringPacksLocaleMetaDataProvider? = LocaleMetaDataProviderImpl()
override fun attachBaseContext(base: Context?) {
registerStringPackIds();
StringPacks.registerStringPackLocaleMetaData(metaData);
StringPacks.getInstance().setUp(base);
super.attachBaseContext(base);
}Take a look at LocaleMetaDataProviderImpl.java in the sample app for reference.
You have added the StringPackIds file to your project, but it has nothing in it yet. It is supposed to hold the mapping from android resource IDs (R.string) to string pack IDs.
The content would be automatically filled in when you run the script that provided by this library.
The mapping information would also be used for generating the .pack files, so they are correctly loaded at runtime.
Execute the python script from your project root directory to assemble the string packs:
python3 ./scripts/assemble_string_packs.py --config ./{path_to}/config.jsonYou will see:
- The
StringPackIdsfile has been updated with the pack ID mapping information; - The translation strings, which are packable, have been moved to different directory, so that they won't be compiled into the APK;
- The
.packfile for different language have been generated under the project assets/ directory.
When you update translations, or change a string in the project, you may run the script again to generate .pack files with latest content.
Those string resource IDs that are not listed in the StringPackIds file, will continue to be kept in the Android system resources, and the StringPacks runtime would automatically fall back to read from there.
Now, you can use gradle to build your application as usual. The application should correctly retrieve the strings from StringPacks.
Copyright (c) Facebook, Inc. and its affiliates.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.