273 lines
10 KiB
Python
273 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
||
'''
|
||
Créer une mémoire de traduction à partir de fichiers texte parallèles.
|
||
|
||
Cet utilitaire produit un fichier TMX conforme à la version 1.4 de la spécification TMX (https://www.gala-global.org/tmx-14b, en anglais) à partir de fichiers encodé en UTF-8 qui contiennent le même nombre de lignes.
|
||
|
||
Limitations :
|
||
|
||
1. Les fichiers texte doivent être placé dans un répertoire `fichiers_texte`
|
||
du répertoire de l'utilitaire.
|
||
2. Ces fichiers doivent avoir une extension `.txt` et être encodés en UTF-8.
|
||
Leur nom doit se terminer par `_LL`, où LL est le code de la langue du
|
||
fichier.
|
||
3. On suppose que la mémoire part d'un texte en français, qui est donc
|
||
désigné comme langue source. (Cela signifie également qu'il doit
|
||
impérativement y avoir un fichier `texte_fr.txt` parmi les fichiers
|
||
à aligner.)
|
||
4. Le fichier TMX est sauvegardé dans un répertoire `fichier_tmx` créé dans
|
||
le répertoire de l'utilitaire, avec le même nom de base que celui du
|
||
fichier en français.
|
||
|
||
Utilisation :
|
||
|
||
1. Mettez les fichiers à aligner dans le sous-répertoire `fichiers_texte`,
|
||
en vous assurant qu'ils sont conformes aux limitations ci-dessus.
|
||
2. Lancez le script depuis la ligne de commande ou votre éditeur.
|
||
Avec un éditeur, il est possible que le chemin d'exécution ne soit pas
|
||
celui du script, et vous obtiendrez une erreur parce que le script ne
|
||
trouvera pas les fichiers texte.
|
||
Dans un tel cas, vérifiez les réglages de votre éditeur, ou modifiez
|
||
le chemin des fichiers dans le code.
|
||
'''
|
||
|
||
##################################################
|
||
#
|
||
# textes_à_tmx.py
|
||
#
|
||
##################################################
|
||
# Version 0.3.1 du 06/01/2023
|
||
#
|
||
# Auteur : Philippe Tourigny (kazephil@gmail.com)
|
||
#
|
||
# Code créé à titre d'exemple dans le cadre du stage
|
||
# des M2 TRE du printemps 2023.
|
||
#
|
||
# À faire :
|
||
#
|
||
# L'utilitaire a été conçu en supposant la création d'une interface graphique
|
||
# dans une version ultérieure.
|
||
# L'interface graphique prévoit :
|
||
# - permettre de sélectionner les fichiers à aligner ;
|
||
# - permettre de saisir les langues source et cible(s) ;
|
||
# - permettre de sélectionner le répertoire et le nom du fichier TMX
|
||
# - donner le choix de continuer ou d'arrêter si les fichiers ne contiennent
|
||
# pas tous le même nombre de lignes.
|
||
#
|
||
# Autres améliorations à apporter :
|
||
# - Ajouter du code pour traiter les erreurs.
|
||
#
|
||
# Licence
|
||
# Le code est distribué sous la license GPL3+
|
||
# https://www.gnu.org/licenses/gpl-3.0.en.html
|
||
#
|
||
##################################################
|
||
|
||
## Paramètres d'importation ##
|
||
# La classe `Path` est importée pour simplifier la manipulation des fichiers.
|
||
# La classe `etree` et `objectify` du module `lxml` sont importées pour
|
||
# assurer la validité du XML.
|
||
# La classe `objectify` du module `lxml` n'est pas essentielle, mais a été
|
||
# choisie parce qu'elle simplifie l'accès aux sous-éléments d'un élément XML.
|
||
|
||
from pathlib import Path
|
||
|
||
from lxml import etree as et
|
||
from lxml import objectify
|
||
|
||
## Constantes ##
|
||
# En Python, contrairement à d'autres langages de programmation, il n'y a pas
|
||
# de « vraies » constantes. Par convention, on désigne des variables toute en
|
||
# majuscules comme « constantes » dont on évite de changer la valeur ailleurs
|
||
# dans le code.
|
||
|
||
# Contante pour créer l'attribut « xml:lang », qui contient l'espace de
|
||
# nom « xml » et qui ne peut être entrée directement dans le code à cause
|
||
# du deux-points.
|
||
XMLLANG = et.QName('http://www.w3.org/XML/1998/namespace', 'lang')
|
||
|
||
# J'ai découver la méthode `.QName()` grâce à un stagiaire. À l'origine,
|
||
# le code utilisait deux constantes, une pour l'espace de nom, et l'autre
|
||
# pour créer l'attribut au complet :
|
||
|
||
# XML = 'http://www.w3.org/XML/1998/namespace' # Espace de nom « xml »
|
||
# LANG = '{' + XML + '}' + 'lang' # Attribut « xml:lang »
|
||
|
||
# Valeurs à récuperer via une interface graphique dans une version ultérieure
|
||
CORPUSPATH = Path(Path().cwd()/'fichiers_texte')
|
||
SOURCE_LANG = 'fr'
|
||
|
||
## Fonctions ##
|
||
# Les fonctions permettent de modulariser le code, ce qui le rend plus facile à modifier, déboguer, et simplifie aussi l'ajout de nouvelles fonctinnalités.
|
||
|
||
def get_languages_and_files(corpuspath):
|
||
'''Récupérer les fichiers et leur langues.'''
|
||
|
||
# La méthode `.glob()` produit une liste de fichiers correspondant
|
||
# au modèle précisé.
|
||
textfiles = corpuspath.glob('*.txt')
|
||
|
||
# On utilise ici une technique appelée "dictionary comprehension" pour
|
||
# récupérer le code de langue en tant que clé du dictionnaire, et l'objet
|
||
# Path correspondant au fichier pour la valeur.
|
||
languages_and_files = {l.stem.rsplit('_')[1]:l for l in textfiles}
|
||
|
||
return languages_and_files
|
||
|
||
|
||
def read_content_by_language(languages_and_files):
|
||
'''Lire le contenu du fichier pour chacune des langues'''
|
||
|
||
# Créer une liste de language avec la langue source en tête.
|
||
|
||
# On produit d'abord une liste qui ne contient que les langues.
|
||
languages = list(languages_and_files.keys())
|
||
|
||
# On s'assure que le français arrive en premier en le retirant
|
||
# de la liste (méthode .pop()) et en le réinsérant en tête de
|
||
# liste (méthode .insert()).
|
||
languages.insert(0,languages.pop(languages.index(SOURCE_LANG)))
|
||
|
||
# Lire le contenu du fichier correspondant à chacune des langues.
|
||
contents = []
|
||
|
||
# Pour chacune des langues, on crée une liste qui contient toutes les
|
||
# lignes du fichier correspondant, et on met ces listes dans une liste
|
||
# avec tout le contenu.
|
||
for language in languages:
|
||
with open(languages_and_files[language], 'r', encoding='utf8') as c:
|
||
contents.append(c.read().splitlines())
|
||
|
||
# On renvoie une contruction "tuple" qui contient la liste des langues
|
||
# et la liste avec les listes de lignes correspondantes.
|
||
return (languages, contents)
|
||
|
||
|
||
def prepare_units(contents):
|
||
'''Jumeler les phrases correspondantes dans chacune des langues.'''
|
||
|
||
# Le ' * ' devant la variable contents est un "unpacking operator".
|
||
# Il sépare la liste de listes de la variable `contents` en listes
|
||
# individuelles.
|
||
|
||
# La fonction `.zip()` se charge en suite de jumeler les lignes de chacune des listes.
|
||
units = zip(*contents)
|
||
|
||
return units
|
||
|
||
|
||
def create_tmx_document():
|
||
'''Créer la structure de base de la TMX.'''
|
||
|
||
# Créer la racine.
|
||
tmx = objectify.Element('tmx', {'version': '1.4'})
|
||
|
||
# Dictionnaire pour définir les attributs requis de l'élément <header>.
|
||
# La valeur de l'attribut `srclang` est récupérée depuis la variable
|
||
# SOURCE_LANG.
|
||
tmxheader = {'creationtool':'Utilitaire stage 2023',
|
||
'creationtoolversion': '0.3', 'datatype': 'plaintext',
|
||
'segtype': 'sentence', 'adminlang': 'en-US',
|
||
'srclang': SOURCE_LANG, 'o-tmf': 'text files'}
|
||
|
||
# Créer les éléments `<header>` et `<body>`.
|
||
|
||
# On utiliser le dictionnaire ci-dessus pour ajouter tous les attributs
|
||
# requis à l'élément `<header`
|
||
header = objectify.SubElement(tmx, 'header', tmxheader)
|
||
body = objectify.SubElement(tmx, 'body')
|
||
|
||
return tmx
|
||
|
||
def make_tu(languages, unit):
|
||
'''Créer un élément <tu> contenant les <tuv> et <seg> pour toutes les langues d'un segment.'''
|
||
|
||
tu = objectify.Element('tu')
|
||
|
||
# On utilise la fonction `enumerate()` fournit un compte des itérations
|
||
# en plus de la valeur de l'itérateur. Ici, à chaque itération, on obtient
|
||
# le compte correspondant à la langue du `<tuv`, et le code de cette langue.
|
||
for l, language in enumerate(languages):
|
||
# Définir et ajouter l'élément <tuv>
|
||
tuv = objectify.SubElement(tu, 'tuv', {XMLLANG: language})
|
||
|
||
|
||
# Définir et ajouter l'élément <seg>
|
||
# On ne peut pas assigner directement l'attribut `@text` avec la
|
||
# classe `objectify`.
|
||
# Syntaxe pour insérer le texte trouvée ici :
|
||
# https://stackoverflow.com/a/2151303/8123921
|
||
|
||
# On utilise le compte pour récupérer le texte de la phrase
|
||
# correspondant à la langue du `<tuv>` dans le tuple de la
|
||
# variable `unit`.
|
||
tuv.seg = objectify.E.seg(unit[l])
|
||
|
||
return tu
|
||
|
||
|
||
def build_final_tree(tmx):
|
||
'''Créer l'arbre XML au complet pour la TMX.'''
|
||
|
||
# Créer l'arbre
|
||
|
||
# Insérer le document TMX dans un `ElementTree` confère certains avantages
|
||
# au niveau de l'écriture du fichier.
|
||
tmxtree = et.ElementTree(tmx)
|
||
|
||
# Ajouter le DTD au niveau du système.
|
||
tmxtree.docinfo.system_url = 'tmx14.dtd'
|
||
|
||
return tmxtree
|
||
|
||
|
||
def save_tmx(basename, tmxtree):
|
||
'''Définir le chemin et le nom complet du fichier TMX, et le sauvegarder.'''
|
||
|
||
# Définir le chemin et nom du fichier.
|
||
tmxext = '.tmx'
|
||
tmxname = basename + tmxext
|
||
|
||
tmxpath = Path(CORPUSPATH.parent/'fichier_tmx')
|
||
|
||
# Créer le chemin s'il n'existe pas.
|
||
if not tmxpath.exists():
|
||
tmxpath.mkdir(parents=True, exist_ok=True)
|
||
|
||
tmxfile = Path(tmxpath/tmxname)
|
||
|
||
# Écrire le fichier
|
||
objectify.deannotate(tmxtree, cleanup_namespaces=True)
|
||
tmxtree.write(tmxfile, encoding='utf-8',
|
||
xml_declaration=True, pretty_print=True)
|
||
|
||
|
||
## Programme principal ##
|
||
|
||
if __name__ == '__main__':
|
||
|
||
# Récupérer les langues et le contenu des fichiers
|
||
languages_and_files = get_languages_and_files(CORPUSPATH)
|
||
languages, contents = read_content_by_language(languages_and_files)
|
||
|
||
# Définir le nom de base du fichier
|
||
basename = languages_and_files[SOURCE_LANG].stem.strip('_'+SOURCE_LANG)
|
||
|
||
# Créer un document TMX
|
||
tmx = create_tmx_document()
|
||
|
||
# Jumeler les unités de traduction
|
||
units = prepare_units(contents)
|
||
|
||
# Créer les éléments `<tu>` avec les `<tuv>` contenant le texte des
|
||
# segments pour chacune des langues.
|
||
for unit in units:
|
||
tu = make_tu(languages, unit)
|
||
tmx.body.append(tu)
|
||
|
||
# Compléter l'arbre XML
|
||
tmxtree = build_final_tree(tmx)
|
||
|
||
# Sauvegarder le fichier TMX
|
||
save_tmx(basename, tmxtree)
|