StringPacks

Project Url: WhatsApp/StringPacks
Introduction: Extracts localized strings from an Android app and stores it in a much more efficient format.
More: Author   ReportBugs   
Tags:

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.

Requirements

  • 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

Setup in Android Project

  1. Copy the scripts/ and pack.gradle from library/ to the root directory of your Android project.
  2. Move either Java or Kotlin version of StringPackIds file from templates/ directory to your project source code directory.
    • Edit package information of the file.
  3. 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 the StringPackIds file.
      • Configure resource_config_setting to generate the necessary aapt config file.
        • config_file_path file path for aapt2 config to set stable id
        • source_file_path for file path of generated Java source
        • string_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.
  4. 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 your config.json file
  5. Make following changes to your Android application's build.gradle

    apply from: "$rootDir/pack.gradle"
    
    dependencies {
      ...
      ...
      implementation 'com.whatsapp.stringpacks:stringpacks:0.3.1'
    }
    
  6. To remove old .pack files from the device's internal storage, on every app upgrade, add MyPackageReplacedReceiver.java and PackFileDeletionService.java to your AndroidManifest.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, call StringPacks.cleanupOldPackFiles(getApplicationContext()) whenever you want. You don't have to include MyPackageReplacedReceiver.java or PackFileDeletionService.java in your AndroidManifest.xml

You now have StringPacks available in your Android project.

Getting Started

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.

Runtime

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

  1. App's minSdkVersion is < 17
  2. You have a dependency on androidx.appcompat:appcompat:1.2.0
  3. 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.

Region specific locales & Fallback

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

  1. Packing translations for multiple locales (for example, es-MX, es) in to one .pack file, or
  2. Fallback feature, or
  3. 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.

Generate .pack files

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.

License

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.
Apps
About Me
GitHub: Trinea
Facebook: Dev Tools