4 votes

Permettre à des tiers indépendants de localiser des applications Android ; données de localisation en dehors de l'APK de l'application.

J'ai besoin de développer une application Android qui charge du texte localisé à partir de ressources externes en dehors de sa propre application. APK primaire .
La raison en est de permettre à des tiers de fournir indépendamment des traductions de l'application. L'application dispose actuellement d'une seule localisation en anglais avec un nombre assez important de chaînes de caractères (~2 000).

Je préférerais ne pas m'éloigner du système de ressources d'Android ; par exemple, je voudrais fournir les chaînes localisées dans la langue principale dans strings.xml comme dans n'importe quelle application Android.

Pour ce faire, j'ai créé une classe qui étend Android.content.res.Resources, en surchargeant les trois éléments suivants getText méthodes. Les implémentations de remplacement renverront les ressources de la source de localisation externe lorsque cela est possible, sinon elles transmettront la requête à la méthode super.getText() mise en œuvre.

Emballage des ressources :

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

J'ai ensuite créé une implémentation de ContextWrapper pour envelopper le contexte d'une activité. La méthode getResources() de l'enveloppe du contexte renvoie l'objet IntegratedResources ci-dessus.

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

Enfin, nous avons la classe "ResourceIntegrator", qui extrait les ressources d'un APK de localisation tiers installé et spécifié.
Une implémentation différente pourrait être créée pour les extraire d'un fichier XML ou de propriétés si vous le souhaitez.

Intégrateur de ressources :

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

Ma question à stackoverflow est la suivante si l'implémentation ci-dessus est une bonne idée Il s'agit de savoir si elle utilise correctement l'API et si sa conception est à l'épreuve des versions inconnues d'Android de demain.
Je n'ai jamais vu personne faire cela auparavant, et je ne trouve rien sur la résolution de ce problème dans la documentation ou sur le web.

L'exigence sous-jacente d'autoriser des traductions indépendantes par des tiers est assez critique. Il n'est actuellement pas possible de maintenir en interne des dizaines de traductions pour cette application, et je n'ai pas la possibilité de vérifier la qualité des traductions fournies par les utilisateurs.
Dans le cas où cette conception est une très mauvaise idée et qu'aucune alternative similaire n'est disponible, la localisation devra alors se faire sans le système de gestion des ressources d'Android.

Dans le cas où cette idée serait bonne, n'hésitez pas à utiliser et à améliorer le code ci-dessus.

1voto

CommonsWare Points 402670

Ma question à stackoverflow est de savoir si ce qui précède est une bonne idée.

Je serai stupéfait si c'est une solution complète, puisque vous ne pouvez pas forcer Android à utiliser votre propre système d'exploitation. ContextWrapper . Cela devrait fonctionner pour n'importe quel endroit où vous appelez manuellement. getString() , getText() et autres, mais je ne vois pas comment cela pourrait fonctionner ailleurs. Android accède à ces ressources au-delà de l'une de vos activités. Il existe de nombreux endroits où vous ne pouvez pas appeler getText() et ainsi de suite, comme :

  • le lanceur d'écran d'accueil
  • les informations relatives à votre application dans Paramètres
  • la liste des tâches récentes

De plus, vous aurez des problèmes de versionnement perpétuel, à moins que vous ne prévoyiez de ne jamais ajouter de nouvelles chaînes. Vous n'avez aucun moyen de forcer les traductions tierces à prendre en charge les nouvelles chaînes, et vous vous retrouverez donc avec une application contenant un mélange de chaînes traduites et non traduites.

s'il utilise l'API correctement

Cela semble correct.

si sa conception est à l'épreuve du temps et des versions inconnues d'Android de demain.

Le nombre de lieux où Android accèdera aux ressources elles-mêmes est susceptible d'augmenter, plutôt que de diminuer.

Il n'est actuellement pas possible de gérer en interne des dizaines de traductions pour cette application, et je n'ai pas la possibilité de vérifier la qualité des traductions fournies par les utilisateurs.

Ensuite, ne soutenez que les langues que vous êtes prêt à gérer vous-même. Tout d'abord, comme je l'ai noté, je ne vois pas comment cela pourrait être une solution complète, sur plusieurs fronts. Deuxièmement, vous semblez penser que votre approche vous évitera d'être blâmé pour des traductions médiocres, et bien que ce soit probablement le cas, vous n'avez pas l'intention de le faire. réduire le site quel que soit le nombre de reproches que vous recevez, cela n'éliminera pas ces reproches. Je suis d'accord avec Squonk à cet égard.

J'admire votre zèle à proposer davantage de traductions, mais, personnellement, je ne m'engagerais pas dans cette voie.

1voto

Joe Bowbeer Points 878

Je ne pense pas que votre technique fonctionnera pour les références de ressources dans les mises en page. Celles-ci sont résolues par le LayoutInflater et finalement par l'implémentation de TypedArray, qui invoque des méthodes privées pour charger les ressources.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X