Développer un plugin pour WordPress
7 – Internationalisation
20 octobre 2016.
Toutes mes excuses
Bon ok, faut admettre, j’ai merdé ! J’aurais dû…! Et je l’ai pas fait…! Et pourtant, je ne le sais que trop bien : reculer, c’est assez moyen ballot en guise de stratégie d’autant que y’a un moment, toujours, où il faut bel et bien se décider à sauter… Et là, forcément, le trou s’est agrandi puisque me voici maintenant avec un fichier de 1000 lignes sur les bras, fichier que je me suis bêtement abstenu par flemme et procrastination maladive de penser d’entrée international histoire d’aller vite et à l’essentiel alors même que je vous ai fait la belle et grande promesse, juré, craché et tout et tout que cette extension avait des prétentions carrément planétaires.
Alors, bon, quand même : pour me dédouaner un chouia, faut bien admettre aussi que si la traduction de la page d’options s’opère les doigts dans le nez avec Poedit, il en va tout autrement pour notre plugin tinyMCE.
Mais bon, quand y faut, y faut !
Fort heureusement, le codex de WordPress a l’amabilité ici de nous détailler la procédure à suivre. Nonobstant, quelque chose me murmure à l’oreille que, si vous êtes en train de lire ces lignes, c’est aussi pour éviter d’aller chercher ailleurs le pourquoi du comment. Et donc, je vous résume vite fait la chose :
- En premier lieu, il nous faut rédiger un fichier php dédié dans lequel, pour vous la jouer simple, nous créons un tableau associant des mots-clés aux différents éléments de textes à traduire.
- Ensuite, mais ça on en a pris l’habitude, il nous faut remplir le formulaire WordPress en trois exemplaires dûment tamponnés par les autorités administratives autorisées et compétentes : autrement dit poser sur le hook mce_external_languages un appel à une fonction qui renverra une string issue de notre tableau…
- Last but not least, dans notre fichier javascript admin/text-floating-image-tinyMCE.js, nous devons songer à remplacer toutes les chaines à traduire par un appel à la fonction getlang() de tinyMCE avec les paramètres qui vont bien.
- Et enfin, il nous faut créer à partir de Poedit les fichiers de langues locaux…
Appel à la localisation
Commençons donc par le commencement.
Dans le dossier languages, nous créons un fichier text-floating-image-tinyMCE-locale.php dans lequel nous glissons les lignes suivantes :
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
<?php if ( ! defined( 'ABSPATH' ) ) exit; if ( ! class_exists( '_WP_Editors' ) ) require( ABSPATH . WPINC . '/class-wp-editor.php' ); function text_floating_image_tinyMCE_translation() { //Pour simplifier l'écriture, on stocke la chaine 'text-floating-image' //qui constituera le corps de notre 'textdomain' dans une variable $tfi = 'text-floating-image'; $strings = array( 'iconTitle' => __('Verticaly align text & image', $tfi), 'windowTitle' => __('Align Settings', $tfi), 'cancel' => __('Cancel', $tfi), 'suppress' => __('Suppress', $tfi), 'width' => __('Image width in percent', $tfi), 'alignRight' => __('Align to the right', $tfi), 'alignRightToolTip' => __('Check to align the image to the right', $tfi), 'marginLeft' => __('Margin-left', $tfi), 'marginRight' => __('Margin-right', $tfi), 'spaceAfter' => __('Space after', $tfi), 'resetToolTip' => __('Reset the settings above', $tfi), 'textBlocks' => __('Text blocks to align', $tfi), 'textBlocks-ToolTip' => __('Reduce aligned text by one block', $tfi), 'textBlocks+ToolTip' => __('Increase aligned text by one block', $tfi), 'alert' => __("The selected image is included in a block containing text.\nDo you agree that it shall be isolated in a seperate paragraph to go on?", $tfi), ); //Notez le recours aux guillemets doubles dans le texte correspondant à 'alert' //pour pouvoir disposer du retour ligne initié par '\n'. //Dans le fichier javascript de notre plugin tinyMCE //le titre porté par le bouton de notre extension //sera appelé comme suit : //title = editor.getLang('text-floating-image.iconTitle') //On récupère notre localisation $locale = _WP_Editors::$mce_locale; //Et on construit la chaine à renvoyer $translated = 'tinyMCE.addI18n("'.$locale.'.text-floating-image", '.json_encode($strings).");\n"; return $translated; } $strings = text_floating_image_tinyMCE_translation(); |
Pour les petits que leur curiosité galopante rendra grands, voici un aperçu de la chaîne renvoyée, laquelle constitue de fait un bout de javascript :
1 2 3 |
tinyMCE.addI18n("fr.text-floating-image", {"iconTitle":"Verticaly align text & image","windowTitle":"Align Settings","cancel":"Cancel","suppress":"Suppress","width":"Image width in percent","alignRight":"Align to the right","alignRightToolTip":"Check to align the image to the right","marginLeft":"Margin-left","marginRight":"Margin-right","spaceAfter":"Space after","resetToolTip":"Reset the settings above","textBlocks":"Text blocks to align","textBlocks-ToolTip":"Reduce aligned text by one block","textBlocks+ToolTip":"Increase aligned text by one block","alert":"The selected image is included in a block containing text.\nDo you agree that it shall be isolated in a seperate paragraph to go on?"}); |
Formalités
Plions nous sans rechigner aux exigences de WordPress et enregistrons la fonction text_floating_image_tinyMCE_languages sur le hook mce_external_languages. Retour donc dans la fonction define_admin_hooks() de includes/class-text-floating-image.php :
231 232 233 234 235 236 237 238 |
/** * Enregistrement du fichier de langues * La fonction text_floating_image_tinymce_languages repose dans la classe * admin/class-text-floating-image-admin.php. */ $this->loader->add_filter('mce_external_languages', $plugin_admin, 'text_floating_image_tinyMCE_languages'); |
Et un saut de plus dans admin/class-text-floating-image-admin.php pour finaliser la procédure :
300 301 302 303 304 305 306 307 308 309 310 311 312 313 |
/** * Enregistrement du fichier de langues * La fonction est appelée sur le hook mce_external_languages * dans la fonction define_admin_hooks() de includes/class-text-floating-image.php * https://codex.wordpress.org/Plugin_API/Filter_Reference/mce_external_languages * * @since 1.0.0 */ function text_floating_image_tinyMCE_languages($locales) { $locales['text-floating-image'] = plugin_dir_path(__DIR__).'languages/text-floating-image-tinyMCE-locale.php'; return $locales; } |
Ultimes corrections
À titre documentaire, je vous donne le code rectifié traduit prêt à l’emploi du fichier admin/text-floating-image-tinyMCE.js même si, je n’en doute pas une seconde et, pour parler franchement, j’en suis intimement persuadé-sûr-convaincu, vous aviez bien évidemment d’ores et déjà et par vous-mêmes apporté les corrections nécessaires :
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 |
(function() { 'use strict'; //Enregistrement du plugin auprès de tinymce tinymce.PluginManager.add('textFloatingImageTinyMCE', function(editor, url) { /* * editor est une référence à l'éditeur de tinyMCE avec lequel on interagit * url est le lien de notre fichier */ /************************ DÉCLARATION DES VARIABLES *************************/ var dom,//référence au dom de l'éditeur. Initialisée dans l'événement init de l'éditeur body,//référence au body de l'éditeur. Initialisée dans l'événement init de l'éditeur doc,//référence au document de l'éditeur. Initialisée dans l'événement init de l'éditeur selection,//L'objet selection de l'éditeur. Initialisée dans l'événement init de l'éditeur img,//L'image sélectionnée. Initialisée dans l'événement NodeChange //dès lors que l'élément sélectionné est une image imgContainer,//Le bloc contenant l'image. Initialisée dans l'événement NodeChange //dès lors que l'élément sélectionné est une image button,//Le bouton de notre extension ajouté à la barre d'outils de tinyMCE alignLeftButton, alignCenterButton, alignRightButton, //Une référence aux trois boutons d'alignement pour pouvoir gérer leur état //Toutes les variables boutons sont initialisées dans la fonction addButton prevEl,//une variable pour gérer plus judicieusement l'événement NodeChange //qui se déclenche deux fois id,//L'id qui sera affecté à l'attribut data-text_floating_image. //Initialisé dans la fonction main myWindow,//Notre popup de paramétrage. Initialisée dans la fonction main $ = tinymce.dom.DomQuery,//http://archive.tinymce.com/wiki.php/api4:class.tinymce.dom.DomQuery selectedTextArray = [],//reseté dans NodeChange. Construit dans la fontion main observer,//Le MutationObserver posé pour surveiller les modifications dans l'éditeur tfiArray = [];//Tableau des instances tfi editor.on('init', function(e) { dom = editor.dom; body = editor.getBody(); doc = editor.getDoc(); selection = editor.selection; //On crée une instance tfi pour chaque image portant l'attribut //data-text_floating_image dom.select('img[data-text_floating_image]').forEach(function(el){ new Tfi(el); }); //Construction d'observer qui va réaliser un update de l'objet tfi //lors de l'ajout d'un paragraphe dans un bloc de paragraphes liés //et la suppression du plugin : // - lors de l'ajout d'une image dans un paragraphe lié // - lors du déplacement d'une image traitée // - lors de la suppresion d'une image traitée ou d'un ensemble comprenant une image traitée // - s'il ne reste qu'un paragraphe lié ne contenant plus de texte observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { //Ajout d'éléments for (var i = 0; i < mutation.addedNodes.length; i++) { if (mutation.addedNodes[i] instanceof HTMLElement) { var nodeAdded = mutation.addedNodes[i]; if (nodeAdded.hasAttribute('data-text_floating_image')) { //Ajout d'un paragraphe lié //Si le rédacteur ajoute un paragraphe en faisant 'Entrée' //ce paragraphe porte les mêmes attributs et le même style //que le paragraphe précédent. //Si le paragraphe précédent est le premier impliqué dans l'alignement, //il est doté d'une 'margin-top' qu'il nous faut annuler //sur le nouveau paragraphe nodeAdded.style.removeProperty('margin-top'); //et, le cas échéant, supprimer complétement l'atribut sytle if (nodeAdded.style.cssText == '') nodeAdded.removeAttribute('style'); //On recherche l'instance tfi correpondante pour procéder à son update tfiArray.forEach(function(el){ //Sur breakme, voir http://stackoverflow.com/a/12388304 breakme: if (el.id == nodeAdded.getAttribute('data-text_floating_image')) { el.update(); break breakme; } }); } else if (nodeAdded.getElementsByTagName('IMG').length > 0) { //Ajout d'une image var imgAdded = nodeAdded.getElementsByTagName('IMG')[0]; //On détermine le bloc contenant l'image var imgAddedContainer = dom.getParent(imgAdded,dom.isBlock); //Si l'image a été insérée dans un bloc portant l'attribut //data-text_floating_image, on supprime le plugin appliqué à l'image if (imgAddedContainer.hasAttribute('data-text_floating_image')) { suppress(imgAddedContainer.getAttribute('data-text_floating_image')); } } } } //Suppression d'éléments for (var i = 0; i < mutation.removedNodes.length; i++) { if (mutation.removedNodes[i] instanceof HTMLElement) { //Déplacement d'une image //Ou suppression d'un ensemble contenant une image if (mutation.removedNodes[i].getElementsByTagName('IMG').length > 0) { var imgRemoved = mutation.removedNodes[i].getElementsByTagName('IMG')[0]; //Si l'image porte un attribut data-text_floating_image, //on supprime le plugin qui lui est appliqué if (imgRemoved.hasAttribute('data-text_floating_image')) { suppress(imgRemoved.getAttribute('data-text_floating_image')); } //Suppression d'une image seule } else if (mutation.removedNodes[i].nodeName == 'IMG') { var imgRemoved = mutation.removedNodes[i]; if (imgRemoved.hasAttribute('data-text_floating_image')) { suppress(imgRemoved.getAttribute('data-text_floating_image')); } //Suppression du dernier paragraphe lié restant } else if (mutation.removedNodes[i].hasAttribute('data-text_floating_image')) { var myId = mutation.removedNodes[i].getAttribute('data-text_floating_image'); //Si on ne trouve aucun bloc portant l'attribut data-text_floating_image, //on supprime le plugin. if (dom.select('*:not(img)[data-text_floating_image=' + myId + ']').length == 0) { suppress(myId); } } //S'il ne reste qu'un paragraphe lié ne contenant plus de texte } else { var parent = dom.getParent(mutation.target,dom.isBlock); if (parent && parent.hasAttribute('data-text_floating_image')) { if (parent.textContent.trim() == '') { var myId = parent.getAttribute('data-text_floating_image'); if (dom.select('*:not(img)[data-text_floating_image=' + myId + ']').length == 1) { suppress(myId); } } } } } }); }); //Si des instances tfi ont été créées, on déclenche la surveillance if (tfiArray.length > 0) observer.observe(body, {childList: true, subtree: true}); //On pose un écouteur sur l'événement resize de la fenêtre de l'éditeur //pour y associer la fonction resize editor.getWin().addEventListener('resize',resize); }); //On déclenche un resize dès lors que l'éditeur a fini son travail de rendu editor.on('PostProcess', function(e) { resize(); }); /*********************************************************** GESTION DU PASSAGE DU MODE VISUEL AU MODE TEXTE DE L'ÉDITEUR ************************************************************ On détecte le passage au mode texte en posant un écouteur sur l'événement click du bouton "Texte" de l'éditeur dont une rapide recherche dans l'inspecteur du navigateur révèle qu'il porte l'id "content-html". Cet événement a l'avantage d'être déclenché avant l'événement "hide" de l'éditeur et nous permet d'assurer le nettoyage de la structure html. */ $('#content-html').on('click',function(){ //On arrête la surveillance observer.disconnect(); //On assure le nettoyage des éléments de tfiArray if (tfiArray.length > 0) { tfiArray.forEach(function(el){ el.clean(); }); //On reset tfiArray tfiArray.length = 0; } }); /* Pour le retour au mode visuel, on utilise l'événement "show" de l'éditeur. À noter que cet événement n'est pas dispatché à l'ouverture de l'éditeur et qu'il n'est donc pas redondant avec l'événement "init" de ce même éditeur http://archive.tinymce.com/wiki.php/api4:event.tinymce.Editor.show */ editor.on('show', function(e) { //On recrée le tableau tfiArray dom.select('img[data-text_floating_image]').forEach(function(el){ new Tfi(el); }); resize(); //On rétablit la surveillance if (tfiArray.length > 0) observer.observe(body, {childList: true, subtree: true}); }); /* CLICK SUR LE BOUTON "METTRE À JOUR" */ editor.on('submit', function(e) { //On assure le nettoyage des éléments de tfiArray tfiArray.forEach(function(el){ el.clean(); }); editor.save(); }); /***************************************************************************** GESTION DE L'ÉTAT DES BOUTONS DE LA BARRE D'OUTILS EN FONCTION DE LA SÉLECTION ****************************************************************************** On s'appuie sur l'évènement "NodeChange" de l'éditeur. http://archive.tinymce.com/wiki.php/api4:event.tinymce.Editor.NodeChange */ editor.on('NodeChange', function() { //On stocke l'élément sélectionné dans l'éditeur //http://archive.tinymce.com/wiki.php/api4:method.tinymce.dom.Selection.getNode var el = selection.getNode(); //L'événement "NodeChange" a tendance a se déclencher deux fois. //Pour alléger la charge de travail, on n'exécute notre tâche que si l'on constate //par nous-même que l'élément actuellement sélectionné est bien différent //de l'élément précédemment sélectionné, lequel est stocké dans la variable prevEL if(el != prevEl) { prevEl = el; //Reset de l'état des boutons button.disabled(true); button.active(false); alignLeftButton.disabled(false); alignCenterButton.disabled(false); alignRightButton.disabled(false); //Si l'élément sélectionné est une image if (el.nodeName == 'IMG') { img = el; //Initialisation de la variable imgContainer imgContainer = dom.getParent(img,dom.isBlock); //Reset du tableau selectedTextArray selectedTextArray.length = 0 //Si l'extension est d'ores et déjà appliquée à l'image if (img.hasAttribute('data-text_floating_image')) { button.disabled(false); button.active(true); //On désactive les boutons d'alignement alignLeftButton.disabled(true); alignCenterButton.disabled(true); alignRightButton.disabled(true); //Sinon et si l'image n'est pas incluse dans un élément li } else if (imgContainer.nodeName != 'LI') { //On s'assure avant tout, qu'au sein même de son block container, //l'image n'est pas suivie d'une autre image. //Pour ce faire, on convertit en tableau la collection HTML //des images contenues dans le container //et on vérifie que l'image sélectionnée est bien le dernier élément //de ce tableau //http://stackoverflow.com/a/222847 var arr = [].slice.call(imgContainer.getElementsByTagName('img')); if (arr.indexOf(img) == arr.length - 1) { //Si l'image est bien la dernière de son block container, //on recherche la présence de texte après l'image //au sein de son container //et, dès lors qu'on en trouve, on active le bouton //http://stackoverflow.com/a/10730777 var n, a = [], walk = doc.createTreeWalker(imgContainer, NodeFilter.SHOW_ALL, null, false); //On construit un tableau des noeuds contenus dans le container while(n = walk.nextNode()) a.push(n); //On réduit ce tableau aux noeuds qui suivent l'image a.splice(0, a.indexOf(img) + 1); //Dès qu'on détecte un noeud contenant du texte //celui-ci devient provisoirement le premier élément de selectedTextArray. //On active le bouton et on passe à la suite. for(var i = 0; i < a.length; i++){ if (a[i].textContent.trim() != '') { selectedTextArray.push(a[i]); button.disabled(false); break; } } //Si le bouton est toujours inactif //(c'est donc qu'on n'a pas encore trouvé de texte) //et qu'un bloc suit le container de l'image, //on n'active le bouton que si ce bloc contient du texte et est dépourvu d'image. if (button.disabled() && dom.getNext(imgContainer,dom.isBlock)) { var myNext = dom.getNext(imgContainer,dom.isBlock); if (myNext.textContent.trim() != '' && myNext.getElementsByTagName('img').length == 0) button.disabled(false); } } } } } }); /******************************************* AJOUT DE NOTRE BOUTON DANS LA BARRE D'OUTILS ******************************************** http://archive.tinymce.com/wiki.php/api4:method.tinymce.Editor.addButton */ editor.addButton('textFloatingImageTinyMCE', { title: editor.getLang('text-floating-image.iconTitle'), //L'image du bouton nommée "icon.png" est rangée dans : //repertoire_Wordpress/wp-content/plugins/text-floating-image/admin/img //url vaut : //repertoire_Wordpress/wp-content/plugins/text-floating-image/admin/js //On retire les deux derniers caractères de url et on y ajoute 'img/icon.png' //pour obtenir l'adresse de l'image : //repertoire_Wordpress/wp-content/plugins/text-floating-image/admin/img/icon.png image: url.substr(0, url.length-2) + 'img/icon.png', onPostRender: function() { //On récupère une référence à notre bouton button = this; //On part à la recherche des boutons d'alignement //pour pouvoir par la suite forcer leur état à disabled quand //l'utilisateur sélectionne une image d'ores et déjà traitée. //http://community.tinymce.com/forum/viewtopic.php?id=31104 var buttons = editor.theme.panel.find('toolbar *'); for (var i = 0; i < buttons.length; i++) { switch (buttons[i]._aria.label) { case 'Align left': alignLeftButton = buttons[i]; break; case 'Align center': alignCenterButton = buttons[i]; break; case 'Align right': alignRightButton = buttons[i]; break; } } }, onclick: function() { if (imgContainer.textContent != '') { //Si l'image est incluse dans un bloc contenant du texte, //on demande poliment l'autorisation de l'isoler avant de continuer //Néanmoins, l'isolation ne sera réalisée que si l'utilisateur valide //la fenêtre de paramétrage (fonction onSubmit de la fenêtre). //https://www.tinymce.com/docs/api/tinymce/tinymce.windowmanager/ editor.windowManager.confirm(editor.getLang('text-floating-image.alert'), function(reponse) { if (reponse) { //On lance la procédure... main(); } } ); } else { //On lance la procédure... main(); } }, }); function main() { var boutonPlus,//Référence au bouton + de la fenêtre boutonMoins,//Référence au bouton - de la fenêtre rng = dom.createRng(),//Objet range qui va nous permettre d'opérer //la sélection des blocs de texte liés tfi,//l'objet tfi de l'image traitée lorsu'il s'agit d'un update availableTextArray = [],//Tableau de tous les blocs de texte //susceptibles d'être impliqués dans l'alignement //Récupération des options imageWidth = text_floating_image_options.imageWidth, alignRight = Boolean(text_floating_image_options.alignRight), left_marginLeft = text_floating_image_options.left_marginLeft, left_marginRight = text_floating_image_options.left_marginRight, right_marginLeft = text_floating_image_options.right_marginLeft, right_marginRight = text_floating_image_options.right_marginRight, spaceAfter = 0, //Variables dédiées à la construction d'availableTextArray n, temp = imgContainer; //Construction d'availableTextArray //Le tableau est constitué de tous les blocs qui suivent le container de l'image //et comportent du texte mais pas d'image //Il s'arrête dès lors qu'on trouve un bloc renfermant une image //ou ne comportant pas de texte while (n = dom.getNext(temp,dom.isBlock)) { if (n.getElementsByTagName('IMG').length == 0 && n.textContent.trim() != '') { availableTextArray.push(n); temp = n; } else break; } //Si l'image dispose déjà d'un attribut data-text_floating_image //et qu'il s'agit donc d'un update if (img.getAttribute('data-text_floating_image')) { //On conserve son id id = img.getAttribute('data-text_floating_image'); //On récupère l'instance tfi associée à l'image tfiArray.forEach(function(el){ breakme: if (el.id == id) { tfi = el; break breakme; } }); //Pour en faciliter la lecture, on transforme en objet //le contenu de l'attribut data-text_floating_image-style de l'image var prop = parseAttr(img.getAttribute('data-text_floating_image-style')); //On récupère les paramètres enregistrés pour les afficher imageWidth = parseInt(prop.width); alignRight = (prop.clear == 'left')?0:1; left_marginLeft = (prop.clear == 'left')?parseInt(prop['margin-left']):left_marginLeft; left_marginRight = (prop.clear == 'left')?parseInt(prop['margin-right']):left_marginRight; right_marginLeft = (prop.clear == 'left')?right_marginLeft:parseInt(prop['margin-left']); right_marginRight = (prop.clear == 'left')?right_marginRight:parseInt(prop['margin-right']); spaceAfter = img.getAttribute('data-text_floating_image-after'); //La propriété texts de l'instance tfi renvoie un tableau //des blocs de texte participant à l'alignement //On définit donc selectedTextArray en y clonant tfi.texts //http://www.dyn-web.com/javascript/arrays/value-vs-reference.php selectedTextArray = tfi.texts.slice(0); //On place le début de notre range avant le premier bloc de texte rng.setStartBefore(tfi.first); //On cale la fin de ce même range après le dernier bloc de texte rng.setEndAfter(tfi.last); //On opère la sélection correspondante selection.setRng(rng); } else { //Première application de notre extension à une image //Calcul de l'id que nous attribuerons à l'image id = new Date().valueOf(); //Si le container de l'image ne contient pas de texte après celle-ci if (selectedTextArray.length == 0) { //On sélectionne le premier bloc de texte suivant le container de l'image var next = dom.getNext(imgContainer,dom.isBlock); selectedTextArray.push(next); selection.select(next); } else { //Sinon, on rajoute provisoirement le noeud de texte stocké dans //selectedTextArray[0] en tête d'availableTextArray //et on sélectionne tout le texte compris entre l'image et la fin de son container availableTextArray.unshift(selectedTextArray[0]); rng.setStartBefore(selectedTextArray[0]); rng.setEndAfter(imgContainer); selection.setRng(rng); } } //CREATION DES BOUTONS OK ET ANNULER //AINSI QUE, LE CAS ÉCHÉANT, DU BOUTON SUPPRIMER var buttons = [ { text: "OK", subtype: 'primary', onclick: function(){ //On enregistre les paramètres myWindow.find('form')[0].submit(); } }, { text: editor.getLang('text-floating-image.cancel'), onclick: function(){ //On referme la fenêtre myWindow.close(); } } ]; //S'il s'agit d'un update, on intercale entre les deux boutons précédemment créés //un troisième bouton "Supprimer" pour supprimer le plugin appliqué sur l'image if (tfi) { buttons.splice(1,0, { text: editor.getLang('text-floating-image.suppress'), classes: 'text_floating_image-suppress', subtype: 'suppress', onclick: function(){suppress(id);} } ); } //FIN DE LA CRÉATION DES BOUTONS //On enregistre une référence à notre fenêtre dans la variable myWindow myWindow = editor.windowManager.open({ onOpen: function(){ //On arrête la surveillance de l'éditeur observer.disconnect(); //Un petit tour dans l'inspecteur nous apprend que myWindow //dispose d'une propriété $el qui renvoie un tableau //dont le premier élément fait justement référence à notre fenêtre //Ne reste plus alors qu'à positionner cette dernière en haut à droite myWindow.$el[0].style.top = 0; myWindow.$el[0].style.left = 'auto'; myWindow.$el[0].style.right = 0; //On réduit l'opacité de la fenêtre modale //pour assurer une visualisation correcte de la sélection de texte //L'inspecteur nous apprend qu'elle porte un id 'mce-modal-block' $('#mce-modal-block').css('opacity',0); //La fenêtre modale bloque le scrolling sur l'éditeur //et empêche le cas échéant de visualiser le texte sélectionné //On pose donc un écouteur sur l'événement wheel //qui va nous permettre de rétablir le scroll de l'éditeur //Référence à l'iframe contenant l'éditeur var $iframe = jQuery('#content_ifr'); //Référence au contenu de l'iframe pour en obtenir la hauteur var $iframeContents = $iframe.contents(); jQuery(window).on('wheel', function(e){ //Valeur du delta appliqué au scroll var delta = 40; //Affectation du signe du delta en fonction du mouvement de la molette : //S'il s'agit d'une descente, delta est positif et on augmente la valeur de scrollTop //tant qu'on n'a pas atteint son maximum possible //Sinon, delta est négatif et on diminue la valeur de scrollTop //tant qu'il est supérieur à zéro delta = (e.originalEvent.deltaY > 0)?delta:-delta; //On stocke la valeur actuelle du scrollTop var scroll = $iframeContents.scrollTop(); //Si delta est négatif et que l'iframe n'est pas remontée au max //Ou si delta est positif et que l'iframe n'est pas descendue au max if ((delta < 0 && scroll + delta > 0) || (delta > 0 && $iframe.height() + scroll < $iframeContents.height())) { //On bloque l'événement sur l'élément window e.preventDefault(); //Et on applique le scroll sur l'iframe $iframeContents.scrollTop(scroll + delta); } }) }, onClose: function(){ //À la fermeture de la fenêtre, on sélectionne l'image selection.select(img); //On dispatch un click sur l'image img.click(); //On reset la variable myWindow myWindow = null; //On rétablit la surveillance de l'éditeur if (tfiArray.length > 0) observer.observe(body, {childList: true, subtree: true}); }, //CONTENU DE LA FENÊTRE title: editor.getLang('text-floating-image.windowTitle'), body: [ { classes: 'text_floating_image-input', type: 'textbox', name: 'width', label: editor.getLang('text-floating-image.width') + ' :', value: imageWidth, min:1, max:100, }, { classes: 'text_floating_image-checkbox', type: 'checkbox', name: 'align', label: editor.getLang('text-floating-image.alignRight') + ' :', tooltip: editor.getLang('text-floating-image.alignRightToolTip'), checked: alignRight, onchange: function(){ alignRight = !alignRight; //Attribution des valeurs enregistrées pour les marges //en fonction de l'alignement if (this.checked()) { myWindow.find('#marginLeft').value(right_marginLeft); myWindow.find('#marginRight').value(right_marginRight); } else { myWindow.find('#marginLeft').value(left_marginLeft); myWindow.find('#marginRight').value(left_marginRight); } } }, { classes: 'text_floating_image-input', type: 'textbox', name: 'marginLeft', label: editor.getLang('text-floating-image.marginLeft') + ' :', value: (alignRight)?right_marginLeft:left_marginLeft, min: 0 }, { classes: 'text_floating_image-input', type: 'textbox', name: 'marginRight', label: editor.getLang('text-floating-image.marginRight') + ' :', value: (alignRight)?right_marginRight:left_marginRight, min: 0 }, { classes: 'text_floating_image-input', type: 'textbox', name: 'spaceAfter', label: editor.getLang('text-floating-image.spaceAfter') + ' :', value: spaceAfter, min: 0 }, { type:'container', classes: 'text_floating_image-resetContainer', items: [ { type: 'button', text: "Reset", size:'medium', tooltip: editor.getLang('text-floating-image.resetToolTip'), onclick:function(){ //Rétablissement des paramètres par défaut left_marginLeft = text_floating_image_options.left_marginLeft, left_marginRight = text_floating_image_options.left_marginRight, right_marginLeft = text_floating_image_options.right_marginLeft, right_marginRight = text_floating_image_options.right_marginRight, myWindow.find('#width').value(text_floating_image_options.imageWidth); myWindow.find('#align').checked(Boolean(text_floating_image_options.alignRight)); myWindow.find('#spaceAfter').value(0); //On passe le focus au premier champs $('.mce-text_floating_image-input')[0].select(); } }, ], }, { type: 'fieldset', classes: 'text_floating_image-fieldset', items: [ { type: 'label', text: editor.getLang('text-floating-image.textBlocks') + ' :', }, { type: 'buttongroup', classes: 'text_floating_image-buttonGroup', border: '0 0 0 0', items: [ { text:'-', tooltip: editor.getLang('text-floating-image.textBlocks-ToolTip'), onPostRender:function(){ boutonMoins = this; //S'il n'y a qu'un bloc de texte selectionné, //on bloque le bouton boutonMoins.disabled(selectedTextArray.length == 1); }, onClick:function(){ boutonPlus.disabled(false); var length = selectedTextArray.length; //Suppression du dernier élément de selectedTextArray selectedTextArray.pop(); //Sélection du texte correspondant rng.setStartBefore(selectedTextArray[0]); rng.setEndAfter(selectedTextArray[length - 2]); selection.setRng(rng); //Si length valait 2 avant la suppresion //c'est qu'il ne reste plus qu'un élément de texte //On bloque donc le bouton boutonMoins.disabled(length == 2); }, }, { text: '+', tooltip: editor.getLang('text-floating-image.textBlocks+ToolTip'), onPostRender:function(){ boutonPlus = this; //Si tous les éléments de texte possibles sont //déjà sélectionnés, on bloque le bouton boutonPlus.disabled(availableTextArray.length == selectedTextArray.length) }, onClick:function(){ boutonMoins.disabled(false); var length = selectedTextArray.length; //On rajoute le prochain bloc de texte disponible //à selectedTextArray length = selectedTextArray.push(availableTextArray[length]); //Et on sélectionne le texte correspondant rng.setStartBefore(selectedTextArray[0]); rng.setEndAfter(selectedTextArray[length - 1]); selection.setRng(rng); //Si tous les éléments de texte possibles sont //déjà sélectionnés, on bloque le bouton boutonPlus.disabled(availableTextArray.length == length); }, } ], }, ], }, ], buttons: buttons, //FIN CONTENU DE LA FENÊTRE onSubmit: function(e) { //Si la largeur rentrée n'est pas acceptable if (e.data.width < 1 || e.data.width > 99) { //On bloque et on passe le focus au premier champs e.preventDefault(); $('.mce-text_floating_image-input')[0].select(); } else { //S'il s'agit d'un update if (tfi) { tfi.preUpdate(); //Sinon } else { //On passe l'id à l'attribut data-text_floating_image de l'image img.setAttribute('data-text_floating_image', id) //Si le container de l'image contient du texte, //nous devons procéder à l'isolation de l'image if (imgContainer.textContent != '') { //Si l'image est contenue dans une ancre //monElement = ancre //sinon //monElement = img var monElement = (img.parentElement.nodeName == "A")?img.parentElement:img; //on effectue un split //http://archive.tinymce.com/wiki.php/api4:method.tinymce.dom.DOMUtils.split dom.split(imgContainer,monElement); //et on wrappe monElement dans un paragraphe //http://archive.tinymce.com/wiki.php/api4:method.tinymce.dom.DomQuery.wrap $(monElement).wrap('p'); //On met à jour la variable imgContainer imgContainer = monElement.parentElement; //S'il y avait du texte après l'image au sein de son container, //le premier élément de selectedTextArray est un noeud plutôt qu'un bloc if (!dom.isBlock(selectedTextArray[0])) { //On le remplace par le bloc issu du split selectedTextArray[0] = dom.getNext(imgContainer,dom.isBlock); } } } //On passe l'id à l'attribut data-text_floating_image des blocs de texte selectedTextArray.forEach(function(el){ el.setAttribute('data-text_floating_image', id); }); //On donne une forme textuelle qui va bien au paramètre d'alignement var myAlign = (e.data.align)?'right':'left'; //On forme une chaine à partir des paramètres entrés par le rédacteur var str = 'width:' + e.data.width + '%' + ';clear:' + myAlign + ';float:' + myAlign + ';margin-left:' + e.data.marginLeft + 'px' + ';margin-right:' + e.data.marginRight + 'px' + ';line-height:0;'; //(En ajoutant un line-height:0, on s'assure que le paragraphe //contenant l'image aura bel et bien la hauteur pile-poil //de l'image même si celle-ci est contenue dans une ancre.) //Et on l'affecte à l'attribut data-text_floating_image-style de l'image. img.setAttribute('data-text_floating_image-style', str); //On enregistre le paramètre rentré pour le champs 'Espace après' //dans l'attibut dédié 'data-text_floating_image-after' img.setAttribute('data-text_floating_image-after', e.data.spaceAfter); if (tfi) { //S'il s'agit d'un update, on update l'instance tfi tfi.update(); } else { //Sinon, on crée l'objet tfi new Tfi(img); } } } }); //On donne l'attibut number aux champs de notre fenêtre $('.mce-text_floating_image-input').attr('type','number'); //On sélectionne le contenu du premier champs //Sans setTimeout, la sélection risque d'être inopérante. setTimeout(function(){$('.mce-text_floating_image-input')[0].select();},1) } function suppress(myId) { //On recherche l'instance tfi correspondante tfiArray.forEach(function(el){ breakme: if (el.id == myId) { el.suppress(); break breakme; } }); //Si la suppression est provoquée par un click sur le bouton 'Supprimer' //de la fenêtre de paramétrage, on referme la fenêtre if (myWindow) myWindow.close(); } function resize(){ if (tfiArray.length > 0) { tfiArray.forEach(function(el){ el.resize(); }); } } var Tfi = function(img){ //Affectation de l'identifiant de l'image à la propriété id this.id = img.getAttribute('data-text_floating_image'); //Référence au bloc container de l'image var imgContainer = dom.getParent(img,dom.isBlock); //Création et insertion de l'élément elastik var elastik = doc.createElement('div'); body.insertBefore(elastik,imgContainer); //Création et insertion de l'élément preElastik var preElastik = doc.createElement('div'); preElastik.style = "clear:both;"; body.insertBefore(preElastik,elastik); //Création et insertion de l'élément <style> var sheet = doc.createElement('style'); doc.head.appendChild(sheet); //On ajoute l'instance créée au tableau tfiArray tfiArray.push(this); //Méthode cleanMarges (utilisation en interne): this.cleanMarges = function () { // reset de la margin-top appliquée au premier bloc de texte if (body.contains(this.first)) { this.first.style.removeProperty('margin-top'); } // suppression de la classe attribuée au dernier bloc de texte pour // gérer son pseudo-élément ::after if (body.contains(this.last)) { this.last.classList.remove('tfi-' + this.id); if (this.last.getAttribute('class') == '') this.last.removeAttribute('class'); } } //Méthode clean appelée : // - lorsque l'on passe du mode visuel au mode texte de l'éditeur // - lorsqu'on met à jour le post // - depuis la méthode suppress de l'objet lui-même this.clean = function() { // reset des styles du premier et dernier bloc de texte via cleanMarges this.cleanMarges(); this.first.style.removeProperty('padding-top'); if (this.first.style.cssText == '') this.first.removeAttribute('style'); this.last.style.removeProperty('margin-bottom'); this.last.style.removeProperty('padding-bottom'); if (this.last.style.cssText == '') this.last.removeAttribute('style'); // supression de l'élément <style> doc.head.removeChild(sheet); // suppression des éléments preElastik et elastik if (body.contains(preElastik)) body.removeChild(preElastik); if (body.contains(elastik)) body.removeChild(elastik); // suppression de l'attribut style sur le bloc container de l'image if (body.contains(imgContainer)) imgContainer.removeAttribute('style'); //nettoyage des attributs posés sur l'image if (body.contains(img)) { img.removeAttribute('data-mce-placeholder'); if (img.hasAttribute('data-text_floating_image-classes')) { img.className = img.getAttribute('data-text_floating_image-classes'); img.removeAttribute('data-text_floating_image-classes'); } } } // Méthode preUpdate. Appelée lors d'un update // au début de la fonction 'onSubmit' de la fenêtre de paramétrage this.preUpdate = function() { // reset des styles du premier et dernier bloc de texte via cleanMarges this.cleanMarges(); //Nettoyage de margin-bottom = 0 //et padding-bottom = 0 //posés sur le dernier bloc de texte this.last.style.removeProperty('margin-bottom'); this.last.style.removeProperty('padding-bottom'); if (this.last.style.cssText == '') this.last.removeAttribute('style'); //Nettoyage de l'attribut 'data-text_floating_image-after' posé sur l'image img.removeAttribute('data-text_floating_image-after'); // retrait de l'attribut 'data-text_floating_image' // sur tous les blocs de texte affiliés this.texts.forEach(function(el){ if (body.contains(el)) el.removeAttribute('data-text_floating_image'); }); } // Méthode update. Appelée lors d'un update // à la fin de la fonction 'onSubmit' de la fenêtre de paramétrage // et en interne pour finaliser la construction de l'instance this.update = function() { // reset des styles du premier et dernier bloc de texte via cleanMarges this.cleanMarges(); // affectation des propriétés de style au bloc container de l'image imgContainer.style = img.getAttribute('data-text_floating_image-style'); // affectation de la propriété float à l'élément elastik elastik.style.float = imgContainer.style.float; // création ou reset des propriétés texts, first et last de l'instance tfi this.texts = dom.select('*:not(img)[data-text_floating_image=' + this.id + ']'); this.first = this.texts[0]; this.first.style.paddingTop = 0; this.last = this.texts[this.texts.length - 1]; this.last.style.marginBottom = 0; this.last.style.paddingBottom = 0; // construction du contenu de l'élément <style> sheet.innerHTML = ".tfi-" + this.id +"::after{content:'';display:block;clear:both;height:" + img.getAttribute('data-text_floating_image-after') + "px;}"; //On empêche l'apparition de la barre d'outils du plugin image //Voir la function isPlaceholder() au début de //wp-includes/js/tinymce/plugins/wpeditimage/plugin.js img.setAttribute('data-mce-placeholder', 1); //On sauvegarde les classes de l'image if (img.hasAttribute('class')) { img.setAttribute('data-text_floating_image-classes', img.className); //avant de les supprimer img.removeAttribute('class'); } // appel de la méthode resize pour procéder au positionnement des éléments this.resize(); } // Méthode resize. Appelée sur l'événement resize de la fenêtre de l'éditeur // et en interne depuis la méthode update this.resize = function() { // reset de la hauteur de l'élément elastik elastik.style.height = 0; // reset des styles du premier et dernier bloc de texte via cleanMarges this.cleanMarges(); //Calcul de la hauteur des blocs de texte var h = 0; this.texts.forEach(function(el){ h += outerHeight(el); }); var imgHeight = outerHeight(img); if (imgHeight > h) { //Si l'image est plus haute que les blocs de texte //on applique une margin-top au premier bloc this.first.style.marginTop = (imgHeight - h) / 2 + 'px'; } else { //Si la hauteur totale des blocs de texte est supérieure à celle de l'image //on calcule la hauteur résultante de l'élément elastik elastik.style.height = (h - imgHeight) / 2 + 'px'; } //On applique la classe définie dans l'élément <style> //au dernier bloc de texte this.last.classList.add('tfi-' + this.id); } //Méthode suppress this.suppress = function() { // reset des styles du premier et dernier bloc de texte // via la fonction cleanMarges appelée dans la fonction preUpdate // et retrait de l'attribut 'data-text_floating_image' // sur tous les blocs de texte affiliés this.preUpdate(); //Nettoyage de padding-top = 0 //posé sur le premier bloc de texte this.first.style.removeProperty('padding-top'); if (this.first.style.cssText == '') this.first.removeAttribute('style'); // supression de l'élément <style> doc.head.removeChild(sheet); // suppression des éléments preElastik et elastik if (body.contains(preElastik)) body.removeChild(preElastik); if (body.contains(elastik)) body.removeChild(elastik); //Nettoyage de l'élément imgContainer if (body.contains(imgContainer)) { if (imgContainer.getElementsByTagName('IMG').length == 0) { //S'il ne contient plus d'image, on le supprime body.removeChild(imgContainer); } else { //Sinon, on supprime son attribut style imgContainer.removeAttribute('style'); } } //On vérifie que l'image existe toujours var myImg = dom.select('img[data-text_floating_image=' + this.id + ']')[0]; //Si c'est le cas if (myImg) { //On supprime tous les attributs ajoutés à l'image myImg.removeAttribute('data-text_floating_image'); myImg.removeAttribute('data-text_floating_image-style'); myImg.removeAttribute('data-text_floating_image-after'); myImg.removeAttribute('data-mce-placeholder'); //On lui rend ses classes myImg.setAttribute('class',img.getAttribute('data-text_floating_image-classes')); //On passe la balayette myImg.removeAttribute('data-text_floating_image-classes'); } //On supprime l'instance tfi du tableau tfiArray tfiArray.splice(tfiArray.indexOf(this), 1); //S'il reste des images traitées //on lance la fonction clean sur les instances tfi correspondantes //pour pouvoir les reconstruire sur des bases saines if (tfiArray.length > 0) { tfiArray.forEach(function(el){ el.clean(); }); } //On reset le tableau tfiArray tfiArray.length = 0; //On crée de nouvelles instance tfi toutes neuves pour les images à traiter dom.select('img[data-text_floating_image]').forEach(function(el){ new Tfi(el); }); //On reset prevEl et on déclenche un événement 'NodeChange' sur l'éditeur //pour actualiser l'état des boutons prevEl = null; editor.nodeChanged(); } //On appelle la méthode update this.update(); } /********** UTILITAIRES *********** Conversion d'une chaine de la forme "width:60%;clear:right;float:right;margin-left:20px;margin-right:0px;" en objet : { width:"60%", clear:"right", float:"right", margin-left:"20px", margin-right:"0px" } http://stackoverflow.com/a/33030078 */ function parseAttr(string) { var regex = /([\w-]*)\s*:\s*([^;]*)/g; var match, prop={}; while(match=regex.exec(string)) prop[match[1]] = match[2]; return prop; } /* Retourne la hauteur totale d'un élément en incluant paddings, bordures et marges http://youmightnotneedjquery.com/ */ function outerHeight(el) { var height = el.offsetHeight; var style = getComputedStyle(el); height += parseInt(style.marginTop) + parseInt(style.marginBottom); return height; } }); })(); |
Poedit
Cela ne vous aura pas échappé sans pour autant vous surprendre : notre pop-up s’exprime désormais en un anglais de cuisine qu’on jurerait qu’il a été pensé par un français…
Nous voilà bien avancés, me direz vous !
Eh ben, oui, justement, nous approchons du but et, si l’explication qui s’en vient maintenant peut sembler un peu longuette aux impatients, la marche à suivre qu’elle va s’efforcer de détailler l’est en fait beaucoup moins :
- Or donc, pour celles et ceux qui n’en disposeraient pas déjà, il vous faut vous munir subito presto voire même là maintenant tout de suite et sans plus tarder de Poedit.
- Et puisque vous êtes au rayon des téléchargements, rendez-vous donc ici pour vous saisir du fichier Blank-WordPress.pot généreusement mis à notre disposition par un expert de la chose. Vous renommez la chose en text-floating-image.pot et vous la rangez précautionneusement aux côtés de text-floating-image-tinyMCE-locale.php dans le répertoire languages prévu à cet effet par notre très cher boilerplate.
- Une fois Poedit installé et lancé, cliquez sur Créer une nouvelle traduction et naviguez jusqu’à votre fichier fraîchement débarqué languages/text-floating-image.pot.
- Faites OK pour valider le français proposé par défaut comme langue de traduction.
- Poedit vous fait alors part de sa déconvenue en vous signalant le plus aimablement du monde qu’il n’y a aucune traduction et que ma foi ce n’est pas courant.
- Qu’à cela ne tienne : cliquez donc pour commencer sur le bouton Enregistrer de la barre d’outils et sauvegardez votre non-travail actuel dans le dossier languages sous le nom de text-floating-image-fr_FR.po.
- Cliquez ensuite sur le bouton Mettre à jour dans la barre d’outils en haut.
- Et là, ô miracle, il ne vous reste plus qu’à assurer la traduction dans notre belle langue des expressions anglaises promptement listées.
- Votre mission s’achève naturellement en enregistrant votre travail et vous n’avez plus alors qu’à aller contempler béatement le résultat avec la satisfaction du travailleur fier de son ouvrage…
Bon, il me faut quand même vous signaler que cette manière d’employer Poedit n’est pas des plus honnêtes puisque la traduction d’un thème ou d’une extension ex-nihilo requiert normalement la version Pro, laquelle s’achète au jour d’aujourd’hui moyennant la modique somme de 22,57€. Nous avons contourné les limitations de la version gratuite en partant d’un fichier .pot vide mais je ne doute pas que vous aurez à cœur de vous acquitter de la licence dès lors que la commercialisation de vos propres travaux vous aura rapporté votre premier million…
En guise de non-conclusion
Ben vi, vous pourriez croire que nous en avons fini. Mais non…
Il nous reste encore un détail à régler : en écologiste forcené que je suis, j’ai pris l’habitude, suivant l’expression consacrée, de laisser les lieux que je quitte dans un état proche de celui où je les ai trouvé à mon arrivée. Pour ne point contrevenir à cette règle de vie, il me reste à assurer l’effacement de toutes les traces introduites par l’extension lorsqu’au final, l’utilisateur, déçu-dégoutté-pas-content-mais-c’est-son-droit, prend la décision radicale de la supprimer.
Nous verrons comment procéder dans le prochain et dernier chapitre de cette longue aventure…