lucidiot's cybrecluster

Les traductions compilées de Qt

Lucidiot Informatique 2022-10-23
Documentation d'un format interne de Qt utilisé dans un GPS de randonnée vieux de 10 ans.


Il y a plusieurs années, à une époque où je n'avais pas encore de téléphone et où je n'en avais pas besoin, j'avais obtenu un Magellan eXplorist 110. C'est un GPS de randonnée exécutant Windows CE, utilisant deux piles AA pour une autonomie de 24 heures environ en veille (écran éteint, avec enregistrement de la position GPS toutes les quelques secondes), et qui prend nativement en charge les géocaches. À cette époque, ma principale excuse pour sortir dehors était le géocaching, donc c'était parfait pour mes besoins. Je dispose maintenant d'un téléphone Android avec un forfait de données assez bon et un récepteur GPS plus performant, donc j'utilise directement OsmAnd ainsi que l'application c:geo pour profiter de cartes précises avec OpenStreetMap et me passer du travail fastidieux d'export des caches vers mon GPS.

Parmi la longue liste de recherches que je veux faire, il y a de plus en plus de sujets qui tournent autour de Windows Mobile, l'ancêtre de Windows Phone, et Windows CE. Je regarde parfois les annonces eBay sur des périphériques sous un de ces deux systèmes, et j'ai déjà quelques notes concernant quelques fouilles effectuées avec un émulateur. Je me suis rappelé de ce GPS, et je me suis demandé ce que je pourrais faire avec. La réponse semble être « pas grand chose », puisque le GPS n'exécute pas tout le shell de Windows CE avec juste une application en plus comme certains GPS font, mais utilise juste le noyau de Windows CE en exécutant son propre shell dessus. Cela dit, une après-midi de recherches sur ce GPS et ses données m'a donné envie d'en savoir plus dessus.

Après avoir découvert qu'il existe des documentations très incomplètes des formats de fichiers de carte, qu'il existe des cartes OpenStreetMap à jour pour mon GPS, et que le logiciel VantagePoint qui permet de gérer son GPS et d'accéder aux API Digital Globe ou Geocaching.com ne semble jamais avoir été vraiment exploré par les gens, je me suis fixé comme objectif de documenter autant des fichiers du GPS que possible, qu'ils soient textuels ou binaires, et peut-être aussi plus tard de jouer avec VantagePoint pour rétablir certaines des fonctionnalités en ligne qui semblent ne plus fonctionner désormais.

Le système de mon GPS utilise Qt comme bibliothèque graphique et dispose de traductions dans de nombreuses langues. Il est possible de changer la langue du système très facilement et il y en a un bon nombre disponibles. Dans le système du GPS, qui est complètement accessible en écriture sans restrictions par USB, j'ai pu trouver un grand nombre de fichiers avec l'extension .qm qui semblaient être associés à ces traductions. J'avais envie de mieux comprendre ces fichiers, notamment parce que les fichiers de traduction sont souvent un bon moyen d'associer le texte que je vois sur le GPS à des attributs que je pourrais trouver ailleurs sur le disque. C'est le tout premier format de fichier que j'ai pu terminer de documenter sur mon wiki, donc c'est celui que je publie maintenant, mais ce n'est pas le premier avec lequel j'ai joué chronologiquement.

Pour gérer les traductions, Qt fonctionne selon ce processus général :

  1. Les développeurs utilisent des fonctions et macros particulières pour déclarer des chaînes de caractères à traduire, fournir des explications sur le contexte de ce texte aux traducteurs, etc.
  2. Des fichiers XML sont générés à partir du code source et sont fournis aux traducteurs.
  3. Les traducteurs utilisent QT Linguist pour traduire le texte.
  4. Des fichiers .qm sont générés à partir des traductions terminées et sont fournis avec le programme compilé, pour permettre leur utilisation à l'exécution.

Puisqu'ils relèvent de l'implémentation de Qt lui-même et ne sont pas vraiment censés être touchés par des développeurs utilisant simplement Qt ni par des traducteurs, les fichiers .qm sont donc peu documentés, alors les fonctions C++, le logiciel de traduction et le format XML le sont plus. Pour comprendre comment ces fichiers sont structurés, je me suis donc basé sur des déductions à partir des documentations existantes pour retrouver les concepts de la traduction dans Qt dans ces fichiers, ainsi que le code source de Qt pour la lecture et écriture des fichiers .qm et la traduction du texte à l'exécution des programmes. J'ai également exploré des versions de ces fichiers dans l'archive des anciennes versions de Qt, pour retracer l'évolution de ce format et comprendre l'origine de certaines structures dépréciées, obsolètes ou redondantes.

Principes de base

On va poser quelques principes de base pour que je n'aie pas à me répéter trop tout au long de ma description de la structure des fichiers .qm:

Sauf mention contraire, à chaque fois que je parlerai de nombre ou de chaîne de caractères, ces principes s'appliqueront.

Structure générale

Un fichier de traductions compilées de Qt commence par un nombre magique rendant son format facilement identifiable, et se suit d'une série de blocs jusqu'à sa fin :

3C B8 64 18 CA EF 9C 95 CD 21 1C BF 60 A1 BD DD Bloc 1 Bloc 2 ... Nombre magique Blocs

Bloc

Un bloc se définit par un tag, qui indique le type du bloc, puis un nombre spécifiant la taille de son contenu, suivi de son contenu.

Tag Longueur Contenu

Le tag est un octet non signé dont les seules valeurs connues sont les suivantes :

0x2F
Bloc de contextes
0x42
Bloc de hachages
0x69
Bloc de messages
0x88
Bloc de règles de numération
0x96
Bloc de dépendances
0xA7
Bloc de langue

La longueur d'un bloc est un nombre non signé sur 32 bits. Le contenu de chacun des blocs est décrit ci-après.

Contextes

Quand le fichier QM a été généré en utilisant lrelease -compress, les messages dans le fichier sont regroupés selon leur préfixe commun : leur hachage, ou leur hachage et contexte, ou leur hachage, contexte et texte source. Le préfixe commun sera stocké dans ce bloc de contextes, et il ne sera plus mentionné dans le bloc de messages que pour le tout premier message qui a ce préfixe.

Table de hachages Ensemble de contextes

Table de hachages

La table de hachages permet d'associer à un hachage une position dans l'ensemble de contextes à partir de laquelle le contexte doit être recherché. On calcule un hachage du contexte qu'on recherche, qui correspondra toujours à un index dans la table. On recherche donc la valeur à la position donnée dans la table de hachages. Si cette valeur est de zéro, alors ce contexte n'est pas pris en charge par ce fichier de traductions. Sinon, on peut rechercher dans l'ensemble des contextes à partir de 2× la position qu'on vient d'obtenir, jusqu'à ce qu'on trouve le contexte qu'on voulait, ou qu'on trouve une chaîne de caractères vide.

Longueur Position 1 Position 2 ...
Longueur
Entier à 16 bits indiquant le nombre de positions définies dans cette table. La longueur ne peut excéder 131072 : si elle dépasse, alors lrelease se comportera comme si -compress n'a pas été défini, et le bloc de contextes n'existera pas et le bloc de messages se comportera normalement.
Position
Entier à 16 bits, multiple de 2, indiquant la position dans l'ensemble de contextes à partir de laquelle on peut commencer à rechercher le contexte. Si la position est zéro, alors le contexte désigné par le hachage n'est pas pris en charge.

Ensemble de contextes

L'ensemble de contextes est juste une série de contextes, sans autres attributs particuliers, car il n'est pas conçu pour un accès séquentiel classique mais pour un accès à partir des positions définies dans la table de hachages.

0x0000 Contexte 1 Contexte 2 ...

Puisque la position zéro dans la table de hachages indique un hachage invalide, les deux premiers octets sont toujours définis à zéro, ce qui correspond à un contexte vide.

Contexte

Un contexte, dans le contexte de ce bloc de contextes, n'est pas la même chose qu'un contexte dans le contexte du bloc de messages, et devrait plutôt être défini comme un préfixe commun.

Longueur Texte Marge
Longueur
Un octet définissant la longueur du texte de ce contexte.
Texte
Chaîne de caractères représentant ce préfixe commun. Si le préfixe commun dépasse 255 caractères, il est tronqué.
Marge
Un octet nul (0x00) qui est ajouté pour permettre à la taille de l'ensemble du bloc d'être toujours un multiple de 2 ; il est donc ajouté à chaque fois que la longueur est un multiple de 2.

Hachages

Le bloc de hachages est un autre bloc permettant une recherche rapide d'un message dans le bloc de messages. Il n'a pas de lien avec la table de hachages définie dans le bloc de contextes, et est présent même quand on n'active pas l'option -compress lors de la génération du fichier QM avec lrelease.

Hachage 1 Hachage 2 ...

Hachage

Cet élément permet d'associer à un hachage d'un texte source et d'un commentaire la position au sein du bloc de messages où le message correspondant sera trouvé. Contrairement aux positions de la table de hachages du bloc de contextes, cette position est exacte.

Hachage Position
Hachage
Entier à 4 octets représentant le hachage de la concaténation du texte source et du commentaire d'un messsage.
Position
Position exacte, en octets à partir du début du contenu du bloc de messages, du message correspondant.

Messages

Les messages sont constitués d'une série d'attributs, qui servent à leur donner diverses propriétés. La fin d'un message est marquée par la présence d'un attribut de type fin. Ce sont ces messages qui contiennent les traductions en elles-mêmes.

Attribut 1 Attribut 2 ... Attribut de fin

Attribut

Un attribut d'un message se compose d'un tag, distinct des tags de blocs et qui donne leur signification, ainsi que d'un contenu arbitraire.

Tag Contenu

Le tag d'un attribut est un octet non signé pouvant avoir les valeurs suivantes :

  1. Attribut de fin
  2. Texte source (UTF-16)
  3. Traduction
  4. Contexte (UTF-16)
  5. Hachage, ou Obsolete 1
  6. Texte source
  7. Contexte
  8. Commentaire
  9. Obsolete 2

On notera qu'à divers endroits du code source, des règles spécifiques sont appliquées pour vérifier qu'un message est valide :

Attribut de fin

Un attribut de fin n'a pas de contenu du tout. Il indique juste la fin du message.

Texte source

Le texte source est la chaîne de caractères à traduire, définie par les développeurs dans le code source. Si un fichier de traduction était introuvable pour la langue sélectionnée, c'est ce texte qui serait affiché.

Longueur Texte

La longueur est un entier à 4 octets. Le texte est encodé en UTF-16 si le tag choisi indique UTF-16, sinon c'est en UTF-8.

Si les traductions se basent sur des identifiants, et pas sur du texte à taduire directement, alors le texte source sera l'identifiant de la traduction, et les autres attributs de contexte et de commentaire seront vides.

Traduction

La traduction est la chaîne de caractères traduite. Elle se représente d'une façon très proche du texte source :

Longueur Texte

En toutes circonstances, le texte sera en UTF-16.

Contexte

Le contexte est une chaîne de caractères spécifiant le contexte dans lequel ce message apparaît. C'est généralement le nom d'une classe Qt.

Longueur Texte

La longueur est un entier à 4 octets. Le texte est encodé en UTF-16 si le tag choisi indique UTF-16, sinon c'est en UTF-8.

Hachage (obsolète)

Cet attribut stocke un hachage de ce message, ce qui était censé aider à la recherche rapide de ce message. Ce hachage est maintenant placé dans le bloc de hachages, ce qui accélère la recherche d'un message sans avoir à d'abord lire l'ensemble du bloc de messages.

Hachage

Le hachage est un entier à 4 octets, défini de la même manière que le hachage d'un message dans le bloc de hachages.

Commentaire

Le commentaire, aussi appelé la désambiguisation, est une chaîne de caractères que les développeurs peuvent optionnellement définir dans le code source pour aider les traducteurs à comprendre comment ils doivent traduire le message.

Longueur Texte

Attribut inconnu obsolète

Dans le code source de Qt 2.2.0, on voit apparaître un tag Obsolete 1, et je déduis du code source que les blocs avec ce tag ne contenaient qu'un seul octet. La signification de cet octet m'échappe, puisque aucune version de Qt avant 2.2.0 ne semble disposer d'un tag qui aurait pu devenir ce tag obsolète ; cette nouvelle version de Qt a introduit une fonctionnalité déjà obsolète à sa sortie !

octet Mysté rieux

On notera que dans les versions modernes de Qt, ce tag s'appelle maintenant Obsolete 2. Le nom Obsolete 1 a été pris par le bloc de hachages.

Règles de numération

Les règles de numération ont pour nom original numerus rules, où rules est l'anglais pour règles et numerus est du latin. Ces règles servent à définir dans quels contextes on doit utiliser une forme de pluriel pour certaines traductions. En français, on va utiliser le singulier pour zéro ou un, et le pluriel pour tout le reste : zéro objet, un objet, deux objets, etc. En anglais, on utilise le singulier uniquement pour un : zero objects, one object, two objects. Dans d'autres languages, des règles beaucoup plus complexes peuvent s'appliquer : en écossais, on a le nullaire pour zéro, le singulier pour un, le dual de deux à cinq, le sexal pour six, et le pluriel pour le reste.

Composant 1 Composant 2 ...

Les règles de numération forment une liste de composants de règles. Chaque composant de règle commence toujours par un opérateur. Un opérateur est un seul octet dont les bits forment une série de drapeaux booléens. La signification de chacun de ces bits peut être représentée avec trois structures différentes pour ce seul octet.

Qt définit les formes disponibles pour chaque langue d'une façon qui n'est pas configurable, tout comme les règles de numération, donc je ne vois pas vraiment à quoi servent les règles de numération dans les fichiers QM, mais elles sont là. Lorsqu'un langage a un nombre N de formes plurielles, il devrait y avoir N-1 règles. Les formes sont définies dans le code de Qt dans un ordre donné. La position de chaque règle définit à quelle forme elle correspond. Si une règle s'applique à une valeur, alors on choisit la forme correspondante. Si aucune règle ne s'applique, on choisit la dernière forme de la liste.

Opérateurs logiques

Les composants de règle représentant des opérateurs logiques n'apparaissent jamais en début ou en fin de règle ; ils sont toujours entourés de composants arithmétiques. L'octet qui représente un opérateur logique se décompose ainsi :

1 1 1 1 1 1 Et Ou

Les 6 premiers bits sont à 1 n'ont pas d'autre signification particulière. Les deux derniers bits définissent le comportement qui sera appliqué pour cet opérateur :

Opérateurs arithmétiques binaires

Les composants de règle représentant des opérateurs arithmétiques sont toujours suivi de un ou de deux octets représentant des nombres entiers non signés. Dans le cas d'un opérateur arithmétique binaire, il n'y a qu'un seul octet, qui représente la seconde opérande. La première opérande est la valeur qu'on essaie de comparer pour déterminer la forme de pluralisation à appliquer.

Octets du composant de : binaire 2 gle Opé rateur arithmé tique Opé rande

Les bits de l'octet qui représentent cet opérateur sont plus complexes :

Bits de : 0 Milliers Modulo 100 Modulo 10 Non 0 à l'opé rateur Infé rieur É galité

Le premier bit est toujours défini à zéro, ce qui permet de distinguer entre un opérateur logique et un arithmétique. Les deux derniers bits indiquent si l'opérateur est inférieur à, égal à ou inférieur ou égal à. Définir Inférieur à et Égalité à zéro est invalide.

Les bits 2 à 5 définissent des options qui s'appliquent à l'opérateur :

Non
Inverser l'opérateur : égal à devient différent de, inférieur à devient supérieur ou égal à, et inférieur ou égal à devient inférieur à.
Modulo 10
Le reste de la division euclidienne par 10 de la valeur à comparer est utilisé avec cet opérateur.
Modulo 100
Le reste de la division euclidienne par 100 de la valeur à comparer est utilisé avec cet opérateur.
Milliers
Tant que la valeur est supérieure à 1000, on la divise par 1000. Ainsi, 1234 devient 1, 3 000 000 devient 3, et 111 222 333 444 devient 111. Cette option ne semble plus utilisée dans aucune règle depuis Qt 5, mais elle est toujours disponible.

Opérateur arithmétique ternaire

Il existe un seul opérateur arithmétique ternaire, l'opérateur « entre ». Il est suivi par deux octets qui représentent les limites inférieures et supérieures de l'intervalle. L'intervalle inclut les limites définies.

Octets du composant de : ternaire Limite Limite gle Opé rateur arithmé tique infé rieure supé rieure

Cet opérateur réutilise les quatre options de l'opérateur binaire, et utilise les trois derniers bits différemment :

Bits de : 0 Milliers Modulo 100 Modulo 10 Non 1 0 0 l'opé rateur

On peut certainement regrouper cet opérateur avec la définition de l'opérateur arithmétique binaire faite au-dessus, mais indiquer que les octets qui suivent ont une signification différente aurait été complexe.

Dépendances

Le bloc de dépendances liste les fichiers de traduction dont dépend ce fichier de traduction.

Fichier 1 Fichier 2 ...

Chaque fichier est une chaîne de caractères. Chaque octet nul fait office de délimitation entre chaque nom de fichier.

Langue

Le contenu du bloc de langue est plus court que son en-tête :

Code de langue

Le bloc de langue ne contient qu'une chaîne de caractères qui indique la langue utilisée par ce fichier de traduction. Le code de langue se constitue d'un code de langue de deux lettres ISO 639-1 ou trois lettres ISO 639-2. Il est possible de spécifier le pays pour indiquer une variante possible du langage, en ajoutant le code du pays en majuscules après un caractère _ : fr_FR indique le français en France, et fr_CA le français au Canada.

Si ce bloc n'est pas spécifié, Qt peut potentiellement déterminer le code de langue depuis le nom du fichier.

Conclusion

Documenter ce format de fichier m'a demandé pas mal de travail, bien plus que ce que je prévoyais au départ. Le fait que Qt soit une bibliothèque logicielle vieille de plus de 30 ans, qu'elle aie été gérée par plusieurs entreprises différentes, qu'elle aie été propriétaire puis open-source, etc. a un impact sur la complexité de ce format, puisque certaines portions ne sont plus du tout clairement définies de par leur disparition dans des versions plus récentes.

Je compte plus tard formaliser cette spécification en un schéma Kaitai Struct. Je ne suis pas encore sûr de si je veux ou non créer un éditeur en C# pour ces fichiers de traduction ; ça me permettrait de modifier quelques traductions incorrectes dans les fichiers français, mais je ne sais pas si ça en vaut vraiment l'effort. Je vais peut-être d'abord me concentrer sur le fait de comprendre le reste des nombreux fichiers de mon GPS.


Commentaires

Il n'y a pour l'instant aucun commentaire. Soyez le premier !