├── .gitignore ├── LICENSE.md ├── Makefile ├── README.md ├── css ├── custom.css └── jquery.webui-popover.css ├── docs ├── loading.md ├── screenshot.png ├── server-side-component.md └── translator-button.png ├── lib ├── batcher.js ├── glotpress.js ├── index.js ├── jquery.webui-popover.js ├── jquery.webui-popover.licence ├── locale.js ├── original.js ├── popover.js ├── translation-pair.js ├── translation.js └── walker.js ├── package.json └── test ├── batcher.js ├── de.js ├── glotpress.js ├── index.js ├── jp.js ├── ru.js ├── string-extraction ├── test.html ├── translation-data │ ├── en-uk.js │ ├── es.js │ ├── he.js │ └── pt-br.js └── translation-pair.js └── translation-pair.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | node_modules 4 | *.sublime-* 5 | # Community translator output files 6 | community-translator.js 7 | community-translator.min.js 8 | community-translator.css 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The files in this project--except those listed under the section "Included Files"--are Copyright © Automattic Inc, 2015, and licensed under the The GPLv2 (or later) from the [Free Software Foundation](http://www.fsf.org/). Its text follows. 2 | 3 | Version 2, June 1991 4 | 5 | Copyright © 1989, 1991 Free Software Foundation, Inc. 6 | 51 Franklin St, Fifth Floor, Boston, MA 02110, USA 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. 9 | 10 | ### Preamble 11 | 12 | The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software --- to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. 13 | 14 | When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. 15 | 16 | To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. 17 | 18 | For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. 19 | 20 | We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. 21 | 22 | Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. 23 | 24 | Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. 25 | 26 | The precise terms and conditions for copying, distribution and modification follow. 27 | 28 | ### GNU General Public License Terms and Conditions for Copying, Distribution, and Modification 29 | 30 | 31 |
    32 | 33 |
  1. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.
  2. 34 |
  3. You may copy and distribute verbatim copies of the Program’s source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
  4. 35 |
  5. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: 36 |
      37 |
    1. You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.
    2. 38 |
    3. You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.
    4. 39 |
    5. If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)
    6. 40 |
    41 | These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
  6. 42 |
  7. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: 43 |
      44 |
    1. Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
    2. 45 |
    3. Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
    4. 46 |
    5. Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.
    6. 47 |
    48 |
  8. 49 |
  9. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
  10. 50 |
  11. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.
  12. 51 |
  13. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.
  14. 52 |
  15. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
  16. 53 |
  17. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
  18. 54 |
  19. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.
  20. 55 |
  21. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
  22. 56 |
  23. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
  24. 57 |
  25. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
  26. 58 |
59 | 60 | 61 | Included Files 62 | ========================== 63 | 64 | This project includes: 65 | 66 | - [jQuery](https://jquery.com/), which is Copyright © 2005, 2014 jQuery Foundation, Inc. and other contributors, and Released under the MIT license http://jquery.org/license 67 | - [WebUI-Popover](https://github.com/sandywalker/webui-popover), Copyright © 2014 Sandy Duan, under the MIT License (MIT) 68 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAKEFILE_PATH := $(dir $(lastword $(MAKEFILE_LIST))) 2 | NODE_PATH := $(MAKEFILE_PATH)test,$(MAKEFILE_PATH):$(NODE_PATH) 3 | REPORTER ?= spec 4 | MOCHA ?= ./node_modules/.bin/mocha 5 | BROWSERIFY = ./node_modules/.bin/browserify 6 | UGLIFYJS = ./node_modules/.bin/uglifyjs 7 | FLAGS = -t uglifyify 8 | 9 | 10 | all: community-translator.js community-translator.min.js community-translator.css 11 | 12 | community-translator.js: browserify 13 | 14 | community-translator.min.js: uglifyjs 15 | 16 | browserify: 17 | $(BROWSERIFY) $(FLAGS) lib/index.js --standalone communityTranslator -o community-translator.js 18 | 19 | uglifyjs: 20 | $(UGLIFYJS) community-translator.js -c > community-translator.min.js 21 | 22 | community-translator.css: css/custom.css 23 | cat css/jquery.webui*.css css/custom.css > community-translator.css 24 | 25 | test: 26 | @NODE_PATH=$(NODE_PATH) $(MOCHA) --reporter $(REPORTER) test/index.js 27 | 28 | .PHONY: test 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Community Translator 2 | The Community Translator aims to be a front end tool for translations stored in GlotPress. 3 | It does so by providing on-screen translation ability, so that the community can easily fix a missing translation in-place and make use of the context in which the string appears. 4 | 5 | ![Screenshot](docs/screenshot.png) 6 | 7 | ### Requirements 8 | - jQuery 9 | - The [GlotPress as a WordPress plugin](https://github.com/GlotPress/GlotPress-WP) 10 | - The [`gp-translation-extended-api`](https://github.com/david-binda/gp-extended-api-plugins) plugin for GlotPress as a WordPress plugin 11 | 12 | ### Build 13 | To create the `community-translator.js` and `community-translator.css` files which should be loaded in the translatable site. 14 | 15 | * run `npm install` 16 | * run `make`. 17 | 18 | You can also use `fswatch-run lib css make` to watch the directories `lib` and `css` for changed files and run `make` automatically. 19 | 20 | ### Loading 21 | 22 | The code in this repository is loaded by including the script as follows (generated above). 23 | ``` 24 | [...][...] 25 | 26 | 27 | 31 | ``` 32 | 33 | **Note** a [server-side component](docs/server-side-component.md) is needed. 34 | 35 | ![Translator Button](docs/translator-button.png) 36 | 37 | You might want to load the translator with a button, see the [documentation about loading](docs/loading.md) for details. 38 | 39 | ### Debugging 40 | Set or add `community-translator` to the `debug` value in localeStorage. i.e: `localStorage.setItem( 'debug', 'community-translator' )` 41 | 42 | -------------------------------------------------------------------------------- /css/custom.css: -------------------------------------------------------------------------------- 1 | .webui-popover { 2 | z-index: 100000; 3 | direction: ltr; 4 | } 5 | 6 | .webui-popover p { 7 | margin: 0; 8 | } 9 | 10 | .webui-popover p.context, .webui-popover p.comment { 11 | margin: 0 0 .5em 0; 12 | display: none; 13 | font-color: #999; 14 | font-style: italic; 15 | } 16 | 17 | .webui-popover p.info { 18 | margin: .5em 0; 19 | display: none; 20 | } 21 | 22 | .webui-popover tt { 23 | padding: .3em; 24 | background-color: #eee; 25 | } 26 | 27 | .webui-popover textarea { 28 | height: 100px; 29 | margin-bottom: 0.8em; 30 | color: #000; 31 | background: #fff 32 | } 33 | 34 | /* We override color & background-color with and without ::selection to 35 | * ensure that input methods are rendered visibly. 36 | */ 37 | .webui-popover textarea::selection { 38 | color: #2e4453; 39 | background: rgba(120, 220, 250, 0.7); 40 | } 41 | 42 | .webui-popover textarea::-moz-selection { 43 | color: #2e4453; 44 | background: rgba(120, 220, 250, 0.7); 45 | } 46 | 47 | .webui-popover button { 48 | float: right; 49 | margin-top: 8px; 50 | } 51 | 52 | .webui-popover div.a8c-similar-translations { 53 | display: none; 54 | height: 30px; 55 | overflow: auto; 56 | } 57 | 58 | .webui-popover-title span { 59 | float: right; 60 | margin-left:6px; 61 | } 62 | 63 | .webui-popover-title a { 64 | text-decoration: none; 65 | } 66 | 67 | .webui-popover hr { 68 | margin-bottom: 0; 69 | } 70 | 71 | .webui-popover .pairs { 72 | margin-top: 1em 73 | } 74 | 75 | .translator-translated { 76 | text-shadow: 0px 0px 10px #0f0, 0px 0px 2px #4cb420 !important; 77 | cursor: pointer; 78 | } 79 | .translator-checking { 80 | text-shadow: 0px 0px 1px #e83 !important; 81 | } 82 | .translator-user-translated { 83 | text-shadow: 0px 0px 10px #ff0, 0px 0px 2px #ffba00 !important; 84 | cursor: pointer; 85 | } 86 | .translator-untranslated { 87 | text-shadow: 0px 0px 10px #f00, 0px 0px 2px #dd3d36 !important; 88 | cursor: pointer; 89 | } 90 | 91 | .translator-highlight { 92 | border: 5px solid #dd3d36 !important; 93 | margin: -5px !important; 94 | } 95 | 96 | iframe.translator-untranslatable::after { 97 | content: 'Cannot be translated:'; 98 | color: #00f; 99 | } 100 | 101 | iframe.translator-untranslatable { 102 | border: 1px dotted #00f !important; 103 | } 104 | -------------------------------------------------------------------------------- /css/jquery.webui-popover.css: -------------------------------------------------------------------------------- 1 | /* 2 | * webui popover plugin - v1.0.5 3 | * A lightWeight popover plugin with jquery ,enchance the popover plugin of bootstrap with some awesome new features. It works well with bootstrap ,but bootstrap is not necessary! 4 | * https://github.com/sandywalker/webui-popover 5 | * 6 | * Made by Sandy Duan 7 | * Under MIT License 8 | */ 9 | /* webui popover */ 10 | .webui-popover { 11 | position: absolute; 12 | top: 0; 13 | left: 0; 14 | z-index: 1060; 15 | display: none; 16 | width: 276px; 17 | min-height: 50px; 18 | padding: 1px; 19 | text-align: left; 20 | white-space: normal; 21 | background-color: #ffffff; 22 | background-clip: padding-box; 23 | border: 1px solid #cccccc; 24 | border: 1px solid rgba(0, 0, 0, 0.2); 25 | border-radius: 6px; 26 | -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); 27 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); 28 | } 29 | .webui-popover.top, 30 | .webui-popover.top-left, 31 | .webui-popover.top-right { 32 | margin-top: -10px; 33 | } 34 | .webui-popover.right, 35 | .webui-popover.right-top, 36 | .webui-popover.right-bottom { 37 | margin-left: 10px; 38 | } 39 | .webui-popover.bottom, 40 | .webui-popover.bottom-left, 41 | .webui-popover.bottom-right { 42 | margin-top: 10px; 43 | } 44 | .webui-popover.left, 45 | .webui-popover.left-top, 46 | .webui-popover.left-bottom { 47 | margin-left: -10px; 48 | } 49 | .webui-popover-inner .close { 50 | font-family: arial; 51 | margin: 5px 10px 0 0; 52 | float: right; 53 | font-size: 20px; 54 | font-weight: bold; 55 | line-height: 20px; 56 | color: #000000; 57 | text-shadow: 0 1px 0 #fff; 58 | opacity: 0.2; 59 | filter: alpha(opacity=20); 60 | text-decoration: none; 61 | } 62 | .webui-popover-inner .close:hover, 63 | .webui-popover-inner .close:focus { 64 | opacity: 0.5; 65 | filter: alpha(opacity=50); 66 | } 67 | .webui-popover-title { 68 | padding: 8px 14px; 69 | margin: 0; 70 | font-size: 14px; 71 | font-weight: normal; 72 | line-height: 18px; 73 | background-color: #f7f7f7; 74 | border-bottom: 1px solid #ebebeb; 75 | border-radius: 5px 5px 0 0; 76 | } 77 | .webui-popover-content { 78 | padding: 9px 14px; 79 | overflow: auto; 80 | } 81 | .webui-popover-inverse { 82 | background-color: #333333; 83 | color: #eeeeee; 84 | } 85 | .webui-popover-inverse .webui-popover-title { 86 | background: #3b3b3b; 87 | border-bottom: none; 88 | color: #eeeeee; 89 | } 90 | .webui-no-padding .webui-popover-content { 91 | padding: 0; 92 | } 93 | .webui-no-padding .list-group-item { 94 | border-right: none; 95 | border-left: none; 96 | } 97 | .webui-no-padding .list-group-item:first-child { 98 | border-top: 0; 99 | } 100 | .webui-no-padding .list-group-item:last-child { 101 | border-bottom: 0; 102 | } 103 | .webui-popover > .arrow, 104 | .webui-popover > .arrow:after { 105 | position: absolute; 106 | display: block; 107 | width: 0; 108 | height: 0; 109 | border-color: transparent; 110 | border-style: solid; 111 | } 112 | .webui-popover > .arrow { 113 | border-width: 11px; 114 | } 115 | .webui-popover > .arrow:after { 116 | border-width: 10px; 117 | content: ""; 118 | } 119 | .webui-popover.top > .arrow, 120 | .webui-popover.top-right > .arrow, 121 | .webui-popover.top-left > .arrow { 122 | bottom: -11px; 123 | left: 50%; 124 | margin-left: -11px; 125 | border-top-color: #999999; 126 | border-top-color: rgba(0, 0, 0, 0.25); 127 | border-bottom-width: 0; 128 | } 129 | .webui-popover.top > .arrow:after, 130 | .webui-popover.top-right > .arrow:after, 131 | .webui-popover.top-left > .arrow:after { 132 | content: " "; 133 | bottom: 1px; 134 | margin-left: -10px; 135 | border-top-color: #ffffff; 136 | border-bottom-width: 0; 137 | } 138 | .webui-popover.right > .arrow, 139 | .webui-popover.right-top > .arrow, 140 | .webui-popover.right-bottom > .arrow { 141 | top: 50%; 142 | left: -11px; 143 | margin-top: -11px; 144 | border-left-width: 0; 145 | border-right-color: #999999; 146 | border-right-color: rgba(0, 0, 0, 0.25); 147 | } 148 | .webui-popover.right > .arrow:after, 149 | .webui-popover.right-top > .arrow:after, 150 | .webui-popover.right-bottom > .arrow:after { 151 | content: " "; 152 | left: 1px; 153 | bottom: -10px; 154 | border-left-width: 0; 155 | border-right-color: #ffffff; 156 | } 157 | .webui-popover.bottom > .arrow, 158 | .webui-popover.bottom-right > .arrow, 159 | .webui-popover.bottom-left > .arrow { 160 | top: -11px; 161 | left: 50%; 162 | margin-left: -11px; 163 | border-bottom-color: #999999; 164 | border-bottom-color: rgba(0, 0, 0, 0.25); 165 | border-top-width: 0; 166 | } 167 | .webui-popover.bottom > .arrow:after, 168 | .webui-popover.bottom-right > .arrow:after, 169 | .webui-popover.bottom-left > .arrow:after { 170 | content: " "; 171 | top: 1px; 172 | margin-left: -10px; 173 | border-bottom-color: #ffffff; 174 | border-top-width: 0; 175 | } 176 | .webui-popover.left > .arrow, 177 | .webui-popover.left-top > .arrow, 178 | .webui-popover.left-bottom > .arrow { 179 | top: 50%; 180 | right: -11px; 181 | margin-top: -11px; 182 | border-right-width: 0; 183 | border-left-color: #999999; 184 | border-left-color: rgba(0, 0, 0, 0.25); 185 | } 186 | .webui-popover.left > .arrow:after, 187 | .webui-popover.left-top > .arrow:after, 188 | .webui-popover.left-bottom > .arrow:after { 189 | content: " "; 190 | right: 1px; 191 | border-right-width: 0; 192 | border-left-color: #ffffff; 193 | bottom: -10px; 194 | } 195 | .webui-popover-inverse.top > .arrow, 196 | .webui-popover-inverse.top-left > .arrow, 197 | .webui-popover-inverse.top-right > .arrow, 198 | .webui-popover-inverse.top > .arrow:after, 199 | .webui-popover-inverse.top-left > .arrow:after, 200 | .webui-popover-inverse.top-right > .arrow:after { 201 | border-top-color: #333333; 202 | } 203 | .webui-popover-inverse.right > .arrow, 204 | .webui-popover-inverse.right-top > .arrow, 205 | .webui-popover-inverse.right-bottom > .arrow, 206 | .webui-popover-inverse.right > .arrow:after, 207 | .webui-popover-inverse.right-top > .arrow:after, 208 | .webui-popover-inverse.right-bottom > .arrow:after { 209 | border-right-color: #333333; 210 | } 211 | .webui-popover-inverse.bottom > .arrow, 212 | .webui-popover-inverse.bottom-left > .arrow, 213 | .webui-popover-inverse.bottom-right > .arrow, 214 | .webui-popover-inverse.bottom > .arrow:after, 215 | .webui-popover-inverse.bottom-left > .arrow:after, 216 | .webui-popover-inverse.bottom-right > .arrow:after { 217 | border-bottom-color: #333333; 218 | } 219 | .webui-popover-inverse.left > .arrow, 220 | .webui-popover-inverse.left-top > .arrow, 221 | .webui-popover-inverse.left-bottom > .arrow, 222 | .webui-popover-inverse.left > .arrow:after, 223 | .webui-popover-inverse.left-top > .arrow:after, 224 | .webui-popover-inverse.left-bottom > .arrow:after { 225 | border-left-color: #333333; 226 | } 227 | .webui-popover i.icon-refresh { 228 | display: block; 229 | width: 30px; 230 | height: 30px; 231 | font-size: 20px; 232 | top: 50%; 233 | left: 50%; 234 | position: absolute; 235 | background: url(../img/loading.gif) no-repeat; 236 | } 237 | @-webkit-keyframes rotate { 238 | 100% { 239 | -webkit-transform: rotate(360deg); 240 | } 241 | } 242 | @keyframes rotate { 243 | 100% { 244 | transform: rotate(360deg); 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /docs/loading.md: -------------------------------------------------------------------------------- 1 | # Loading 2 | 3 | An alternative to loading the Community Translator right away with the code from the [README](../README.md), you can also load the button just on demand (this is a very basic example): 4 | 5 | ![Translator Button](docs/translator-button.png) 6 | 7 | ``` 8 | [...][...] 9 | 10 | 11 | 12 | 13 | 14 | 17 | ``` 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/community-translator/0147634fa005f97de52bae42a666360ad4c17f63/docs/screenshot.png -------------------------------------------------------------------------------- /docs/server-side-component.md: -------------------------------------------------------------------------------- 1 | # Server-side Component: Translator Jumpstart 2 | 3 | The Community Translator needs a server-side component to tell it which strings can be translated and thus be highlighted. 4 | 5 | ## Example of server-side output 6 | 7 | A good place to output the data is in the footer of a page, for example for a WordPress site using the [`wp_footer`](http://codex.wordpress.org/Plugin_API/Action_Reference/wp_footer) action. 8 | 9 | ``` 10 | 24 | ``` 25 | 26 | In this example we are displaying a page in Brasilian Portuguese (`localeCode: "pt-br"`). The Community Translator currently also needs to know about the plural forms for the language, so that it can display the right input fields of plurals. 27 | 28 | 29 | ### `stringsUsedOnPage` 30 | This is the key object that contains the strings as they appear on the current page. We have one entry in this example: 31 | 32 | `Categorias` is the Portuguese string as it was displayed in the page (in WordPress this would have gone through the [`gettext`](http://codex.wordpress.org/Plugin_API/Action_Reference/gettext) filter). It is the key for an array of varying formats: 33 | 34 | #### Singular only case 35 | 36 | `"Categorias": ["Categories", ["taxonomy general name"] ]` 37 | 38 | It is the key for an array of the following format `[ "original string", [ "context" ] ]`. 39 | 40 | #### Containing a numeric placeholder 41 | `"%d anos": [ "%d years", "([0-9]{0,15}?) anos", [ "time span" ] ]` 42 | 43 | `"output of gettext", [ "original string (with placeholder)", "Regular expression that will match the string as it appears on the page", [ "context" ] ]` 44 | 45 | The key of a string will contain the placeholders but it will appear on the page with the number filled. That's why this string needs to include a regular expression as the second key of the array. 46 | 47 | #### Containing a string placeholder 48 | 49 | `"Novo coment\u00e1rio de %s": [ "New comment by %s", "Novo coment\u00e1rio de (.{0,200}?)" ]` 50 | 51 | Basically the same as above, just the regex is different. In this case a context is omitted. 52 | 53 | #### Singular/Plural case 54 | 55 | `"%s Coment\u00e1rios": [ [ "%s Comment", "%s Comments" ], "(.{0,200}?) Coment\u00e1rios" ] ]` 56 | 57 | In this case, the original is an array with singular and plural. The key string still represents the output of gettext. 58 | 59 | ### Remarks 60 | 61 | The original string(s) is used to get the full translation record from GlotPress. 62 | -------------------------------------------------------------------------------- /docs/translator-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/community-translator/0147634fa005f97de52bae42a666360ad4c17f63/docs/translator-button.png -------------------------------------------------------------------------------- /lib/batcher.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a utility function to help reduce the number of calls made to 3 | * the GlotPress database ( or generic backend ), especially as we're 4 | * loading a new page. 5 | * It takes a function that takes an array and callback, and generates a new 6 | * function that takes a single argument and returns a Deferred object. 7 | * i.e. 8 | * function ( arrayArgument, callback ) 9 | * to: 10 | * function ( singleArgument ) { return jQuery.deferred( singleResult ) } 11 | * 12 | * Internally, the function collects up a series of these singleArgument 13 | * calls and makes a single call to the original function ( presumably the 14 | * backend ) after a brief delay. 15 | */ 16 | var debug = require( 'debug' )( 'automattic:community-translator' ); 17 | 18 | 19 | function handleBatchedResponse( response, originalToCallbacksMap ) { 20 | var i, data, j, key; 21 | if ( 'undefined' === typeof response ) { 22 | return false; 23 | } 24 | 25 | if ( 'undefined' === typeof response[ 0 ] ) { 26 | response = [ response ]; 27 | } 28 | 29 | for ( i = 0; ( data = response[ i ] ); i++ ) { 30 | if ( 'undefined' === typeof data || 'undefined' === typeof data.original ) { 31 | // if there is not a single valid original 32 | break; 33 | } 34 | 35 | key = data.original.singular; 36 | if ( 'undefined' !== typeof data.original.context && data.original.context ) { 37 | key = data.original.context + '\u0004' + key; 38 | } 39 | 40 | if ( 'undefined' === typeof originalToCallbacksMap[ key ] || ! 41 | originalToCallbacksMap[ key ] ) { 42 | continue; 43 | } 44 | 45 | for ( j = 0; j < originalToCallbacksMap[ key ].length; j++ ) { 46 | originalToCallbacksMap[ key ][ j ].resolve( data ); 47 | } 48 | 49 | originalToCallbacksMap[ key ] = null; 50 | delete originalToCallbacksMap[ key ]; 51 | } 52 | 53 | // reject any keys that have not been handled 54 | for ( key in originalToCallbacksMap ) { 55 | if ( ! originalToCallbacksMap[ key ] ) { 56 | continue; 57 | } 58 | 59 | for ( j = 0; j < originalToCallbacksMap[ key ].length; j++ ) { 60 | originalToCallbacksMap[ key ][ j ].reject(); 61 | } 62 | } 63 | } 64 | 65 | module.exports = function( functionToWrap, options ) { 66 | var batchDelay = 200, 67 | originalToCallbacksMap = {}, 68 | batchedOriginals = [], 69 | batchTimeout, 70 | hash; 71 | 72 | if ( 'function' !== typeof ( functionToWrap ) ) { 73 | debug( 74 | 'batcher expects the first argument to be a function that takes an array and a callback, got ', 75 | functionToWrap ); 76 | return null; 77 | } 78 | 79 | // Functions that need to close over state 80 | hash = function( original ) { 81 | var key = original.singular; 82 | if ( 'undefined' !== typeof original.context ) { 83 | key = original.context + '\u0004' + key; 84 | } 85 | return key; 86 | }; 87 | 88 | delayMore = function() { 89 | if ( batchTimeout ) { 90 | clearTimeout( batchTimeout ); 91 | } 92 | batchTimeout = setTimeout( resolveBatch, batchDelay ); 93 | }; 94 | 95 | // Actually make the call through the original function 96 | resolveBatch = function() { 97 | // Capture the data relevant to this request 98 | var originals = batchedOriginals.slice(), 99 | callbacks = originalToCallbacksMap; 100 | 101 | // Then clear out the data so it's ready for the next batch. 102 | batchTimeout = null; 103 | originalToCallbacksMap = {}; 104 | batchedOriginals = []; 105 | 106 | if ( 0 === originals.length ) { 107 | return; 108 | } 109 | 110 | functionToWrap( originals, function( response ) { 111 | handleBatchedResponse( response, callbacks ); 112 | } ); 113 | }; 114 | 115 | if ( options ) { 116 | if ( options.batchDelay ) { 117 | batchDelay = options.batchDelay; 118 | } 119 | if ( options.hash ) { 120 | hash = options.hash; 121 | } 122 | } 123 | 124 | return function( original ) { 125 | var deferred = new jQuery.Deferred(), 126 | key = hash( original ); 127 | 128 | if ( key in originalToCallbacksMap ) { 129 | originalToCallbacksMap[ key ].push( deferred ); 130 | } else { 131 | batchedOriginals.push( original ); 132 | originalToCallbacksMap[ key ] = [ deferred ]; 133 | } 134 | 135 | delayMore(); 136 | 137 | return deferred; 138 | }; 139 | }; 140 | -------------------------------------------------------------------------------- /lib/glotpress.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Community Translation GlotPress module 3 | */ 4 | 'use strict'; 5 | 6 | var debug = require( 'debug' )( 'automattic:community-translator' ); 7 | var batcher = require( './batcher.js' ); 8 | 9 | function GlotPress( locale ) { 10 | 11 | var server = { 12 | url: '', 13 | project: '', 14 | translation_set_slug: 'default', 15 | }, 16 | projectIdMap = {}; 17 | 18 | function ajax( options ) { 19 | options = jQuery.extend( { 20 | type: 'POST', 21 | data: {}, 22 | dataType: 'json', 23 | xhrFields: { 24 | withCredentials: true 25 | }, 26 | crossDomain: true 27 | }, options ); 28 | return jQuery.ajax( options ); 29 | } 30 | 31 | function getServerUrl( path ) { 32 | return server.url + path; 33 | } 34 | 35 | function fetchOriginals( originals, callback ) { 36 | ajax( { 37 | url: getServerUrl( '/api/translations/-query-by-originals' ), 38 | data: { 39 | project: server.project, 40 | translation_set_slug: server.translation_set_slug, 41 | locale_slug: locale.getLocaleCode(), 42 | original_strings: JSON.stringify( originals ) 43 | } 44 | } ).done( function( response ) { 45 | callback( response ); 46 | } ); 47 | } 48 | 49 | return { 50 | getPermalink: function( translationPair ) { 51 | var originalId = translationPair.getOriginal().getId(), 52 | projectSlug = server.project, 53 | translateSetSlug = server.translation_set_slug, 54 | translationId; 55 | 56 | if ( translationPair.getGlotPressProject() ) { 57 | projectSlug = translationPair.getGlotPressProject(); 58 | } 59 | 60 | var url = server.url + '/projects/' + projectSlug + '/' + locale.getLocaleCode() + '/' + translateSetSlug + '?filters[original_id]=' + originalId; 61 | 62 | if ( 'undefined' !== typeof translationId ) { 63 | url += '&filters[translation_id]=' + translationId; 64 | } 65 | 66 | return url; 67 | }, 68 | 69 | loadSettings: function( gpInstance ) { 70 | 71 | if ( 'undefined' !== typeof gpInstance.url ) { 72 | server.url = gpInstance.url; 73 | } else { 74 | debug( 'Missing GP server url' ); 75 | } 76 | 77 | if ( 'undefined' !== typeof gpInstance.url ) { 78 | server.project = gpInstance.project; 79 | } else { 80 | debug( 'Missing GP project path' ); 81 | } 82 | 83 | if ( 'undefined' !== typeof gpInstance.translation_set_slug ) { 84 | server.translation_set_slug = gpInstance.translation_set_slug; 85 | } 86 | }, 87 | 88 | queryByOriginal: batcher( fetchOriginals ), 89 | 90 | submitTranslation: function( translation ) { 91 | return ajax( { 92 | url: getServerUrl( '/api/translations/-new' ), 93 | data: { 94 | project: server.project, 95 | translation_set_slug: server.translation_set_slug, 96 | locale_slug: locale.getLocaleCode(), 97 | translation: translation 98 | } 99 | } ); 100 | } 101 | }; 102 | } 103 | 104 | module.exports = GlotPress; 105 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Community Translation core module 3 | */ 4 | 'use strict'; 5 | 6 | /** 7 | * External dependencies 8 | */ 9 | var debug = require( 'debug' )( 'community-translator' ); 10 | 11 | /** 12 | * Internal dependencies 13 | */ 14 | var TranslationPair = require( './translation-pair' ), 15 | Walker = require( './walker' ), 16 | Locale = require( './locale' ), 17 | Popover = require( './popover' ), 18 | GlotPress = require ( './glotpress' ), 19 | WebUIPopover = require('./jquery.webui-popover.js' ); 20 | 21 | /** 22 | * Local variables 23 | */ 24 | var debounceTimeout, 25 | currentlyWalkingTheDom = false, 26 | loadCSS, loadData, registerContentChangedCallback, registerDomChangedCallback, 27 | registerPopoverHandlers, findNewTranslatableTexts, 28 | glotPress, currentUserId, walker, 29 | baseUrl = 'https://widgets.wp.com/community-translator/', 30 | translationData = { 31 | currentUserId: false, 32 | localeCode: 'en', 33 | languageName: 'English', 34 | pluralForms: 'nplurals=2; plural=(n != 1)', 35 | contentChangedCallback: function() {}, 36 | glotPress: { 37 | url: 'http://glotpress.dev', 38 | project: 'test' 39 | } 40 | }, 41 | translationUpdateCallbacks = []; 42 | 43 | module.exports = { 44 | 45 | load: function() { 46 | if ( 'undefined' === typeof window.translatorJumpstart ) { 47 | return false; 48 | } 49 | loadCSS(); 50 | loadData( window.translatorJumpstart ); 51 | 52 | registerPopoverHandlers(); 53 | registerContentChangedCallback(); 54 | findNewTranslatableTexts(); 55 | }, 56 | 57 | unload: function() { 58 | if ( debounceTimeout ) { 59 | clearTimeout( debounceTimeout ); 60 | } 61 | if ( 'object' === typeof window.translatorJumpstart ) { 62 | window.translatorJumpstart.contentChangedCallback = function() {}; 63 | } 64 | unRegisterPopoverHandlers(); 65 | removeCssClasses(); 66 | }, 67 | 68 | registerTranslatedCallback: function( callback ) { 69 | translationUpdateCallbacks.push( callback ); 70 | } 71 | 72 | }; 73 | 74 | function notifyTranslated( newTranslationPair ) { 75 | debug( 'Notifying string translated', newTranslationPair.serialize() ); 76 | translationUpdateCallbacks.forEach( function( hook ) { 77 | hook( newTranslationPair.serialize() ); 78 | } ); 79 | } 80 | 81 | loadCSS = function() { 82 | var s = document.createElement( 'link' ); 83 | s.setAttribute( 'rel', 'stylesheet' ); 84 | s.setAttribute( 'type', 'text/css' ); 85 | s.setAttribute( 'href', baseUrl + 'community-translator.css' ); 86 | document.getElementsByTagName( 'head' )[ 0 ].appendChild( s ); 87 | 88 | var t = document.createElement( 'link' ); 89 | t.setAttribute( 'rel', 'stylesheet' ); 90 | t.setAttribute( 'type', 'text/css' ); 91 | t.setAttribute( 'href', 'https://s1.wp.com/i/noticons/noticons.css' ); 92 | document.getElementsByTagName( 'head' )[ 0 ].appendChild( t ); 93 | 94 | jQuery( 'iframe' ).addClass( 'translator-untranslatable' ); 95 | }; 96 | 97 | loadData = function( translationDataFromJumpstart ) { 98 | if ( 99 | typeof translationDataFromJumpstart === 'object' && 100 | typeof translationDataFromJumpstart.localeCode === 'string' 101 | ) { 102 | translationData = translationDataFromJumpstart; 103 | } 104 | 105 | translationData.locale = new Locale( translationData.localeCode, translationData.languageName, translationData.pluralForms ); 106 | currentUserId = translationData.currentUserId; 107 | 108 | glotPress = new GlotPress( translationData.locale ); 109 | if ( 'undefined' !== typeof translationData.glotPress ) { 110 | glotPress.loadSettings( translationData.glotPress ); 111 | } else { 112 | debug( 'Missing GlotPress settings' ); 113 | } 114 | 115 | TranslationPair.setTranslationData( translationData ); 116 | walker = new Walker( TranslationPair, jQuery, document ); 117 | }; 118 | 119 | registerContentChangedCallback = function() { 120 | if ( 'object' === typeof window.translatorJumpstart ) { 121 | debug( 'Registering translator contentChangedCallback' ); 122 | window.translatorJumpstart.contentChangedCallback = function() { 123 | if ( debounceTimeout ) { 124 | clearTimeout( debounceTimeout ); 125 | } 126 | debounceTimeout = setTimeout( findNewTranslatableTexts, 250 ); 127 | }; 128 | 129 | if ( typeof window.translatorJumpstart.stringsUsedOnPage === 'object' ) { 130 | registerDomChangedCallback(); 131 | } 132 | 133 | } 134 | }; 135 | 136 | // This is a not very elegant but quite efficient way to check if the DOM has changed 137 | // after the initial walking of the DOM 138 | registerDomChangedCallback = function() { 139 | var checksRemaining = 10, 140 | lastBodySize = document.body.innerHTML.length, 141 | checkBodySize = function() { 142 | var bodySize; 143 | 144 | if ( --checksRemaining <= 0 ) { 145 | return; 146 | } 147 | 148 | bodySize = document.body.innerHTML.length; 149 | if ( lastBodySize !== bodySize ) { 150 | lastBodySize = bodySize; 151 | 152 | if ( debounceTimeout ) { 153 | clearTimeout( debounceTimeout ); 154 | } 155 | debounceTimeout = setTimeout( findNewTranslatableTexts, 1700 ); 156 | } 157 | setTimeout( checkBodySize, 1500 ); 158 | }; 159 | 160 | setTimeout( checkBodySize, 1500 ); 161 | }; 162 | 163 | registerPopoverHandlers = function() { 164 | 165 | jQuery( document ).on( 'keyup', 'textarea.translation', function() { 166 | var textareasWithInput, 167 | $form = jQuery( this ).parents( 'form.ct-new-translation' ), 168 | $allTextareas = $form.find( 'textarea' ), 169 | $button = $form.find( 'button' ); 170 | 171 | textareasWithInput = $allTextareas.filter( function() { 172 | return this.value.length; 173 | } ); 174 | 175 | // disable if no textarea has an input 176 | $button.prop( 'disabled', 0 === textareasWithInput.length ); 177 | } ); 178 | 179 | jQuery( document ).on( 'submit', 'form.ct-new-translation', function() { 180 | var $form = jQuery( this ), 181 | $node = jQuery( '.' + $form.data( 'nodes' ) ), 182 | val = $form.find( 'textarea' ).val(), 183 | translationPair = $form.data( 'translationPair' ), 184 | newTranslationStringsFromForm = $form.find( 'textarea' ).map( function() { 185 | return jQuery( this ).val(); 186 | } ).get(); 187 | 188 | function notEmpty( string ) { 189 | return string.trim().length > 0; 190 | } 191 | 192 | if ( ! newTranslationStringsFromForm.every( notEmpty ) ) { 193 | return false; 194 | } 195 | 196 | // We're optimistic 197 | // TODO: reset on failure. 198 | // TODO: use Jed to insert with properly replaced variables 199 | $node.addClass( 'translator-user-translated' ).removeClass( 'translator-untranslated' ); 200 | 201 | $form.closest( '.webui-popover' ).hide(); 202 | 203 | // Reporting to GlotPress 204 | jQuery 205 | .when( translationPair.getOriginal().getId() ) 206 | .done( function( originalId ) { 207 | var submittedTranslations = jQuery.makeArray( newTranslationStringsFromForm ), 208 | translation = {}; 209 | 210 | translation[ originalId ] = submittedTranslations; 211 | glotPress.submitTranslation( translation ).done( function( data ) { 212 | 213 | 214 | if ( typeof data[ originalId ] === 'undefined' ) { 215 | return; 216 | } 217 | 218 | translationPair.updateAllTranslations( data[ originalId ], currentUserId ); 219 | makeTranslatable( translationPair, $node ); 220 | notifyTranslated( translationPair ); 221 | 222 | } ).fail( function() { 223 | debug( 'Submitting new translation failed', translation ); 224 | } ); 225 | } ).fail( function() { 226 | debug( 'Original cannot be found in GlotPress' ); 227 | } ); 228 | 229 | return false; 230 | } ); 231 | 232 | jQuery( document ).on( 'submit', 'form.ct-existing-translation', function() { 233 | var enclosingNode = jQuery( this ), popover, 234 | translationPair = enclosingNode.data( 'translationPair' ); 235 | 236 | if ( 'object' !== typeof translationPair ) { 237 | debug( 'could not find translation for node', enclosingNode ); 238 | return false; 239 | } 240 | 241 | popover = new Popover( translationPair, translationData.locale, glotPress ); 242 | enclosingNode.parent().empty().append( popover.getTranslationHtml() ); 243 | 244 | return false; 245 | } ); 246 | }; 247 | 248 | function removeCssClasses() { 249 | var classesToDrop = [ 250 | 'translator-checked', 251 | 'translator-untranslated', 252 | 'translator-translated', 253 | 'translator-user-translated', 254 | 'translator-untranslatable', 255 | 'translator-dont-translate' ]; 256 | 257 | jQuery( '.' + classesToDrop.join( ', .' ) ).removeClass( classesToDrop.join( ' ' ) ); 258 | } 259 | 260 | function unRegisterPopoverHandlers() { 261 | jQuery( document ).off( 'submit', 'form.ct-existing-translation,form.ct-new-translation' ); 262 | jQuery( '.translator-translatable' ).webuiPopover( 'destroy' ); 263 | } 264 | 265 | function makeUntranslatable( $node ) { 266 | debug( 'makeUntranslatable:', $node ); 267 | $node.removeClass( 'translator-untranslated translator-translated translator-translatable translator-checking' ); 268 | $node.addClass( 'translator-dont-translate' ); 269 | } 270 | 271 | function makeTranslatable( translationPair, node ) { 272 | translationPair.createPopover( node, glotPress ); 273 | node.removeClass( 'translator-checking' ).addClass( 'translator-translatable' ); 274 | if ( translationPair.isFullyTranslated() ) { 275 | if ( translationPair.isTranslationWaiting() ) { 276 | node.removeClass( 'translator-translated' ).addClass( 'translator-user-translated' ); 277 | } else { 278 | node.removeClass( 'translator-user-translated' ).addClass( 'translator-translated' ); 279 | } 280 | } else { 281 | node.addClass( 'translator-untranslated' ); 282 | } 283 | } 284 | 285 | findNewTranslatableTexts = function() { 286 | if ( currentlyWalkingTheDom ) { 287 | if ( debounceTimeout ) { 288 | clearTimeout( debounceTimeout ); 289 | } 290 | debounceTimeout = setTimeout( findNewTranslatableTexts, 1500 ); 291 | return; 292 | } 293 | 294 | currentlyWalkingTheDom = true; 295 | 296 | debug( 'Searching for translatable texts' ); 297 | walker.walkTextNodes( document.body, function( translationPair, enclosingNode ) { 298 | enclosingNode.addClass( 'translator-checking' ); 299 | 300 | translationPair.fetchOriginalAndTranslations( glotPress, currentUserId ) 301 | .fail( 302 | // Failure indicates that the string is not in GlotPress yet 303 | makeUntranslatable.bind( null, enclosingNode ) 304 | ) 305 | .done( 306 | makeTranslatable.bind( null, translationPair, enclosingNode ) 307 | ); 308 | 309 | }, function() { 310 | currentlyWalkingTheDom = false; 311 | } ); 312 | }; 313 | -------------------------------------------------------------------------------- /lib/jquery.webui-popover.js: -------------------------------------------------------------------------------- 1 | /* 2 | * webui popover plugin - v1.0.5 3 | * A lightWeight popover plugin with jquery ,enchance the popover plugin of bootstrap with some awesome new features. It works well with bootstrap ,but bootstrap is not necessary! 4 | * https://github.com/sandywalker/webui-popover 5 | * 6 | * Made by Sandy Duan 7 | * Under MIT License 8 | */ (function( $, window, document, undefined ) { 9 | 10 | // Create the defaults once 11 | var pluginName = 'webuiPopover'; 12 | var pluginClass = 'webui-popover'; 13 | var pluginType = 'webui.popover'; 14 | var defaults = { 15 | placement: 'auto', 16 | width: 400, 17 | height: 'auto', 18 | trigger: 'rightclick', 19 | style: '', 20 | delay: 300, 21 | cache: false, 22 | multi: false, 23 | arrow: true, 24 | title: '', 25 | content: '', 26 | closeable: false, 27 | padding: true, 28 | url: '', 29 | type: 'html', 30 | onload: false, 31 | translationPair: null, 32 | template: '
' + 33 | '
' + 34 | '
' + 35 | 'x' + 36 | '

' + 37 | '

 

' + 38 | '
' + 39 | '
' 40 | }; 41 | 42 | 43 | // The actual plugin constructor 44 | function WebuiPopover( element, options ) { 45 | this.$element = $( element ); 46 | 47 | // IE & Firefox wont deliver events to $element inside a button, so 48 | // delgate in this case 49 | // https://github.com/Automattic/community-translator/issues/37 50 | this.$eventDelegate = this.$element; 51 | if ( this.$element.closest( 'button' ).length ) { 52 | this.$eventDelegate = this.$element.closest( 'button' ); 53 | } 54 | 55 | this.options = $.extend( {}, defaults, options ); 56 | this._defaults = defaults; 57 | this._name = pluginName; 58 | this.init(); 59 | } 60 | 61 | WebuiPopover.prototype = { 62 | //init webui popover 63 | init: function() { 64 | //init the event handlers 65 | if ( this.options.trigger === 'click' ) { 66 | this.$eventDelegate.off( 'click' ).on( 'click', $.proxy( this.toggle, this ) ); 67 | } else if ( this.options.trigger === 'rightclick' ) { 68 | this.$eventDelegate.off( 'contextmenu' ).on( 'contextmenu', $.proxy( this.toggle, this ) ); 69 | this.$eventDelegate.off( 'click' ).on( 'click', function( e ) { 70 | if ( e.ctrlKey ) return false; // ctrl-click in safari should only be handled by the contextmenu handler; 71 | } ); 72 | } else { 73 | this.$eventDelegate.off( 'mouseenter mouseleave' ) 74 | .on( 'mouseenter', $.proxy( this.mouseenterHandler, this ) ) 75 | .on( 'mouseleave', $.proxy( this.mouseleaveHandler, this ) ); 76 | } 77 | this._poped = false; 78 | this._inited = true; 79 | }, 80 | /* api methods and actions */ 81 | destroy: function() { 82 | this.hide(); 83 | this.$element.data( 'plugin_' + pluginName, null ); 84 | this.$eventDelegate.off(); 85 | if ( this.$target ) { 86 | this.$target.remove(); 87 | } 88 | }, 89 | hide: function( e ) { 90 | if ( e ) { 91 | e.preventDefault(); 92 | e.stopPropagation(); 93 | } 94 | var e = $.Event( 'hide.' + pluginType ); 95 | this.$element.trigger( e ); 96 | if ( this.$target ) { 97 | this.$target.removeClass( 'in' ).hide(); 98 | } 99 | this.$element.trigger( 'hidden.' + pluginType ); 100 | }, 101 | toggle: function( e ) { 102 | if ( e ) { 103 | e.preventDefault(); 104 | e.stopPropagation(); 105 | } 106 | this[ this.getTarget().hasClass( 'in' ) ? 'hide' : 'show' ]( e ); 107 | }, 108 | hideAll: function() { 109 | $( 'div.webui-popover' ).not( '.webui-popover-fixed' ).removeClass( 'in' ).hide(); 110 | }, 111 | /*core method ,show popover */ 112 | show: function( e ) { 113 | if ( e ) { 114 | e.preventDefault(); 115 | e.stopPropagation(); 116 | } 117 | var $target = this.getTarget().removeClass().addClass( pluginClass ); 118 | if ( ! this.options.multi ) { 119 | this.hideAll(); 120 | } 121 | // use cache by default, if not cache setted , reInit the contents 122 | if ( ! this.options.cache || ! this._poped ) { 123 | this.setTitle( this.getTitle() ); 124 | if ( ! this.options.closeable ) { 125 | $target.find( '.close' ).off( 'click' ).remove(); 126 | } 127 | if ( ! this.isAsync() ) { 128 | this.setContent( this.getContent() ); 129 | } else { 130 | this.setContentASync( this.options.content ); 131 | this.displayContent(); 132 | return; 133 | } 134 | $target.show(); 135 | } 136 | this.displayContent(); 137 | this.bindBodyEvents(); 138 | }, 139 | displayContent: function() { 140 | var 141 | //element postion 142 | elementPos = this.getElementPosition(), 143 | //target postion 144 | $target = this.getTarget().removeClass().addClass( pluginClass ), 145 | //target content 146 | $targetContent = this.getContentElement(), 147 | //target Width 148 | targetWidth = $target[ 0 ].offsetWidth, 149 | //target Height 150 | targetHeight = $target[ 0 ].offsetHeight, 151 | //placement 152 | placement = 'bottom', 153 | e = $.Event( 'show.' + pluginType ); 154 | //if (this.hasContent()){ 155 | this.$element.trigger( e ); 156 | //} 157 | if ( this.options.width !== 'auto' ) { 158 | $target.width( this.options.width ); 159 | } 160 | if ( this.options.height !== 'auto' ) { 161 | $targetContent.height( this.options.height ); 162 | } 163 | 164 | //init the popover and insert into the document body 165 | if ( ! this.options.arrow ) { 166 | $target.find( '.arrow' ).remove(); 167 | } 168 | $target.remove().css( { 169 | top: -1000, 170 | left: -1000, 171 | display: 'block' 172 | } ).appendTo( document.body ); 173 | targetWidth = $target[ 0 ].offsetWidth; 174 | targetHeight = $target[ 0 ].offsetHeight; 175 | placement = this.getPlacement( elementPos, targetHeight ); 176 | this.initTargetEvents(); 177 | var postionInfo = this.getTargetPositin( elementPos, placement, targetWidth, targetHeight ); 178 | this.$target.css( postionInfo.position ).addClass( placement ).addClass( 'in' ); 179 | 180 | if ( this.options.type === 'iframe' ) { 181 | var $iframe = $target.find( 'iframe' ); 182 | $iframe.width( $target.width() ).height( $iframe.parent().height() ); 183 | } 184 | 185 | if ( this.options.style ) { 186 | this.$target.addClass( pluginClass + '-' + this.options.style ); 187 | } 188 | 189 | if ( ! this.options.padding ) { 190 | $targetContent.css( 'height', $targetContent.outerHeight() ); 191 | this.$target.addClass( 'webui-no-padding' ); 192 | } 193 | if ( ! this.options.arrow ) { 194 | this.$target.css( { 195 | 'margin': 0 196 | } ); 197 | } 198 | if ( this.options.arrow ) { 199 | var $arrow = this.$target.find( '.arrow' ); 200 | $arrow.removeAttr( 'style' ); 201 | if ( postionInfo.arrowOffset ) { 202 | $arrow.css( postionInfo.arrowOffset ); 203 | } 204 | } 205 | if ( this.options.onload && typeof this.options.onload == "function" ) { 206 | this.options.onload( $targetContent, this.$target ); 207 | } 208 | 209 | if ( this.options.translationPair ) { 210 | $targetContent.find( "form" ).data( "translationPair", this.options.translationPair ); 211 | } 212 | 213 | this._poped = true; 214 | this.$element.trigger( 'shown.' + pluginType ); 215 | 216 | }, 217 | 218 | isTargetLoaded: function() { 219 | return this.getTarget().find( 'i.glyphicon-refresh' ).length === 0; 220 | }, 221 | 222 | /*getter setters */ 223 | getTarget: function() { 224 | if ( ! this.$target ) { 225 | this.$target = $( this.options.template ); 226 | } 227 | return this.$target; 228 | }, 229 | getTitleElement: function() { 230 | return this.getTarget().find( '.' + pluginClass + '-title' ); 231 | }, 232 | getContentElement: function() { 233 | return this.getTarget().find( '.' + pluginClass + '-content' ); 234 | }, 235 | getTitle: function() { 236 | return this.options.title || this.$element.attr( 'data-title' ) || this.$element.attr( 'title' ); 237 | }, 238 | setTitle: function( title ) { 239 | var $titleEl = this.getTitleElement(); 240 | if ( title ) { 241 | $titleEl.html( title ); 242 | } else { 243 | $titleEl.remove(); 244 | } 245 | }, 246 | hasContent: function() { 247 | return this.getContent(); 248 | }, 249 | getContent: function() { 250 | if ( this.options.url ) { 251 | if ( this.options.type === 'iframe' ) { 252 | this.content = $( '' ).attr( 'src', this.options.url ); 253 | } 254 | } else if ( ! this.content ) { 255 | var content = ''; 256 | if ( $.isFunction( this.options.content ) ) { 257 | content = this.options.content.apply( this.$element[ 0 ], arguments ); 258 | } else { 259 | content = this.options.content; 260 | } 261 | this.content = this.$element.attr( 'data-content' ) || content; 262 | } 263 | return this.content; 264 | }, 265 | setContent: function( content ) { 266 | var $target = this.getTarget(); 267 | this.getContentElement().html( content ); 268 | this.$target = $target; 269 | }, 270 | isAsync: function() { 271 | return this.options.type === 'async'; 272 | }, 273 | setContentASync: function( content ) { 274 | var that = this; 275 | $.ajax( { 276 | url: this.options.url, 277 | type: 'GET', 278 | cache: this.options.cache, 279 | success: function( data ) { 280 | if ( content && $.isFunction( content ) ) { 281 | that.content = content.apply( that.$element[ 0 ], [ data ] ); 282 | } else { 283 | that.content = data; 284 | } 285 | that.setContent( that.content ); 286 | var $targetContent = that.getContentElement(); 287 | $targetContent.removeAttr( 'style' ); 288 | that.displayContent(); 289 | } 290 | } ); 291 | }, 292 | 293 | bindBodyEvents: function() { 294 | $( 'body' ).off( 'keyup.webui-popover' ).on( 'keyup.webui-popover', $.proxy( this.escapeHandler, this ) ); 295 | $( 'body' ).off( 'click.webui-popover' ).on( 'click.webui-popover', $.proxy( this.bodyClickHandler, this ) ); 296 | }, 297 | 298 | /* event handlers */ 299 | mouseenterHandler: function() { 300 | var self = this; 301 | if ( self._timeout ) { 302 | clearTimeout( self._timeout ); 303 | } 304 | if ( ! self.getTarget().is( ':visible' ) ) { 305 | self.show(); 306 | } 307 | }, 308 | mouseleaveHandler: function() { 309 | var self = this; 310 | //key point, set the _timeout then use clearTimeout when mouse leave 311 | self._timeout = setTimeout( function() { 312 | self.hide(); 313 | }, self.options.delay ); 314 | }, 315 | escapeHandler: function( e ) { 316 | if ( e.keyCode === 27 ) { 317 | this.hideAll(); 318 | } 319 | }, 320 | bodyClickHandler: function( e ) { 321 | this.hideAll(); 322 | }, 323 | 324 | targetClickHandler: function( e ) { 325 | e.stopPropagation(); 326 | }, 327 | 328 | //reset and init the target events; 329 | initTargetEvents: function() { 330 | if ( this.options.trigger === 'click' ) { 331 | } else if ( this.options.trigger === 'rightclick' ) { 332 | this.$target.find( '.close' ).off( 'click' ).on( 'click', $.proxy( this.hide, this ) ); 333 | } else { 334 | this.$target.off( 'mouseenter mouseleave' ) 335 | .on( 'mouseenter', $.proxy( this.mouseenterHandler, this ) ) 336 | .on( 'mouseleave', $.proxy( this.mouseleaveHandler, this ) ); 337 | } 338 | this.$target.find( '.close' ).off( 'click' ).on( 'click', $.proxy( this.hide, this ) ); 339 | this.$target.off( 'click.webui-popover' ).on( 'click.webui-popover', $.proxy( this.targetClickHandler, this ) ); 340 | }, 341 | /* utils methods */ 342 | //caculate placement of the popover 343 | getPlacement: function( pos, targetHeight ) { 344 | var placement, 345 | de = document.documentElement, 346 | db = document.body, 347 | clientWidth = de.clientWidth, 348 | clientHeight = de.clientHeight, 349 | scrollTop = Math.max( db.scrollTop, de.scrollTop ), 350 | scrollLeft = Math.max( db.scrollLeft, de.scrollLeft ), 351 | pageX = Math.max( 0, pos.left - scrollLeft ), 352 | pageY = Math.max( 0, pos.top - scrollTop ), 353 | arrowSize = 20; 354 | 355 | //if placement equals auto,caculate the placement by element information; 356 | if ( typeof ( this.options.placement ) === 'function' ) { 357 | placement = this.options.placement.call( this, this.getTarget()[ 0 ], this.$element[ 0 ] ); 358 | } else { 359 | placement = this.$element.data( 'placement' ) || this.options.placement; 360 | } 361 | 362 | if ( placement === 'auto' ) { 363 | if ( pageX < clientWidth / 3 ) { 364 | if ( pageY < clientHeight / 3 ) { 365 | placement = 'bottom-right'; 366 | } else if ( pageY < clientHeight * 2 / 3 ) { 367 | placement = 'right'; 368 | } else { 369 | placement = 'top-right'; 370 | } 371 | //placement= pageY>targetHeight+arrowSize?'top-right':'bottom-right'; 372 | } else if ( pageX < clientWidth * 2 / 3 ) { 373 | if ( pageY < clientHeight / 3 ) { 374 | placement = 'bottom'; 375 | } else if ( pageY < clientHeight * 2 / 3 ) { 376 | placement = 'bottom'; 377 | } else { 378 | placement = 'top'; 379 | } 380 | } else { 381 | placement = pageY > targetHeight + arrowSize ? 'top-left' : 'bottom-left'; 382 | if ( pageY < clientHeight / 3 ) { 383 | placement = 'bottom-left'; 384 | } else if ( pageY < clientHeight * 2 / 3 ) { 385 | placement = 'left'; 386 | } else { 387 | placement = 'top-left'; 388 | } 389 | } 390 | } 391 | return placement; 392 | }, 393 | getElementPosition: function() { 394 | return $.extend( {}, this.$element.offset(), { 395 | width: this.$element[ 0 ].offsetWidth, 396 | height: this.$element[ 0 ].offsetHeight 397 | } ); 398 | }, 399 | 400 | getTargetPositin: function( elementPos, placement, targetWidth, targetHeight ) { 401 | var pos = elementPos, 402 | elementW = this.$element.outerWidth(), 403 | elementH = this.$element.outerHeight(), 404 | position = {}, 405 | arrowOffset = null, 406 | arrowSize = this.options.arrow ? 28 : 0, 407 | fixedW = elementW < arrowSize + 10 ? arrowSize : 0, 408 | fixedH = elementH < arrowSize + 10 ? arrowSize : 0; 409 | switch ( placement ) { 410 | case 'bottom': 411 | position = { 412 | top: pos.top + pos.height, 413 | left: pos.left + pos.width / 2 - targetWidth / 2 414 | }; 415 | break; 416 | case 'top': 417 | position = { 418 | top: pos.top - targetHeight, 419 | left: pos.left + pos.width / 2 - targetWidth / 2 420 | }; 421 | break; 422 | case 'left': 423 | position = { 424 | top: pos.top + pos.height / 2 - targetHeight / 2, 425 | left: pos.left - targetWidth 426 | }; 427 | break; 428 | case 'right': 429 | position = { 430 | top: pos.top + pos.height / 2 - targetHeight / 2, 431 | left: pos.left + pos.width 432 | }; 433 | break; 434 | case 'top-right': 435 | position = { 436 | top: pos.top - targetHeight, 437 | left: pos.left - fixedW 438 | }; 439 | arrowOffset = { 440 | left: elementW / 2 + fixedW 441 | }; 442 | break; 443 | case 'top-left': 444 | position = { 445 | top: pos.top - targetHeight, 446 | left: pos.left - targetWidth + pos.width + fixedW 447 | }; 448 | arrowOffset = { 449 | left: targetWidth - elementW / 2 - fixedW 450 | }; 451 | break; 452 | case 'bottom-right': 453 | position = { 454 | top: pos.top + pos.height, 455 | left: pos.left - fixedW 456 | }; 457 | arrowOffset = { 458 | left: elementW / 2 + fixedW 459 | }; 460 | break; 461 | case 'bottom-left': 462 | position = { 463 | top: pos.top + pos.height, 464 | left: pos.left - targetWidth + pos.width + fixedW 465 | }; 466 | arrowOffset = { 467 | left: targetWidth - elementW / 2 - fixedW 468 | }; 469 | break; 470 | case 'right-top': 471 | position = { 472 | top: pos.top - targetHeight + pos.height + fixedH, 473 | left: pos.left + pos.width 474 | }; 475 | arrowOffset = { 476 | top: targetHeight - elementH / 2 - fixedH 477 | }; 478 | break; 479 | case 'right-bottom': 480 | position = { 481 | top: pos.top - fixedH, 482 | left: pos.left + pos.width 483 | }; 484 | arrowOffset = { 485 | top: elementH / 2 + fixedH 486 | }; 487 | break; 488 | case 'left-top': 489 | position = { 490 | top: pos.top - targetHeight + pos.height + fixedH, 491 | left: pos.left - targetWidth 492 | }; 493 | arrowOffset = { 494 | top: targetHeight - elementH / 2 - fixedH 495 | }; 496 | break; 497 | case 'left-bottom': 498 | position = { 499 | top: pos.top, 500 | left: pos.left - targetWidth 501 | }; 502 | arrowOffset = { 503 | top: elementH / 2 504 | }; 505 | break; 506 | 507 | } 508 | return { 509 | position: position, 510 | arrowOffset: arrowOffset 511 | }; 512 | } 513 | }; 514 | $.fn[ pluginName ] = function( options ) { 515 | return this.each( function() { 516 | var webuiPopover = $.data( this, 'plugin_' + pluginName ); 517 | if ( ! webuiPopover ) { 518 | if ( ! options ) { 519 | webuiPopover = new WebuiPopover( this, null ); 520 | } else if ( typeof options === 'string' ) { 521 | if ( options !== 'destroy' ) { 522 | webuiPopover = new WebuiPopover( this, null ); 523 | webuiPopover[ options ](); 524 | } 525 | } else if ( typeof options === 'object' ) { 526 | webuiPopover = new WebuiPopover( this, options ); 527 | } 528 | $.data( this, 'plugin_' + pluginName, webuiPopover ); 529 | } else { 530 | if ( options === 'destroy' ) { 531 | webuiPopover.destroy(); 532 | } else if ( typeof options === 'string' ) { 533 | webuiPopover[ options ](); 534 | } 535 | } 536 | } ); 537 | }; 538 | 539 | })( jQuery, window, document ); 540 | 541 | 542 | -------------------------------------------------------------------------------- /lib/jquery.webui-popover.licence: -------------------------------------------------------------------------------- 1 | jquery.webui-popover.js is derived from WebUI-Popover Copyright (c) 2014 Sandy Duan, https://github.com/sandywalker/webui-popover under the MIT Licence: 2 | 3 | The MIT License (MIT) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /lib/locale.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Locale module 3 | */ 4 | 5 | /** 6 | * External dependencies 7 | */ 8 | var Jed = require( 'jed' ); 9 | 10 | function Locale( localeCode, languageName, pluralForms ) { 11 | var getPluralIndex = Jed.PF.compile( pluralForms ), 12 | nplurals_re = /nplurals\=(\d+);/, 13 | nplurals_matches = pluralForms.match( nplurals_re ), 14 | numberOfPlurals = 2; 15 | 16 | // Find the nplurals number 17 | if ( nplurals_matches.length > 1 ) { 18 | numberOfPlurals = nplurals_matches[ 1 ]; 19 | } 20 | 21 | return { 22 | getLocaleCode: function() { 23 | return localeCode; 24 | }, 25 | getLanguageName: function() { 26 | return languageName; 27 | }, 28 | getInfo: function() { 29 | return localeCode; 30 | }, 31 | getPluralCount: function() { 32 | return numberOfPlurals; 33 | }, 34 | // port from GlotPress locales.php:numbers_for_index 35 | getNumbersForIndex: function( index ) { 36 | var number, 37 | howMany = 3, 38 | testUpTo = 1000, 39 | numbers = []; 40 | for ( number = 0; number < testUpTo; ++number ) { 41 | if ( getPluralIndex( number ) == index ) { 42 | numbers.push( number ); 43 | if ( numbers.length >= howMany ) { 44 | break; 45 | } 46 | } 47 | } 48 | return numbers; 49 | } 50 | }; 51 | } 52 | 53 | module.exports = Locale; 54 | -------------------------------------------------------------------------------- /lib/original.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Original module 3 | */ 4 | var Translation = require( './translation' ); 5 | 6 | function Original( original ) { 7 | var singular, 8 | plural = null, 9 | comment = null, 10 | originalId = null; 11 | 12 | 13 | if ( 'string' === typeof original ) { 14 | singular = original; 15 | } else if ( 16 | 'object' === typeof original && 17 | 'string' === typeof original.singular 18 | ) { 19 | singular = original.singular; 20 | plural = original.plural; 21 | } else { 22 | singular = original[ 0 ]; 23 | plural = original[ 1 ]; 24 | } 25 | 26 | if ( 'undefined' === typeof plural || '' === plural ) { 27 | plural = null; 28 | } 29 | 30 | if ( 'undefined' !== typeof original.originalId ) { 31 | originalId = original.originalId; 32 | } 33 | 34 | if ( 'undefined' !== typeof original.comment ) { 35 | comment = original.comment; 36 | } 37 | 38 | function objectify( context ) { 39 | var result = { 40 | singular: singular 41 | }; 42 | 43 | if ( plural ) { 44 | result.plural = plural; 45 | } 46 | 47 | if ( context ) { 48 | result.context = context; 49 | } 50 | 51 | return result; 52 | } 53 | 54 | return { 55 | type: 'Original', 56 | getSingular: function() { 57 | return singular; 58 | }, 59 | getPlural: function() { 60 | return plural; 61 | }, 62 | generateJsonHash: function( context ) { 63 | 64 | if ( 'string' === typeof context && '' !== context ) { 65 | return context + '\u0004' + singular; 66 | } 67 | 68 | return singular; 69 | }, 70 | getEmptyTranslation: function( locale ) { 71 | var forms = [ '' ]; 72 | 73 | if ( plural !== null ) { 74 | for ( i = 1; i < locale.getPluralCount(); i++ ) { 75 | forms.push( '' ); 76 | } 77 | } 78 | 79 | return new Translation( locale, forms ); 80 | }, 81 | objectify: objectify, 82 | fetchIdAndTranslations: function( glotPress, context ) { 83 | return glotPress.queryByOriginal( objectify( context ) ).done( function( data ) { 84 | originalId = data.original_id; 85 | if ( typeof data.original_comment === 'string' ) { 86 | comment = data.original_comment.replace( /^translators: /, '' ); 87 | } 88 | 89 | } ); 90 | }, 91 | getId: function() { 92 | return originalId; 93 | }, 94 | getComment: function() { 95 | return comment; 96 | } 97 | }; 98 | } 99 | 100 | module.exports = Original; 101 | -------------------------------------------------------------------------------- /lib/popover.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Popover module 3 | */ 4 | 5 | var locale; 6 | function Popover( translationPair, _locale, glotPress ) { 7 | var form, nodeClass; 8 | locale = _locale; 9 | 10 | if ( translationPair.isFullyTranslated() ) { 11 | form = getOverview( translationPair ); 12 | } else { 13 | form = getInputForm( translationPair ); 14 | } 15 | 16 | nodeClass = 'translator-original-' + translationPair.getOriginal().getId(); 17 | 18 | var getPopoverHtml = function() { 19 | form.attr( 'data-nodes', nodeClass ); 20 | form.data( 'translationPair', translationPair ); 21 | 22 | return form; 23 | }; 24 | 25 | var getPopoverTitle = function() { 26 | return 'Translate to ' + locale.getLanguageName() + ''; 27 | }; 28 | 29 | return { 30 | attachTo: function( enclosingNode ) { 31 | enclosingNode.addClass( nodeClass ).webuiPopover( { 32 | title: getPopoverTitle(), 33 | content: jQuery( "
" ).append( getPopoverHtml() ).html(), 34 | onload: popoverOnload, 35 | translationPair: translationPair 36 | } ); 37 | }, 38 | getTranslationHtml: function() { 39 | form = getInputForm( translationPair ); 40 | return getPopoverHtml(); 41 | } 42 | }; 43 | } 44 | 45 | function popoverOnload( el ) { 46 | jQuery( el ).find( 'textarea' ).eq( 0 ).focus(); 47 | } 48 | 49 | 50 | function getOriginalHtml( translationPair ) { 51 | var originalHtml, 52 | plural = translationPair.getOriginal().getPlural(); 53 | if ( plural ) { 54 | originalHtml = 'Singular: ' + 55 | '
Plural: '; 56 | } else { 57 | originalHtml = ''; 58 | } 59 | 60 | originalHtml = jQuery( '
' + originalHtml ); 61 | originalHtml.find( 'strong.singular' ).text( translationPair.getOriginal().getSingular() ); 62 | 63 | if ( plural ) { 64 | originalHtml.find( 'strong.plural' ).text( plural ); 65 | } 66 | 67 | return originalHtml; 68 | } 69 | 70 | function getInputForm( translationPair ) { 71 | // TODO: add input checking and bail for empty or unexpected values 72 | 73 | var form = getHtmlTemplate( 'new-translation' ).clone(), 74 | original = form.find( 'div.original' ), 75 | pair = form.find( 'div.pair' ), 76 | pairs = form.find( 'div.pairs' ), 77 | item; 78 | 79 | original.html( getOriginalHtml( translationPair ) ); 80 | 81 | if ( translationPair.getContext() ) { 82 | form.find( 'p.context' ).text( translationPair.getContext() ).show(); 83 | } 84 | 85 | if ( translationPair.getOriginal().getComment() ) { 86 | form.find( 'p.comment' ).text( translationPair.getOriginal().getComment() ).show(); 87 | } 88 | 89 | item = translationPair.getTranslation().getTextItems(); 90 | for ( var i = 0; i < item.length; i++ ) { 91 | if ( i > 0 ) { 92 | pair = pair.eq( 0 ).clone(); 93 | } 94 | 95 | pair.find( 'p' ).text( item[ i ].getCaption() ); 96 | pair.find( 'textarea' ).text( item[ i ].getText() ).attr( 'placeholder', 'Could you help us and translate this to ' + locale.getLanguageName() + '? Thanks!' ); 97 | 98 | if ( i > 0 ) { 99 | pairs.append( pair ); 100 | } 101 | } 102 | 103 | return form; 104 | } 105 | 106 | function getOverview( translationPair ) { 107 | // TODO: add input checking and bail for empty or unexpected values 108 | 109 | var form = getHtmlTemplate( 'existing-translation' ).clone(), 110 | original = form.find( 'div.original' ), 111 | pair = form.find( 'div.pair' ), 112 | pairs = form.find( 'div.pairs' ), 113 | item, description; 114 | 115 | original.html( getOriginalHtml( translationPair ) ); 116 | 117 | if ( translationPair.getContext() ) { 118 | form.find( 'p.context' ).text( translationPair.getContext() ).show(); 119 | } 120 | 121 | if ( translationPair.getOriginal().getComment() ) { 122 | form.find( 'p.comment' ).text( translationPair.getOriginal().getComment() ).show(); 123 | } 124 | 125 | item = translationPair.getTranslation().getTextItems(); 126 | for ( var i = 0; i < item.length; i++ ) { 127 | if ( i > 0 ) { 128 | pair = pair.eq( 0 ).clone(); 129 | } 130 | 131 | description = item[ i ].getInfoText(); 132 | if ( description !== '' ) { 133 | pair.find( 'span.type' ).text( description + ': ' ); 134 | } 135 | pair.find( 'span.translation' ).text( item[ i ].getText() ); 136 | if ( i > 0 ) { 137 | pairs.append( pair ); 138 | } 139 | } 140 | 141 | return form; 142 | } 143 | 144 | function getHtmlTemplate( popoverType ) { 145 | switch ( popoverType ) { 146 | case 'existing-translation': 147 | return jQuery( 148 | '
' + 149 | '
' + 150 | '

' + 151 | '

' + 152 | '
' + 153 | '

' + 154 | '
' + 155 | '
' + 156 | '

' + 157 | '' + 158 | '

' + 159 | '
' + 160 | '
' + 161 | '' + 162 | '
' 163 | ); 164 | 165 | case 'new-translation': 166 | return jQuery( 167 | '
' + 168 | '
' + 169 | '

' + 170 | '

' + 171 | '

' + 172 | '
' + 173 | '
' + 174 | '

' + 175 | '' + 176 | '' + 177 | '
' + 178 | '
' + 179 | '' + 180 | '
' 181 | ); 182 | } 183 | } 184 | 185 | module.exports = Popover; 186 | -------------------------------------------------------------------------------- /lib/translation-pair.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TranslationPair module 3 | */ 4 | var Original = require( './original' ), 5 | Translation = require( './translation' ), 6 | Popover = require( './popover' ), 7 | translationData; 8 | 9 | function TranslationPair( locale, original, context, translation ) { 10 | var translations = [], selectedTranslation, glotPressProject, 11 | screenText = false; 12 | 13 | if ( 'object' !== typeof original || original.type !== 'Original' ) { 14 | original = new Original( original ); 15 | } 16 | 17 | if ( 'object' === typeof translation ) { 18 | 19 | if ( translation.type !== 'Translation' ) { 20 | translation = new Translation( locale, translation ); 21 | } 22 | 23 | translations.push( translation ); 24 | } else { 25 | translation = original.getEmptyTranslation( locale ); 26 | } 27 | 28 | selectedTranslation = translation; 29 | 30 | function addTranslation( translation ) { 31 | if ( 'object' !== typeof translation || translation.type !== 'Translation' ) { 32 | translation = new Translation( locale, translation.slice() ); 33 | } 34 | 35 | if ( selectedTranslation.getTextItems().length !== translation.getTextItems().length ) { 36 | // translations have to match the existing number of translation items ( singular = 1, plural = dependent on language ) 37 | return false; 38 | } 39 | 40 | translations.push( translation ); 41 | selectedTranslation = translation; 42 | } 43 | 44 | function loadTranslations( newTranslations ) { 45 | var i, j, t, translation; 46 | 47 | translations = []; 48 | 49 | for ( i = 0; i < newTranslations.length; i++ ) { 50 | translation = []; 51 | for ( j = 0; ( t = newTranslations[ i ][ 'translation_' + j ] ); j++ ) { 52 | translation.push( t ); 53 | } 54 | translation = new Translation( locale, translation.slice(), newTranslations[ i ] ); 55 | addTranslation( translation ); 56 | } 57 | 58 | } 59 | 60 | function sortTranslationsByDate() { 61 | if ( translations.length <= 1 ) { 62 | return; 63 | } 64 | 65 | translations.sort( function( a, b ) { 66 | return b.getComparableDate() - a.getComparableDate(); 67 | } ); 68 | } 69 | 70 | function setSelectedTranslation( currentUserId ) { 71 | 72 | if ( 'number' === typeof currentUserId ) { 73 | currentUserId = currentUserId.toString(); 74 | } 75 | 76 | sortTranslationsByDate(); 77 | 78 | for ( var i = 0; i < translations.length; i++ ) { 79 | if ( translations[ i ].getUserId() === currentUserId && translations[ i ].getStatus() ) { 80 | selectedTranslation = translations[ i ]; 81 | return; 82 | } 83 | 84 | if ( translations[ i ].isCurrent() ) { 85 | selectedTranslation = translations[ i ]; 86 | } 87 | } 88 | } 89 | 90 | function setGlotPressProject( project ) { 91 | return ( glotPressProject = project ); 92 | } 93 | 94 | return { 95 | type: 'TranslationPair', 96 | createPopover: function( enclosingNode, glotPress ) { 97 | var popover = new Popover( this, locale, glotPress ); 98 | popover.attachTo( enclosingNode ); 99 | }, 100 | isFullyTranslated: function() { 101 | return selectedTranslation.isFullyTranslated(); 102 | }, 103 | isTranslationWaiting: function() { 104 | return selectedTranslation.isWaiting(); 105 | }, 106 | getOriginal: function() { 107 | return original; 108 | }, 109 | getContext: function() { 110 | return context; 111 | }, 112 | getLocale: function() { 113 | return locale; 114 | }, 115 | getScreenText: function() { 116 | return screenText; 117 | }, 118 | setScreenText: function( _screenText ) { 119 | screenText = _screenText; 120 | }, 121 | getTranslation: function() { 122 | return selectedTranslation; 123 | }, 124 | getGlotPressProject: function() { 125 | return glotPressProject; 126 | }, 127 | updateAllTranslations: function( newTranslations, currentUserId ) { 128 | if ( ! loadTranslations( newTranslations ) ) { 129 | return false; 130 | } 131 | 132 | if ( 'undefined' === typeof currentUserId ) { 133 | setSelectedTranslation( currentUserId ); 134 | } 135 | }, 136 | serialize: function() { 137 | // the parameters as array 138 | return { 139 | singular: original.getSingular(), 140 | plural: original.getPlural(), 141 | context: context, 142 | translations: selectedTranslation.serialize(), 143 | key: original.generateJsonHash( context ) 144 | }; 145 | }, 146 | fetchOriginalAndTranslations: function( glotPress, currentUserId ) { 147 | var promise, sendContext; 148 | promise = original.fetchIdAndTranslations( glotPress, context ) 149 | .done( function( data ) { 150 | 151 | if ( 'undefined' === typeof data.translations ) { 152 | return; 153 | } 154 | 155 | loadTranslations( data.translations ); 156 | setSelectedTranslation( currentUserId ); 157 | 158 | if ( typeof data.project !== 'undefined' ) { 159 | setGlotPressProject( data.project ); 160 | } 161 | 162 | } ); 163 | return promise; 164 | } 165 | }; 166 | } 167 | 168 | function extractFromDataElement( dataElement ) { 169 | var translationPair, 170 | original = { 171 | singular: dataElement.data( 'singular' ) 172 | }; 173 | 174 | if ( dataElement.data( 'plural' ) ) { 175 | original.plural = dataElement.data( 'plural' ); 176 | } 177 | 178 | if ( dataElement.data( 'context' ) ) { 179 | original.context = dataElement.data( 'context' ); 180 | } 181 | 182 | translationPair = new TranslationPair( translationData.locale, original, original.context ); 183 | translationPair.setScreenText( dataElement.text() ); 184 | 185 | return translationPair; 186 | } 187 | 188 | function trim( text ) { 189 | if ( typeof text === 'undefined' ) { 190 | return ''; 191 | } 192 | return text.replace( /(?:(?:^|\n)\s+|\s+(?:$|\n))/g, '' ); 193 | } 194 | 195 | function extractWithStringsUsedOnPage( enclosingNode ) { 196 | 197 | var text, textWithoutSiblings, enclosingNodeWithoutSiblings, context; 198 | if ( 199 | typeof translationData.stringsUsedOnPage !== 'object' || 200 | // not meant to be translatable: 201 | enclosingNode.is( 'style,script' ) || 202 | enclosingNode.closest( '#querylist' ).length 203 | ) { 204 | return false; 205 | } 206 | 207 | if ( enclosingNode.is( '[data-i18n-context]' ) ) { 208 | context = enclosingNode.data( 'i18n-context' ); 209 | } else { 210 | context = enclosingNode.closest( '[data-i18n-context]' ); 211 | if ( context.length ) { 212 | context = context.data( 'i18n-context' ); 213 | } else { 214 | context = false; 215 | } 216 | } 217 | 218 | translationPair = getTranslationPairForTextUsedOnPage( enclosingNode, context ); 219 | 220 | if ( false === translationPair ) { 221 | // remove adjescent nodes for text that is used without immidiately surrounding tag 222 | enclosingNode = enclosingNode.clone( true ); 223 | textWithoutSiblings = trim( enclosingNode.find( '*' ).remove().end().text() ); 224 | if ( text !== textWithoutSiblings ) { 225 | translationPair = getTranslationPairForTextUsedOnPage( enclosingNode, context ); 226 | } 227 | } 228 | 229 | return translationPair; 230 | } 231 | 232 | function anyChildMatches( node, regex ) { 233 | var i, children; 234 | 235 | if ( typeof regex === 'string' ) { 236 | regex = new RegExp( regex ); 237 | } 238 | 239 | if ( regex instanceof RegExp ) { 240 | children = node.children(); 241 | for ( i = 0; i < children.length; i++ ) { 242 | if ( regex.test( children[ i ].innerHTML ) || 243 | regex.test( children[ i ].textContent ) ) { 244 | return true; 245 | } 246 | } 247 | } 248 | 249 | return false; 250 | } 251 | 252 | function getTranslationPairForTextUsedOnPage( node, context ) { 253 | var original, placeholderRegex, 254 | contexts, 255 | translationPairs, 256 | translationPair, newPlaceholder, 257 | entry = false, 258 | nodeText, nodeHtml; 259 | 260 | nodeText = trim( node.text() ); 261 | 262 | if ( ! nodeText.length || nodeText.length > 3000 ) { 263 | return false; 264 | } 265 | 266 | if ( typeof translationData.stringsUsedOnPage[ nodeText ] !== 'undefined' ) { 267 | entry = translationData.stringsUsedOnPage[ nodeText ]; 268 | 269 | context = entry[ 1 ]; 270 | if ( typeof context !== 'undefined' && context && context.length === 1 ) { 271 | context = context[ 0 ]; 272 | } 273 | translationPair = new TranslationPair( translationData.locale, entry[ 0 ], context ); 274 | translationPair.setScreenText( nodeText ); 275 | 276 | return translationPair; 277 | } 278 | 279 | // html to support translate( 'Translatable Text' ) 280 | nodeHtml = trim( node.html() ); 281 | 282 | for ( i = 0; i < translationData.placeholdersUsedOnPage.length; i++ ) { 283 | entry = translationData.placeholdersUsedOnPage[ i ]; 284 | 285 | if ( entry.regex.test( nodeHtml ) ) { 286 | 287 | // We want the innermost node that matches, so 288 | if ( anyChildMatches( node, entry.regex ) ) { 289 | continue; 290 | } 291 | 292 | translationPair = new TranslationPair( translationData.locale, entry.original, entry.context ); 293 | translationPair.setScreenText( nodeText ); 294 | 295 | return translationPair; 296 | } 297 | } 298 | 299 | return false; 300 | } 301 | 302 | TranslationPair.extractFrom = function( enclosingNode ) { 303 | 304 | if ( typeof translationData !== 'object' ) { 305 | return false; 306 | } 307 | 308 | if ( enclosingNode.is( 'data.translatable' ) ) { 309 | return extractFromDataElement( enclosingNode ); 310 | } 311 | 312 | if ( enclosingNode.closest( 'data.translatable' ).length ) { 313 | return extractFromDataElement( enclosingNode.closest( 'data.translatable' ) ); 314 | } 315 | 316 | return extractWithStringsUsedOnPage( enclosingNode ); 317 | }; 318 | 319 | TranslationPair.setTranslationData = function( newTranslationData ) { 320 | var key, entry, context, 321 | placeholdersUsedOnPage = []; 322 | 323 | translationData = newTranslationData; 324 | 325 | // convert regular expressions to RegExp objects for later use 326 | if ( typeof translationData.placeholdersUsedOnPage === 'object' ) { 327 | for ( key in translationData.placeholdersUsedOnPage ) { 328 | entry = translationData.placeholdersUsedOnPage[ key ]; 329 | 330 | if ( typeof entry.regex === 'undefined' ) { 331 | entry = { 332 | original: entry[ 0 ], 333 | regex: new RegExp( '^\\s*' + entry[ 1 ] + '\\s*$' ), 334 | context: entry[ 2 ] 335 | }; 336 | } 337 | placeholdersUsedOnPage.push( entry ); 338 | } 339 | } 340 | translationData.placeholdersUsedOnPage = placeholdersUsedOnPage; 341 | }; 342 | 343 | TranslationPair._test = { 344 | anyChildMatches: anyChildMatches 345 | }; 346 | 347 | module.exports = TranslationPair; 348 | -------------------------------------------------------------------------------- /lib/translation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Translation module 3 | */ 4 | function Translation( locale, items, glotPressMetadata ) { 5 | var Item, i, status, translationId, userId, dateAdded, 6 | dateAddedUnixTimestamp = 0; 7 | 8 | if ( 'object' === typeof glotPressMetadata ) { 9 | if ( 'undefined' !== glotPressMetadata.status ) { 10 | status = glotPressMetadata.status; 11 | } 12 | if ( 'undefined' !== glotPressMetadata.translation_id ) { 13 | translationId = glotPressMetadata.translation_id; 14 | } 15 | if ( 'undefined' !== glotPressMetadata.user_id ) { 16 | userId = glotPressMetadata.user_id; 17 | } 18 | if ( 'undefined' !== glotPressMetadata.date_added ) { 19 | dateAdded = glotPressMetadata.date_added; 20 | } 21 | } 22 | 23 | if ( 'string' !== typeof status ) { 24 | status = 'current'; 25 | } 26 | 27 | if ( isNaN( translationId ) ) { 28 | translationId = false; 29 | } 30 | 31 | if ( isNaN( userId ) ) { 32 | userId = false; 33 | } 34 | 35 | if ( dateAdded ) { 36 | dateAddedUnixTimestamp = getUnixTimestamp( dateAdded ); 37 | } 38 | 39 | function getUnixTimestamp( mysqlDate ) { 40 | var dateParts = mysqlDate.split( '-' ); 41 | var timeParts = dateParts[ 2 ].substr( 3 ).split( ':' ); 42 | 43 | return new Date( 44 | dateParts[ 0 ], 45 | dateParts[ 1 ] - 1, 46 | dateParts[ 2 ].substr( 0, 2 ), 47 | timeParts[ 0 ], 48 | timeParts[ 1 ], 49 | timeParts[ 2 ] 50 | ); 51 | } 52 | 53 | Item = function( i, text ) { 54 | return { 55 | isTranslated: function() { 56 | return text.length > 0; 57 | }, 58 | getCaption: function() { 59 | var numbers; 60 | 61 | if ( items.length === 1 ) { 62 | return ""; 63 | } 64 | 65 | if ( items.length === 2 ) { 66 | if ( i === 0 ) { 67 | return "Singular"; 68 | } 69 | return "Plural"; 70 | } 71 | 72 | numbers = locale.getNumbersForIndex( i ); 73 | 74 | if ( numbers.length ) { 75 | return "For numbers like: " + numbers.join( ", " ); 76 | } 77 | 78 | return ""; 79 | }, 80 | getInfoText: function() { 81 | var numbers; 82 | 83 | if ( items.length === 1 ) { 84 | return ""; 85 | } 86 | 87 | if ( items.length === 2 ) { 88 | if ( i === 0 ) { 89 | return "Singular"; 90 | } 91 | return "Plural"; 92 | } 93 | 94 | numbers = locale.getNumbersForIndex( i ); 95 | 96 | if ( numbers.length ) { 97 | return numbers.join( ", " ); 98 | } 99 | 100 | return ""; 101 | }, 102 | getText: function() { 103 | return text; 104 | } 105 | }; 106 | }; 107 | 108 | if ( 'object' !== typeof items || 'number' !== typeof items.length ) { 109 | return false; 110 | } 111 | 112 | for ( i = 0; i < items.length; i++ ) { 113 | items[ i ] = new Item( i, items[ i ] ); 114 | } 115 | 116 | return { 117 | type: 'Translation', 118 | isFullyTranslated: function() { 119 | var i; 120 | 121 | for ( i = 0; i < items.length; i++ ) { 122 | if ( false === items[ i ].isTranslated() ) { 123 | return false; 124 | } 125 | } 126 | return true; 127 | }, 128 | isCurrent: function() { 129 | return 'current' === status; 130 | }, 131 | isWaiting: function() { 132 | return 'waiting' === status; 133 | }, 134 | getStatus: function() { 135 | return status; 136 | }, 137 | getDate: function() { 138 | return dateAdded; 139 | }, 140 | getComparableDate: function() { 141 | return dateAddedUnixTimestamp; 142 | }, 143 | getUserId: function() { 144 | return userId; 145 | }, 146 | getTextItems: function() { 147 | return items; 148 | }, 149 | serialize: function() { 150 | var i, 151 | serializedItems = []; 152 | 153 | for ( i = 0; i < items.length; i++ ) { 154 | serializedItems.push( items[ i ].getText() ); 155 | } 156 | return serializedItems; 157 | } 158 | }; 159 | } 160 | 161 | module.exports = Translation; 162 | -------------------------------------------------------------------------------- /lib/walker.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function( TranslationPair, jQuery, document ) { 3 | return { 4 | walkTextNodes: function( origin, callback, finishedCallback ) { 5 | var node, walker; 6 | 7 | if ( typeof document === 'object' ) { 8 | walker = document.createTreeWalker( origin, NodeFilter.SHOW_TEXT, null, false ); 9 | 10 | while ( ( node = walker.nextNode() ) ) { 11 | walk( node ); 12 | } 13 | } else { 14 | jQuery( origin ).find( '*' ).contents().filter( function() { 15 | return this.nodeType === 3; // Node.TEXT_NODE 16 | } ).each( function() { 17 | walk( this ); 18 | } ); 19 | } 20 | 21 | if ( typeof finishedCallback === 'function' ) { 22 | finishedCallback(); 23 | } 24 | 25 | function walk( textNode ) { 26 | var translationPair, 27 | enclosingNode = jQuery( textNode.parentNode ); 28 | 29 | if ( 30 | enclosingNode.is( 'script' ) || 31 | enclosingNode.hasClass( 'translator-checked' ) 32 | ) { 33 | return false; 34 | } 35 | 36 | enclosingNode.addClass( 'translator-checked' ); 37 | 38 | if ( 39 | enclosingNode.closest( '.webui-popover' ).length 40 | ) { 41 | return false; 42 | } 43 | 44 | translationPair = TranslationPair.extractFrom( enclosingNode ); 45 | 46 | if ( false === translationPair ) { 47 | enclosingNode.addClass( 'translator-dont-translate' ); 48 | return false; 49 | } 50 | 51 | if ( typeof callback === 'function' ) { 52 | callback( translationPair, enclosingNode ); 53 | } 54 | 55 | return true; 56 | } 57 | } 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "community-translator", 3 | "version": "0.0.1", 4 | "description": "An in-Page translation tool.", 5 | "author": { 6 | "name": "Alex Kirk", 7 | "email": "alex.kirk@automattic.com" 8 | }, 9 | "keywords": [], 10 | "dependencies": { 11 | "JSON2": "*", 12 | "debug": ">=2.6.9", 13 | "jed": "*" 14 | }, 15 | "devDependencies": { 16 | "better-assert": "*", 17 | "browserify": "*", 18 | "chai": "^2.1.2", 19 | "jquery": "^3.3.1", 20 | "jsdom": "^11.6.2", 21 | "mocha": "*", 22 | "uglify-js": "*", 23 | "uglifyify": "*" 24 | }, 25 | "license": "GPL-2.0+", 26 | "scripts": { 27 | "test": "make test" 28 | }, 29 | "readme": "", 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/Automattic/community-translator.git" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/batcher.js: -------------------------------------------------------------------------------- 1 | var betterAssert = require( 'better-assert' ), 2 | assert = require( 'assert' ), 3 | Locale = require( '../lib/locale' ), 4 | TranslationPair = require( '../lib/translation-pair' ), 5 | html = '', 6 | jsdom = require( 'jsdom' ); 7 | 8 | 9 | // Set up jQuery related global parameters 10 | var dom = new jsdom.JSDOM( html, { 11 | resources: 'usable', 12 | runScripts: 'dangerously', 13 | } ); 14 | var jquery = require( 'jquery' )( dom.window ); 15 | global.window = dom.window; 16 | global.document = dom.window.document; 17 | global.jQuery = global.$ = jquery; 18 | 19 | // Module level vars that depend on jQuery 20 | var batcher = require( '../lib/batcher.js' ), 21 | batchedTestFunctionFoo = batcher( testFunctionFoo ); 22 | 23 | function testFunctionFoo( arrayArg, callback ) { 24 | return ( arrayArg.map( function( v ) { 25 | return v + 'foo'; 26 | } ) ); 27 | } 28 | 29 | describe( 'Batcher', function() { 30 | // Sanity check. 31 | it( 'depends on jQuery.Deferred', function() { 32 | assert( jQuery ); 33 | assert( jQuery.Deferred ); 34 | } ); 35 | 36 | it( 'returns the same value for a single call', 37 | function() { 38 | 39 | batchedResult = batchedTestFunctionFoo( '1' ); 40 | 41 | testFunctionFoo( [ '1' ], 42 | function( originalResult ) { 43 | jQuery.when() 44 | .then( function( result ) { 45 | assert.deepEqual( originalResult[0], result ); 46 | } ); 47 | } ); 48 | } ); 49 | 50 | it( 'returns the same values with multiple calls', 51 | function() { 52 | jQuery.when( batchedTestFunctionFoo( 'a' ), 53 | batchedTestFunctionFoo( 'b' ), 54 | batchedTestFunctionFoo( 'c' ), 55 | testFunctionFoo( [ 'a', 'b', 'c' ] ) ) 56 | .then( function( a, b, c, originalFunctionResult ) { 57 | assert.deepEqual( [ a, b, c ], originalFunctionResult ); 58 | } ); 59 | } ); 60 | 61 | it( 'fails if given a non-function', function() { 62 | assert( ! batcher( "foo" ) ); 63 | } ); 64 | 65 | it( 'returns a function when given a function', function() { 66 | assert.equal( 'function', typeof batcher( function() {} ) ); 67 | } ); 68 | 69 | it( 'produces a function that returns jQuery.Deferred objects', 70 | function() { 71 | // Quick and dirty test for jQuery().Deferred 72 | var isWhenable = function( object ) { 73 | return object && object.then && ( 74 | 'function' === typeof object.then ); 75 | }; 76 | 77 | var result = batchedTestFunctionFoo( "test" ); 78 | 79 | assert( isWhenable( batchedTestFunctionFoo( "test" ) ) ); 80 | } ); 81 | 82 | it( 'calls the original function only once', 83 | function() { 84 | var counter = 0, 85 | batchedCountingFunction = batcher( countingFunction ); 86 | 87 | function identity( v ) { 88 | return v; 89 | } 90 | 91 | function countingFunction( arrayArg, callback ) { 92 | counter++; 93 | return arrayArg.map( identity ); 94 | } 95 | 96 | assert.equal( counter, 0 ); 97 | jQuery.when( batchedCountingFunction( 'a' ), 98 | batchedCountingFunction( 'b' ), 99 | batchedCountingFunction( 'c' ) ) 100 | .then( function() { 101 | assert.equal( counter, 1 ); 102 | } ); 103 | } ); 104 | } ); 105 | -------------------------------------------------------------------------------- /test/de.js: -------------------------------------------------------------------------------- 1 | var assert = require('better-assert'), 2 | Locale = require('../lib/locale'); 3 | TranslationPair = require('../lib/translation-pair'); 4 | 5 | describe('German', function () { 6 | var locale = new Locale( 'de', 'German', 'nplurals=2; plural=n != 1;'); 7 | var translationPair = new TranslationPair( locale, ['%(numberOfThings) thing', '%(numberOfThings) things'] ); 8 | 9 | it( 'should have 2 plurals', function () { 10 | assert( 2 === translationPair.getTranslation().getTextItems().length); 11 | }); 12 | 13 | translationPair.updateAllTranslations( [ [ '1 things', '2 things'] ] ); 14 | it( 'should accept a new translation', function () { 15 | assert( 2 === translationPair.getTranslation().getTextItems().length); 16 | }); 17 | 18 | translationPair.updateAllTranslations( [ [ '1 thing', '2 things', '3 things' ] ] ); 19 | it( 'should not accept a new translation with wrong number of plurals', function () { 20 | assert( 2 === translationPair.getTranslation().getTextItems().length); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /test/glotpress.js: -------------------------------------------------------------------------------- 1 | var assert = require( 'chai' ).assert, 2 | GlotPress = require( '../lib/glotpress' ); 3 | 4 | describe( 'GlotPress', function() { 5 | 6 | var GlotPressInstance; 7 | 8 | describe( 'getPermalink', function() { 9 | var glotPressProject = false, 10 | translationPairMock = { 11 | getGlotPressProject: function() { 12 | return glotPressProject; 13 | }, 14 | getOriginal: function() { 15 | return { 16 | getId: function() { 17 | return 123; 18 | } 19 | }; 20 | }, 21 | }; 22 | 23 | before( function( done ) { 24 | GlotPressInstance = new GlotPress( { 25 | getLocaleCode: function() { 26 | return 'en'; 27 | } 28 | } ); 29 | done(); 30 | } ); 31 | 32 | it( 'should return the correct default permalink', function() { 33 | GlotPressInstance.loadSettings( { 34 | url: 'https://translate.wordpress.com', 35 | project: 'test', 36 | } ); 37 | assert.equal( 38 | GlotPressInstance.getPermalink( translationPairMock ), 39 | 'https://translate.wordpress.com/projects/test/en/default?filters[original_id]=123' 40 | ); 41 | } ); 42 | 43 | it( 'should return the correct permalink with translation set slug', function() { 44 | GlotPressInstance.loadSettings( { 45 | url: 'https://translate.wordpress.com', 46 | project: 'test', 47 | translation_set_slug: 'formal', 48 | } ); 49 | assert.equal( 50 | GlotPressInstance.getPermalink( translationPairMock ), 51 | 'https://translate.wordpress.com/projects/test/en/formal?filters[original_id]=123' 52 | ); 53 | } ); 54 | it( 'should return the correct permalink with value from getGlotPressProject()', function() { 55 | GlotPressInstance.loadSettings( { 56 | url: 'https://translate.wordpress.com', 57 | project: 'test', 58 | translation_set_slug: 'formal', 59 | } ); 60 | glotPressProject = 'wpcom'; 61 | assert.equal( 62 | GlotPressInstance.getPermalink( translationPairMock ), 63 | 'https://translate.wordpress.com/projects/wpcom/en/formal?filters[original_id]=123' 64 | ); 65 | } ); 66 | } ); 67 | 68 | } ); -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | console.log( 'Running unit tests' ); 2 | 3 | require( "./glotpress" ); 4 | require( "./translation-pair" ); 5 | require( "./string-extraction/translation-pair" ); 6 | require( "./de" ); 7 | require( "./ru" ); 8 | require( "./jp" ); 9 | require( "./batcher.js" ); 10 | -------------------------------------------------------------------------------- /test/jp.js: -------------------------------------------------------------------------------- 1 | var assert = require('better-assert'), 2 | Locale = require('../lib/locale'); 3 | TranslationPair = require('../lib/translation-pair'); 4 | 5 | describe('Japanese', function () { 6 | var locale = new Locale( 'jp', 'Japanese', 'nplurals=1; plural=0;'); 7 | var translationPair = new TranslationPair( locale, ['%(numberOfThings) thing', null, '%(numberOfThings) things'] ); 8 | 9 | it( 'should have 1 plural', function () { 10 | assert( 1 === translationPair.getTranslation().getTextItems().length); 11 | }); 12 | 13 | translationPair.updateAllTranslations( [ ['things'] ] ); 14 | it( 'should accept a new translation', function () { 15 | assert( 1 === translationPair.getTranslation().getTextItems().length); 16 | }); 17 | 18 | translationPair.updateAllTranslations( [ ['1 thing', '2 things'] ] ); 19 | it( 'should not accept a new translation with wrong number of plurals', function () { 20 | assert( 1 === translationPair.getTranslation().getTextItems().length); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /test/ru.js: -------------------------------------------------------------------------------- 1 | var assert = require('better-assert'), 2 | Locale = require('../lib/locale'); 3 | TranslationPair = require('../lib/translation-pair'); 4 | 5 | describe('Russian', function () { 6 | var locale = new Locale( 'ru', 'Russian', 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);'); 7 | var translationPair = new TranslationPair( locale, ['%(numberOfThings) thing', '%(numberOfThings) things'] ); 8 | 9 | it( 'should have 3 plurals', function () { 10 | assert( 3 === translationPair.getTranslation().getTextItems().length); 11 | }); 12 | 13 | translationPair.updateAllTranslations( [ [ '1 things', '2 things', 'a few things' ] ] ); 14 | it( 'should accept a new translation', function () { 15 | assert( 3 === translationPair.getTranslation().getTextItems().length); 16 | }); 17 | 18 | translationPair.updateAllTranslations( [ [ '1 thing', '2 things' ] ] ); 19 | it( 'should not accept a new translation with wrong number of plurals', function () { 20 | assert( 3 === translationPair.getTranslation().getTextItems().length); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /test/string-extraction/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

5 | Akismet blocks spam from getting to your blog.
6 | There’s nothing in your spam queue at the moment. 7 |

8 |

Espacio de almacenamiento

9 |
10 | 18 |
19 |
20 |
46 | 47 |
48 | 49 | 50 | 53 | 54 |

השתיקה היא מה שנשאר אחרי הפחד

55 |
56 | 57 |
58 |

On Saturday 21st March, I spoke at WordCamp London about building themes with the upcoming, core WordPress REST API (WP-API).

59 |
60 | 61 |
76 | 77 | 78 | -------------------------------------------------------------------------------- /test/string-extraction/translation-data/en-uk.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stringsUsedOnPage": { 3 | "Leave a Reply": [ "Leave a Reply", null ], 4 | "Tags": [ "Tags", "taxonomy general name" ], 5 | "taxonomy general name\\u0004Tags": [ "Tags", "taxonomy general name" ], 6 | "Categories": [ "Categories", "taxonomy general name" ], 7 | "taxonomy general name\\u0004Categories": [ "Categories", "taxonomy general name" ], 8 | "Tag": [ "Tag", "taxonomy singular name" ], 9 | "taxonomy singular name\\u0004Tag": [ "Tag", "taxonomy singular name" ], 10 | "Category": [ "Category", "taxonomy singular name" ], 11 | "taxonomy singular name\\u0004Category": [ "Category", "taxonomy singular name" ], 12 | "Search Tags": [ "Search Tags", null ], 13 | "Search Categories": [ "Search Categories", null ], 14 | "Popular Tags": [ "Popular Tags", null ], 15 | "All Tags": [ "All Tags", null ], 16 | "All Categories": [ "All Categories", null ], 17 | "Parent Category": [ "Parent Category", null ], 18 | "Parent Category:": [ "Parent Category:", null ], 19 | "Edit Tag": [ "Edit Tag", null ], 20 | "Edit Category": [ "Edit Category", null ], 21 | "View Tag": [ "View Tag", null ], 22 | "View Category": [ "View Category", null ], 23 | "Update Tag": [ "Update Tag", null ], 24 | "Update Category": [ "Update Category", null ], 25 | "Add New Tag": [ "Add New Tag", null ], 26 | "Add New Category": [ "Add New Category", null ], 27 | "New Tag Name": [ "New Tag Name", null ], 28 | "New Category Name": [ "New Category Name", null ], 29 | "Separate tags with commas": [ "Separate tags with commas", null ], 30 | "Add or remove tags": [ "Add or remove tags", null ], 31 | "Choose from the most used tags": [ "Choose from the most used tags", null ], 32 | "No tags found.": [ "No tags found.", null ], 33 | "No categories found.": [ "No categories found.", null ], 34 | "Background": [ "Background", null ], 35 | "Headings": [ "Headings", null ], 36 | "Links": [ "Links", null ], 37 | "Accent #1": [ "Accent #1", null ], 38 | "Accent #2": [ "Accent #2", null ], 39 | "Header and Sidebar Background Color": [ "Header and Sidebar Background Color", null ], 40 | "Header and Sidebar Link Color": [ "Header and Sidebar Link Color", null ], 41 | "Header and Sidebar Text Color": [ "Header and Sidebar Text Color", null ], 42 | "Yellow": [ "Yellow", null ], 43 | "Pink": [ "Pink", null ], 44 | "Purple": [ "Purple", null ], 45 | "Blue": [ "Blue", null ], 46 | "Dark": [ "Dark", null ], 47 | "Feedback": [ "Feedback", null ], 48 | "Search Feedback": [ "Search Feedback", null ], 49 | "No feedback found": [ "No feedback found", null ], 50 | "Posts": [ "Posts", "post type general name" ], 51 | "post type general name\\u0004Posts": [ "Posts", "post type general name" ], 52 | "Pages": [ "Pages", "post type general name" ], 53 | "post type general name\\u0004Pages": [ "Pages", "post type general name" ], 54 | "Post": [ "Post", "post type singular name" ], 55 | "post type singular name\\u0004Post": [ "Post", "post type singular name" ], 56 | "Page": [ "Page", "post type singular name" ], 57 | "post type singular name\\u0004Page": [ "Page", "post type singular name" ], 58 | "Add New": [ "Add New", "post" ], 59 | "post\\u0004Add New": [ "Add New", "post" ], 60 | "Add a New Post": [ "Add New Post", null ], 61 | "Add New Page": [ "Add New Page", null ], 62 | "Edit Post": [ "Edit Post", null ], 63 | "Edit Page": [ "Edit Page", null ], 64 | "New Post": [ "New Post", null ], 65 | "New Page": [ "New Page", null ], 66 | "View Post": [ "View Post", null ], 67 | "View Page": [ "View Page", null ], 68 | "Search Posts": [ "Search Posts", null ], 69 | "Search Pages": [ "Search Pages", null ], 70 | "No posts found.": [ "No posts found.", null ], 71 | "No pages found.": [ "No pages found.", null ], 72 | "No posts found in Bin.": [ "No posts found in Trash.", null ], 73 | "No pages found in Bin.": [ "No pages found in Trash.", null ], 74 | "Parent Page:": [ "Parent Page:", null ], 75 | "All Posts": [ "All Posts", null ], 76 | "All Pages": [ "All Pages", null ], 77 | "Custom Themes": [ "Custom Themes", null ], 78 | "Custom Theme": [ "Custom Theme", null ], 79 | "All Custom Themes": [ "All Custom Themes", null ], 80 | "Create New Theme": [ "Create New Theme", null ], 81 | "Edit Custom Theme": [ "Edit Custom Theme", null ], 82 | "New Theme": [ "New Theme", null ], 83 | "View Theme": [ "View Theme", null ], 84 | "Custom Themes Archive": [ "Custom Themes Archive", null ], 85 | "Search Custom Themes": [ "Search Custom Themes", null ], 86 | "No Custom Themes found": [ "No Custom Themes found", null ], 87 | "No Custom Themes found in trash": [ "No Custom Themes found in trash", null ], 88 | "Custom Theme:": [ "Custom Theme:", null ], 89 | "Customised Themes": [ "Customized Themes", null ], 90 | "Customised Theme": [ "Customized Theme", null ], 91 | "Cust’d Themes": [ "Cust’d Themes", null ], 92 | "All Customised Themes": [ "All Customized Themes", null ], 93 | "Edit Customised Theme": [ "Edit Customized Theme", null ], 94 | "Customised Themes Archive": [ "Customized Themes Archive", null ], 95 | "Search Customised Themes": [ "Search Customized Themes", null ], 96 | "No Customised Themes found": [ "No Customized Themes found", null ], 97 | "No Customised Themes found in trash": [ "No Customized Themes found in trash", null ], 98 | "Customised Theme:": [ "Customized Theme:", null ], 99 | "No posts to show.": [ "No posts to show.", null ], 100 | "“": [ "“", "opening curly double quote" ], 101 | "opening curly double quote\\u0004“": [ "“", "opening curly double quote" ], 102 | "”": [ "”", "closing curly double quote" ], 103 | "closing curly double quote\\u0004”": [ "”", "closing curly double quote" ], 104 | "’": [ "’", "apostrophe" ], 105 | "apostrophe\\u0004’": [ "’", "apostrophe" ], 106 | "′": [ "′", "prime" ], 107 | "prime\\u0004′": [ "′", "prime" ], 108 | "″": [ "″", "double prime" ], 109 | "double prime\\u0004″": [ "″", "double prime" ], 110 | "‘": [ "‘", "opening curly single quote" ], 111 | "opening curly single quote\\u0004‘": [ "‘", "opening curly single quote" ], 112 | "–": [ "–", "en dash" ], 113 | "en dash\\u0004–": [ "–", "en dash" ], 114 | "—": [ "—", "em dash" ], 115 | "em dash\\u0004—": [ "—", "em dash" ], 116 | "on": [ "on", "Noto Sans font: on or off" ], 117 | "Noto Sans font: on or off\\u0004on": [ "on", "Noto Sans font: on or off" ], 118 | "no-subset": [ "no-subset", "Add new subset (greek, cyrillic, devanagari, vietnamese)" ], 119 | "Add new subset (greek, cyrillic, devanagari, vietnamese)\\u0004no-subset": [ "no-subset", "Add new subset (greek, cyrillic, devanagari, vietnamese)" ], 120 | "expand child menu": [ "expand child menu", null ], 121 | "collapse child menu": [ "collapse child menu", null ], 122 | "Report this content": [ "Report this content", null ], 123 | "Share this:": [ "Share this:", null ], 124 | "»": [ "»", "feed link" ], 125 | "feed link\\u0004»": [ "»", "feed link" ], 126 | "about": [ "about", "Default page slug" ], 127 | "Default page slug\\u0004about": [ "about", "Default page slug" ], 128 | "Subscribe": [ "Subscribe", null ], 129 | "WordPress.com Support": [ "WordPress.com Support", null ], 130 | "WordPress.com Forums": [ "WordPress.com Forums", null ], 131 | "Skip to content": [ "Skip to content", null ], 132 | "Menu and widgets": [ "Menu and widgets", null ], 133 | "Custom": [ "Custom", null ], 134 | "…": [ "…", null ], 135 | "words": [ "words", "word count: words or characters?" ], 136 | "word count: words or characters?\\u0004words": [ "words", "word count: words or characters?" ], 137 | "Pages:": [ "Pages:", null ], 138 | "Next page": [ "Next page", null ], 139 | "Previous page": [ "Previous page", null ], 140 | "Format": [ "Format", "Used before post format." ], 141 | "Used before post format.\\u0004Format": [ "Format", "Used before post format." ], 142 | "Standard": [ "Standard", "Post format" ], 143 | "Post format\\u0004Standard": [ "Standard", "Post format" ], 144 | "Aside": [ "Aside", "Post format" ], 145 | "Post format\\u0004Aside": [ "Aside", "Post format" ], 146 | "Chat": [ "Chat", "Post format" ], 147 | "Post format\\u0004Chat": [ "Chat", "Post format" ], 148 | "Gallery": [ "Gallery", "Post format" ], 149 | "Post format\\u0004Gallery": [ "Gallery", "Post format" ], 150 | "Link": [ "Link", "Post format" ], 151 | "Post format\\u0004Link": [ "Link", "Post format" ], 152 | "Image": [ "Image", "Post format" ], 153 | "Post format\\u0004Image": [ "Image", "Post format" ], 154 | "Quote": [ "Quote", "Post format" ], 155 | "Post format\\u0004Quote": [ "Quote", "Post format" ], 156 | "Status": [ "Status", "Post format" ], 157 | "Post format\\u0004Status": [ "Status", "Post format" ], 158 | "Video": [ "Video", "Post format" ], 159 | "Post format\\u0004Video": [ "Video", "Post format" ], 160 | "Audio": [ "Audio", "Post format" ], 161 | "Post format\\u0004Audio": [ "Audio", "Post format" ], 162 | "Posted on": [ "Posted on", "Used before publish date." ], 163 | "Used before publish date.\\u0004Posted on": [ "Posted on", "Used before publish date." ], 164 | "Leave a comment": [ "Leave a comment", null ], 165 | "1 Comment": [ "1 Comment", null ], 166 | "% Comments": [ "% Comments", null ], 167 | "Comments Off": [ "Comments Off", null ], 168 | "Edit": [ "Edit", null ], 169 | "Previous": [ "Previous", "previous post" ], 170 | "previous post\\u0004Previous": [ "Previous", "previous post" ], 171 | "Next": [ "Next", "next post" ], 172 | "next post\\u0004Next": [ "Next", "next post" ], 173 | "Posts navigation": [ "Posts navigation", null ], 174 | "« Previous": [ "« Previous", null ], 175 | "Next »": [ "Next »", null ], 176 | "https://wordpress.org/": [ "https://wordpress.org/", null ], 177 | "Older posts": [ "Older posts", null ], 178 | "Scroll back to top": [ "Scroll back to top", null ], 179 | "Comment": [ "Comment", null ], 180 | "Post Comment": [ "Post Comment", null ], 181 | "Write a Comment...": [ "Write a Comment...", null ], 182 | "Loading Comments...": [ "Loading Comments...", null ], 183 | "Please be sure to submit some text with your comment.": [ "Please be sure to submit some text with your comment.", null ], 184 | "Please provide an email address to comment.": [ "Please provide an email address to comment.", null ], 185 | "Please provide your name to comment.": [ "Please provide your name to comment.", null ], 186 | "Sorry, but there was an error posting your comment. Please try again later.": [ "Sorry, but there was an error posting your comment. Please try again later.", null ], 187 | "Your comment was approved.": [ "Your comment was approved.", null ], 188 | "Your comment is in moderation.": [ "Your comment is in moderation.", null ], 189 | "Camera": [ "Camera", null ], 190 | "Aperture": [ "Aperture", null ], 191 | "Shutter Speed": [ "Shutter Speed", null ], 192 | "Focal Length": [ "Focal Length", null ], 193 | "Reblog": [ "Reblog", null ], 194 | "Reblogged": [ "Reblogged", null ], 195 | "Add your thoughts here... (optional)": [ "Add your thoughts here... (optional)", null ], 196 | "Reblogging...": [ "Reblogging...", null ], 197 | "Post Reblog": [ "Post Reblog", null ], 198 | "Follow": [ "Follow", null ], 199 | "Unfollow": [ "Unfollow", null ], 200 | "Following": [ "Following", null ], 201 | "Edit the excerpt as needed.": [ "Edit the excerpt as needed.", null ], 202 | "Pick an image to be used as a featured image.": [ "Pick an image to be used as a featured image.", null ], 203 | "Logging In…": [ "Logging In…", null ], 204 | "Posting Comment…": [ "Posting Comment…", null ], 205 | "Log Out": [ "Log Out", null ], 206 | "Log In": [ "Log In", null ], 207 | "Please enter a comment": [ "Please enter a comment", null ], 208 | "Please enter your email address here": [ "Please enter your email address here", null ], 209 | "Invalid email address": [ "Invalid email address", null ], 210 | "Please enter your name here": [ "Please enter your name here", null ], 211 | "This picture will show whenever you leave a comment. Click to customise it.": [ "This picture will show whenever you leave a comment. Click to customize it.", null ], 212 | "Log in to use details from one of these accounts.": [ "Log in to use details from one of these accounts.", null ], 213 | "Change": [ "Change", null ], 214 | "Change Account": [ "Change Account", null ], 215 | "Blog at WordPress.com": [ "Blog at WordPress.com", null ], 216 | "Theme": [ "Theme", null ], 217 | "by": [ "by", null ], 218 | "Learn more about this theme": [ "Learn more about this theme", null ], 219 | "Post to": [ "Post to", null ], 220 | "Cancel": [ "Cancel", null ], 221 | "Reblog Post": [ "Reblog Post", null ], 222 | "Create a free website or blog at WordPress.com": [ "Create a free website or blog at WordPress.com", null ], 223 | "Domingo": [ "Sunday", null ], 224 | "Lunes": [ "Monday", null ], 225 | "Martes": [ "Tuesday", null ], 226 | "Miércoles": [ "Wednesday", null ], 227 | "Jueves": [ "Thursday", null ], 228 | "Viernes": [ "Friday", null ], 229 | "Sábado": [ "Saturday", null ], 230 | "D": [ "S_Sunday_initial", null ], 231 | "L": [ "M_Monday_initial", null ], 232 | "M": [ "T_Tuesday_initial", null ], 233 | "X": [ "W_Wednesday_initial", null ], 234 | "J": [ "T_Thursday_initial", null ], 235 | "V": [ "F_Friday_initial", null ], 236 | "S": [ "S_Saturday_initial", null ], 237 | "Dom": [ "Sun", null ], 238 | "Lun": [ "Mon", null ], 239 | "Mar": [ "Tue", null ], 240 | "Mie": [ "Wed", null ], 241 | "Jue": [ "Thu", null ], 242 | "Vie": [ "Fri", null ], 243 | "Sab": [ "Sat", null ], 244 | "enero": [ "January", null ], 245 | "febrero": [ "February", null ], 246 | "marzo": [ "March", null ], 247 | "abril": [ "April", null ], 248 | "mayo": [ "May", null ], 249 | "junio": [ "June", null ], 250 | "julio": [ "July", null ], 251 | "agosto": [ "August", null ], 252 | "septiembre": [ "September", null ], 253 | "octubre": [ "October", null ], 254 | "noviembre": [ "November", null ], 255 | "diciembre": [ "December", null ], 256 | "ene": [ "Jan_January_abbreviation", null ], 257 | "feb": [ "Feb_February_abbreviation", null ], 258 | "mar": [ "Mar_March_abbreviation", null ], 259 | "abr": [ "Apr_April_abbreviation", null ], 260 | "may": [ "May_May_abbreviation", null ], 261 | "jun": [ "Jun_June_abbreviation", null ], 262 | "jul": [ "Jul_July_abbreviation", null ], 263 | "ago": [ "Aug_August_abbreviation", null ], 264 | "sep": [ "Sep_September_abbreviation", null ], 265 | "oct": [ "Oct_October_abbreviation", null ], 266 | "nov": [ "Nov_November_abbreviation", null ], 267 | "dic": [ "Dec_December_abbreviation", null ], 268 | "am": [ "am", null ], 269 | "pm": [ "pm", null ], 270 | "AM": [ "AM", null ], 271 | "PM": [ "PM", null ], 272 | ".": [ "number_format_thousands_sep", null ], 273 | ",": [ "number_format_decimal_point", null ], 274 | "ltr": [ "ltr", "text direction" ], 275 | "text direction\\u0004ltr": [ "ltr", "text direction" ], 276 | "Mis sitios": [ "My Sites", null ], 277 | "Cambiar sitio": [ "Switch Site", null ], 278 | "Ver sitio": [ "View Site", null ], 279 | "WP Admin": [ "WP Admin", null ], 280 | "Estadísticas": [ "Stats", null ], 281 | "Comentarios": [ "Comments", null ], 282 | "Entradas del blog": [ "Blog Posts", null ], 283 | "Publicar": [ "Publish", "admin bar menu group label" ], 284 | "admin bar menu group label\\u0004Publicar": [ "Publish", "admin bar menu group label" ], 285 | "Añadir": [ "Add", "admin bar menu new item label" ], 286 | "admin bar menu new item label\\u0004Añadir": [ "Add", "admin bar menu new item label" ], 287 | "Aspecto": [ "Look and Feel", "admin bar menu group label" ], 288 | "admin bar menu group label\\u0004Aspecto": [ "Look and Feel", "admin bar menu group label" ], 289 | "Temas": [ "Themes", null ], 290 | "Personalizar": [ "Customize", null ], 291 | "Menús": [ "Menus", null ], 292 | "Configuración": [ "Configuration", null ], 293 | "Compartir": [ "Sharing", null ], 294 | "Usuarios": [ "Users", null ], 295 | "Mejoras": [ "Upgrades", null ], 296 | "Enlace corto": [ "Shortlink", null ], 297 | "Lector": [ "Reader", null ], 298 | "Blogs que sigo": [ "Blogs I Follow", null ], 299 | "Descubrir": [ "Discover", "admin bar menu group label" ], 300 | "admin bar menu group label\\u0004Descubrir": [ "Discover", "admin bar menu group label" ], 301 | "Más recientes": [ "Freshly Pressed", null ], 302 | "Blogs recomendados": [ "Recommended Blogs", null ], 303 | "Buscar amigos": [ "Find Friends", null ], 304 | "Mi Actividad": [ "My Activity", null ], 305 | "Mis Comentarios": [ "My Comments", null ], 306 | "Mis me gusta": [ "My Likes", null ], 307 | "Seguir": [ "Follow", null ], 308 | "Mostrar las visitas del sitio por hora para las últimas 48 horas. Haz clic en todas las estadísticas del sitio.": [ "Showing site views per hour for the last 48 hours. Click for full Site Stats.", null ], 309 | "Denunciar este contenido": [ "Report this content", null ], 310 | "Cerrar sesión": [ "Sign Out", null ], 311 | "Perfil": [ "Profile", null ], 312 | "Configuración de la cuenta": [ "Account Settings", null ], 313 | "Colección de trofeos": [ "Trophy Case", null ], 314 | "Facturación": [ "Billing", null ], 315 | "Añadidos": [ "Extras", null ], 316 | "Ayuda": [ "Help", null ], 317 | "Este blog es de acceso público": [ "This blog is public", null ], 318 | "Blog Links": [ "Blog Links", null ], 319 | "Not currently in the User Showcase": [ "Not currently in the User Showcase", null ], 320 | "Go to Showcase Guidelines": [ "Go to Showcase Guidelines", null ], 321 | "Go to Theme Credits": [ "Go to Theme Credits", null ], 322 | "WordPress.com Showcase": [ "WordPress.com Showcase", null ], 323 | "Email Blog Owner": [ "Email Blog Owner", null ], 324 | "El ID de menú, no puede estar vacio.": [ "The menu ID should not be empty.", null ], 325 | "Blog Id: 8729814": [ "Blog Id: 8729814", null ], 326 | "Blog Dashboard": [ "Blog Dashboard", null ], 327 | "Estadísticas del sitio": [ "Site Stats", null ], 328 | "Entradas": [ "Posts", null ], 329 | "Mis mejoras": [ "My Upgrades", null ], 330 | "Dominios": [ "Domains", null ], 331 | "All Blog Options": [ "All Blog Options", null ], 332 | "Blog's Network Admin": [ "Blog's Network Admin", null ], 333 | "Blog Info": [ "Blog Info", null ], 334 | "Blog Users": [ "Blog Users", null ], 335 | "Blog Themes": [ "Blog Themes", null ], 336 | "Blog Upgrades": [ "Blog Upgrades", null ], 337 | "User's Network Admin": [ "User's Network Admin", null ], 338 | "User's Store Admin": [ "User's Store Admin", null ], 339 | "Load Page Without Cache": [ "Load Page Without Cache", null ], 340 | "Clear Page's Cache": [ "Clear Page's Cache", null ], 341 | "Posts Per Page = 1": [ "Posts Per Page = 1", null ], 342 | "Desactivar Scroll infinito": [ "Disable Infinite Scroll", null ], 343 | "Debug": [ "Debug", null ], 344 | "Ocultar anuncios": [ "Hide adverts", null ], 345 | "Abrir la barra de herramientas": [ "Skip to toolbar", null ], 346 | "Barra de herramientas en la parte superior.": [ "Top navigation toolbar.", null ], 347 | "PHP": [ "PHP", null ], 348 | "DB": [ "DB", null ], 349 | "Memory Usage": [ "Memory Usage", null ], 350 | "Please Enable": [ "Please Enable", null ], 351 | "No": [ "No", null ], 352 | "Total Events": [ "Total Events", null ], 353 | "Doing Cron": [ "Doing Cron", null ], 354 | "Next Event": [ "Next Event", null ], 355 | "Current Time": [ "Current Time", null ], 356 | "Custom Events": [ "Custom Events", null ], 357 | "Next Execution": [ "Next Execution", null ], 358 | "Conexión": [ "Hook", null ], 359 | "Interval Hook": [ "Interval Hook", null ], 360 | "Interval Value": [ "Interval Value", null ], 361 | "Args": [ "Args", null ], 362 | "Schedules": [ "Schedules", null ], 363 | "Interval (S)": [ "Interval (S)", null ], 364 | "Interval (M)": [ "Interval (M)", null ], 365 | "Interval (H)": [ "Interval (H)", null ], 366 | "Display Name": [ "Display Name", null ], 367 | "Cada hora": [ "Once Hourly", null ], 368 | "Dos veces al día": [ "Twice Daily", null ], 369 | "Cada día": [ "Once Daily", null ], 370 | "Core Events": [ "Core Events", null ], 371 | "Sunday": [ "Sunday", null ], 372 | "Monday": [ "Monday", null ], 373 | "Tuesday": [ "Tuesday", null ], 374 | "Wednesday": [ "Wednesday", null ], 375 | "Thursday": [ "Thursday", null ], 376 | "Friday": [ "Friday", null ], 377 | "Saturday": [ "Saturday", null ], 378 | "T": [ "T_Tuesday_initial", null ], 379 | "W": [ "W_Wednesday_initial", null ], 380 | "F": [ "F_Friday_initial", null ], 381 | "Sun": [ "Sun", null ], 382 | "Mon": [ "Mon", null ], 383 | "Tue": [ "Tue", null ], 384 | "Wed": [ "Wed", null ], 385 | "Thu": [ "Thu", null ], 386 | "Fri": [ "Fri", null ], 387 | "Sat": [ "Sat", null ], 388 | "January": [ "January", null ], 389 | "February": [ "February", null ], 390 | "March": [ "March", null ], 391 | "April": [ "April", null ], 392 | "May": [ "May", null ], 393 | "June": [ "June", null ], 394 | "July": [ "July", null ], 395 | "August": [ "August", null ], 396 | "September": [ "September", null ], 397 | "October": [ "October", null ], 398 | "November": [ "November", null ], 399 | "December": [ "December", null ], 400 | "Jan": [ "Jan_January_abbreviation", null ], 401 | "Feb": [ "Feb_February_abbreviation", null ], 402 | "Apr": [ "Apr_April_abbreviation", null ], 403 | "Jun": [ "Jun_June_abbreviation", null ], 404 | "Jul": [ "Jul_July_abbreviation", null ], 405 | "Aug": [ "Aug_August_abbreviation", null ], 406 | "Sep": [ "Sep_September_abbreviation", null ], 407 | "Oct": [ "Oct_October_abbreviation", null ], 408 | "Nov": [ "Nov_November_abbreviation", null ], 409 | "Dec": [ "Dec_December_abbreviation", null ] 410 | }, 411 | "placeholdersUsedOnPage": { 412 | "%1$s %2$s Feed": [ "%1$s %2$s Feed", "\u001f(.{0,100}?) (.{0,100}?) Feed\u001f", null ], 413 | "%1$s %2$s Comments Feed": [ "%1$s %2$s Comments Feed", "\u001f(.{0,100}?) (.{0,100}?) Comments Feed\u001f", null ], 414 | "%1$s %2$s %3$s Comments Feed": [ "%1$s %2$s %3$s Comments Feed", "\u001f(.{0,100}?) (.{0,100}?) (.{0,100}?) Comments Feed\u001f", null ], 415 | "%1$s %2$s %3$s Category Feed": [ "%1$s %2$s %3$s Category Feed", "\u001f(.{0,100}?) (.{0,100}?) (.{0,100}?) Category Feed\u001f", null ], 416 | "%1$s %2$s %3$s Tag Feed": [ "%1$s %2$s %3$s Tag Feed", "\u001f(.{0,100}?) (.{0,100}?) (.{0,100}?) Tag Feed\u001f", null ], 417 | "%1$s %2$s Posts by %3$s Feed": [ "%1$s %2$s Posts by %3$s Feed", "\u001f(.{0,100}?) (.{0,100}?) Posts by (.{0,100}?) Feed\u001f", null ], 418 | "%1$s %2$s Search Results for “%3$s” Feed": [ "%1$s %2$s Search Results for “%3$s” Feed", "\u001f(.{0,100}?) (.{0,100}?) Search Results for “(.{0,100}?)” Feed\u001f", null ], 419 | "%1$s %2$s %3$s Feed": [ "%1$s %2$s %3$s Feed", "\u001f(.{0,100}?) (.{0,100}?) (.{0,100}?) Feed\u001f", null ], 420 | "(by %1$s)": [ "(by %1$s)", "\u001f\\(by (.{0,100}?)\\)\u001f", "(by User Name)" ], 421 | "(by User Name)\\u0004(by %1$s)": [ "(by %1$s)", "\u001f\\(by (.{0,100}?)\\)\u001f", "(by User Name)" ], 422 | "%1$s on %2$s": [ "%1$s on %2$s", "\u001f(.{0,100}?) on (.{0,100}?)\u001f", "Blog Title on WordPress.com" ], 423 | "Blog Title on WordPress.com\\u0004%1$s on %2$s": [ "%1$s on %2$s", "\u001f(.{0,100}?) on (.{0,100}?)\u001f", "Blog Title on WordPress.com" ], 424 | "Continue reading %s": [ "Continue reading %s", "\u001fContinue reading (.{0,100}?)\u001f", null ], 425 | "\u001fContinue reading %s\u001f": [ "Continue reading %s", "\u001f\u001fContinue reading (.{0,100}?)\u001f\u001f", null ], 426 | "Watch: %s": [ "Watch: %s", "\u001fWatch\\: (.{0,100}?)\u001f", null ], 427 | "JavaScript required to play %s.": [ "JavaScript required to play %s.", "\u001fJavaScript required to play (.{0,100}?)\\.\u001f", null ], 428 | "Comment on %s": [ "Comment on %s", "\u001fComment on (.{0,100}?)\u001f", null ], 429 | "Proudly powered by %s": [ "Proudly powered by %s", "\u001fProudly powered by (.{0,100}?)\u001f", null ], 430 | "\u001fProudly powered by %s\u001f": [ "Proudly powered by %s", "\u001f\u001fProudly powered by (.{0,100}?)\u001f\u001f", null ], 431 | "View full size %1$s×%2$s": [ "View full size %1$s×%2$s", "\u001fView full size \\(.{0,100}?)\\×\\<\\/span\\>(.{0,100}?)\\<\\/span\\>\u001f", null ], 432 | "\u001fView full size %1$s×%2$s\u001f": [ "View full size %1$s×%2$s", "\u001f\u001fView full size \\(.{0,100}?)\\×\\<\\/span\\>(.{0,100}?)\\<\\/span\\>\u001f\u001f", null ], 433 | "Commenting as %s": [ "Commenting as %s", "\u001fCommenting as (.{0,100}?)\u001f", null ], 434 | "\u001fCommenting as %s\u001f": [ "Commenting as %s", "\u001f\u001fCommenting as (.{0,100}?)\u001f\u001f", null ], 435 | "New posts from this blog will now appear in your reader.": [ "New posts from this blog will now appear in your reader.", "\u001fNew posts from this blog will now appear in \\your reader\\<\\/a\\>\\.\u001f", null ], 436 | "Submit this post to the %s TopicPress queue.": [ "Submit this post to the %s TopicPress queue.", "\u001fSubmit this post to the (.{0,100}?) TopicPress queue\\.\u001f", null ], 437 | "Connecting to %s": [ "Connecting to %s", "\u001fConnecting to (.{0,100}?)\u001f", null ], 438 | "%1$s: You are commenting using your %2$s account.": [ "%1$s: You are commenting using your %2$s account.", "\u001f(.{0,100}?)\\: You are commenting using your (.{0,100}?) account\\.\u001f", null ], 439 | "Theme: %1$s.": [ "Theme: %1$s.", "\u001fTheme\\: (.{0,100}?)\\.\u001f", null ], 440 | "\u001fTheme: %1$s.\u001f": [ "Theme: %1$s.", "\u001f\u001fTheme\\: (.{0,100}?)\\.\u001f\u001f", null ], 441 | "The %s Theme": [ "The %s Theme", "\u001fThe (.{0,100}?) Theme\u001f", null ], 442 | "%s bytes": [ "%s bytes", "\u001f(.{0,100}?) bytes\u001f", null ], 443 | "\u001f%s bytes\u001f": [ "%s bytes", "\u001f\u001f(.{0,100}?) bytes\u001f\u001f", null ] 444 | }, 445 | "localeCode": "en-gb", 446 | "languageName": "English (UK)", 447 | "pluralForms": "nplurals=2; plural=n != 1", 448 | "glotPress": { 449 | "url": "https://translate.wordpress.com", 450 | "project": "wpcom,wpcom/themes/twentyfifteen" 451 | } 452 | }; 453 | -------------------------------------------------------------------------------- /test/string-extraction/translation-data/es.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stringsUsedOnPage": { 3 | "Tags": [ "Tags", [ "taxonomy general name" ] ], 4 | "Categorias": [ "Categories", [ "taxonomy general name" ] ], 5 | "Tag": [ "Tag", [ "taxonomy singular name" ] ], 6 | "Categoria": [ "Category", [ "taxonomy singular name" ] ], 7 | "Pesquisar Tags": [ "Search Tags" ], 8 | "Pesquisar categorias": [ "Search Categories" ], 9 | "Tags Populares": [ "Popular Tags" ], 10 | "Todas as tags": [ "All Tags" ], 11 | "Todas as Categorias": [ "All Categories" ], 12 | "Categoria mãe": [ "Parent Category" ], 13 | "Categoria mãe:": [ "Parent Category:" ], 14 | "Editar tag": [ "Edit Tag" ], 15 | "Editar categoria": [ "Edit Category" ], 16 | "Ver Tag": [ "View Tag" ], 17 | "Ver Categoria": [ "View Category" ], 18 | "Atualizar tag": [ "Update Tag" ], 19 | "Atualizar categoria": [ "Update Category" ], 20 | "Adicionar nova tag": [ "Add New Tag" ], 21 | "Adicionar nova categoria": [ "Add New Category" ], 22 | "Nome da nova tag": [ "New Tag Name" ], 23 | "Nome da nova categoria": [ "New Category Name" ], 24 | "Separe as tags com vírgulas": [ "Separate tags with commas" ], 25 | "Adicionar ou remover tags": [ "Add or remove tags" ], 26 | "Escolha entre as tags mais usadas": [ "Choose from the most used tags" ], 27 | "Nenhuma tag encontrada.": [ "No tags found." ], 28 | "No categories found.": [ "No categories found." ], 29 | "Fundo": [ "Background" ], 30 | "Cabeçalhos": [ "Headings" ], 31 | "Links": [ "Links" ], 32 | "Tom #1": [ "Accent #1" ], 33 | "Tom #2": [ "Accent #2" ], 34 | "Resposta": [ "Reply" ], 35 | "Rolar": [ "Scroll" ], 36 | "Ir para os comentários": [ "Scroll to comments" ], 37 | "Editar": [ "Edit" ], 38 | "Lixeira": [ "Trash" ], 39 | "URL encurtado": [ "Shortlink" ], 40 | "Stick post to home": [ "Stick post to home" ], 41 | "Unstick post from home": [ "Unstick post from home" ], 42 | "Normal": [ "Normal" ], 43 | "A fazer": [ "To-do" ], 44 | "Unresolved": [ "Unresolved" ], 45 | "Mark as done": [ "Mark as done" ], 46 | "Resolved": [ "Resolved" ], 47 | "Clear to-do": [ "Clear to-do" ], 48 | "Feedback": [ "Feedback" ], 49 | "Buscar Feedback": [ "Search Feedback" ], 50 | "Feedback não encontrado": [ "No feedback found" ], 51 | "Posts": [ "Posts", [ "post type general name" ] ], 52 | "Páginas": [ "Pages", [ "post type general name" ] ], 53 | "Post": [ "Post", [ "post type singular name" ] ], 54 | "Página": [ "Page", [ "post type singular name" ] ], 55 | "Adicionar Novo": [ "Add New", [ "post" ] ], 56 | "Adicionar Nova": [ "Add New", [ "page" ] ], 57 | "Adicionar novo post": [ "Add New Post" ], 58 | "Adicionar nova página": [ "Add New Page" ], 59 | "Editar post": [ "Edit Post" ], 60 | "Editar página": [ "Edit Page" ], 61 | "Novo Post": [ "New Post" ], 62 | "Nova Página": [ "New Page" ], 63 | "Ver Post": [ "View Post" ], 64 | "Ver Página": [ "View Page" ], 65 | "Pesquisar posts": [ "Search Posts" ], 66 | "Pesquisar páginas": [ "Search Pages" ], 67 | "Nenhum post encontrado.": [ "No posts found." ], 68 | "Páginas não encontradas.": [ "No pages found." ], 69 | "Nenhum post encontrado na Lixeira.": [ "No posts found in Trash." ], 70 | "Nenhuma página encontrada na Lixeira.": [ "No pages found in Trash." ], 71 | "Página mãe:": [ "Parent Page:" ], 72 | "Todos os posts": [ "All Posts" ], 73 | "Todas as páginas": [ "All Pages" ], 74 | "Temas Personalizados": [ "Cust’d Themes" ], 75 | "Tema Personalizado": [ "Customized Theme" ], 76 | "Todos os Temas Personalizados": [ "All Customized Themes" ], 77 | "Criar Novo Tema": [ "Create New Theme" ], 78 | "Editar Tema Personalizado": [ "Edit Customized Theme" ], 79 | "Novo Tema": [ "New Theme" ], 80 | "Ver Tema": [ "View Theme" ], 81 | "Arquivo de Temas Personalizados": [ "Customized Themes Archive" ], 82 | "Buscar Temas Personalizados": [ "Search Customized Themes" ], 83 | "Nenhum Tema Personalizado encontrado": [ "No Custom Themes found" ], 84 | "Nenhum Tema Personalizado encontrado na lixeira": [ "No Customized Themes found in trash" ], 85 | "Tema Personalizado:": [ "Customized Theme:" ], 86 | "Nenhum Tema Personalizado foi encontrado": [ "No Customized Themes found" ], 87 | "em": [ "on", [ "Open Sans font: on or off" ] ], 88 | "on": [ "on", [ "Serif: on or off" ] ], 89 | "No posts to show.": [ "No posts to show." ], 90 | "Seguir": [ "Follow" ], 91 | "Seguir comentários": [ "Follow comments" ], 92 | "Seguindo": [ "Following" ], 93 | "Não seguir comentários": [ "Unfollow comments" ], 94 | "Parar de seguir": [ "Stop following" ], 95 | "“": [ "“", [ "opening curly double quote" ] ], 96 | "”": [ "”", [ "closing curly double quote" ] ], 97 | "’": [ "’", [ "closing curly single quote" ] ], 98 | "′": [ "′", [ "prime" ] ], 99 | "″": [ "″", [ "double prime" ] ], 100 | "‘": [ "‘", [ "opening curly single quote" ] ], 101 | "–": [ "–", [ "en dash" ] ], 102 | "—": [ "—", [ "em dash" ] ], 103 | "Carregando...": [ "Loading…" ], 104 | "Olá, {name}! O que está acontecendo?": [ "Hi, {name}! What's happening?" ], 105 | "Agora": [ "Now" ], 106 | "alguns segundos": [ "a few seconds", [ "unit of time" ] ], 107 | "um min": [ "a min", [ "abbreviation of minute" ] ], 108 | "uma hora": [ "an hour" ], 109 | "um dia": [ "a day" ], 110 | "um mês": [ "a month" ], 111 | "cada ano": [ "a year" ], 112 | "Mostre post inteiro": [ "Show full post" ], 113 | "Esconder post estendido": [ "Hide extended post" ], 114 | "Show comment threads": [ "Show comment threads" ], 115 | "Hide comment threads": [ "Hide comment threads" ], 116 | "Avise-me sobre novos comentários por email.": [ "Notify me of new comments via email." ], 117 | "Você possui mudanças que não foram salvas.": [ "You have unsaved changes." ], 118 | "Nem todas as alterações foram salvas no servidor ainda. Por favor fique nesta página até que eles sejam salvos.": [ "Not all changes have been saved to the server yet. Please stay on this page until they are saved." ], 119 | "Sua sessão expirou. Clique aqui para fazer o login novamente. Suas mudaças não seram perdidas.": [ "Your session has expired. Click here to log in again. Your changes will not be lost." ], 120 | "Você se conectou novamente com sucesso.": [ "You have successfully logged back in." ], 121 | "Novo comentário por alguém": [ "New comment by someone" ], 122 | "Sorry, file not uploaded (unrecognized file type).": [ "Sorry, file not uploaded (unrecognized file type)." ], 123 | "A página solicitada não foi encontrada. Talvez a busca ajude.": [ "Apologies, but the page you requested could not be found. Perhaps searching will help." ], 124 | "Não consegui encontrar nenhum resultado para esse termo de busca. Tente novamente.": [ "Apologies, but I could not find any results for that search term. Please try again." ], 125 | "Um erro ocorreu. Por favor, atualize a página e tente novamente.": [ "An unexpected error occurred. Please refresh the page and try again." ], 126 | "Gerando pré-visualização...": [ "Generating preview..." ], 127 | "Pronto para publicar seu primeiro post? Apenas use o formulário acima.": [ "Ready to publish your first post? Simply use the form above." ], 128 | "Toque o controle de nova publicação abaixo para começar a escrever seu novo post.": [ "Tap the new post control below to begin writing your first post." ], 129 | "Este comentário está aguardando aprovação.": [ "This comment is awaiting approval." ], 130 | "Este comentário foi excluído.": [ "This comment was deleted." ], 131 | "Cancelar": [ "Cancel" ], 132 | "E-mail": [ "Email" ], 133 | "Nome": [ "Name" ], 134 | "Link Permanente": [ "Permalink" ], 135 | "Publicar": [ "Publish", [ "admin bar menu group label" ] ], 136 | "Salvar": [ "Save" ], 137 | "Salvando": [ "Saving" ], 138 | "Site": [ "Website" ], 139 | "Busca": [ "Search" ], 140 | "Alguém": [ "Someone" ], 141 | "Visualizar": [ "Preview" ], 142 | "Posts mais antigos": [ "Older posts" ], 143 | "Posts mais novos": [ "Newer posts" ], 144 | "Faça login para deixar um comentário.": [ "Login to leave a comment." ], 145 | "Preencha com suas informações abaixo": [ "Fill in your details below." ], 146 | "Cuidado! Você está editando o comentário de alguém": [ "Careful! You are editing someone else's comment." ], 147 | "Mostrar comentários": [ "Show Comments" ], 148 | "Esconder Comentários": [ "Hide Comments" ], 149 | "Este post foi movido para a lixeira. Você será redirecionado para a página inicial agora.": [ "This post was trashed. You will be redirected home now." ], 150 | "Esta página foi movida para a lixeira. Você será redirecionado para a página inicial agora.": [ "This page was trashed. You will be redirected home now." ], 151 | "Este post está sendo movido para a lixeira.": [ "This post is being trashed." ], 152 | "Esta página está sendo movida para a lixeira.": [ "This page is being trashed." ], 153 | "Houve um erro ao mover esse post para a lixeira. Tente novamente daqui a pouco.": [ "There was an error trashing that post. Please try again in a moment." ], 154 | "Houve um erro ao mover esta página para a lixeira. Tente novamente daqui a pouco.": [ "There was an error trashing that page. Please try again in a moment." ], 155 | "A conexão com o servidor foi interrompida. Reconecte. ": [ "The connection to the server has been interrupted. Please reconnect." ], 156 | "Digite o texto para o novo item": [ "Enter the text for the new item" ], 157 | "Atualize o texto do item abaixo": [ "Update the item text below" ], 158 | "Tem certeza de que deseja excluir este item?": [ "Are you sure you want to delete this item?" ], 159 | "CheckList Error": [ "CheckList Error" ], 160 | "Ocorreu um erro desconhecido.": [ "An unknown error occurred" ], 161 | "Uma resposta incompleta foi recebida": [ "A malformed response was received" ], 162 | "Não seguir": [ "Unfollow" ], 163 | "Ocorreu um erro ao atualizar suas seguintes preferências:": [ "There was a problem updating your following preferences." ], 164 | "Seguindo todos": [ "Following all" ], 165 | "Você já está seguindo todos os comentários desse site.": [ "You are already following all comments on this site." ], 166 | "Limpar": [ "Clear All" ], 167 | "Unstick Post from Home": [ "Unstick Post from Home" ], 168 | "Stick Post to Home": [ "Stick Post to Home" ], 169 | "Compartilhe isso:": [ "Share this:" ], 170 | "»": [ "»", [ "feed link" ] ], 171 | "sobre": [ "about", [ "Default page slug" ] ], 172 | "Escrever um post": [ "Write a post" ], 173 | "Moderar comentários": [ "Moderate comments" ], 174 | "Fazer upload de nova mídia": [ "Upload new media" ], 175 | "Estatística do blog": [ "Blog stats" ], 176 | "Skip to content": [ "Skip to content" ], 177 | "Personalizado": [ "Custom" ], 178 | "…": [ "…" ], 179 | "palavras": [ "words", [ "word count: words or characters?" ] ], 180 | "Atualizações Recentes": [ "Recent Updates" ], 181 | "
Tags:": [ "
Tags:" ], 182 | "Continuar lendo ": [ "Continue reading " ], 183 | "Curtir isso:": [ "Like this:" ], 184 | "Curtir": [ "Like" ], 185 | "Ações de post": [ "Post Actions" ], 186 | "Padrão": [ "Standard", [ "Post format" ] ], 187 | "Nota": [ "Aside", [ "Post format" ] ], 188 | "Chat": [ "Chat", [ "Post format" ] ], 189 | "Galeria": [ "Gallery", [ "Post format" ] ], 190 | "Link": [ "Link", [ "Post format" ] ], 191 | "Imagem": [ "Image", [ "Post format" ] ], 192 | "Citação": [ "Quote", [ "Post format" ] ], 193 | "Status": [ "Status", [ "Post format" ] ], 194 | "Vídeo": [ "Video", [ "Post format" ] ], 195 | "Áudio": [ "Audio", [ "Post format" ] ], 196 | "Páginas:": [ "Pages:" ], 197 | "Próxima página": [ "Next page" ], 198 | "Página anterior": [ "Previous page" ], 199 | "Remover": [ "Untrash" ], 200 | "to-do": [ "to-do" ], 201 | "Curtido por": [ "Liked by" ], 202 | "Concluído": [ "done" ], 203 | "Navegação de Posts": [ "Post navigation" ], 204 | "Próxima Página »": [ "Next Page »" ], 205 | " Posts Anteriores": [ " Older posts" ], 206 | "« Página anterior": [ "« Previous Page" ], 207 | "Search": [ "Search", [ "assistive text" ] ], 208 | "Pesquisar …": [ "Search …", [ "placeholder" ] ], 209 | "Pesquisar": [ "Search", [ "submit button" ] ], 210 | "Seen recently": [ "Seen recently" ], 211 | "Comentários recentes": [ "Recent Comments" ], 212 | "Rolar de volta ao topo": [ "Scroll back to top" ], 213 | "Comentário": [ "Comment" ], 214 | "Publicar comentário": [ "Post Comment" ], 215 | "Escreva um Comentário": [ "Write a Comment..." ], 216 | "Carregando comentários...": [ "Loading Comments..." ], 217 | "Certifique-se de enviar algum texto com o seu comentário.": [ "Please be sure to submit some text with your comment." ], 218 | "Fornecer um endereço de e-mail para comentar.": [ "Please provide an email address to comment." ], 219 | "Forneça seu nome para comentar.": [ "Please provide your name to comment." ], 220 | "Desculpe, mas ocorreu um erro ao postar seu comentário. Tente novamente mais tarde.": [ "Sorry, but there was an error posting your comment. Please try again later." ], 221 | "Seu comentário foi aprovado.": [ "Your comment was approved." ], 222 | "Seu comentário está aguardando moderação.": [ "Your comment is in moderation." ], 223 | "Câmera": [ "Camera" ], 224 | "Abertura": [ "Aperture" ], 225 | "Velocidade do Obturador": [ "Shutter Speed" ], 226 | "Comprimento Focal": [ "Focal Length" ], 227 | "Reblogar": [ "Reblog" ], 228 | "Re-blogado": [ "Reblogged" ], 229 | "Adicione suas ideias aqui... (opcional)": [ "Add your thoughts here... (optional)" ], 230 | "Reblogando...": [ "Reblogging..." ], 231 | "Post Reblog": [ "Post Reblog" ], 232 | "Crie um website ou blog gratuito no WordPress.com": [ "Create a free website or blog at WordPress.com" ], 233 | "Tema": [ "Theme" ], 234 | "por": [ "by" ], 235 | "Saiba mais sobre este tema": [ "Learn more about this theme" ], 236 | "Publicar em": [ "Post to" ], 237 | "Reblogar Post": [ "Reblog Post" ], 238 | "Domingo": [ "Sunday" ], 239 | "Lunes": [ "Monday" ], 240 | "Martes": [ "Tuesday" ], 241 | "Miércoles": [ "Wednesday" ], 242 | "Jueves": [ "Thursday" ], 243 | "Viernes": [ "Friday" ], 244 | "Sábado": [ "Saturday" ], 245 | "D": [ "S_Sunday_initial" ], 246 | "L": [ "M_Monday_initial" ], 247 | "M": [ "T_Tuesday_initial" ], 248 | "X": [ "W_Wednesday_initial" ], 249 | "J": [ "T_Thursday_initial" ], 250 | "V": [ "F_Friday_initial" ], 251 | "S": [ "S_Saturday_initial" ], 252 | "Dom": [ "Sun" ], 253 | "Lun": [ "Mon" ], 254 | "Mar": [ "Tue" ], 255 | "Mie": [ "Wed" ], 256 | "Jue": [ "Thu" ], 257 | "Vie": [ "Fri" ], 258 | "Sab": [ "Sat" ], 259 | "enero": [ "January" ], 260 | "febrero": [ "February" ], 261 | "marzo": [ "March" ], 262 | "abril": [ "April" ], 263 | "mayo": [ "May" ], 264 | "junio": [ "June" ], 265 | "julio": [ "July" ], 266 | "agosto": [ "August" ], 267 | "septiembre": [ "September" ], 268 | "octubre": [ "October" ], 269 | "noviembre": [ "November" ], 270 | "diciembre": [ "December" ], 271 | "ene": [ "Jan_January_abbreviation" ], 272 | "feb": [ "Feb_February_abbreviation" ], 273 | "mar": [ "Mar_March_abbreviation" ], 274 | "abr": [ "Apr_April_abbreviation" ], 275 | "may": [ "May_May_abbreviation" ], 276 | "jun": [ "Jun_June_abbreviation" ], 277 | "jul": [ "Jul_July_abbreviation" ], 278 | "ago": [ "Aug_August_abbreviation" ], 279 | "sep": [ "Sep_September_abbreviation" ], 280 | "oct": [ "Oct_October_abbreviation" ], 281 | "nov": [ "Nov_November_abbreviation" ], 282 | "dic": [ "Dec_December_abbreviation" ], 283 | "am": [ "am" ], 284 | "pm": [ "pm" ], 285 | "AM": [ "AM" ], 286 | "PM": [ "PM" ], 287 | ".": [ "number_format_thousands_sep" ], 288 | ",": [ "number_format_decimal_point" ], 289 | "ltr": [ "ltr", [ "text direction" ] ], 290 | "Mis sitios": [ "My Sites" ], 291 | "Avatar del blog actual": [ "Current blog avatar" ], 292 | "Cambiar sitio": [ "Switch Site" ], 293 | "Ver sitio": [ "View Site" ], 294 | "WP Admin": [ "WP Admin" ], 295 | "VIP": [ "VIP" ], 296 | "Estadísticas": [ "Stats" ], 297 | "Comentarios": [ "Comments" ], 298 | "Entradas del blog": [ "Blog Posts" ], 299 | "Añadir": [ "Add", [ "admin bar menu new item label" ] ], 300 | "Aspecto": [ "Look and Feel", [ "admin bar menu group label" ] ], 301 | "Temas": [ "Themes" ], 302 | "Personalizar": [ "Customize" ], 303 | "Menús": [ "Menus" ], 304 | "Configuración": [ "Settings" ], 305 | "Compartir": [ "Sharing" ], 306 | "Usuarios": [ "Users" ], 307 | "Mejoras": [ "Upgrades" ], 308 | "Enlace corto": [ "Shortlink" ], 309 | "Lector": [ "Reader" ], 310 | "Blogs que sigo": [ "Blogs I Follow" ], 311 | "Descubrir": [ "Discover", [ "admin bar menu group label" ] ], 312 | "Más recientes": [ "Freshly Pressed" ], 313 | "Blogs recomendados": [ "Recommended Blogs" ], 314 | "Buscar amigos": [ "Find Friends" ], 315 | "Mi Actividad": [ "My Activity" ], 316 | "Mis Comentarios": [ "My Comments" ], 317 | "Mis me gusta": [ "My Likes" ], 318 | "Siguiendo": [ "Following" ], 319 | "Mostrar las visitas del sitio por hora para las últimas 48 horas. Haz clic en todas las estadísticas del sitio.": [ "Showing site views per hour for the last 48 hours. Click for full Site Stats." ], 320 | "Cerrar sesión": [ "Log Out" ], 321 | "Perfil": [ "Profile" ], 322 | "Configuración de la cuenta": [ "Account Settings" ], 323 | "Colección de trofeos": [ "Trophy Case" ], 324 | "Facturación": [ "Billing" ], 325 | "Añadidos": [ "Extras" ], 326 | "Ayuda": [ "Help" ], 327 | "Este blog es privado": [ "This blog is private" ], 328 | "Blog Links": [ "Blog Links" ], 329 | "Blog Id: 8046731": [ "Blog Id: 8046731" ], 330 | "Blog Dashboard": [ "Blog Dashboard" ], 331 | "Estadísticas del sitio": [ "Site Stats" ], 332 | "Entradas": [ "Posts" ], 333 | "Mis mejoras": [ "My Upgrades" ], 334 | "Dominios": [ "Domains" ], 335 | "All Blog Options": [ "All Blog Options" ], 336 | "Blog's Network Admin": [ "Blog's Network Admin" ], 337 | "Blog Info": [ "Blog Info" ], 338 | "Blog Users": [ "Blog Users" ], 339 | "Blog Themes": [ "Blog Themes" ], 340 | "Blog Upgrades": [ "Blog Upgrades" ], 341 | "User's Network Admin": [ "User's Network Admin" ], 342 | "User's Store Admin": [ "User's Store Admin" ], 343 | "Load Page Without Cache": [ "Load Page Without Cache" ], 344 | "Clear Page's Cache": [ "Clear Page's Cache" ], 345 | "Posts Per Page = 1": [ "Posts Per Page = 1" ], 346 | "Desactivar Scroll infinito": [ "Disable Infinite Scroll" ], 347 | "Debug": [ "Debug" ], 348 | "Ocultar anuncios": [ "Hide adverts" ], 349 | "Abrir la barra de herramientas": [ "Skip to toolbar" ], 350 | "Barra de herramientas en la parte superior.": [ "Top navigation toolbar." ], 351 | "PHP": [ "PHP" ], 352 | "DB": [ "DB" ], 353 | "Memory Usage": [ "Memory Usage" ], 354 | "Please Enable": [ "Please Enable" ], 355 | "No": [ "No" ], 356 | "Total Events": [ "Total Events" ], 357 | "Doing Cron": [ "Doing Cron" ], 358 | "Next Event": [ "Next Event" ], 359 | "Current Time": [ "Current Time" ], 360 | "Custom Events": [ "Custom Events" ], 361 | "Next Execution": [ "Next Execution" ], 362 | "Conexión": [ "Hook" ], 363 | "Interval Hook": [ "Interval Hook" ], 364 | "Interval Value": [ "Interval Value" ], 365 | "Args": [ "Args" ], 366 | "Schedules": [ "Schedules" ], 367 | "Interval (S)": [ "Interval (S)" ], 368 | "Interval (M)": [ "Interval (M)" ], 369 | "Interval (H)": [ "Interval (H)" ], 370 | "Display Name": [ "Display Name" ], 371 | "Cada hora": [ "Once Hourly" ], 372 | "Dos veces al día": [ "Twice Daily" ], 373 | "Cada día": [ "Once Daily" ], 374 | "Core Events": [ "Core Events" ], 375 | "domingo": [ "Sunday" ], 376 | "segunda-feira": [ "Monday" ], 377 | "terça-feira": [ "Tuesday" ], 378 | "quarta-feira": [ "Wednesday" ], 379 | "quinta-feira": [ "Thursday" ], 380 | "sexta-feira": [ "Friday" ], 381 | "sábado": [ "Saturday" ], 382 | "T": [ "T_Tuesday_initial" ], 383 | "Q": [ "T_Thursday_initial" ], 384 | "dom": [ "Sun" ], 385 | "seg": [ "Mon" ], 386 | "ter": [ "Tue" ], 387 | "qua": [ "Wed" ], 388 | "qui": [ "Thu" ], 389 | "sex": [ "Fri" ], 390 | "sáb": [ "Sat" ], 391 | "janeiro": [ "January" ], 392 | "fevereiro": [ "February" ], 393 | "março": [ "March" ], 394 | "maio": [ "May" ], 395 | "junho": [ "June" ], 396 | "julho": [ "July" ], 397 | "setembro": [ "September" ], 398 | "outubro": [ "October" ], 399 | "novembro": [ "November" ], 400 | "dezembro": [ "December" ], 401 | "jan": [ "Jan_January_abbreviation" ], 402 | "fev": [ "Feb_February_abbreviation" ], 403 | "mai": [ "May_May_abbreviation" ], 404 | "set": [ "Sep_September_abbreviation" ], 405 | "out": [ "Oct_October_abbreviation" ], 406 | "dez": [ "Dec_December_abbreviation" ] 407 | }, 408 | "placeholdersUsedOnPage": { 409 | "%1$s em %2$s": [ "%1$s on %2$s", "\u001f(.{0,200}?) em (.{0,200}?)\u001f", [ "Recent Comments Widget" ] ], 410 | "\u001f%1$s em %2$s\u001f": [ "%1$s on %2$s", "\u001f\u001f(.{0,200}?) em (.{0,200}?)\u001f\u001f" ], 411 | "Ontem às %s": [ "Yesterday at %s", "\u001fOntem às (.{0,200}?)\u001f" ], 412 | "\u001fOntem às %s\u001f": [ "Yesterday at %s", "\u001f\u001fOntem às (.{0,200}?)\u001f\u001f" ], 413 | "%smes": [ "%smon", "\u001f(.{0,200}?)mes\u001f", [ "time in months abbreviation" ] ], 414 | "\u001f%smes\u001f": [ "%smon", "\u001f\u001f(.{0,200}?)mes\u001f\u001f", [ "time in months abbreviation" ] ], 415 | "em %s": [ "in %s", "\u001fem (.{0,200}?)\u001f", [ "time from now" ] ], 416 | "\u001fem %s\u001f": [ "in %s", "\u001f\u001fem (.{0,200}?)\u001f\u001f", [ "time from now" ] ], 417 | "%s atrás": [ "%s ago", "\u001f(.{0,200}?) atrás\u001f", [ "time ago" ] ], 418 | "\u001f%s atrás\u001f": [ "%s ago", "\u001f\u001f(.{0,200}?) atrás\u001f\u001f", [ "time ago" ] ], 419 | "%d mins": [ "%d mins", "\u001f([0-9]{0,15}?) mins\u001f", [ "abbreviation of minutes" ] ], 420 | "\u001f%d mins\u001f": [ "%d mins", "\u001f\u001f([0-9]{0,15}?) mins\u001f\u001f", [ "abbreviation of minutes" ] ], 421 | "%d horas": [ "%d hours", "\u001f([0-9]{0,15}?) horas\u001f" ], 422 | "\u001f%d horas\u001f": [ "%d hours", "\u001f\u001f([0-9]{0,15}?) horas\u001f\u001f" ], 423 | "%d dias": [ "%d days", "\u001f([0-9]{0,15}?) dias\u001f" ], 424 | "\u001f%d dias\u001f": [ "%d days", "\u001f\u001f([0-9]{0,15}?) dias\u001f\u001f" ], 425 | "%d meses": [ "%d months", "\u001f([0-9]{0,15}?) meses\u001f" ], 426 | "\u001f%d meses\u001f": [ "%d months", "\u001f\u001f([0-9]{0,15}?) meses\u001f\u001f" ], 427 | "%d anos": [ "%d years", "\u001f([0-9]{0,15}?) anos\u001f" ], 428 | "\u001f%d anos\u001f": [ "%d years", "\u001f\u001f([0-9]{0,15}?) anos\u001f\u001f" ], 429 | "Posts por %1$s (%2$s)": [ "Posts by %1$s (%2$s)", "\u001fPosts por (.{0,200}?) \\((.{0,200}?)\\)\u001f" ], 430 | "\u001fPosts por %1$s (%2$s)\u001f": [ "Posts by %1$s (%2$s)", "\u001f\u001fPosts por (.{0,200}?) \\((.{0,200}?)\\)\u001f\u001f" ], 431 | "Novo comentário de %s": [ "New comment by %s", "\u001fNovo comentário de (.{0,200}?)\u001f" ], 432 | "\u001fNovo comentário de %s\u001f": [ "New comment by %s", "\u001f\u001fNovo comentário de (.{0,200}?)\u001f\u001f" ], 433 | "Novo post de %s": [ "New post by %s", "\u001fNovo post de (.{0,200}?)\u001f" ], 434 | "\u001fNovo post de %s\u001f": [ "New post by %s", "\u001f\u001fNovo post de (.{0,200}?)\u001f\u001f" ], 435 | "%1$s mencionou você: \"%2$s...\"": [ "%1$s mentioned you: \"%2$s\"", "\u001f(.{0,200}?) mencionou você\\: \"(.{0,200}?)\\.\\.\\.\"\u001f" ], 436 | "\u001f%1$s mencionou você: \"%2$s...\"\u001f": [ "%1$s mentioned you: \"%2$s\"", "\u001f\u001f(.{0,200}?) mencionou você\\: \"(.{0,200}?)\\.\\.\\.\"\u001f\u001f" ], 437 | "%1$s não foi carregado (arquivos %2$s não são permitidos).": [ "%1$s was not uploaded (%2$s files are not allowed).", "\u001f(.{0,200}?) não foi carregado \\(arquivos (.{0,200}?) não são permitidos\\)\\.\u001f" ], 438 | "%1$s não foi carregado (tipo de arquivo não reconhecido).": [ "%1$s was not uploaded (unrecognized file type).", "\u001f(.{0,200}?) não foi carregado \\(tipo de arquivo não reconhecido\\)\\.\u001f" ], 439 | "Arquivos %1$s não são permitidos.": [ "Sorry, %1$s files are not allowed.", "\u001fArquivos (.{0,200}?) não são permitidos\\.\u001f" ], 440 | "Feed de %1$s %2$s": [ "%1$s %2$s Feed", "\u001fFeed de (.{0,200}?) (.{0,200}?)\u001f" ], 441 | "%1$s %2$s Feed de comentários": [ "%1$s %2$s Comments Feed", "\u001f(.{0,200}?) (.{0,200}?) Feed de comentários\u001f" ], 442 | "%1$s %2$s %3$s Feed de comentários": [ "%1$s %2$s %3$s Comments Feed", "\u001f(.{0,200}?) (.{0,200}?) (.{0,200}?) Feed de comentários\u001f" ], 443 | "Feed da Categoria %1$s %2$s %3$s": [ "%1$s %2$s %3$s Category Feed", "\u001fFeed da Categoria (.{0,200}?) (.{0,200}?) (.{0,200}?)\u001f" ], 444 | "Feed da tag %1$s %2$s %3$s": [ "%1$s %2$s %3$s Tag Feed", "\u001fFeed da tag (.{0,200}?) (.{0,200}?) (.{0,200}?)\u001f" ], 445 | "%1$s %2$s Feed dos Posts de %3$s": [ "%1$s %2$s Posts by %3$s Feed", "\u001f(.{0,200}?) (.{0,200}?) Feed dos Posts de (.{0,200}?)\u001f" ], 446 | "%1$s %2$s Feed do resultado da busca por “%3$s”": [ "%1$s %2$s Search Results for “%3$s” Feed", "\u001f(.{0,200}?) (.{0,200}?) Feed do resultado da busca por “(.{0,200}?)”\u001f" ], 447 | "Feed de %1$s %2$s %3$s": [ "%1$s %2$s %3$s Feed", "\u001fFeed de (.{0,200}?) (.{0,200}?) (.{0,200}?)\u001f" ], 448 | "Posts by %1$s ( @%2$s )": [ "Posts by %1$s ( @%2$s )", "\u001fPosts by (.{0,200}?) \\( @(.{0,200}?) \\)\u001f" ], 449 | "\u001fPosts by %1$s ( @%2$s )\u001f": [ "Posts by %1$s ( @%2$s )", "\u001f\u001fPosts by (.{0,200}?) \\( @(.{0,200}?) \\)\u001f\u001f" ], 450 | "%1$s on %2$s": [ "%1$s on %2$s", "\u001f(.{0,200}?) \\on\\<\\/em\\> (.{0,200}?)\u001f" ], 451 | "\u001f%1$s on %2$s\u001f": [ "%1$s on %2$s", "\u001f\u001f(.{0,200}?) \\on\\<\\/em\\> (.{0,200}?)\u001f\u001f" ], 452 | "%1$s marcou isto como %2$s": [ "%1$s marked this %2$s", "\u001f(.{0,200}?) marcou isto como (.{0,200}?)\u001f" ], 453 | "\u001f%1$s marcou isto como %2$s\u001f": [ "%1$s marked this %2$s", "\u001f\u001f(.{0,200}?) marcou isto como (.{0,200}?)\u001f\u001f" ], 454 | "%s pessoa": [ "%s person", "\u001f(.{0,200}?) pessoa\u001f" ], 455 | "%s pessoas": [ "%s people", "\u001f(.{0,200}?) pessoas\u001f" ], 456 | "Near %s": [ "Near %s", "\u001fNear (.{0,200}?)\u001f" ], 457 | "\u001fNear %s\u001f": [ "Near %s", "\u001f\u001fNear (.{0,200}?)\u001f\u001f" ], 458 | "Last seen %s ago": [ "Last seen %s ago", "\u001fLast seen (.{0,200}?) ago\u001f" ], 459 | "\u001fLast seen %s ago\u001f": [ "Last seen %s ago", "\u001f\u001fLast seen (.{0,200}?) ago\u001f\u001f" ], 460 | "Proudly powered by %s": [ "Proudly powered by %s", "\u001fProudly powered by (.{0,200}?)\u001f" ], 461 | "\u001fProudly powered by %s\u001f": [ "Proudly powered by %s", "\u001f\u001fProudly powered by (.{0,200}?)\u001f\u001f" ], 462 | "Theme: %1$s by %2$s.": [ "Theme: %1$s by %2$s.", "\u001fTheme\\: (.{0,200}?) by (.{0,200}?)\\.\u001f" ], 463 | "\u001fTheme: %1$s by %2$s.\u001f": [ "Theme: %1$s by %2$s.", "\u001f\u001fTheme\\: (.{0,200}?) by (.{0,200}?)\\.\u001f\u001f" ], 464 | "Visualizar tamanho original %1$s×%2$s": [ "View full size %1$s×%2$s", "\u001fVisualizar tamanho original \\(.{0,200}?)\\×\\<\\/span\\>(.{0,200}?)\\<\\/span\\>\u001f" ], 465 | "\u001fVisualizar tamanho original %1$s×%2$s\u001f": [ "View full size %1$s×%2$s", "\u001f\u001fVisualizar tamanho original \\(.{0,200}?)\\×\\<\\/span\\>(.{0,200}?)\\<\\/span\\>\u001f\u001f" ], 466 | "Comentando como %s": [ "Commenting as %s", "\u001fComentando como (.{0,200}?)\u001f" ], 467 | "\u001fComentando como %s\u001f": [ "Commenting as %s", "\u001f\u001fComentando como (.{0,200}?)\u001f\u001f" ], 468 | "Os novos posts desse blog agora aparecem em
seu Leitor.": [ "New posts from this blog will now appear in your reader.", "\u001fOs novos posts desse blog agora aparecem em \\seu Leitor\\<\\/a\\>\\.\u001f" ], 469 | "%1$s, %2$s comentários": [ "%1$s, %2$s comments", "\u001f(.{0,200}?), (.{0,200}?) comentários\u001f" ], 470 | "\u001f%1$s, %2$s comentários\u001f": [ "%1$s, %2$s comments", "\u001f\u001f(.{0,200}?), (.{0,200}?) comentários\u001f\u001f" ], 471 | "Showing %1$s of %2$s %3$s posts": [ "Showing %1$s of %2$s %3$s posts", "\u001fShowing (.{0,200}?) of (.{0,200}?) (.{0,200}?) posts\u001f" ], 472 | "\u001fShowing %1$s of %2$s %3$s posts\u001f": [ "Showing %1$s of %2$s %3$s posts", "\u001f\u001fShowing (.{0,200}?) of (.{0,200}?) (.{0,200}?) posts\u001f\u001f" ], 473 | "Tema: %1$s.": [ "Theme: %1$s.", "\u001fTema\\: (.{0,200}?)\\.\u001f" ], 474 | "\u001fTema: %1$s.\u001f": [ "Theme: %1$s.", "\u001f\u001fTema\\: (.{0,200}?)\\.\u001f\u001f" ], 475 | "O tema %s": [ "The %s Theme", "\u001fO tema (.{0,200}?)\u001f" ], 476 | "%d blogueiros gostam disto:": [ "%d bloggers like this:", "\u001f\\([0-9]{0,15}?)\\<\\/span\\> blogueiros gostam disto\\:\u001f" ], 477 | "\u001f%d blogueiros gostam disto:\u001f": [ "%d bloggers like this:", "\u001f\u001f\\([0-9]{0,15}?)\\<\\/span\\> blogueiros gostam disto\\:\u001f\u001f" ], 478 | "%s bytes": [ "%s bytes", "\u001f(.{0,200}?) bytes\u001f" ], 479 | "\u001f%s bytes\u001f": [ "%s bytes", "\u001f\u001f(.{0,200}?) bytes\u001f\u001f" ] 480 | }, 481 | "localeCode": "pt-br", 482 | "languageName": "Português do Brasil", 483 | "pluralForms": "nplurals=2; plural=(n > 1)", 484 | "glotPress": { 485 | "url": "https://translate.wordpress.com", 486 | "project": "wpcom,wpcom/themes/p2-breathe" 487 | } 488 | }; 489 | -------------------------------------------------------------------------------- /test/string-extraction/translation-data/he.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stringsUsedOnPage": { 3 | "כתיבת תגובה": [ 4 | "Leave a Reply", 5 | null 6 | ], 7 | "תגיות": [ 8 | "Tags", 9 | "taxonomy general name" 10 | ], 11 | "taxonomy general name\\u0004תגיות": [ 12 | "Tags", 13 | "taxonomy general name" 14 | ], 15 | "קטגוריות": [ 16 | "Categories", 17 | "taxonomy general name" 18 | ], 19 | "taxonomy general name\\u0004קטגוריות": [ 20 | "Categories", 21 | "taxonomy general name" 22 | ], 23 | "תגית": [ 24 | "Tag", 25 | "taxonomy singular name" 26 | ], 27 | "taxonomy singular name\\u0004תגית": [ 28 | "Tag", 29 | "taxonomy singular name" 30 | ], 31 | "קטגוריה": [ 32 | "Category", 33 | "taxonomy singular name" 34 | ], 35 | "taxonomy singular name\\u0004קטגוריה": [ 36 | "Category", 37 | "taxonomy singular name" 38 | ], 39 | "חיפוש תגיות": [ 40 | "Search Tags", 41 | null 42 | ], 43 | "חיפוש קטגוריות": [ 44 | "Search Categories", 45 | null 46 | ], 47 | "תגיות פופולריות": [ 48 | "Popular Tags", 49 | null 50 | ], 51 | "כל התגיות": [ 52 | "All Tags", 53 | null 54 | ], 55 | "כל הקטגוריות": [ 56 | "All Categories", 57 | null 58 | ], 59 | "הורה": [ 60 | "Parent Category", 61 | null 62 | ], 63 | "הורה:": [ 64 | "Parent Category:", 65 | null 66 | ], 67 | "עריכת תגית": [ 68 | "Edit Tag", 69 | null 70 | ], 71 | "עריכת קטגוריה": [ 72 | "Edit Category", 73 | null 74 | ], 75 | "הצג תגית": [ 76 | "View Tag", 77 | null 78 | ], 79 | "הצג קטגוריה": [ 80 | "View Category", 81 | null 82 | ], 83 | "עדכון": [ 84 | "Update Tag", 85 | null 86 | ], 87 | "תגית חדשה": [ 88 | "Add New Tag", 89 | null 90 | ], 91 | "קטגוריה חדשה": [ 92 | "Add New Category", 93 | null 94 | ], 95 | "שם": [ 96 | "New Tag Name", 97 | null 98 | ], 99 | "להפריד תגיות בפסיקים": [ 100 | "Separate tags with commas", 101 | null 102 | ], 103 | "להוסיף או להסיר תגיות": [ 104 | "Add or remove tags", 105 | null 106 | ], 107 | "לבחור מבין התגיות הנפוצות ביותר": [ 108 | "Choose from the most used tags", 109 | null 110 | ], 111 | "לא נמצאו תגיות.": [ 112 | "No tags found.", 113 | null 114 | ], 115 | "לא נמצאו קטגוריות.": [ 116 | "No categories found.", 117 | null 118 | ], 119 | "רקע": [ 120 | "Background", 121 | null 122 | ], 123 | "כותרות": [ 124 | "Headings", 125 | null 126 | ], 127 | "קישורים": [ 128 | "Links", 129 | null 130 | ], 131 | "דגש #1": [ 132 | "Accent #1", 133 | null 134 | ], 135 | "דגש #2": [ 136 | "Accent #2", 137 | null 138 | ], 139 | "דגש עיקרי": [ 140 | "Main Accent", 141 | null 142 | ], 143 | "לא בשימוש": [ 144 | "Unused", 145 | null 146 | ], 147 | "משוב": [ 148 | "Feedback", 149 | null 150 | ], 151 | "חיפוש משובים": [ 152 | "Search Feedback", 153 | null 154 | ], 155 | "לא נמצא משוב": [ 156 | "No feedback found", 157 | null 158 | ], 159 | "פוסטים": [ 160 | "Posts", 161 | "post type general name" 162 | ], 163 | "post type general name\\u0004פוסטים": [ 164 | "Posts", 165 | "post type general name" 166 | ], 167 | "עמודים": [ 168 | "Pages", 169 | "post type general name" 170 | ], 171 | "post type general name\\u0004עמודים": [ 172 | "Pages", 173 | "post type general name" 174 | ], 175 | "פוסט": [ 176 | "Post", 177 | "post type singular name" 178 | ], 179 | "post type singular name\\u0004פוסט": [ 180 | "Post", 181 | "post type singular name" 182 | ], 183 | "עמוד": [ 184 | "Page", 185 | "post type singular name" 186 | ], 187 | "post type singular name\\u0004עמוד": [ 188 | "Page", 189 | "post type singular name" 190 | ], 191 | "פוסט חדש": [ 192 | "Add New", 193 | "post" 194 | ], 195 | "post\\u0004פוסט חדש": [ 196 | "Add New", 197 | "post" 198 | ], 199 | "עמוד חדש": [ 200 | "Add New", 201 | "page" 202 | ], 203 | "page\\u0004עמוד חדש": [ 204 | "Add New", 205 | "page" 206 | ], 207 | "עריכה": [ 208 | "Edit Post", 209 | null 210 | ], 211 | "הצג פוסט": [ 212 | "View Post", 213 | null 214 | ], 215 | "הצג עמוד": [ 216 | "View Page", 217 | null 218 | ], 219 | "חיפוש פוסטים": [ 220 | "Search Posts", 221 | null 222 | ], 223 | "חיפוש עמודים": [ 224 | "Search Pages", 225 | null 226 | ], 227 | "לא נמצאו פוסטים.": [ 228 | "No posts found.", 229 | null 230 | ], 231 | "לא נמצאו עמודים.": [ 232 | "No pages found.", 233 | null 234 | ], 235 | "לא נמצאו פוסטים בפח.": [ 236 | "No posts found in Trash.", 237 | null 238 | ], 239 | "לא נמצאו עמודים בפח.": [ 240 | "No pages found in Trash.", 241 | null 242 | ], 243 | "כל הפוסטים": [ 244 | "All Posts", 245 | null 246 | ], 247 | "כל העמודים": [ 248 | "All Pages", 249 | null 250 | ], 251 | "ערכות עיצוב מותאמות אישית": [ 252 | "Custom Themes", 253 | null 254 | ], 255 | "ערכת עיצוב מותאמת אישית": [ 256 | "Custom Theme", 257 | null 258 | ], 259 | "All Custom Themes": [ 260 | "All Custom Themes", 261 | null 262 | ], 263 | "Create New Theme": [ 264 | "Create New Theme", 265 | null 266 | ], 267 | "Edit Custom Theme": [ 268 | "Edit Custom Theme", 269 | null 270 | ], 271 | "New Theme": [ 272 | "New Theme", 273 | null 274 | ], 275 | "View Theme": [ 276 | "View Theme", 277 | null 278 | ], 279 | "Custom Themes Archive": [ 280 | "Custom Themes Archive", 281 | null 282 | ], 283 | "Search Custom Themes": [ 284 | "Search Custom Themes", 285 | null 286 | ], 287 | "No Custom Themes found": [ 288 | "No Custom Themes found", 289 | null 290 | ], 291 | "No Custom Themes found in trash": [ 292 | "No Custom Themes found in trash", 293 | null 294 | ], 295 | "ערכת נושא מותאמת אישית": [ 296 | "Custom Theme:", 297 | null 298 | ], 299 | "Customized Themes": [ 300 | "Customized Themes", 301 | null 302 | ], 303 | "Customized Theme": [ 304 | "Customized Theme", 305 | null 306 | ], 307 | "Cust’d Themes": [ 308 | "Cust’d Themes", 309 | null 310 | ], 311 | "כל ערכות העיצוב שהותאמו אישית.": [ 312 | "All Customized Themes", 313 | null 314 | ], 315 | "Edit Customized Theme": [ 316 | "Edit Customized Theme", 317 | null 318 | ], 319 | "Customized Themes Archive": [ 320 | "Customized Themes Archive", 321 | null 322 | ], 323 | "Search Customized Themes": [ 324 | "Search Customized Themes", 325 | null 326 | ], 327 | "No Customized Themes found": [ 328 | "No Customized Themes found", 329 | null 330 | ], 331 | "No Customized Themes found in trash": [ 332 | "No Customized Themes found in trash", 333 | null 334 | ], 335 | "Customized Theme:": [ 336 | "Customized Theme:", 337 | null 338 | ], 339 | "No posts to show.": [ 340 | "No posts to show.", 341 | null 342 | ], 343 | "\"": [ 344 | "“", 345 | "opening curly double quote" 346 | ], 347 | "opening curly double quote\\u0004\"": [ 348 | "“", 349 | "opening curly double quote" 350 | ], 351 | "'": [ 352 | "’", 353 | "apostrophe" 354 | ], 355 | "apostrophe\\u0004'": [ 356 | "’", 357 | "apostrophe" 358 | ], 359 | "–": [ 360 | "–", 361 | "en dash" 362 | ], 363 | "en dash\\u0004–": [ 364 | "–", 365 | "en dash" 366 | ], 367 | "—": [ 368 | "—", 369 | "em dash" 370 | ], 371 | "em dash\\u0004—": [ 372 | "—", 373 | "em dash" 374 | ], 375 | "על": [ 376 | "on", 377 | "Open Sans font: on or off" 378 | ], 379 | "Open Sans font: on or off\\u0004על": [ 380 | "on", 381 | "Open Sans font: on or off" 382 | ], 383 | "on": [ 384 | "on", 385 | "Noto Serif font: on or off" 386 | ], 387 | "Noto Serif font: on or off\\u0004on": [ 388 | "on", 389 | "Noto Serif font: on or off" 390 | ], 391 | "דווח על תוכן זה": [ 392 | "Report this content", 393 | null 394 | ], 395 | "לשתף": [ 396 | "Share this:", 397 | null 398 | ], 399 | "»": [ 400 | "»", 401 | "feed link" 402 | ], 403 | "feed link\\u0004»": [ 404 | "»", 405 | "feed link" 406 | ], 407 | "about": [ 408 | "about", 409 | "Default page slug" 410 | ], 411 | "Default page slug\\u0004about": [ 412 | "about", 413 | "Default page slug" 414 | ], 415 | "להירשם": [ 416 | "Subscribe", 417 | null 418 | ], 419 | "WordPress.com תמיכה": [ 420 | "WordPress.com Support", 421 | null 422 | ], 423 | "פורומים של WordPress.com": [ 424 | "WordPress.com Forums", 425 | null 426 | ], 427 | "עדכוני טוויטר": [ 428 | "Twitter Updates", 429 | null 430 | ], 431 | "שגיאה: טוויטר לא הגיב. בבקשה המתן כמה דקות ורענן את הדף.": [ 432 | "Error: Twitter did not respond. Please wait a few minutes and refresh this page.", 433 | null 434 | ], 435 | "לעקוב אחרי הבלוג באימייל": [ 436 | "Follow Blog via Email", 437 | null 438 | ], 439 | "את/ה מנוי/ה לבלוג הזה": [ 440 | "You are following this blog", 441 | null 442 | ], 443 | "הכניסו את כתובת האימייל שלכם, כדי לעקוב אחרי הבלוג ולקבל עדכונים על פוסטים חדשים במייל.": [ 444 | "Enter your email address to follow this blog and receive notifications of new posts by email.", 445 | null 446 | ], 447 | "הרשמה": [ 448 | "Follow", 449 | null 450 | ], 451 | "לחצו כדי לעקוב אחרי הבלוג ולקבל במייל עדכונים על פוסטים חדשים.": [ 452 | "Click to follow this blog and receive notifications of new posts by email.", 453 | null 454 | ], 455 | "דילוג לתוכן": [ 456 | "Skip to content", 457 | null 458 | ], 459 | "וידג'טים": [ 460 | "Widgets", 461 | null 462 | ], 463 | "המשך…": [ 464 | "(more…)", 465 | null 466 | ], 467 | "אימייל": [ 468 | "Email", 469 | "share to" 470 | ], 471 | "share to\\u0004אימייל": [ 472 | "Email", 473 | "share to" 474 | ], 475 | "לחץ כדי לשלוח את זה לחבר בדואר אלקטרוני": [ 476 | "Click to email this to a friend", 477 | null 478 | ], 479 | "Twitter": [ 480 | "Twitter", 481 | "share to" 482 | ], 483 | "share to\\u0004Twitter": [ 484 | "Twitter", 485 | "share to" 486 | ], 487 | "לחץ כדי לשתף בטוויטר": [ 488 | "Click to share on Twitter", 489 | null 490 | ], 491 | "גוגל": [ 492 | "Google", 493 | "share to" 494 | ], 495 | "share to\\u0004גוגל": [ 496 | "Google", 497 | "share to" 498 | ], 499 | "לחץ כדי לשתף ב-Google+": [ 500 | "Click to share on Google+", 501 | null 502 | ], 503 | "פרסם את זה": [ 504 | "Press This", 505 | "share to" 506 | ], 507 | "share to\\u0004פרסם את זה": [ 508 | "Press This", 509 | "share to" 510 | ], 511 | "לחץ כדי לפרסם את זה!": [ 512 | "Click to Press This!", 513 | null 514 | ], 515 | "פייסבוק": [ 516 | "Facebook", 517 | "share to" 518 | ], 519 | "share to\\u0004פייסבוק": [ 520 | "Facebook", 521 | "share to" 522 | ], 523 | "שתף בפייסבוק": [ 524 | "Share on Facebook", 525 | null 526 | ], 527 | "אהבתי": [ 528 | "Like this:", 529 | null 530 | ], 531 | "טוען...": [ 532 | "Loading...", 533 | null 534 | ], 535 | ":עמודים": [ 536 | "Pages:", 537 | null 538 | ], 539 | "העמוד הבא": [ 540 | "Next page", 541 | null 542 | ], 543 | "העמוד הקודם": [ 544 | "Previous page", 545 | null 546 | ], 547 | "להמשיך לקרוא ": [ 548 | "Continue reading ", 549 | null 550 | ], 551 | "רגיל": [ 552 | "Standard", 553 | "Post format" 554 | ], 555 | "Post format\\u0004רגיל": [ 556 | "Standard", 557 | "Post format" 558 | ], 559 | "הערה": [ 560 | "Aside", 561 | "Post format" 562 | ], 563 | "Post format\\u0004הערה": [ 564 | "Aside", 565 | "Post format" 566 | ], 567 | "צ'אט": [ 568 | "Chat", 569 | "Post format" 570 | ], 571 | "Post format\\u0004צ'אט": [ 572 | "Chat", 573 | "Post format" 574 | ], 575 | "גלריה": [ 576 | "Gallery", 577 | "Post format" 578 | ], 579 | "Post format\\u0004גלריה": [ 580 | "Gallery", 581 | "Post format" 582 | ], 583 | "קישור": [ 584 | "Link", 585 | "Post format" 586 | ], 587 | "Post format\\u0004קישור": [ 588 | "Link", 589 | "Post format" 590 | ], 591 | "תמונה": [ 592 | "Image", 593 | "Post format" 594 | ], 595 | "Post format\\u0004תמונה": [ 596 | "Image", 597 | "Post format" 598 | ], 599 | "ציטוט": [ 600 | "Quote", 601 | "Post format" 602 | ], 603 | "Post format\\u0004ציטוט": [ 604 | "Quote", 605 | "Post format" 606 | ], 607 | "סטטוס": [ 608 | "Status", 609 | "Post format" 610 | ], 611 | "Post format\\u0004סטטוס": [ 612 | "Status", 613 | "Post format" 614 | ], 615 | "וידאו": [ 616 | "Video", 617 | "Post format" 618 | ], 619 | "Post format\\u0004וידאו": [ 620 | "Video", 621 | "Post format" 622 | ], 623 | "אודיו": [ 624 | "Audio", 625 | "Post format" 626 | ], 627 | "Post format\\u0004אודיו": [ 628 | "Audio", 629 | "Post format" 630 | ], 631 | "פוסטים ישנים": [ 632 | "Older posts", 633 | null 634 | ], 635 | "פוסטים חדשים": [ 636 | "Newer posts", 637 | null 638 | ], 639 | "ניווט": [ 640 | "Posts navigation", 641 | null 642 | ], 643 | "פלטפורמה סמנטית לפרסום תוכן": [ 644 | "A Semantic Personal Publishing Platform", 645 | null 646 | ], 647 | "גלול חזרה למעלה": [ 648 | "Scroll back to top", 649 | null 650 | ], 651 | "תגובה": [ 652 | "Comment", 653 | null 654 | ], 655 | "להגיב": [ 656 | "Post Comment", 657 | null 658 | ], 659 | "לכתוב תגובה...": [ 660 | "Write a Comment...", 661 | null 662 | ], 663 | "טוען תגובות...": [ 664 | "Loading Comments...", 665 | null 666 | ], 667 | "שלח טקסט עם התגובה שלך.": [ 668 | "Please be sure to submit some text with your comment.", 669 | null 670 | ], 671 | "אנא הכנס כתובת אימייל על מנת להגיב.": [ 672 | "Please provide an email address to comment.", 673 | null 674 | ], 675 | "אנא הכנס את שמך על מנת להגיב.": [ 676 | "Please provide your name to comment.", 677 | null 678 | ], 679 | "מצטעירם, אבל היתה בעייה בפרסום תגובתך. נסה שוב מאוחר יותר": [ 680 | "Sorry, but there was an error posting your comment. Please try again later.", 681 | null 682 | ], 683 | "תגובתך אושרה.": [ 684 | "Your comment was approved.", 685 | null 686 | ], 687 | "תגובתך מחכה לאישור.": [ 688 | "Your comment is in moderation.", 689 | null 690 | ], 691 | "מצלמה": [ 692 | "Camera", 693 | null 694 | ], 695 | "מפתח": [ 696 | "Aperture", 697 | null 698 | ], 699 | "מהירות צמצם": [ 700 | "Shutter Speed", 701 | null 702 | ], 703 | "אורך מוקד": [ 704 | "Focal Length", 705 | null 706 | ], 707 | "פרסם מחדש בבלוג": [ 708 | "Reblog", 709 | null 710 | ], 711 | "פורסם מחדש בבלוג": [ 712 | "Reblogged", 713 | null 714 | ], 715 | "הוסף את המחשבות שלך כאן... (אופציונלי)": [ 716 | "Add your thoughts here... (optional)", 717 | null 718 | ], 719 | "מפרסם מחדש בבלוג...": [ 720 | "Reblogging...", 721 | null 722 | ], 723 | "להפסיק מעקב": [ 724 | "Unfollow", 725 | null 726 | ], 727 | "רשום": [ 728 | "Following", 729 | null 730 | ], 731 | "Edit the excerpt as needed.": [ 732 | "Edit the excerpt as needed.", 733 | null 734 | ], 735 | "נכנס...": [ 736 | "Logging In…", 737 | null 738 | ], 739 | "שליחת תגובה…": [ 740 | "Posting Comment…", 741 | null 742 | ], 743 | "יציאה": [ 744 | "Log Out", 745 | null 746 | ], 747 | "התחבר": [ 748 | "Log In", 749 | null 750 | ], 751 | "רשום את תגובתך כאן": [ 752 | "Please enter a comment", 753 | null 754 | ], 755 | "אנא הכנס/י את כתובת הדוא\"ל שלך כאן": [ 756 | "Please enter your email address here", 757 | null 758 | ], 759 | "כתובת אימייל שגויה": [ 760 | "Invalid email address", 761 | null 762 | ], 763 | "הזן את השם שלך כאן": [ 764 | "Please enter your name here", 765 | null 766 | ], 767 | "התמונה תוצג בכל פעם שתכתוב תגובה. לחץ כדי להתאים אותה אישית.": [ 768 | "This picture will show whenever you leave a comment. Click to customize it.", 769 | null 770 | ], 771 | "היכנס כדי להשתמש בפרטים מאחד החשבונות האלה.": [ 772 | "Log in to use details from one of these accounts.", 773 | null 774 | ], 775 | "לשנות": [ 776 | "Change", 777 | null 778 | ], 779 | "שינוי חשבון": [ 780 | "Change Account", 781 | null 782 | ], 783 | "יצירה של אתר חינמי או בלוג ב־WordPress.com": [ 784 | "Create a free website or blog at WordPress.com", 785 | null 786 | ], 787 | "ערכת עיצוב": [ 788 | "Theme", 789 | null 790 | ], 791 | "מאת": [ 792 | "by", 793 | null 794 | ], 795 | "שלח לכתובת דואר אלקטרוני": [ 796 | "Send to Email Address", 797 | null 798 | ], 799 | "שלח דואר אלקטרוני": [ 800 | "Send Email", 801 | null 802 | ], 803 | "בטל": [ 804 | "Cancel", 805 | null 806 | ], 807 | "הפוסט לא נשלח - בדוק את כתובות המייל בבקשה!": [ 808 | "Post was not sent - check your email addresses!", 809 | null 810 | ], 811 | "הפעולה נכשלה, בקשה נסה שוב": [ 812 | "Email check failed, please try again", 813 | null 814 | ], 815 | "מצטערים, הבלוג שלך אינו יכול לשתף פוסטים בדואר אלקטרוני.": [ 816 | "Sorry, your blog cannot share posts by email.", 817 | null 818 | ], 819 | "פרסם ל-": [ 820 | "Post to", 821 | null 822 | ], 823 | "פרסם מחדש פוסט בבלוג": [ 824 | "Reblog Post", 825 | null 826 | ], 827 | "בלוג בוורדפרס.קום": [ 828 | "Blog at WordPress.com", 829 | null 830 | ], 831 | "למידע נוסף על ערכת עיצוב זו": [ 832 | "Learn more about this theme", 833 | null 834 | ], 835 | "Domingo": [ 836 | "Sunday", 837 | null 838 | ], 839 | "Lunes": [ 840 | "Monday", 841 | null 842 | ], 843 | "Martes": [ 844 | "Tuesday", 845 | null 846 | ], 847 | "Miércoles": [ 848 | "Wednesday", 849 | null 850 | ], 851 | "Jueves": [ 852 | "Thursday", 853 | null 854 | ], 855 | "Viernes": [ 856 | "Friday", 857 | null 858 | ], 859 | "Sábado": [ 860 | "Saturday", 861 | null 862 | ], 863 | "D": [ 864 | "S_Sunday_initial", 865 | null 866 | ], 867 | "L": [ 868 | "M_Monday_initial", 869 | null 870 | ], 871 | "M": [ 872 | "T_Tuesday_initial", 873 | null 874 | ], 875 | "X": [ 876 | "W_Wednesday_initial", 877 | null 878 | ], 879 | "J": [ 880 | "T_Thursday_initial", 881 | null 882 | ], 883 | "V": [ 884 | "F_Friday_initial", 885 | null 886 | ], 887 | "S": [ 888 | "S_Saturday_initial", 889 | null 890 | ], 891 | "Dom": [ 892 | "Sun", 893 | null 894 | ], 895 | "Lun": [ 896 | "Mon", 897 | null 898 | ], 899 | "Mar": [ 900 | "Tue", 901 | null 902 | ], 903 | "Mie": [ 904 | "Wed", 905 | null 906 | ], 907 | "Jue": [ 908 | "Thu", 909 | null 910 | ], 911 | "Vie": [ 912 | "Fri", 913 | null 914 | ], 915 | "Sab": [ 916 | "Sat", 917 | null 918 | ], 919 | "enero": [ 920 | "January", 921 | null 922 | ], 923 | "febrero": [ 924 | "February", 925 | null 926 | ], 927 | "marzo": [ 928 | "March", 929 | null 930 | ], 931 | "abril": [ 932 | "April", 933 | null 934 | ], 935 | "mayo": [ 936 | "May", 937 | null 938 | ], 939 | "junio": [ 940 | "June", 941 | null 942 | ], 943 | "julio": [ 944 | "July", 945 | null 946 | ], 947 | "agosto": [ 948 | "August", 949 | null 950 | ], 951 | "septiembre": [ 952 | "September", 953 | null 954 | ], 955 | "octubre": [ 956 | "October", 957 | null 958 | ], 959 | "noviembre": [ 960 | "November", 961 | null 962 | ], 963 | "diciembre": [ 964 | "December", 965 | null 966 | ], 967 | "ene": [ 968 | "Jan_January_abbreviation", 969 | null 970 | ], 971 | "feb": [ 972 | "Feb_February_abbreviation", 973 | null 974 | ], 975 | "mar": [ 976 | "Mar_March_abbreviation", 977 | null 978 | ], 979 | "abr": [ 980 | "Apr_April_abbreviation", 981 | null 982 | ], 983 | "may": [ 984 | "May_May_abbreviation", 985 | null 986 | ], 987 | "jun": [ 988 | "Jun_June_abbreviation", 989 | null 990 | ], 991 | "jul": [ 992 | "Jul_July_abbreviation", 993 | null 994 | ], 995 | "ago": [ 996 | "Aug_August_abbreviation", 997 | null 998 | ], 999 | "sep": [ 1000 | "Sep_September_abbreviation", 1001 | null 1002 | ], 1003 | "oct": [ 1004 | "Oct_October_abbreviation", 1005 | null 1006 | ], 1007 | "nov": [ 1008 | "Nov_November_abbreviation", 1009 | null 1010 | ], 1011 | "dic": [ 1012 | "Dec_December_abbreviation", 1013 | null 1014 | ], 1015 | "am": [ 1016 | "am", 1017 | null 1018 | ], 1019 | "pm": [ 1020 | "pm", 1021 | null 1022 | ], 1023 | "AM": [ 1024 | "AM", 1025 | null 1026 | ], 1027 | "PM": [ 1028 | "PM", 1029 | null 1030 | ], 1031 | ".": [ 1032 | "number_format_thousands_sep", 1033 | null 1034 | ], 1035 | ",": [ 1036 | "number_format_decimal_point", 1037 | null 1038 | ], 1039 | "Mis sitios": [ 1040 | "My Sites", 1041 | null 1042 | ], 1043 | "Avatar del blog actual": [ 1044 | "Current blog avatar", 1045 | null 1046 | ], 1047 | "Cambiar sitio": [ 1048 | "Switch Site", 1049 | null 1050 | ], 1051 | "Ver sitio": [ 1052 | "View Site", 1053 | null 1054 | ], 1055 | "WP Admin": [ 1056 | "WP Admin", 1057 | null 1058 | ], 1059 | "Estadísticas": [ 1060 | "Stats", 1061 | null 1062 | ], 1063 | "Comentarios": [ 1064 | "Comments", 1065 | null 1066 | ], 1067 | "Entradas del blog": [ 1068 | "Blog Posts", 1069 | null 1070 | ], 1071 | "Publicar": [ 1072 | "Publish", 1073 | "admin bar menu group label" 1074 | ], 1075 | "admin bar menu group label\\u0004Publicar": [ 1076 | "Publish", 1077 | "admin bar menu group label" 1078 | ], 1079 | "Añadir": [ 1080 | "Add", 1081 | "admin bar menu new item label" 1082 | ], 1083 | "admin bar menu new item label\\u0004Añadir": [ 1084 | "Add", 1085 | "admin bar menu new item label" 1086 | ], 1087 | "Aspecto": [ 1088 | "Look and Feel", 1089 | "admin bar menu group label" 1090 | ], 1091 | "admin bar menu group label\\u0004Aspecto": [ 1092 | "Look and Feel", 1093 | "admin bar menu group label" 1094 | ], 1095 | "Temas": [ 1096 | "Themes", 1097 | null 1098 | ], 1099 | "Personalizar": [ 1100 | "Customize", 1101 | null 1102 | ], 1103 | "Menús": [ 1104 | "Menus", 1105 | null 1106 | ], 1107 | "Configuración": [ 1108 | "Configuration", 1109 | null 1110 | ], 1111 | "Compartir": [ 1112 | "Sharing", 1113 | null 1114 | ], 1115 | "Usuarios": [ 1116 | "Users", 1117 | null 1118 | ], 1119 | "Mejoras": [ 1120 | "Upgrades", 1121 | null 1122 | ], 1123 | "Enlace corto": [ 1124 | "Shortlink", 1125 | null 1126 | ], 1127 | "Lector": [ 1128 | "Reader", 1129 | null 1130 | ], 1131 | "Blogs que sigo": [ 1132 | "Blogs I Follow", 1133 | null 1134 | ], 1135 | "Descubrir": [ 1136 | "Discover", 1137 | "admin bar menu group label" 1138 | ], 1139 | "admin bar menu group label\\u0004Descubrir": [ 1140 | "Discover", 1141 | "admin bar menu group label" 1142 | ], 1143 | "Más recientes": [ 1144 | "Freshly Pressed", 1145 | null 1146 | ], 1147 | "Blogs recomendados": [ 1148 | "Recommended Blogs", 1149 | null 1150 | ], 1151 | "Buscar amigos": [ 1152 | "Find Friends", 1153 | null 1154 | ], 1155 | "Mi Actividad": [ 1156 | "My Activity", 1157 | null 1158 | ], 1159 | "Mis Comentarios": [ 1160 | "My Comments", 1161 | null 1162 | ], 1163 | "Mis me gusta": [ 1164 | "My Likes", 1165 | null 1166 | ], 1167 | "Seguir": [ 1168 | "Follow", 1169 | null 1170 | ], 1171 | "Mostrar las visitas del sitio por hora para las últimas 48 horas. Haz clic en todas las estadísticas del sitio.": [ 1172 | "Showing site views per hour for the last 48 hours. Click for full Site Stats.", 1173 | null 1174 | ], 1175 | "Denunciar este contenido": [ 1176 | "Report this content", 1177 | null 1178 | ], 1179 | "Cerrar sesión": [ 1180 | "Sign Out", 1181 | null 1182 | ], 1183 | "Perfil": [ 1184 | "Profile", 1185 | null 1186 | ], 1187 | "Configuración de la cuenta": [ 1188 | "Account Settings", 1189 | null 1190 | ], 1191 | "Colección de trofeos": [ 1192 | "Trophy Case", 1193 | null 1194 | ], 1195 | "Facturación": [ 1196 | "Billing", 1197 | null 1198 | ], 1199 | "Añadidos": [ 1200 | "Extras", 1201 | null 1202 | ], 1203 | "Ayuda": [ 1204 | "Help", 1205 | null 1206 | ], 1207 | "Este blog es de acceso público": [ 1208 | "This blog is public", 1209 | null 1210 | ], 1211 | "Blog Links": [ 1212 | "Blog Links", 1213 | null 1214 | ], 1215 | "Not currently in the User Showcase": [ 1216 | "Not currently in the User Showcase", 1217 | null 1218 | ], 1219 | "Go to Showcase Guidelines": [ 1220 | "Go to Showcase Guidelines", 1221 | null 1222 | ], 1223 | "Go to Theme Credits": [ 1224 | "Go to Theme Credits", 1225 | null 1226 | ], 1227 | "WordPress.com Showcase": [ 1228 | "WordPress.com Showcase", 1229 | null 1230 | ], 1231 | "Email Blog Owner": [ 1232 | "Email Blog Owner", 1233 | null 1234 | ], 1235 | "El ID de menú, no puede estar vacio.": [ 1236 | "The menu ID should not be empty.", 1237 | null 1238 | ], 1239 | "Blog Id: 9310870": [ 1240 | "Blog Id: 9310870", 1241 | null 1242 | ], 1243 | "Blog Dashboard": [ 1244 | "Blog Dashboard", 1245 | null 1246 | ], 1247 | "Estadísticas del sitio": [ 1248 | "Site Stats", 1249 | null 1250 | ], 1251 | "Entradas": [ 1252 | "Posts", 1253 | null 1254 | ], 1255 | "Mis mejoras": [ 1256 | "My Upgrades", 1257 | null 1258 | ], 1259 | "Dominios": [ 1260 | "Domains", 1261 | null 1262 | ], 1263 | "All Blog Options": [ 1264 | "All Blog Options", 1265 | null 1266 | ], 1267 | "Blog's Network Admin": [ 1268 | "Blog's Network Admin", 1269 | null 1270 | ], 1271 | "Blog Info": [ 1272 | "Blog Info", 1273 | null 1274 | ], 1275 | "Blog Users": [ 1276 | "Blog Users", 1277 | null 1278 | ], 1279 | "Blog Themes": [ 1280 | "Blog Themes", 1281 | null 1282 | ], 1283 | "Blog Upgrades": [ 1284 | "Blog Upgrades", 1285 | null 1286 | ], 1287 | "User's Network Admin": [ 1288 | "User's Network Admin", 1289 | null 1290 | ], 1291 | "User's Store Admin": [ 1292 | "User's Store Admin", 1293 | null 1294 | ], 1295 | "Load Page Without Cache": [ 1296 | "Load Page Without Cache", 1297 | null 1298 | ], 1299 | "Clear Page's Cache": [ 1300 | "Clear Page's Cache", 1301 | null 1302 | ], 1303 | "Posts Per Page = 1": [ 1304 | "Posts Per Page = 1", 1305 | null 1306 | ], 1307 | "Desactivar Scroll infinito": [ 1308 | "Disable Infinite Scroll", 1309 | null 1310 | ], 1311 | "Ocultar anuncios": [ 1312 | "Hide adverts", 1313 | null 1314 | ], 1315 | "Abrir la barra de herramientas": [ 1316 | "Skip to toolbar", 1317 | null 1318 | ], 1319 | "Barra de herramientas en la parte superior.": [ 1320 | "Top navigation toolbar.", 1321 | null 1322 | ], 1323 | "יום ראשון": [ 1324 | "Sunday", 1325 | null 1326 | ], 1327 | "יום שני": [ 1328 | "Monday", 1329 | null 1330 | ], 1331 | "יום שלישי": [ 1332 | "Tuesday", 1333 | null 1334 | ], 1335 | "יום רביעי": [ 1336 | "Wednesday", 1337 | null 1338 | ], 1339 | "יום חמישי": [ 1340 | "Thursday", 1341 | null 1342 | ], 1343 | "יום שישי": [ 1344 | "Friday", 1345 | null 1346 | ], 1347 | "יום שבת": [ 1348 | "Saturday", 1349 | null 1350 | ], 1351 | "א": [ 1352 | "S_Sunday_initial", 1353 | null 1354 | ], 1355 | "ב": [ 1356 | "M_Monday_initial", 1357 | null 1358 | ], 1359 | "ג": [ 1360 | "T_Tuesday_initial", 1361 | null 1362 | ], 1363 | "ד": [ 1364 | "W_Wednesday_initial", 1365 | null 1366 | ], 1367 | "ה": [ 1368 | "T_Thursday_initial", 1369 | null 1370 | ], 1371 | "ו": [ 1372 | "F_Friday_initial", 1373 | null 1374 | ], 1375 | "ש": [ 1376 | "S_Saturday_initial", 1377 | null 1378 | ], 1379 | "ינואר": [ 1380 | "January", 1381 | null 1382 | ], 1383 | "פברואר": [ 1384 | "February", 1385 | null 1386 | ], 1387 | "מרץ": [ 1388 | "March", 1389 | null 1390 | ], 1391 | "אפריל": [ 1392 | "April", 1393 | null 1394 | ], 1395 | "מאי": [ 1396 | "May", 1397 | null 1398 | ], 1399 | "יוני": [ 1400 | "June", 1401 | null 1402 | ], 1403 | "יולי": [ 1404 | "July", 1405 | null 1406 | ], 1407 | "אוגוסט": [ 1408 | "August", 1409 | null 1410 | ], 1411 | "ספטמבר": [ 1412 | "September", 1413 | null 1414 | ], 1415 | "אוקטובר": [ 1416 | "October", 1417 | null 1418 | ], 1419 | "נובמבר": [ 1420 | "November", 1421 | null 1422 | ], 1423 | "דצמבר": [ 1424 | "December", 1425 | null 1426 | ] 1427 | }, 1428 | "placeholdersUsedOnPage": { 1429 | "%1$s %2$s פיד‏": [ 1430 | "%1$s %2$s Feed", 1431 | "(.*?) (.*?) פיד‏", 1432 | null 1433 | ], 1434 | "%1$s %2$s פיד תגובות‏": [ 1435 | "%1$s %2$s Comments Feed", 1436 | "(.*?) (.*?) פיד תגובות‏", 1437 | null 1438 | ], 1439 | "%1$s %2$s תגובות על הפוסט \"%3$s\"‏": [ 1440 | "%1$s %2$s %3$s Comments Feed", 1441 | "(.*?) (.*?) תגובות על הפוסט \"(.*?)\"‏", 1442 | null 1443 | ], 1444 | "%1$s %2$s פוסטים מהקטגוריה \"%3$s\"‏": [ 1445 | "%1$s %2$s %3$s Category Feed", 1446 | "(.*?) (.*?) פוסטים מהקטגוריה \"(.*?)\"‏", 1447 | null 1448 | ], 1449 | "%1$s %2$s פוסטים עם התג \"%3$s\"‏": [ 1450 | "%1$s %2$s %3$s Tag Feed", 1451 | "(.*?) (.*?) פוסטים עם התג \"(.*?)\"‏", 1452 | null 1453 | ], 1454 | "%1$s %2$s פוסטים מאת %3$s‏": [ 1455 | "%1$s %2$s Posts by %3$s Feed", 1456 | "(.*?) (.*?) פוסטים מאת (.*?)‏", 1457 | null 1458 | ], 1459 | "%1$s %2$s תוצאות החיפוש \"%3$s\" (פיד עדכונים)‏": [ 1460 | "%1$s %2$s Search Results for “%3$s” Feed", 1461 | "(.*?) (.*?) תוצאות החיפוש \"(.*?)\" \\(פיד עדכונים\\)‏", 1462 | null 1463 | ], 1464 | "הזנת %1$s %2$s %3$s": [ 1465 | "%1$s %2$s %3$s Feed", 1466 | "הזנת (.*?) (.*?) (.*?)", 1467 | null 1468 | ], 1469 | "חבר פוסטים, נהל תגובות ונהל את %s.": [ 1470 | "Author posts, manage comments, and manage %s.", 1471 | "חבר פוסטים, נהל תגובות ונהל את (.*?)\\.", 1472 | null 1473 | ], 1474 | "(by %1$s)": [ 1475 | "(by %1$s)", 1476 | "\\(by (.*?)\\)", 1477 | "(by User Name)" 1478 | ], 1479 | "(by User Name)\\u0004(by %1$s)": [ 1480 | "(by %1$s)", 1481 | "\\(by (.*?)\\)", 1482 | "(by User Name)" 1483 | ], 1484 | "%1$s ב- %2$s": [ 1485 | "%1$s on %2$s", 1486 | "(.*?) ב\\- (.*?)", 1487 | "Blog Title on WordPress.com" 1488 | ], 1489 | "Blog Title on WordPress.com\\u0004%1$s ב- %2$s": [ 1490 | "%1$s on %2$s", 1491 | "(.*?) ב\\- (.*?)", 1492 | "Blog Title on WordPress.com" 1493 | ], 1494 | "%1$s על %2$s": [ 1495 | "%1$s on %2$s", 1496 | "(.*?) על (.*?)", 1497 | "Recent Comments Widget" 1498 | ], 1499 | "Recent Comments Widget\\u0004%1$s על %2$s": [ 1500 | "%1$s on %2$s", 1501 | "(.*?) על (.*?)", 1502 | "Recent Comments Widget" 1503 | ], 1504 | "פורסם בתאריך %s": [ 1505 | "Posted on %s", 1506 | "פורסם בתאריך (.*?)", 1507 | "post date" 1508 | ], 1509 | "post date\\u0004פורסם בתאריך %s": [ 1510 | "Posted on %s", 1511 | "פורסם בתאריך (.*?)", 1512 | "post date" 1513 | ], 1514 | "in %1$s": [ 1515 | "in %1$s", 1516 | "in (.*?)", 1517 | "categories list" 1518 | ], 1519 | "categories list\\u0004in %1$s": [ 1520 | "in %1$s", 1521 | "in (.*?)", 1522 | "categories list" 1523 | ], 1524 | "כל הרשומות של %s": [ 1525 | "All %s posts", 1526 | "כל הרשומות של (.*?)", 1527 | null 1528 | ], 1529 | "פועל על %s": [ 1530 | "Proudly powered by %s", 1531 | "פועל על (.*?)", 1532 | null 1533 | ], 1534 | "ערכת עיצוב: %1$s של %2$s": [ 1535 | "Theme: %1$s by %2$s.", 1536 | "ערכת עיצוב\\: (.*?) של (.*?)", 1537 | null 1538 | ], 1539 | "הצג בגודל מלא %1$s×%2$s": [ 1540 | "View full size %1$s×%2$s", 1541 | "הצג בגודל מלא \\(.*?)\\×\\<\\/span\\>(.*?)\\<\\/span\\>", 1542 | null 1543 | ], 1544 | "מגיב כ-%s": [ 1545 | "Commenting as %s", 1546 | "מגיב כ\\-(.*?)", 1547 | null 1548 | ], 1549 | "New posts from this blog will now appear in your reader.": [ 1550 | "New posts from this blog will now appear in your reader.", 1551 | "New posts from this blog will now appear in \\your reader\\<\\/a\\>\\.", 1552 | null 1553 | ], 1554 | "Submit this post to the %s TopicPress queue.": [ 1555 | "Submit this post to the %s TopicPress queue.", 1556 | "Submit this post to the (.*?) TopicPress queue\\.", 1557 | null 1558 | ], 1559 | "מתחבר ל-%s": [ 1560 | "Connecting to %s", 1561 | "מתחבר ל\\-(.*?)", 1562 | null 1563 | ], 1564 | "%1$s: אתה מגיב באמצעות חשבון %2$s שלך.": [ 1565 | "%1$s: You are commenting using your %2$s account.", 1566 | "(.*?)\\: אתה מגיב באמצעות חשבון (.*?) שלך\\.", 1567 | null 1568 | ], 1569 | "ערכת נושא: %1$s.": [ 1570 | "Theme: %1$s.", 1571 | "ערכת נושא\\: (.*?)\\.", 1572 | null 1573 | ], 1574 | "%d בלוגרים אהבו את זה:": [ 1575 | "%d bloggers like this:", 1576 | "\\(.*?)\\<\\/span\\> בלוגרים אהבו את זה\\:", 1577 | null 1578 | ], 1579 | "ערכות עיצוב %s": [ 1580 | "The %s Theme", 1581 | "ערכות עיצוב (.*?)", 1582 | null 1583 | ] 1584 | }, 1585 | "localeCode": "he", 1586 | "languageName": "עברית", 1587 | "pluralForms": "nplurals=2; plural=n != 1", 1588 | "glotPress": { 1589 | "url": "https://translate.wordpress.com", 1590 | "project": "wpcom,wpcom/themes/hew" 1591 | } 1592 | }; 1593 | -------------------------------------------------------------------------------- /test/string-extraction/translation-data/pt-br.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stringsUsedOnPage: { 3 | 4 | }, 5 | placeholdersUsedOnPage: { 6 | "%1$s MB (%2$s%%) de espacio usado": [ 7 | "%1$s MB (%2$s%%) Space Used", 8 | "(.{0,100}) MB \\\((.{0,100})%\\\) de espacio usado", 9 | null 10 | ], 11 | 'Showing %1$s of %2$s %3$s posts': [ 12 | "Showing %1$s of %2$s %3$s posts", 13 | "Showing (.{0,100}) of (.{0,100}) (.{0,100}) posts", 14 | null 15 | ] 16 | }, 17 | languageName: "Português do Brasil", 18 | localeCode: "pt-br", 19 | pluralForms: "nplurals=2; plural=(n > 1)", 20 | glotPress: { 21 | url: 'https://translate.wordpress.com', 22 | project: 'wpcom,wpcom/themes/scrawl' 23 | }, 24 | locale: {} 25 | }; 26 | -------------------------------------------------------------------------------- /test/string-extraction/translation-pair.js: -------------------------------------------------------------------------------- 1 | var assert = require( 'chai' ).assert, 2 | fs = require( 'fs' ), 3 | Locale = require( '../../lib/locale' ), 4 | Walker = require( '../../lib/walker' ), 5 | TranslationPair = require( '../../lib/translation-pair' ); 6 | 7 | describe( 'String Extraction', function() { 8 | 9 | 10 | beforeEach( function( done ) { 11 | 12 | fs.readFile( './test/string-extraction/test.html', 'utf-8', function( err, htmlFromFile ) { 13 | if ( err ) { 14 | throw err; 15 | } 16 | $( 'body' ).html( htmlFromFile ); 17 | done(); 18 | } ); 19 | 20 | } ); 21 | 22 | describe( 'Brazilian Portuguese', function() { 23 | var translationData = require( './translation-data/pt-br.js' ); 24 | before( function( done ) { 25 | TranslationPair.setTranslationData( translationData ); 26 | done(); 27 | } ); 28 | 29 | it( 'should not match the outer elements', function() { 30 | assert.isFalse( TranslationPair.extractFrom( $( 'body' ) ) ); 31 | } ); 32 | 33 | it( 'should match the inner elements', function() { 34 | assert.equal( TranslationPair.extractFrom( $( '#inner-element' ) ).getOriginal().getSingular(), 35 | translationData.placeholdersUsedOnPage[ 0 ].original 36 | ); 37 | 38 | assert.equal( TranslationPair.extractFrom( $( '#o2_extend_resolved_posts_unresolved_posts-2 div div span.showing' ) ).getOriginal().getSingular(), 39 | translationData.placeholdersUsedOnPage[ 1 ].original 40 | ); 41 | } ); 42 | 43 | } ); 44 | 45 | describe( 'Hebrew', function() { 46 | var walker, 47 | translationData = require( './translation-data/he.js' ); 48 | before( function( done ) { 49 | TranslationPair.setTranslationData( translationData ); 50 | walker = new Walker( TranslationPair, $ ); 51 | done(); 52 | } ); 53 | 54 | it( 'should not match the outer elements', function() { 55 | assert.equal( TranslationPair.extractFrom( $( 'body' ) ), false ); 56 | } ); 57 | 58 | it( 'should match the inner elements', function() { 59 | assert.equal( TranslationPair.extractFrom( $( '#yoav span.posted-on' ) ).getOriginal().getSingular(), 60 | translationData.placeholdersUsedOnPage[ 15 ].original 61 | ); 62 | } ); 63 | it( 'should not match too much', function() { 64 | walker.walkTextNodes( $( 'body' )[ 0 ], function( translationPair, enclosingNode ) { 65 | enclosingNode.addClass( 'translator-translatable' ); 66 | } ); 67 | assert.isFalse( $( '#yoav div.entry-meta' ).hasClass( 'translator-translatable' ) ); 68 | assert.equal( $( '#yoav .translator-translatable' ).length, 2 ); 69 | assert.isTrue( $( '#yoav .translator-checked' ).length > 0 ); 70 | } ); 71 | 72 | } ); 73 | 74 | describe( 'English UK', function() { 75 | var walker, translationData = require( './translation-data/en-uk.js' ); 76 | before( function( done ) { 77 | TranslationPair.setTranslationData( translationData ); 78 | walker = new Walker( TranslationPair, $ ); 79 | done(); 80 | } ); 81 | 82 | it( 'should not match the outer elements', function() { 83 | assert.equal( TranslationPair.extractFrom( $( 'body' ) ), false ); 84 | } ); 85 | 86 | 87 | it( 'should not match the content text', function() { 88 | walker.walkTextNodes( $( '#jack-lenox' )[ 0 ], function( translationPair, enclosingNode ) { 89 | enclosingNode.addClass( 'translator-translatable' ); 90 | } ); 91 | assert.isTrue( $( '#jack-lenox p' ).hasClass( 'translator-checked' ) ); 92 | assert.isFalse( $( '#jack-lenox p' ).hasClass( 'translator-translatable' ) ); 93 | assert.equal( $( '.translator-translatable' ).length, 0 ); 94 | } ); 95 | 96 | } ); 97 | 98 | describe( 'Spanish', function() { 99 | var walker, 100 | translationData = require( './translation-data/es.js' ); 101 | before( function( done ) { 102 | TranslationPair.setTranslationData( translationData ); 103 | walker = new Walker( TranslationPair, $ ); 104 | done(); 105 | } ); 106 | 107 | it( 'should match the Reply button', function() { 108 | walker.walkTextNodes( $( '#jp-post-flair' )[ 0 ], function( translationPair, enclosingNode ) { 109 | enclosingNode.addClass( 'translator-translatable' ); 110 | } ); 111 | assert.isTrue( $( '#jp-post-flair a.o2-reply' ).hasClass( 'translator-checked' ) ); 112 | assert.isTrue( $( '#jp-post-flair a.o2-reply' ).hasClass( 'translator-translatable' ) ); 113 | } ); 114 | 115 | } ); 116 | 117 | describe( 'anyChildMatches', function() { 118 | 119 | it( 'should matches direct children', function() { 120 | assert.isTrue( TranslationPair._test.anyChildMatches( $( 'div.sub' ), /blocks\s*spam/ ) ); 121 | } ); 122 | 123 | it( 'should matches indirect children', function() { 124 | assert.isTrue( TranslationPair._test.anyChildMatches( $( 'body' ), /blocks\s*spam/ ) ); 125 | } ); 126 | 127 | it( 'should match placeholders', function() { 128 | var spaceUsedRegex = /\s*(.*?) MB \((.*?)%\) de espacio usado\s*/; 129 | assert.isTrue( TranslationPair._test.anyChildMatches( $( 'div.sub' ), spaceUsedRegex ) ); 130 | } ); 131 | 132 | it( 'Should not match itself', function() { 133 | assert.isFalse( TranslationPair._test.anyChildMatches( $( 'p.akismet-right-now' ), /blocks\s*spam/ ) ); 134 | } ); 135 | } ); 136 | 137 | } ); 138 | -------------------------------------------------------------------------------- /test/translation-pair.js: -------------------------------------------------------------------------------- 1 | var assert = require( 'chai' ).assert, 2 | fs = require( 'fs' ), 3 | Locale = require( '../lib/locale' ), 4 | TranslationPair = require( '../lib/translation-pair' ), 5 | jsdom = require( 'jsdom' ); 6 | 7 | describe( 'Translation Pair: Existing translation', function() { 8 | var locale = new Locale( 'de', 'German', 'nplurals=2; plural=n != 1;' ); 9 | var translationPair1 = new TranslationPair( locale, [ 'Add' ], null, [ 'Hinzufügen' ] ); 10 | var translationPair2 = new TranslationPair( locale, [ '%(numberOfThings) thing', '%(numberOfThings) things' ], null, [ '%(numberOfThings) Ding', '%(numberOfThings) Dinge' ] ); 11 | 12 | it( 'should have an original', function() { 13 | assert( 'Original' === translationPair1.getOriginal().type ); 14 | } ); 15 | 16 | it( 'should have a translation', function() { 17 | assert( 'Translation' === translationPair1.getTranslation().type ); 18 | } ); 19 | 20 | } ); 21 | 22 | describe( 'Translation Pair: No translation', function() { 23 | var tr, 24 | locale = new Locale( 'de', 'German', 'nplurals=2; plural=n != 1;' ); 25 | var translationPair1 = new TranslationPair( locale, [ 'Add' ], null ); 26 | var translationPair2 = new TranslationPair( locale, [ '%(numberOfThings) thing', '%(numberOfThings) things' ] ); 27 | 28 | it( 'should have an original', function() { 29 | assert( 'Original' === translationPair1.getOriginal().type ); 30 | } ); 31 | 32 | it( 'should have a translation object', function() { 33 | assert( 'Translation' === translationPair1.getTranslation().type ); 34 | } ); 35 | 36 | it( 'should have all empty textitems', function() { 37 | assert( "" === translationPair1.getTranslation().getTextItems()[ 0 ].getText() ); 38 | } ); 39 | 40 | it( 'should have only 1 translation fields for a singular-only original', function() { 41 | assert( 1 === translationPair1.getTranslation().getTextItems().length ); 42 | } ); 43 | 44 | it( 'should have 2 translation fields for a singular-plural original', function() { 45 | assert( 2 === translationPair2.getTranslation().getTextItems().length ); 46 | } ); 47 | 48 | it( 'should retain a replaced translation', function() { 49 | tr = [ 'Hinzufügen' ]; 50 | replaceTranslation( translationPair1, tr ); 51 | assert( tr[ 0 ] === translationPair1.getTranslation().getTextItems()[ 0 ].getText() ); 52 | assert( 1 === translationPair1.getTranslation().getTextItems().length ); 53 | } ); 54 | 55 | it( 'should retain replaced translations', function() { 56 | tr = [ '%(numberOfThings) Ding', '%(numberOfThings) Dinge' ]; 57 | replaceTranslation( translationPair2, tr ); 58 | assert( tr[ 0 ] === translationPair2.getTranslation().getTextItems()[ 0 ].getText() ); 59 | assert( tr[ 1 ] === translationPair2.getTranslation().getTextItems()[ 1 ].getText() ); 60 | assert( 2 === translationPair2.getTranslation().getTextItems().length ); 61 | } ); 62 | 63 | it( 'should not allow a wrong number of translations', function() { 64 | tr = [ 'Hinzufügen' ]; 65 | assert( false === replaceTranslation( translationPair1, [ 'Hinzufügen', 'Hinzufügen' ] ) ); 66 | assert( false === replaceTranslation( translationPair1, [ '%(numberOfThings) Ding', '%(numberOfThings) Dinge' ] ) ); 67 | assert( false === replaceTranslation( translationPair2, [ 'Hinzufügen' ] ) ); 68 | assert( false === replaceTranslation( translationPair2, [ '%(numberOfThings) Ding' ] ) ); 69 | assert( tr[ 0 ] === translationPair1.getTranslation().getTextItems()[ 0 ].getText() ); 70 | } ); 71 | 72 | function replaceTranslation( translationPair, translation ) { 73 | var i, 74 | t = {}; 75 | for ( i = 0; i < translation.length; i++ ) { 76 | t[ 'translation_' + i ] = translation[ i ]; 77 | } 78 | return translationPair.updateAllTranslations( [ t ] ); 79 | } 80 | 81 | } ); 82 | --------------------------------------------------------------------------------