使独立的第三方能够本地化Android应用程序;应用APK之外的本地化数据

我需要开发一个
Android应用程序,从其主要APK之外的外部资源加载本地化文本.这样做的原因是为了使第三方能够独立提供应用程序的翻译.该应用程序目前具有单个英语本地化,具有相当多的字符串(~2,000).

我宁愿不打破Android的资源系统;例如,我想在strings.xml中提供主要语言本地化字符串,就像在任何Android应用程序中一样.

为了实现这一点,我创建了一个扩展android.content.res.Resources的类,重写了三个getText方法.覆盖实现将尽可能从外部本地化源返回资源,否则将请求转发给super.getText()实现.

资源包装器:

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);
    }
}

然后我创建了一个ContextWrapper实现来包装Activity的上下文.上下文包装器的getResources()方法将返回上面的IntegratedResources对象.

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;
    }
}

最后我们有了“ResourceIntegrator”类,它从指定的已安装的第三方本地化APK中选择资源.如果需要,可以创建不同的实现以从XML或属性文件中提取它们.

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;
    }
}

我对stackoverflow的问题是上面的实现是否是一个好主意,它是否正确使用API​​,以及它的设计是否能够面向未来的Android未知版本.我之前没有见过有人这样做过,而且似乎无法在文档或网络上找到解决此问题的任何内容.

允许独立第三方翻译的基本要求非常关键.目前在内部维护此应用程序的数十种翻译是不可行的,而且我没有能力审查用户提供的翻译质量.如果这个设计是一个非常糟糕的想法并且没有类似的替代方案可用,那么本地化可能必须在没有Android的资源管理系统的情况下完成.

如果这是一个好主意,请随意使用和改进上述代码.

最佳答案

My question to stackoverflow is whether the above is a good idea

如果它是一个完整的解决方案,我会感到震惊,因为你不能强迫Android使用你的自定义ContextWrapper.它应该适用于您手动调用getString(),getText()等的任何地方,但我不知道它如何在Android访问超出您的某个活动之外的那些资源的任何地方工作.有很多地方你不能自己调用​​getText()等,例如:

>主屏幕发射器
>“设置”中的应用信息
>最近的任务清单

此外,除非您打算永远不要添加新字符串,否则您将遇到永久版本问题.您无法强制第三方翻译支持新字符串,因此您最终会得到一个混合了翻译和非翻译字符串的应用程序.

whether it’s using the API properly

那似乎没问题.

whether its design is future-proof against the unknown versions of Android of tomorrow

Android将访问资源本身的位置数量可能会增加而不是减少.

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.

然后只支持您愿意自己管理的语言.首先,正如我已经指出的那样,我不会在几个方面看到这将是一个完整的解决方案.其次,你似乎认为你的方法会让你不会因为糟糕的翻译而受到指责,虽然它可能会减少你得到的责任,但它不会消除这种责任.在这方面我同意Squonk的观点.

我很钦佩你提供更多翻译的热情,但是,就个人而言,我不会走这条特定的道路.

点赞