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-files
to 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
StringPackIds
file 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_path
to the path where you put theStringPackIds
file. - Configure
resource_config_setting
to generate the necessary aapt config file.config_file_path
file path for aapt2 config to set stable idsource_file_path
for file path of generated Java sourcestring_offset
a hex string for string id offset (usually "0x7f120000")plurals_offset
a hex string for plural id offset (usually "0x7f100000")package_name
for 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.json
file
- Replace
- Make following changes to your Android application's
build.gradle
apply from: "$rootDir/pack.gradle" dependencies { ... ... implementation 'com.whatsapp.stringpacks:stringpacks:0.3.1' }
- To remove old
.pack
files 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
.pack
files, 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
minSdkVersion
is < 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.pack
file, 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.json
You will see:
- The
StringPackIds
file 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
.pack
file 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.