Bienvenue sur IndexError.

Ici vous pouvez poser des questions sur Python et le Framework Django.

Mais aussi sur les technos front comme React, Angular, Typescript et Javascript en général.

Consultez la FAQ pour améliorer vos chances d'avoir des réponses à vos questions.

Changer la langue et le thème d'une application interactivement

+2 votes

C'est un problème qui concerne l'approche d'une conception à adopter plutôt qu'un problème de code.

Je résume.
J'ai une application graphique créée avec tkinter, dans celle-ci j'ai comme tant d'autres une partie gérant les préférences utilisateurs, notamment la langue, et le thème (couleurs/images) qui peuvent être changés.

La solution la plus simple est d'afficher un message informatif stipulant que les modifications du thème et langage ne seront effectifs qu'après relancement de l'application, mais ça j'aimerais éviter.

Donc je voudrais que cela se fasse interactivement, comme le font les systèmes d'exploitations lorsque l'on change icones/polices/couleurs/etc.
(je suis incapable de comprendre le code source d'un gestionnaire graphique, trop complexe pour mes connaissances basiques)

Et pour cela je ne sais comment implémenter ça, ni qu'elle approche adopter.
Suis-je contraint de modifier chaque classe de mon application pour y ajouter une méthode destinée à cette tâche ?
Y a t-il des designs patterns utiles pour faire ce genre de chose ?

Encore que pour changer la langue de l'application cela sera je pense bien plus simple que pour changer le thème graphique.
Car celle-ci possède beaucoup de widget tkinter, notamment un canvas contenant plusieurs objets et qui ont évidemment des paramètres graphiques différents.

Ce qui me pose problème c'est cette modification/répercussion en profondeur dans toutes les classes.

J'ai vu qu'il existait gettext, mais je ne sais si c'est vraiment ça dont j'ai besoin, puisque tout ce qui concerne le langage (j'ai pas trop l'intention de faire ça en 300 langues différentes), et thèmes sont définis dans des fichiers.

Donc pour résumer, ma question porte sur :
Comment modifier le langage et thème d'une application lorsque elle est en cours d'exécution, donc qu'après chaque changement tout soit modifié automatiquement ?

demandé 17-Aou-2016 par toupie carrée

2 Réponses

+1 vote
 
Meilleure réponse

Changer la langue sur l'ensemble des widget affichant du texte ne devrait pas être problèmatique.
La technique «de base» serais de parcourir l'ensemble des widgets et de changer leur label avec widget.configure ou widget.set_text.

Une autre solution glanée ici utilise tkinter.StringVar. Cet objet se conporte comme une string enployée comme label, mais possède un side effect intéressant : si elle change, les widgets l'utilisant comme label seront également mis à jour.

Je n'ai jamais utilisé tkinter, le code suivant est donc probablement faux concernant les appels à tkinter :

# Languages
LANGUAGE = {
    'français': {
        'update': 'mise à jour',
        'close_window': 'fermer la fenêtre',
        'exit': 'quitter',
    }, 'english': {
        'update': 'update',
        'close_window': 'close window',
        'exit': 'exit',
    }
}

LANGUAGE_DEFAULT = LANGUAGE['français']


# keep together all the string that are related to language
variable_strings = {label: tkinter.StringVar(translation)
                    for label, translation in LANGUAGE_DEFAULT.items()}

# create widget
update_button = tkinter.Button(label=variable_strings['update'])
close_button = tkinter.Button(label=variable_strings['close_window'])
exit_button = tkinter.Button(label=variable_strings['exit'])
language_box = tkinter.ListBox(LANGUAGE.keys())  # don't know if it works that way


def change_language(variable_strings:dict, new_lang=LANGUAGE['english']):
    """Change translations in given {label: translation} to given new language"""
    for label, translation in variable_strings:
        translation.set(new_lang[label])

Le code pour créer les boutons et la liste de langues à sélectionner est probablement faux,
mais il donne l'idée : utiliser le dictionnaire variable_strings pour accéder aux instances de tkinter.StringVar, et les donner aux constructeurs de widgets.

La fonction change_language gère la modification des instances de tkinter.StringVar,
en leur donnant la traduction donnée en argument (par défaut, l'anglais). Probablement faudrait-il faire en sorte qu'elle soit appelée avec les bons arguments lorsque language_box est changée.

Concernant la définition des langues, souvent on met les dictionnaires dans un fichier au format human-readable (JSON, souvent), et on lis le ou les fichier(s) au démarrage du programme.
Ça permet d'externaliser du code source des définitions dont personne n'a besoin pour comprendre le code. L'autre solution est de les mettre dans un module dédié, encodé en dur en python.

répondu 18-Aou-2016 par lucas (2,332 points)
sélectionné 21-Aou-2016 par max

Ah mais c'est carrément bien pensé !

Le pire c'est que j'utilise déjà pas mal StringVar, IntVar dans mon code, mais j'ai pas percuté que ça pouvait également servir à faire cela.

Comme quoi il est bon de poser une question avant de s'embourber dans son code.

Merci, ça me convient parfaitement =)

Pas de problème !

Merci d'accepter la réponse si elle a répondu à la question.

Je ne peux le faire, c'est réservé aux gens connectés.
Si un membre peux valider la réponse de lucas pour moi, ce serait gentil =)

+1 vote

En complément de la réponse de @lucas, je te fais un retour car j'ai rencontré exactement les mêmes questions à l'époque de mes débuts en dév sur mon projet DicoGIS.

Voici comment j'ai procédé :

  1. un sous-dossier locale (dans mon cas, lui-même dans un sous-dossier data) avec des sous-dossiers nommés par les préfixes ISO de chaque langue (enfin presque)

  2. une liste déroulante (ttk.combobox) dont les valeurs sont remplies avec une ligne de code au chargement de l'interface directement à partir des sous-dossiers et la commande liée à une méthode dédiée :

    lilang = [lg[5:-4] for lg in listdir(r'data/locale')] # languages
    self.ddl
    lang = Combobox(self,
    values=lilang,
    width=5)
    self.ddl
    lang.current(lilang.index(self.deflang))
    self.ddllang.bind("<>", self.changelang)

  3. quand l'utilisateur choisit une langue dans l'interface, ça envoie donc vers la méthode sus-nommée chargée de mettre à jour le dictionnaire des textes, d'appliquer les nouveaux textes aux widgets qui vont bien et de changer la locale (avec la prise en compte des fucking différences d'OS) :

    def changelang(self, event):
    """ update the texts dictionary with the language selected """
    new
    lang = self.ddllang.get()
    # change to the new language selected from dedicated submodule
    TextsManager().load
    texts(dicotexts=self.blabla,
    lang=new
    lang,
    locale_folder=r'data/locale')
    # update widgets text
    self.welcome.config(text=self.blabla.get('hi') + self.uzer)
    [... pareil avec tous les widgets....]

    # setting locale according to the language passed
    try:
        if opersys == 'win32':
            if new_lang.lower() == "fr":
                locale.setlocale(locale.LC_ALL, str("fra_fra"))
            elif new_lang.lower() == "es":
                locale.setlocale(locale.LC_ALL, str("esp_esp"))
            else:
                locale.setlocale(locale.LC_ALL, str("uk_UK"))
        else:
            if new_lang.lower() == "fr":
                locale.setlocale(locale.LC_ALL, str("fr_FR.utf8"))
            elif new_lang.lower() == "es":
                locale.setlocale(locale.LC_ALL, str("es_ES.utf8"))
            else:
                locale.setlocale(locale.LC_ALL, str("en_GB.utf8"))            
    
        logging.info('Language switched to: {0}'\
                         .format(self.ddl_lang.get()))
    except locale.Error:
        logging.error('Selected locale is not installed')
    
    # End of function
    return self.blabla
    

Et le tour est joué !

A noter que :

  • les fichiers contenant les textes sont au format XML
  • c'est pas très beau mais ça marche (et puis bon tkinter/ttk c'est pas fait pour être beau)
  • l'alternative gettext est vraiment pas mal mais demande un peu plus de taff c'est vrai. par contre, il est possible de rendre son code compatible avec gettext et ses conventions sans pour autant l'utiliser (en entourant toute chaîne de _("string") par exemple)

    • on peut imaginer modifier dynamiquement tous les widgets avec un boucle, en mettant par exemple la réf du widget en attribut du xml. j'ai jamais pris le temps de faire ça mais j'ai la base sur un bout de code qui permet d'activer/désactiver dynamiquement tous les widgets d'une classe parent (frame, labelfrmae...) :

    def uiswitch(self, cbvalue, parent, ):
    """ Easy change state of all children widgets
    within a parent class

    cb_value=boolean
    parent=Tkinter class with children (Frame, Labelframe, Tk, etc.)
    """
    if cb_value.get():
        for child in parent.winfo_children():
            child.configure(state=ACTIVE)
    else:
        for child in parent.winfo_children():
            child.configure(state=DISABLED)
    # end of function
    return
    
répondu 25-Aou-2016 par GeoJ (164 points)

Merci du retour (•‿•)

Mais d'après ce que j'ai pu en voir, tout est en dur dans ton code, or dans mon cas, un widget peut ou ne pas exister, un texte dans un canevas, pareil, il peut y avoir des toplevels en cours d'exécution ou non, enfin tout ça pour dire que mon application est assez modulable, et donc faut que je stocke les instances des widgets/éléments text du canvas dans un coin pour que je sache en temps réel ce qui doit être mit à jour.

Pas difficile pour enregistrer les objets dans un "annuaire", mais j'ai pas encore trop réfléchi (enfin si, mais faut que je teste) pour qu'ils en soient sorti (de cet annuaire) automatiquement à la destruction du widget ou du composant canevas.

Et là, c'est pour la partie facile, parce que pour le thème graphique, ça va être à peu près pareil, hormis qu'il faudra fournir en sus la propriété à mettre à jour (background, foreground, etc), et les relations valeurs thème/propriété widget sont souvent 1 à n.

Un petit casse-tête tout ça (°_o)

De toute évidence les vars tkinter vont bien m'être utile, mais faut que je réfléchisse beaucoup sur l'organisation de tout ce tintouin.


Bon indexerror est lourdingue, entre le catchpa à la con qui se met pas à jour et qui mouline indéfiniment (cool de rafraîchir la page 50 fois), et qui fait passer 2 étapes, le javascript complètement buggué, les scripts externes ça pue, google ça pue, les cloud* ça pue, et le fait que le message doit être validé pour être affiché.
Si vous vouliez faire quelque chose de répulsif, c'est réussi, bravo ! Manque plus que la connexion via facebook & autres merdes pour avoir la cerise sur le pompon du gâteau.
Ouais je suis énervé, mais les sites qui fonctionnent pas sans toutes ces merdes externes, généralement je fuis, et là je vais fuir, merci en tout cas d'avoir répondu à ma question.

Personnellement, je n'ai aucun problème, et pourtant google et cloudflare sont bloqués ; c'est peut-être juste que je suis connecté avec un compte.

...