Reputation: 836
I have the requirement of developing an Android application that loads localized text from external resources outside of its primary APK.
The reason for this is to enable third-parties to independently provide translations of the application. The application currently has a single English localization with a fairly large number of strings (~2,000).
I would prefer not to break away form Android's resource system; e.g., I would like to provide the primary-language localized strings in strings.xml
just like in any Android app.
To accomplish this, I have created a class which extends android.content.res.Resources, overriding the three getText
methods. The overriding implementations will return resources from the external localization source when possible, and otherwise forward the request to the super.getText()
implementation.
Resources wrapper:
public class IntegratedResources extends Resources {
private ResourceIntegrator ri;
public IntegratedResources(AssetManager assets, DisplayMetrics metrics, Configuration config, ResourceIntegrator ri) {
super(assets, metrics, config);
this.ri = ri;
}
@Override
public CharSequence getText(int id)
throws NotFoundException {
return ri == null ? super.getText(id) : ri.getText(id);
}
@Override
public CharSequence getText(int id, CharSequence def)
throws NotFoundException {
return ri == null ? super.getText(id, def) : ri.getText(id, def);
}
@Override
public CharSequence[] getTextArray(int id)
throws NotFoundException {
return ri == null ? super.getTextArray(id) : ri.getTextArray(id);
}
}
I then created a ContextWrapper implementation to wrap an Activity's context. The context wrapper's getResources() method will return the IntegratedResources object above.
ContextWrapper:
public class IntegratedResourceContext extends ContextWrapper {
private IntegratedResources integratedResources;
public IntegratedResourceContext(Activity activity, String packageName)
throws NameNotFoundException {
super(activity);
ResourceIntegrator ri = packageName == null ? null : new ResourceIntegrator(activity, packageName);
DisplayMetrics displayMetrics = new DisplayMetrics();
activity.getWindow().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
integratedResources = new IntegratedResources(activity.getAssets(), displayMetrics,
activity.getResources().getConfiguration(), ri);
}
@Override
public Resources getResources() {
return integratedResources;
}
}
And finally we have the "ResourceIntegrator" class, which picks resources out of a specified installed third-party localization APK.
A different implementation could be created to pull them from an XML or properties file if desired.
ResourceIntegrator:
public class ResourceIntegrator {
private Resources rBase;
private Resources rExternal;
private String externalPackageName;
private Map<Integer, Integer> baseIdToExternalId = new HashMap<Integer, Integer>();
public ResourceIntegrator(Context context, String externalPackageName)
throws NameNotFoundException {
super();
rBase = context.getResources();
this.externalPackageName = externalPackageName;
if (externalPackageName != null) {
PackageManager pm = context.getPackageManager();
rExternal = pm.getResourcesForApplication(externalPackageName);
}
}
public CharSequence getText(int id, CharSequence def) {
if (rExternal == null) {
return rBase.getText(id, def);
}
Integer externalId = baseIdToExternalId.get(id);
if (externalId == null) {
// Not loaded yet.
externalId = loadExternal(id);
}
if (externalId == 0) {
// Resource does not exist in external resources, return from base.
return rBase.getText(id, def);
} else {
// Resource has a value in external resources, return it.
return rExternal.getText(externalId);
}
}
public CharSequence getText(int id)
throws NotFoundException {
if (rExternal == null) {
return rBase.getText(id);
}
Integer externalId = baseIdToExternalId.get(id);
if (externalId == null) {
// Not loaded yet.
externalId = loadExternal(id);
}
if (externalId == 0) {
// Resource does not exist in external resources, return from base.
return rBase.getText(id);
} else {
// Resource has a value in external resources, return it.
return rExternal.getText(externalId);
}
}
public CharSequence[] getTextArray(int id)
throws NotFoundException {
if (rExternal == null) {
return rBase.getTextArray(id);
}
Integer externalId = baseIdToExternalId.get(id);
if (externalId == null) {
// Not loaded yet.
externalId = loadExternal(id);
}
if (externalId == 0) {
// Resource does not exist in external resources, return from base.
return rBase.getTextArray(id);
} else {
// Resource has a value in external resources, return it.
return rExternal.getTextArray(externalId);
}
}
private int loadExternal(int baseId) {
int externalId;
try {
String entryName = rBase.getResourceEntryName(baseId);
String typeName = rBase.getResourceTypeName(baseId);
externalId = rExternal.getIdentifier(entryName, typeName, externalPackageName);
} catch (NotFoundException ex) {
externalId = 0;
}
baseIdToExternalId.put(baseId, externalId);
return externalId;
}
}
My question to stackoverflow is whether the above implementation is a good idea, whether it's using the API properly, and whether its design is future-proof against the unknown versions of Android of tomorrow.
I've not seen anyone doing this before, and can't seem to find anything about solving this problem in the docs or on the web.
The underlying requirement of allowing independent third-party translations is fairly critical. It is not currently feasible to internally maintain dozens of translations for this application, and I have no capability to vet user-provided translations for quality.
In the case that this design is a very bad idea and no similar alternative is available, then localization may have to be done without Android's resource-management system altogether.
In the event that this is a good idea, please feel free to use and improve the above code.
Upvotes: 4
Views: 1116
Reputation: 3841
I don't think your technique will work for resource references in layouts. These are resolved by the LayoutInflater and ultimately by the TypedArray implementation, which invokes private methods to load the resources.
Upvotes: 1
Reputation: 1007296
My question to stackoverflow is whether the above is a good idea
I will be stunned if it is a complete solution, since you cannot force Android to use your custom ContextWrapper
. It should work for any place where you manually call getString()
, getText()
, and the like, but I do not see how it will work anywhere that Android accesses those resources beyond one of your activities. There are many places where you cannot call getText()
and so on yourself, such as:
Moreover, you will have perpetual versioning problems, unless you plan to never add new strings. You have no way of forcing third-party translations to support new strings, and so you will wind up with an app with a mix of translated and non-translated strings.
whether it's using the API properly
That seems OK.
whether its design is future-proof against the unknown versions of Android of tomorrow
The number of places where Android will access the resources itself is likely to increase, rather than decrease.
It is not currently feasible to internally maintain dozens of translations for this application, and I have no capability to vet user-provided translations for quality.
Then only support languages that you are willing to manage yourself. First, as I have noted, I do not see how this will be a complete solution, on a couple of fronts. Second, you seem to think that your approach will cause you to not be blamed for poor translations, and while it will probably reduce how much blame you get, it will not eliminate such blame. I agree with Squonk in this regard.
I admire your zeal for offering more translations, but, personally, I wouldn't go down this particular path.
Upvotes: 1