├── .github └── workflows │ └── main.yml ├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── build ├── .gitignore └── logs │ └── .gitignore ├── changelog.md ├── composer.json ├── fixtures ├── .gitignore ├── basic.po ├── basicCollection.po ├── basicCustomHeaders.po ├── basicHeader.po ├── basicHeadersMultiline.po ├── basicMultiline.po ├── basicOnlyHeader.po ├── basicReference.po ├── codeComments.po ├── context.po ├── multiflags.po ├── noblankline.po ├── oldEntries.po ├── plurals.po ├── pluralsMultiline.po ├── previousString.po ├── previousStringMultiline.po ├── quotes.po └── translatorComments.po ├── license.txt ├── phpunit.xml ├── readme.md ├── src ├── Catalog │ ├── Catalog.php │ ├── CatalogArray.php │ ├── Entry.php │ ├── EntryFactory.php │ └── Header.php ├── Exception │ └── ParseException.php ├── Parser.php ├── PoCompiler.php └── SourceHandler │ ├── FileSystem.php │ ├── SourceHandler.php │ └── StringSource.php └── tests ├── AbstractFixtureTest.php ├── EntryBuilder.php ├── UnitTest ├── HeaderTest.php ├── ParserTest.php ├── PoCompilerTest.php └── ReadPoTest.php ├── WriteTest.php └── pofiles ├── context.po ├── flags-phpformat-fuzzy.po ├── flags-phpformat.po ├── healthy.po ├── multilines.po ├── noheader.po ├── pluralsMultiline.po └── previous_unstranslated.po /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: Composer 11 | uses: php-actions/composer@v6 12 | with: 13 | php_version: "7.3" 14 | - name: PHPUnit tests 15 | uses: php-actions/phpunit@v3 16 | with: 17 | version: "4.8.36" 18 | php_version: "7.3" 19 | 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.phar 3 | composer.lock -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | before_commands: 2 | - "composer install --prefer-source" 3 | 4 | tools: 5 | php_code_coverage: 6 | enabled: true 7 | php_code_sniffer: 8 | enabled: true 9 | config: 10 | standard: PSR2 11 | filter: 12 | paths: ["src/*", "tests/*"] 13 | php_cpd: 14 | enabled: true 15 | excluded_dirs: ["build/*", "tests", "vendor"] 16 | php_analyzer: 17 | enabled: true 18 | filter: 19 | paths: ["src/*", "tests/*"] 20 | php_mess_detector: 21 | enabled: true 22 | filter: 23 | paths: ["src/*"] 24 | php_pdepend: 25 | enabled: true 26 | excluded_dirs: ["build", "tests", "vendor"] 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | matrix: 4 | include: 5 | - php: 5.3 6 | dist: precise 7 | - php: 5.4 8 | dist: trusty 9 | - php: 5.5 10 | dist: trusty 11 | - php: 5.6 12 | - php: 7.0 13 | - php: 7.1 14 | - php: 7.2 15 | - php: 7.3 16 | - php: 7.4 17 | 18 | before_script: 19 | - composer self-update 20 | - composer install --prefer-dist 21 | 22 | script: 23 | - ./vendor/bin/phpunit --coverage-clover ./build/logs/clover.xml 24 | -------------------------------------------------------------------------------- /build/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !logs 3 | !.gitignore 4 | !coverage-checker.php -------------------------------------------------------------------------------- /build/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | v5.1.7 (2019-06-03) 2 | * Fix not fully flagging obsolete entries (thanks @Stefaminator). 3 | * Fix long lines wrapping not taking into account multi-byte characters. (thanks @Stefaminator). 4 | 5 | v5.1.6 (2018-07-20) 6 | * Fix wrong initialization of plural strings. 7 | 8 | v5.1.5 (2018-04-17) 9 | * Fix #79 Prevent splitting multibyte characters across lines (thanks @chrisminett) 10 | 11 | v5.1.4 (2018-04-05) 12 | * Fix #86 by making parseFile and parseString both return a Catalog 13 | 14 | v5.1.3 (2018-03-28) 15 | * Fix #83, double escaped special characters. 16 | * Add support for `msgid_plural` in `EntryFactory::createFromArray` 17 | 18 | v5.1.2 (2018-02-26) 19 | * Fix PoCompiler by adding missing quotes and \n in headers. 20 | 21 | v5.1.1 (2018-02-10) 22 | * Header::setHeaders() to allow modifying PO headers. 23 | 24 | v5.1 (2018-02-04) 25 | * Parser::parser() optionally accepts a Catalog interface implementation. 26 | * Parser refactor for easier maintenance. 27 | * Fix parsing comments without space between `#` and text. 28 | * Fix parsing multiline Headers. 29 | * Improve Compiler by creating an empty `msgstr[n]` for every plural form defined. 30 | * Improve parsing headers by offering a base for more granular interpretation. 31 | 32 | v5.0 (2018-02-02) 33 | * Backwards incompatible version! Check [v5 Documentation]() and [Migration guide]() for more information. 34 | * Refactored to avoid usage issues like [this](https://github.com/raulferras/PHP-po-parser/issues/67), [this](https://github.com/raulferras/PHP-po-parser/issues/62), [this](https://github.com/raulferras/PHP-po-parser/issues/52), [this](https://github.com/raulferras/PHP-po-parser/issues/50) 35 | * PSR-4 36 | * New feature: All entry properties of an entry can now be edited. 37 | * New feature: Output files wraps long strings. 38 | * Fixes parsing previous strings wrapped in multiple lines. 39 | * Fix: Obsolete entries were ignoring `msgctxt` properties. 40 | * Fix: Obsolete entries does not output `msgstr` properties. 41 | * Fixes some corner cases reported. 42 | * More tests! 43 | * Main deprecations (check [Migration guide]() for more information.): 44 | - Namespace changed to `Sepia\PoParser` 45 | - `PoParser` renamed to `Parser` 46 | - `parser` method does not return an array anymore, instead a `CatalogArray` object. 47 | - No need for options anymore. 48 | 49 | v4.2.2 50 | * More PHPDocs fixes 51 | * Strict comparisons used where safe. 52 | * Fix example for `writeFile`. 53 | * Support for EOL line formatting. 54 | 55 | v4.2.1 56 | * Support multiline for plural entries (thanks @Ben-Ho) 57 | 58 | v4.2.0 59 | * Add function to add plural and context to existing entry (thanks @Ben-Ho) 60 | * Add ability to change msg id of entry (thanks @wildex) 61 | 62 | v4.1.1 63 | * Fixes with multi-flags entries (thanks @gnouet) 64 | 65 | v4.1 66 | * Constructor now accepts options to define separator used in multiline msgid entries. 67 | * New method `getOptions()`. 68 | 69 | v4.0 70 | 71 | * new methods parseString() and parseFile() replace the old parse()` 72 | * new method writeFile() replaces the old write(). 73 | * new method compile() which takes all parsed entries and coverts back to a PO formatted string. 74 | 75 | [See whole changelog](https://github.com/raulferras/PHP-po-parser/wiki/Changelog) 76 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sepia/po-parser", 3 | "description": "Gettext *.PO file parser for PHP.", 4 | "type": "library", 5 | "keywords": [ 6 | "i18n", 7 | "i10n", 8 | "po", 9 | "gettext" 10 | ], 11 | "homepage": "https://github.com/raulferras/PHP-po-parser", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Raúl Ferràs", 16 | "email": "raul.ferras@gmail.com", 17 | "role": "developer" 18 | } 19 | ], 20 | "support": { 21 | "issues": "https://github.com/raulferras/PHP-po-parser/issues" 22 | }, 23 | "require": { 24 | "php": ">=5.3", 25 | "symfony/polyfill-mbstring": "^1" 26 | }, 27 | "require-dev": { 28 | "squizlabs/php_codesniffer": "^2.0", 29 | "fzaninotto/faker": "^1.7", 30 | "phpunit/phpunit": "^5" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Sepia\\PoParser\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Sepia\\Test\\": "tests/" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /fixtures/.gitignore: -------------------------------------------------------------------------------- 1 | /temp.po 2 | -------------------------------------------------------------------------------- /fixtures/basic.po: -------------------------------------------------------------------------------- 1 | msgid "string.1" 2 | msgstr "translation.1" 3 | 4 | msgid "string.2" 5 | msgstr "translation \"quoted\"" -------------------------------------------------------------------------------- /fixtures/basicCollection.po: -------------------------------------------------------------------------------- 1 | msgid "string.1" 2 | msgstr "translation.1" 3 | 4 | msgid "string.2" 5 | msgstr "translation.2" 6 | -------------------------------------------------------------------------------- /fixtures/basicCustomHeaders.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "X-Poedit-SourceCharset: UTF-8\n" 4 | "X-Poedit-KeywordsList: __;_e;_n;_t\n" 5 | "X-Textdomain-Support: yes\n" 6 | "X-Poedit-Basepath: .\n" 7 | "X-Generator: Poedit 1.5.7\n" 8 | "X-Poedit-SearchPath-0: .\n" 9 | "X-Poedit-SearchPath-1: ../..\n" 10 | "X-Poedit-SearchPath-2: ../../../modules\n" 11 | 12 | msgid "string.1" 13 | msgstr "translation.1" 14 | -------------------------------------------------------------------------------- /fixtures/basicHeader.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: volga-volga.local\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2014-07-08 07:51+0400\n" 6 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 7 | "Last-Translator: FULL NAME \n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 12 | "X-Poedit-SourceCharset: UTF-8\n" 13 | "X-Poedit-KeywordsList: __;_e;_n;_t\n" 14 | "X-Textdomain-Support: yes\n" 15 | "X-Poedit-Basepath: .\n" 16 | "X-Generator: Poedit 1.5.7\n" 17 | "X-Poedit-SearchPath-0: .\n" 18 | "X-Poedit-SearchPath-1: ../..\n" 19 | "X-Poedit-SearchPath-2: ../../../modules\n" 20 | 21 | msgid "string.1" 22 | msgstr "translation.1" 23 | -------------------------------------------------------------------------------- /fixtures/basicHeadersMultiline.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: myproject\n" 4 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 5 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 6 | "POT-Creation-Date: 2017-10-10 16:50+0200\n" 7 | 8 | msgid "string.1" 9 | msgstr "translation.1" -------------------------------------------------------------------------------- /fixtures/basicMultiline.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | "string.1" 3 | msgstr "" 4 | "translation line 1 " 5 | "translation line 2" 6 | -------------------------------------------------------------------------------- /fixtures/basicOnlyHeader.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: volga-volga.local\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2014-07-08 07:51+0400\n" 6 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 7 | "Last-Translator: FULL NAME \n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 12 | "X-Poedit-SourceCharset: UTF-8\n" 13 | "X-Poedit-KeywordsList: __;_e;_n;_t\n" 14 | "X-Textdomain-Support: yes\n" 15 | "X-Poedit-Basepath: .\n" 16 | "X-Generator: Poedit 1.5.7\n" 17 | "X-Poedit-SearchPath-0: .\n" 18 | "X-Poedit-SearchPath-1: ../..\n" 19 | "X-Poedit-SearchPath-2: ../../../modules\n" 20 | -------------------------------------------------------------------------------- /fixtures/basicReference.po: -------------------------------------------------------------------------------- 1 | #: src/views/forms.php:44 2 | msgid "string.1" 3 | msgstr "translation.1" 4 | -------------------------------------------------------------------------------- /fixtures/codeComments.po: -------------------------------------------------------------------------------- 1 | #. code comment 2 | #. code translator comment 3 | msgid "string.1" 4 | msgstr "translation.1" 5 | -------------------------------------------------------------------------------- /fixtures/context.po: -------------------------------------------------------------------------------- 1 | msgid "string.1" 2 | msgctxt "register" 3 | msgstr "translation.1" 4 | 5 | msgid "string.1" 6 | msgstr "translation.2" -------------------------------------------------------------------------------- /fixtures/multiflags.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: \n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2013-09-25 15:55+0100\n" 6 | "PO-Revision-Date: \n" 7 | "Last-Translator: Raúl Ferràs \n" 8 | "Language-Team: \n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 13 | "X-Poedit-SourceCharset: UTF-8\n" 14 | "X-Poedit-KeywordsList: __;_e;_n;_t\n" 15 | "X-Textdomain-Support: yes\n" 16 | "X-Poedit-Basepath: .\n" 17 | "X-Generator: Poedit 1.5.7\n" 18 | 19 | #: wp-admin/custom-background.php:305 20 | #, php-format, fuzzy 21 | msgctxt "Background Attachment" 22 | msgid "Attachment" 23 | msgstr "Adjunto" 24 | -------------------------------------------------------------------------------- /fixtures/noblankline.po: -------------------------------------------------------------------------------- 1 | msgid "one" 2 | msgstr "uno" 3 | msgid "two" 4 | msgstr "dos" -------------------------------------------------------------------------------- /fixtures/oldEntries.po: -------------------------------------------------------------------------------- 1 | # @ default 2 | #~ msgid "Arrastra imagenes aquí para subirlas." 3 | #~ msgstr "Arrossega les teves imatges aquí per pujarles." -------------------------------------------------------------------------------- /fixtures/plurals.po: -------------------------------------------------------------------------------- 1 | # Translation of Administration in Spanish (Spain) 2 | # This file is distributed under the same license as the Administration package. 3 | msgid "" 4 | msgstr "" 5 | "PO-Revision-Date: 2013-10-23 09:51:48+0000\n" 6 | "MIME-Version: 1.0\n" 7 | "Content-Type: text/plain; charset=UTF-8\n" 8 | "Content-Transfer-Encoding: 8bit\n" 9 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 10 | "X-Generator: GlotPress/0.1\n" 11 | "Project-Id-Version: Administration\n" 12 | 13 | 14 | #: wp-admin/edit.php:238 15 | msgid "%s post not updated, somebody is editing it." 16 | msgid_plural "%s posts not updated, somebody is editing them." 17 | msgstr[0] "%s entrada no actualizada, alguien la está editando." 18 | msgstr[1] "%s entradas no actualizadas, alguien las está editando." 19 | 20 | #: wp-admin/edit.php:239 21 | msgid "%s post permanently deleted." 22 | msgid_plural "%s posts permanently deleted." 23 | msgstr[0] "%s entrada borrada permanentemente." 24 | msgstr[1] "%s entradas borradas permanentemente." 25 | 26 | #: wp-admin/edit.php:240 27 | msgid "%s post moved to the Trash." 28 | msgid_plural "%s posts moved to the Trash." 29 | msgstr[0] "%s entrada movida a la Papelera." 30 | msgstr[1] "%s entradas movidas a la Papelera." 31 | 32 | #: wp-admin/edit.php:241 33 | msgid "%s post restored from the Trash." 34 | msgid_plural "%s posts restored from the Trash." 35 | msgstr[0] "%s entrada restaurada desde la Papelera." 36 | msgstr[1] "%s entradas restauradas desde la Papelera." 37 | 38 | #: wp-admin/edit.php:244 39 | msgid "%s page updated." 40 | msgid_plural "%s pages updated." 41 | msgstr[0] "%s página actualizada." 42 | msgstr[1] "%s páginas actualizadas." 43 | 44 | #: wp-admin/edit.php:245 45 | msgid "%s page not updated, somebody is editing it." 46 | msgid_plural "%s pages not updated, somebody is editing them." 47 | msgstr[0] "%s página no actualizada, alguien la está editando." 48 | msgstr[1] "%s páginas no actualizadas, alguien las está editando." 49 | 50 | #: wp-admin/edit.php:246 51 | msgid "%s page permanently deleted." 52 | msgid_plural "%s pages permanently deleted." 53 | msgstr[0] "%s página borrada permanentemente." 54 | msgstr[1] "%s páginas borradas permanentemente." 55 | 56 | #: wp-admin/edit.php:247 57 | msgid "%s page moved to the Trash." 58 | msgid_plural "%s pages moved to the Trash." 59 | msgstr[0] "%s página movida a la Papelera." 60 | msgstr[1] "%s páginas movidas a la Papelera." 61 | 62 | #: wp-admin/edit.php:248 63 | msgid "%s page restored from the Trash." 64 | msgid_plural "%s pages restored from the Trash." 65 | msgstr[0] "%s página restaurapada desde la Papelera." 66 | msgstr[1] "%s páginas restauradas desde la Papelera." 67 | 68 | #: wp-admin/revision.php:107 69 | msgid "Compare two different revisions by selecting the “Compare any two revisions” box to the side." 70 | msgstr "Compara dos revisiones seleccionando “Compara dos revisiones cualquiera” en el lateral." 71 | 72 | #: wp-admin/includes/template.php:1891 73 | msgid "Compare Revisions" 74 | msgstr "Comparar revisiones" 75 | 76 | #: wp-admin/includes/class-wp-ms-themes-list-table.php:207 77 | msgid "Broken (%s)" 78 | msgid_plural "Broken (%s)" 79 | msgstr[0] "Roto (%s)" 80 | msgstr[1] "Rotos (%s)" 81 | 82 | #: wp-admin/includes/theme.php:169 83 | msgid "Light" 84 | msgstr "Ligero" 85 | 86 | msgid "http://akismet.com/?return=true" 87 | msgstr "http://akismet.com/?return=true" 88 | 89 | msgid "Used by millions, Akismet is quite possibly the best way in the world to protect your blog from comment and trackback spam. It keeps your site protected from spam even while you sleep. To get started: 1) Click the \"Activate\" link to the left of this description, 2) Sign up for an Akismet API key, and 3) Go to your Akismet configuration page, and save your API key." 90 | msgstr "Utilizado por millones de usuarios, Akismet es probablemente el mejor modo que hay en el mundo para proteger tu sitio de spam en comentarios y trackbacks. Mantiene tu sitio protegido de spam incluso mientras duermes. Para empezar: 1) Haz clic en el enlace \"Activar\" a la izquierda de la descripción, 2) Regístrate para obtener una clave API de Akismet, y 3) Ve a tu página de configuración de Akismet y guarda tu clave API." 91 | -------------------------------------------------------------------------------- /fixtures/pluralsMultiline.po: -------------------------------------------------------------------------------- 1 | msgid "%s post not updated," 2 | "somebody is editing it." 3 | msgid_plural "%s posts not updated," 4 | "somebody is editing them." 5 | msgstr[0] "%s entrada no actualizada," 6 | "alguien la está editando." 7 | msgstr[1] "%s entradas no actualizadas," 8 | "alguien las está editando." 9 | 10 | #: wp-admin/edit.php:239 11 | msgid "" 12 | "%s post permanently deleted." 13 | msgid_plural "%s posts permanently deleted." 14 | msgstr[0] "" 15 | "%s entrada borrada" 16 | "permanentemente." 17 | msgstr[1] "" 18 | "%s entradas borradas" 19 | "permanentemente." 20 | -------------------------------------------------------------------------------- /fixtures/previousString.po: -------------------------------------------------------------------------------- 1 | #| msgid "this is a previous string" 2 | #| msgstr "this is a previous translation string" 3 | msgid "this is a string" 4 | msgstr "this is a translation" 5 | 6 | -------------------------------------------------------------------------------- /fixtures/previousStringMultiline.po: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #| msgid "this is a previous string" 5 | #| msgstr "" 6 | #| "Doloribus nulla odit et aut est. Rerum molestiae pariatur suscipit unde in quide" 7 | #| "m alias alias. Ut ea omnis placeat rerum quae asperiores. Et recusandae praesent" 8 | #| "ium ea." 9 | msgid "this is a string" 10 | msgstr "this is a translation" -------------------------------------------------------------------------------- /fixtures/quotes.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: PROJECT VERSION\n" 4 | "POT-Creation-Date: 2018-03-05 08:57+0100\n" 5 | "PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\n" 6 | "Last-Translator: NAME \n" 7 | "Language-Team: LANGUAGE \n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=utf-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" 12 | 13 | msgid "a\nb\nc" 14 | msgstr "linebreaks" 15 | 16 | msgid "a\"b\"c" 17 | msgstr "quotes in \"translation\"" 18 | -------------------------------------------------------------------------------- /fixtures/translatorComments.po: -------------------------------------------------------------------------------- 1 | # translator comment 2 | #second translator comment 3 | msgid "string.1" 4 | msgstr "translation.1" 5 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012 Raúl Ferràs raul.ferras@gmail.com 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 1. Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 3. Neither the name of copyright holders nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 21 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS 22 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | PoParser 2 | ======= 3 | PoParser is a personal project to fulfill a need I got: parse Gettext Portable files (*.po files) and edit its content using PHP. 4 | 5 | PoParser requires PHP >= 5.4, but may work in 5.3 too. 6 | [Changelog](changelog.md) 7 | 8 | [![Latest Stable Version](https://poser.pugx.org/sepia/po-parser/v/stable)](https://packagist.org/packages/sepia/po-parser) 9 | [![Total Downloads](https://poser.pugx.org/sepia/po-parser/downloads)](https://packagist.org/packages/sepia/po-parser) 10 | [![License](https://poser.pugx.org/sepia/po-parser/license)](https://packagist.org/packages/sepia/po-parser) 11 | [![Build Status](https://travis-ci.org/raulferras/PHP-po-parser.png?branch=master)](https://travis-ci.org/raulferras/PHP-po-parser) 12 | [![Code Coverage](https://scrutinizer-ci.com/g/raulferras/PHP-po-parser/badges/coverage.png?s=a19ece2a8543b085ab1a5db319ded3bc4530b567)](https://scrutinizer-ci.com/g/raulferras/PHP-po-parser/) 13 | [![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/raulferras/PHP-po-parser/badges/quality-score.png?s=6aaf3c31ce15cebd1d4bed718cd41fd2d921fd31)](https://scrutinizer-ci.com/g/raulferras/PHP-po-parser/) 14 | 15 | [![Gitter](https://badges.gitter.im/raulferras/PHP-po-parser.svg)](https://gitter.im/raulferras/PHP-po-parser?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 16 | 17 | 18 | Features 19 | ======== 20 | It supports following parsing features: 21 | 22 | - header section. 23 | - msgid, both single and multiline. 24 | - msgstr, both single and multiline. 25 | - msgctxt (Message context). 26 | - msgid_plural (plurals forms). 27 | - #, keys (flags). 28 | - # keys (translator comments). 29 | - #. keys (Comments extracted from source code). 30 | - #: keys (references). 31 | - #| keys (previous strings), both single and multiline. 32 | - #~ keys (old entries), both single and multiline. 33 | 34 | Installation 35 | ============ 36 | 37 | ``` 38 | composer require sepia/po-parser 39 | ``` 40 | 41 | Usage 42 | ===== 43 | ```php 44 | parse(); 50 | 51 | // Get an entry 52 | $entry = $catalog->getEntry('welcome.user'); 53 | 54 | // Update entry 55 | $entry = new Entry('welcome.user', 'Welcome User!'); 56 | $catalog->setEntry($entry); 57 | 58 | // You can also modify other entry attributes as translator comments, code comments, flags... 59 | $entry->setTranslatorComments(array('This is shown whenever a new user registers in the website')); 60 | $entry->setFlags(array('fuzzy', 'php-code')); 61 | ``` 62 | 63 | ## Save Changes back to a file 64 | Use `PoCompiler` together with `FileSystem` to save a catalog back to a file: 65 | 66 | ```php 67 | $fileHandler = new Sepia\PoParser\SourceHandler\FileSystem('en.po'); 68 | $compiler = new Sepia\PoParser\PoCompiler(); 69 | $fileHandler->save($compiler->compile($catalog)); 70 | ``` 71 | 72 | Documentation 73 | ============= 74 | - [v5 Documentation](https://github.com/raulferras/PHP-po-parser/wiki/Documentation-5.0) 75 | - [Migration guide from v4 to v5](https://github.com/raulferras/PHP-po-parser/wiki/Migration-v4-to-v5) 76 | - [v4 documentation](https://github.com/raulferras/PHP-po-parser/wiki/Documentation-4.0) 77 | 78 | 79 | Testing 80 | ======= 81 | Tests are done using PHPUnit. 82 | To execute tests, from command line type: 83 | 84 | ``` 85 | php vendor/bin/phpunit 86 | ``` 87 | 88 | 89 | TODO 90 | ==== 91 | * Add compatibility with older disambiguating contexts formats. 92 | -------------------------------------------------------------------------------- /src/Catalog/Catalog.php: -------------------------------------------------------------------------------- 1 | entries = array(); 19 | $this->headers = new Header(); 20 | foreach ($entries as $entry) { 21 | $this->addEntry($entry); 22 | } 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function addEntry(Entry $entry) 29 | { 30 | $key = $this->getEntryHash( 31 | $entry->getMsgId(), 32 | $entry->getMsgCtxt() 33 | ); 34 | $this->entries[$key] = $entry; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function addHeaders(Header $headers) 41 | { 42 | $this->headers = $headers; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function removeEntry($msgid, $msgctxt = null) 49 | { 50 | $key = $this->getEntryHash($msgid, $msgctxt); 51 | if (isset($this->entries[$key])) { 52 | unset($this->entries[$key]); 53 | } 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function getHeaders() 60 | { 61 | return $this->headers->asArray(); 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function getHeader() 68 | { 69 | return $this->headers; 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function getEntries() 76 | { 77 | return $this->entries; 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function getEntry($msgId, $context = null) 84 | { 85 | $key = $this->getEntryHash($msgId, $context); 86 | if (!isset($this->entries[$key])) { 87 | return null; 88 | } 89 | 90 | return $this->entries[$key]; 91 | } 92 | 93 | /** 94 | * @param string $msgId 95 | * @param string|null $context 96 | * 97 | * @return string 98 | */ 99 | private function getEntryHash($msgId, $context = null) 100 | { 101 | return \md5($msgId.$context); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Catalog/Entry.php: -------------------------------------------------------------------------------- 1 | msgId = $msgId; 47 | $this->msgStr = $msgStr; 48 | $this->msgStrPlurals = array(); 49 | $this->flags = array(); 50 | $this->translatorComments = array(); 51 | $this->developerComments = array(); 52 | $this->reference = array(); 53 | } 54 | 55 | /** 56 | * @param string $msgId 57 | * 58 | * @return Entry 59 | */ 60 | public function setMsgId($msgId) 61 | { 62 | $this->msgId = $msgId; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * @param string|null $msgStr 69 | * 70 | * @return Entry 71 | */ 72 | public function setMsgStr($msgStr) 73 | { 74 | $this->msgStr = $msgStr; 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * @param string|null $msgIdPlural 81 | * 82 | * @return Entry 83 | */ 84 | public function setMsgIdPlural($msgIdPlural) 85 | { 86 | $this->msgIdPlural = $msgIdPlural; 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * @param string|null $msgCtxt 93 | * 94 | * @return Entry 95 | */ 96 | public function setMsgCtxt($msgCtxt) 97 | { 98 | $this->msgCtxt = $msgCtxt; 99 | 100 | return $this; 101 | } 102 | 103 | /** 104 | * @param null|Entry $previousEntry 105 | * 106 | * @return Entry 107 | */ 108 | public function setPreviousEntry($previousEntry) 109 | { 110 | $this->previousEntry = $previousEntry; 111 | 112 | return $this; 113 | } 114 | 115 | /** 116 | * @param bool|null $obsolete 117 | * 118 | * @return Entry 119 | */ 120 | public function setObsolete($obsolete) 121 | { 122 | $this->obsolete = $obsolete; 123 | 124 | return $this; 125 | } 126 | 127 | /** 128 | * @param array $flags 129 | * 130 | * @return Entry 131 | */ 132 | public function setFlags($flags) 133 | { 134 | $this->flags = $flags; 135 | 136 | return $this; 137 | } 138 | 139 | /** 140 | * @param array $translatorComments 141 | * 142 | * @return Entry 143 | */ 144 | public function setTranslatorComments($translatorComments) 145 | { 146 | $this->translatorComments = $translatorComments; 147 | 148 | return $this; 149 | } 150 | 151 | /** 152 | * @param array $developerComments 153 | * 154 | * @return Entry 155 | */ 156 | public function setDeveloperComments($developerComments) 157 | { 158 | $this->developerComments = $developerComments; 159 | 160 | return $this; 161 | } 162 | 163 | /** 164 | * @param array $reference 165 | * 166 | * @return Entry 167 | */ 168 | public function setReference($reference) 169 | { 170 | $this->reference = $reference; 171 | 172 | return $this; 173 | } 174 | 175 | /** 176 | * @param string[] $msgStrPlurals 177 | * 178 | * @return Entry 179 | */ 180 | public function setMsgStrPlurals($msgStrPlurals) 181 | { 182 | $this->msgStrPlurals = $msgStrPlurals; 183 | 184 | return $this; 185 | } 186 | 187 | /** 188 | * @return string 189 | */ 190 | public function getMsgId() 191 | { 192 | return $this->msgId; 193 | } 194 | 195 | /** 196 | * @return string|null 197 | */ 198 | public function getMsgStr() 199 | { 200 | return $this->msgStr; 201 | } 202 | 203 | /** 204 | * @return string|null 205 | */ 206 | public function getMsgIdPlural() 207 | { 208 | return $this->msgIdPlural; 209 | } 210 | 211 | /** 212 | * @return string|null 213 | */ 214 | public function getMsgCtxt() 215 | { 216 | return $this->msgCtxt; 217 | } 218 | 219 | /** 220 | * @return null|Entry 221 | */ 222 | public function getPreviousEntry() 223 | { 224 | return $this->previousEntry; 225 | } 226 | 227 | /** 228 | * @return bool 229 | */ 230 | public function isObsolete() 231 | { 232 | return $this->obsolete === true; 233 | } 234 | 235 | /** 236 | * @return bool 237 | */ 238 | public function isFuzzy() 239 | { 240 | return \in_array('fuzzy', $this->getFlags(), true); 241 | } 242 | 243 | /** 244 | * @return bool 245 | */ 246 | public function isPlural() 247 | { 248 | return $this->getMsgIdPlural() !== null || \count($this->getMsgStrPlurals()) > 0; 249 | } 250 | 251 | /** 252 | * @return array 253 | */ 254 | public function getFlags() 255 | { 256 | return $this->flags; 257 | } 258 | 259 | /** 260 | * @return array 261 | */ 262 | public function getTranslatorComments() 263 | { 264 | return $this->translatorComments; 265 | } 266 | 267 | /** 268 | * @return array 269 | */ 270 | public function getDeveloperComments() 271 | { 272 | return $this->developerComments; 273 | } 274 | 275 | /** 276 | * @return array 277 | */ 278 | public function getReference() 279 | { 280 | return $this->reference; 281 | } 282 | 283 | /** 284 | * @return string[] 285 | */ 286 | public function getMsgStrPlurals() 287 | { 288 | return $this->msgStrPlurals; 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/Catalog/EntryFactory.php: -------------------------------------------------------------------------------- 1 | $value) { 20 | switch (true) { 21 | case $key === 'msgctxt': 22 | $entry->setMsgCtxt($entryArray['msgctxt']); 23 | break; 24 | 25 | case $key === 'flags': 26 | $entry->setFlags($entryArray['flags']); 27 | break; 28 | 29 | case $key === 'reference': 30 | $entry->setReference($entryArray['reference']); 31 | break; 32 | 33 | case $key === 'previous': 34 | $entry->setPreviousEntry(self::createFromArray($entryArray['previous'])); 35 | break; 36 | 37 | case $key === 'tcomment': 38 | $entry->setTranslatorComments($value); 39 | break; 40 | 41 | case $key === 'ccomment': 42 | $entry->setDeveloperComments($value); 43 | break; 44 | 45 | case $key === 'obsolete': 46 | $entry->setObsolete(true); 47 | break; 48 | 49 | case 0 === \strpos($key, 'msgstr['): 50 | $plurals[] = $value; 51 | break; 52 | } 53 | } 54 | 55 | if (\count($plurals) > 0) { 56 | $entry->setMsgStrPlurals($plurals); 57 | if(!empty($entryArray['msgid_plural'])){ 58 | $entry->setMsgIdPlural($entryArray['msgid_plural']); 59 | } 60 | } 61 | 62 | return $entry; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Catalog/Header.php: -------------------------------------------------------------------------------- 1 | setHeaders($headers); 16 | } 17 | 18 | public function getPluralFormsCount() 19 | { 20 | if ($this->nPlurals !== null) { 21 | return $this->nPlurals; 22 | } 23 | 24 | $header = $this->getHeaderValue('Plural-Forms'); 25 | if ($header === null) { 26 | $this->nPlurals = 0; 27 | return $this->nPlurals; 28 | } 29 | 30 | $matches = array(); 31 | if (\preg_match('/nplurals=([0-9]+)/', $header, $matches) !== 1) { 32 | $this->nPlurals = 0; 33 | return $this->nPlurals; 34 | } 35 | 36 | $this->nPlurals = isset($matches[1]) ? (int)$matches[1] : 0; 37 | 38 | return $this->nPlurals; 39 | } 40 | 41 | public function setHeaders(array $headers) 42 | { 43 | $this->headers = $headers; 44 | } 45 | 46 | /** 47 | * @return array 48 | */ 49 | public function asArray() 50 | { 51 | return $this->headers; 52 | } 53 | 54 | /** 55 | * @param string $headerName 56 | * 57 | * @return string|null 58 | */ 59 | protected function getHeaderValue($headerName) 60 | { 61 | $header = \array_values(\array_filter( 62 | $this->headers, 63 | function ($string) use ($headerName) { 64 | return \preg_match('/'.$headerName.':(.*)/i', $string) == 1; 65 | } 66 | )); 67 | 68 | return \count($header) ? $header[0] : null; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Exception/ParseException.php: -------------------------------------------------------------------------------- 1 | parse(); 72 | } 73 | 74 | /** 75 | * Reads and parses a file 76 | * 77 | * @param string $filePath 78 | * 79 | * @throws \Exception. 80 | * @return Catalog 81 | */ 82 | public static function parseFile($filePath) 83 | { 84 | $parser = new Parser(new FileSystem($filePath)); 85 | 86 | return $parser->parse(); 87 | } 88 | 89 | public function __construct(SourceHandler $sourceHandler) 90 | { 91 | $this->sourceHandler = $sourceHandler; 92 | } 93 | 94 | /** 95 | * Reads and parses strings of a .po file. 96 | * 97 | * @param SourceHandler . Optional 98 | * 99 | * @throws \Exception, \InvalidArgumentException, ParseException 100 | * @return Catalog 101 | */ 102 | public function parse(Catalog $catalog = null) 103 | { 104 | $catalog = $catalog === null ? new CatalogArray() : $catalog; 105 | $this->lineNumber = 0; 106 | $entry = array(); 107 | $this->property = null; // current property 108 | 109 | // Flags 110 | $headersFound = false; 111 | 112 | while (!$this->sourceHandler->ended()) { 113 | $line = \trim($this->sourceHandler->getNextLine()); 114 | 115 | if ($this->shouldIgnoreLine($line, $entry)) { 116 | $this->lineNumber++; 117 | continue; 118 | } 119 | 120 | if ($this->shouldCloseEntry($line, $entry)) { 121 | if (!$headersFound && $this->isHeader($entry)) { 122 | $headersFound = true; 123 | $catalog->addHeaders( 124 | $this->parseHeaders($entry['msgstr']) 125 | ); 126 | } else { 127 | $catalog->addEntry(EntryFactory::createFromArray($entry)); 128 | } 129 | 130 | $entry = array(); 131 | $this->property = null; 132 | 133 | if (empty($line)) { 134 | $this->lineNumber++; 135 | continue; 136 | } 137 | } 138 | 139 | $entry = $this->parseLine($line, $entry); 140 | 141 | $this->lineNumber++; 142 | continue; 143 | } 144 | $this->sourceHandler->close(); 145 | 146 | // add final entry 147 | if (\count($entry)) { 148 | if ($this->isHeader($entry)) { 149 | $catalog->addHeaders( 150 | $this->parseHeaders($entry['msgstr']) 151 | ); 152 | } else { 153 | $catalog->addEntry(EntryFactory::createFromArray($entry)); 154 | } 155 | } 156 | 157 | return $catalog; 158 | } 159 | 160 | /** 161 | * @param string $line 162 | * @param array $entry 163 | * 164 | * @return array 165 | * @throws ParseException 166 | */ 167 | protected function parseLine($line, $entry) 168 | { 169 | $firstChar = \strlen($line) > 0 ? $line[0] : ''; 170 | 171 | switch ($firstChar) { 172 | case '#': 173 | $entry = $this->parseComment($line, $entry); 174 | break; 175 | 176 | case 'm': 177 | $entry = $this->parseProperty($line, $entry); 178 | break; 179 | 180 | case '"': 181 | $entry = $this->parseMultiline($line, $entry); 182 | break; 183 | } 184 | 185 | return $entry; 186 | } 187 | 188 | /** 189 | * @param string $line 190 | * @param array $entry 191 | * 192 | * @return array 193 | * @throws ParseException 194 | */ 195 | protected function parseProperty($line, array $entry) 196 | { 197 | list($key, $value) = $this->getProperty($line); 198 | 199 | if (!isset($entry[$key])) { 200 | $entry[$key] = ''; 201 | } 202 | 203 | switch (true) { 204 | case $key === 'msgctxt': 205 | case $key === 'msgid': 206 | case $key === 'msgid_plural': 207 | case $key === 'msgstr': 208 | $entry[$key] .= $this->unquote($value); 209 | $this->property = $key; 210 | break; 211 | 212 | case \strpos($key, 'msgstr[') !== false: 213 | $entry[$key] .= $this->unquote($value); 214 | $this->property = $key; 215 | break; 216 | 217 | default: 218 | throw new ParseException(\sprintf('Could not parse %s at line %d', $key, $this->lineNumber)); 219 | } 220 | 221 | return $entry; 222 | } 223 | 224 | /** 225 | * @param string $line 226 | * @param array $entry 227 | * 228 | * @return array 229 | * @throws ParseException 230 | */ 231 | protected function parseMultiline($line, $entry) 232 | { 233 | switch (true) { 234 | case $this->property === 'msgctxt': 235 | case $this->property === 'msgid': 236 | case $this->property === 'msgid_plural': 237 | case $this->property === 'msgstr': 238 | case \strpos($this->property, 'msgstr[') !== false: 239 | $entry[$this->property] .= $this->unquote($line); 240 | break; 241 | 242 | default: 243 | throw new ParseException( 244 | \sprintf('Error parsing property %s as multiline.', $this->property) 245 | ); 246 | } 247 | 248 | return $entry; 249 | } 250 | 251 | /** 252 | * @param string $line 253 | * @param array $entry 254 | * 255 | * @return array 256 | * @throws ParseException 257 | */ 258 | protected function parseComment($line, $entry) 259 | { 260 | $comment = \trim(\substr($line, 0, 2)); 261 | 262 | switch ($comment) { 263 | case '#,': 264 | $line = \trim(\substr($line, 2)); 265 | $entry['flags'] = \preg_split('/,\s*/', $line); 266 | break; 267 | 268 | case '#.': 269 | $entry['ccomment'] = !isset($entry['ccomment']) ? array() : $entry['ccomment']; 270 | $entry['ccomment'][] = \trim(\substr($line, 2)); 271 | break; 272 | 273 | 274 | case '#|': // Previous string 275 | case '#~': // Old entry 276 | case '#~|': // Previous string old 277 | $mode = array( 278 | '#|' => 'previous', 279 | '#~' => 'obsolete', 280 | '#~|' => 'previous-obsolete' 281 | ); 282 | 283 | $line = \trim(\substr($line, 2)); 284 | $property = $mode[$comment]; 285 | if ($property === 'previous') { 286 | if (!isset($entry[$property])) { 287 | $subEntry = array(); 288 | } else { 289 | $subEntry = $entry[$property]; 290 | } 291 | 292 | $subEntry = $this->parseLine($line, $subEntry); 293 | //$subEntry = $this->parseProperty($line, $subEntry); 294 | $entry[$property] = $subEntry; 295 | } else { 296 | $entry = $this->parseLine($line, $entry); 297 | $entry['obsolete'] = true; 298 | } 299 | break; 300 | 301 | // Reference 302 | case '#:': 303 | $entry['reference'][] = \trim(\substr($line, 2)); 304 | break; 305 | 306 | case '#': 307 | default: 308 | $entry['tcomment'] = !isset($entry['tcomment']) ? array() : $entry['tcomment']; 309 | $entry['tcomment'][] = \trim(\substr($line, 1)); 310 | break; 311 | } 312 | 313 | return $entry; 314 | } 315 | 316 | /** 317 | * @param string $msgstr 318 | * 319 | * @return Header 320 | */ 321 | protected function parseHeaders($msgstr) 322 | { 323 | $headers = \array_filter(\explode("\n", $msgstr)); 324 | 325 | return new Header($headers); 326 | } 327 | 328 | /** 329 | * @param string $line 330 | * @param array $entry 331 | * 332 | * @return bool 333 | */ 334 | protected function shouldIgnoreLine($line, array $entry) 335 | { 336 | return empty($line) && \count($entry) === 0; 337 | } 338 | 339 | /** 340 | * @param string $line 341 | * @param array $entry 342 | * 343 | * @return bool 344 | */ 345 | protected function shouldCloseEntry($line, array $entry) 346 | { 347 | $tokens = $this->getProperty($line); 348 | $property = $tokens[0]; 349 | 350 | return ($line === '' || ($property === 'msgid' && isset($entry['msgid']))); 351 | } 352 | 353 | /** 354 | * @param string $value 355 | * @return string 356 | */ 357 | protected function unquote($value) 358 | { 359 | return \stripcslashes(\preg_replace('/^\"|\"$/', '', $value)); 360 | } 361 | 362 | /** 363 | * Checks if entry is a header by 364 | * 365 | * @param array $entry 366 | * 367 | * @return bool 368 | */ 369 | protected function isHeader(array $entry) 370 | { 371 | if (empty($entry) || !isset($entry['msgstr'])) { 372 | return false; 373 | } 374 | 375 | if (!isset($entry['msgid']) || !empty($entry['msgid'])) { 376 | return false; 377 | } 378 | 379 | $standardHeaders = array( 380 | 'Project-Id-Version:', 381 | 'Report-Msgid-Bugs-To:', 382 | 'POT-Creation-Date:', 383 | 'PO-Revision-Date:', 384 | 'Last-Translator:', 385 | 'Language-Team:', 386 | 'MIME-Version:', 387 | 'Content-Type:', 388 | 'Content-Transfer-Encoding:', 389 | 'Plural-Forms:', 390 | ); 391 | 392 | $headers = \explode("\n", $entry['msgstr']); 393 | // Remove text after double colon 394 | $headers = \array_map( 395 | function ($header) { 396 | $pattern = '/(.*?:)(.*)/i'; 397 | $replace = '${1}'; 398 | return \preg_replace($pattern, $replace, $header); 399 | }, 400 | $headers 401 | ); 402 | 403 | if (\count(\array_intersect($standardHeaders, $headers)) > 0) { 404 | return true; 405 | } 406 | 407 | // If it does not contain any of the standard headers 408 | // Let's see if it contains any custom header. 409 | $customHeaders = \array_filter( 410 | $headers, 411 | function ($header) { 412 | return \preg_match('/^X\-(.*):/i', $header) === 1; 413 | } 414 | ); 415 | 416 | return \count($customHeaders) > 0; 417 | } 418 | 419 | /** 420 | * @param string $line 421 | * 422 | * @return array 423 | */ 424 | protected function getProperty($line) 425 | { 426 | $tokens = \preg_split('/\s+/ ', $line, 2); 427 | 428 | return $tokens; 429 | } 430 | } 431 | -------------------------------------------------------------------------------- /src/PoCompiler.php: -------------------------------------------------------------------------------- 1 | wrappingColumn = $wrappingColumn; 32 | $this->lineEnding = $lineEnding; 33 | $this->tokenCarriageReturn = chr(13); 34 | } 35 | 36 | /** 37 | * Compiles entries into a string 38 | * 39 | * @param Catalog $catalog 40 | * 41 | * @return string 42 | * @throws \Exception 43 | * @todo Write obsolete messages at the end of the file. 44 | */ 45 | public function compile(Catalog $catalog) 46 | { 47 | $output = ''; 48 | 49 | if (\count($catalog->getHeaders()) > 0) { 50 | $output .= 'msgid ""'.$this->eol(); 51 | $output .= 'msgstr ""'.$this->eol(); 52 | foreach ($catalog->getHeaders() as $header) { 53 | $output .= '"'.$header.'\n"'.$this->eol(); 54 | } 55 | $output .= $this->eol(); 56 | } 57 | 58 | 59 | $entriesCount = \count($catalog->getEntries()); 60 | $counter = 0; 61 | foreach ($catalog->getEntries() as $entry) { 62 | if ($entry->isObsolete() === false) { 63 | $output .= $this->buildPreviousEntry($entry); 64 | $output .= $this->buildTranslatorComment($entry); 65 | $output .= $this->buildDeveloperComment($entry); 66 | $output .= $this->buildReference($entry); 67 | } 68 | 69 | $output .= $this->buildFlags($entry); 70 | 71 | // if (isset($entry['@'])) { 72 | // $output .= "#@ ".$entry['@'].$this->eol(); 73 | // } 74 | 75 | $output .= $this->buildContext($entry); 76 | $output .= $this->buildMsgId($entry); 77 | $output .= $this->buildMsgIdPlural($entry); 78 | $output .= $this->buildMsgStr($entry, $catalog->getHeader()); 79 | 80 | 81 | $counter++; 82 | // Avoid inserting an extra newline at end of file 83 | if ($counter < $entriesCount) { 84 | $output .= $this->eol(); 85 | } 86 | } 87 | 88 | return $output; 89 | } 90 | 91 | /** 92 | * @return string 93 | */ 94 | protected function eol() 95 | { 96 | return $this->lineEnding; 97 | } 98 | 99 | /** 100 | * @param $entry 101 | * 102 | * @return string 103 | */ 104 | protected function buildPreviousEntry(Entry $entry) 105 | { 106 | $previous = $entry->getPreviousEntry(); 107 | if ($previous === null) { 108 | return ''; 109 | } 110 | 111 | return '#| msgid '.$this->cleanExport($previous->getMsgId()).$this->eol(); 112 | } 113 | 114 | /** 115 | * @param $entry 116 | * 117 | * @return string 118 | */ 119 | protected function buildTranslatorComment(Entry $entry) 120 | { 121 | if ($entry->getTranslatorComments() === null) { 122 | return ''; 123 | } 124 | 125 | $output = ''; 126 | foreach ($entry->getTranslatorComments() as $comment) { 127 | $output .= '# '.$comment.$this->eol(); 128 | } 129 | 130 | return $output; 131 | } 132 | 133 | protected function buildDeveloperComment(Entry $entry) 134 | { 135 | if ($entry->getDeveloperComments() === null) { 136 | return ''; 137 | } 138 | 139 | $output = ''; 140 | foreach ($entry->getDeveloperComments() as $comment) { 141 | $output .= '#. '.$comment.$this->eol(); 142 | } 143 | 144 | return $output; 145 | } 146 | 147 | protected function buildReference(Entry $entry) 148 | { 149 | $reference = $entry->getReference(); 150 | if ($reference === null || \count($reference) === 0) { 151 | return ''; 152 | } 153 | 154 | $output = ''; 155 | foreach ($reference as $ref) { 156 | $output .= '#: '.$ref.$this->eol(); 157 | } 158 | 159 | return $output; 160 | } 161 | 162 | protected function buildFlags(Entry $entry) 163 | { 164 | $flags = $entry->getFlags(); 165 | if ($flags === null || \count($flags) === 0) { 166 | return ''; 167 | } 168 | 169 | return '#, '.\implode(', ', $flags).$this->eol(); 170 | } 171 | 172 | protected function buildContext(Entry $entry) 173 | { 174 | if ($entry->getMsgCtxt() === null) { 175 | return ''; 176 | } 177 | 178 | return 179 | ($entry->isObsolete() ? '#~ ' : ''). 180 | 'msgctxt '.$this->cleanExport($entry->getMsgCtxt()).$this->eol(); 181 | } 182 | 183 | protected function buildMsgId(Entry $entry) 184 | { 185 | if ($entry->getMsgId() === null) { 186 | return ''; 187 | } 188 | 189 | return $this->buildProperty('msgid', $entry->getMsgId(), $entry->isObsolete()); 190 | } 191 | 192 | protected function buildMsgStr(Entry $entry, Header $headers) 193 | { 194 | $value = $entry->getMsgStr(); 195 | $plurals = $entry->getMsgStrPlurals(); 196 | 197 | if ($value === null && $plurals === null) { 198 | return ''; 199 | } 200 | 201 | if ($entry->isPlural()) { 202 | $output = ''; 203 | $nPlurals = $headers->getPluralFormsCount(); 204 | $pluralsFound = \count($plurals); 205 | $maxIterations = \max($nPlurals, $pluralsFound); 206 | for ($i = 0; $i < $maxIterations; $i++) { 207 | $value = isset($plurals[$i]) ? $plurals[$i] : ''; 208 | $output .= $this->buildProperty('msgstr['.$i.']', $value, $entry->isObsolete()); 209 | } 210 | 211 | return $output; 212 | } 213 | 214 | return $this->buildProperty('msgstr', $value, $entry->isObsolete()); 215 | } 216 | 217 | /** 218 | * @param Entry $entry 219 | * 220 | * @return string 221 | */ 222 | protected function buildMsgIdPlural(Entry $entry) 223 | { 224 | $value = $entry->getMsgIdPlural(); 225 | if ($value === null) { 226 | return ''; 227 | } 228 | 229 | return $this->buildProperty('msgid_plural', $value, $entry->isObsolete()); 230 | } 231 | 232 | protected function buildProperty($property, $value, $obsolete = false) 233 | { 234 | $tokens = $this->wrapString($value); 235 | 236 | $output = ''; 237 | if (\count($tokens) > 1) { 238 | \array_unshift($tokens, ''); 239 | } 240 | 241 | foreach ($tokens as $i => $token) { 242 | $output .= $obsolete ? self::TOKEN_OBSOLETE : ''; 243 | $output .= ($i === 0) ? $property.' ' : ''; 244 | $output .= $this->cleanExport($token).$this->eol(); 245 | } 246 | 247 | return $output; 248 | } 249 | 250 | /** 251 | * Prepares a string to be outputed into a file. 252 | * 253 | * @param string $string The string to be converted. 254 | * 255 | * @return string 256 | */ 257 | protected function cleanExport($string) 258 | { 259 | /** 260 | * Replace newline character with $this->tokenCarriageReturn that is later replaced back and thus 261 | * newline won't be escaped by addcslashes function 262 | */ 263 | $string = str_replace("\n", $this->tokenCarriageReturn , $string); 264 | 265 | // only quotation mark (" or \42) and backslash (\ or \134) chars needs to be escaped 266 | $string = sprintf('"%s"', addcslashes($string, "\42\134")); 267 | 268 | // Replace newline character with \n after addcslashes to prevent escaping backslash. 269 | return str_replace($this->tokenCarriageReturn, '\n', $string); 270 | } 271 | 272 | /** 273 | * @param string $value 274 | * @return array 275 | */ 276 | private function wrapString($value) 277 | { 278 | $length = mb_strlen($value); 279 | if ($length <= $this->wrappingColumn) { 280 | return array($value); 281 | } 282 | 283 | $lines = array(); 284 | $parts = preg_split('/( )/', $value, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); 285 | $lineIndex = 0; 286 | foreach ($parts as $part) { 287 | if ( 288 | array_key_exists($lineIndex, $lines) 289 | && mb_strlen($lines[$lineIndex] . $part) > $this->wrappingColumn 290 | ) { 291 | $lineIndex++; 292 | } 293 | 294 | if (!array_key_exists($lineIndex, $lines)) { 295 | $lines[$lineIndex] = ''; 296 | } 297 | 298 | $lines[$lineIndex] .= $part; 299 | } 300 | 301 | return $lines; 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/SourceHandler/FileSystem.php: -------------------------------------------------------------------------------- 1 | filePath = $filePath; 46 | $this->fileHandle = null; 47 | } 48 | 49 | /** 50 | * @throws \Exception 51 | */ 52 | protected function openFile() 53 | { 54 | if ($this->fileHandle !== null) { 55 | return; 56 | } 57 | 58 | if (\file_exists($this->filePath) === false) { 59 | throw new \Exception('Parser: Input File does not exists: "' . \htmlspecialchars($this->filePath) . '"'); 60 | } 61 | 62 | if (\is_readable($this->filePath) === false) { 63 | throw new \Exception('Parser: File is not readable: "' . \htmlspecialchars($this->filePath) . '"'); 64 | } 65 | 66 | $this->fileHandle = @\fopen($this->filePath, 'rb'); 67 | if ($this->fileHandle===false) { 68 | throw new \Exception('Parser: Could not open file: "' . \htmlspecialchars($this->filePath) . '"'); 69 | } 70 | } 71 | 72 | /** 73 | * @return bool|string 74 | * @throws \Exception 75 | */ 76 | public function getNextLine() 77 | { 78 | $this->openFile(); 79 | 80 | return \fgets($this->fileHandle); 81 | } 82 | 83 | /** 84 | * @return bool 85 | * @throws \Exception 86 | */ 87 | public function ended() 88 | { 89 | $this->openFile(); 90 | 91 | return \feof($this->fileHandle); 92 | } 93 | 94 | public function close() 95 | { 96 | if ($this->fileHandle === null) { 97 | return true; 98 | } 99 | 100 | return @\fclose($this->fileHandle); 101 | } 102 | 103 | /** 104 | * @param $output 105 | * @param $filePath 106 | * 107 | * @return bool 108 | * @throws \Exception 109 | */ 110 | public function save($output) 111 | { 112 | $result = \file_put_contents($this->filePath, $output); 113 | if ($result === false) { 114 | throw new \Exception('Could not write into file '.\htmlspecialchars($this->filePath)); 115 | } 116 | 117 | return true; 118 | } 119 | } -------------------------------------------------------------------------------- /src/SourceHandler/SourceHandler.php: -------------------------------------------------------------------------------- 1 | line = 0; 49 | $this->strings = \explode("\n",$string); 50 | $this->total = \count($this->strings); 51 | } 52 | 53 | public function getNextLine() 54 | { 55 | if (isset($this->strings[$this->line])) { 56 | $result = $this->strings[$this->line]; 57 | $this->line++; 58 | } else { 59 | $result = false; 60 | } 61 | return $result; 62 | } 63 | 64 | public function ended() 65 | { 66 | return ($this->line>=$this->total); 67 | } 68 | 69 | public function close() 70 | { 71 | $this->line = 0; 72 | } 73 | 74 | public function save($ignore) 75 | { 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/AbstractFixtureTest.php: -------------------------------------------------------------------------------- 1 | resourcesPath = \dirname(__DIR__).'/fixtures/'; 17 | } 18 | 19 | /** 20 | * @param string $file 21 | * 22 | * @return Catalog 23 | */ 24 | protected function parseFile($file) 25 | { 26 | //try { 27 | return Parser::parseFile($this->resourcesPath.$file); 28 | //} catch (\Exception $e) { 29 | // $this->fail($e->getMessage()); 30 | //} 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/EntryBuilder.php: -------------------------------------------------------------------------------- 1 | msgId, 13 | $this->msgStr 14 | ); 15 | if ($this->msgPluralId) { 16 | $entry->setMsgIdPlural($this->msgPluralId); 17 | } 18 | if ($this->context) { 19 | $entry->setMsgCtxt($this->context); 20 | } 21 | if ($this->reference) { 22 | $entry->setReference($this->reference); 23 | } 24 | if ($this->translatorComments) { 25 | $entry->setTranslatorComments($this->translatorComments); 26 | } 27 | if ($this->developerComments) { 28 | $entry->setDeveloperComments($this->developerComments); 29 | } 30 | $entry->setFlags($this->flags); 31 | $entry->setPreviousEntry($this->previousEntry); 32 | $entry->setObsolete($this->obsolete); 33 | $entry->setMsgStrPlurals($this->pluralTranslations); 34 | 35 | return $entry; 36 | } 37 | 38 | private $msgId; 39 | private $msgPluralId; 40 | private $msgStr; 41 | private $context; 42 | private $reference; 43 | private $translatorComments; 44 | private $developerComments; 45 | private $flags; 46 | private $previousEntry; 47 | private $obsolete; 48 | private $pluralTranslations; 49 | 50 | public function __construct() 51 | { 52 | $this->msgPluralId = null; 53 | $this->context = null; 54 | $this->reference = []; 55 | $this->translatorComments = []; 56 | $this->developerComments = []; 57 | $this->flags = []; 58 | $this->previousEntry = null; 59 | $this->obsolete = false; 60 | $this->pluralTranslations = []; 61 | } 62 | 63 | public static function anEntry(): self 64 | { 65 | $builder = new EntryBuilder(); 66 | $builder 67 | ->withId('an-id') 68 | ->withTranslation('a translation'); 69 | 70 | return $builder; 71 | } 72 | 73 | public function withId(string $id): self 74 | { 75 | $this->msgId = $id; 76 | return $this; 77 | } 78 | 79 | public function withPluralId(string $id): self 80 | { 81 | $this->msgPluralId = $id; 82 | return $this; 83 | } 84 | 85 | public function withTranslation(string $msgstr): self 86 | { 87 | $this->msgStr = $msgstr; 88 | return $this; 89 | } 90 | 91 | public function withContext(string $context): self 92 | { 93 | $this->context = $context; 94 | return $this; 95 | } 96 | 97 | public function withReference(array $reference): self 98 | { 99 | $this->reference = $reference; 100 | return $this; 101 | } 102 | 103 | public function withTranslatorComment(array $comments): self 104 | { 105 | $this->translatorComments = $comments; 106 | return $this; 107 | } 108 | 109 | public function withDeveloperComment(array $comments): self 110 | { 111 | $this->developerComments = $comments; 112 | return $this; 113 | } 114 | 115 | public function withFlags(array $flags) 116 | { 117 | $this->flags = $flags; 118 | return $this; 119 | } 120 | 121 | public function withPreviousEntry(?Entry $entry): self 122 | { 123 | $this->previousEntry = $entry; 124 | return $this; 125 | } 126 | 127 | public function obsolete(): self 128 | { 129 | $this->obsolete = true; 130 | return $this; 131 | } 132 | 133 | public function withPluralTranslation(int $numeral, string $translation): self 134 | { 135 | $this->pluralTranslations[$numeral] = $translation; 136 | return $this; 137 | } 138 | } -------------------------------------------------------------------------------- /tests/UnitTest/HeaderTest.php: -------------------------------------------------------------------------------- 1 | parseFile(); 13 | 14 | $this->assertEquals(3, $catalog->getHeader()->getPluralFormsCount()); 15 | } 16 | 17 | /** 18 | * @return \Sepia\PoParser\Catalog\Catalog 19 | */ 20 | protected function parseFile() 21 | { 22 | return Parser::parseFile(\dirname(\dirname(__DIR__)).'/fixtures/basicHeadersMultiline.po'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/UnitTest/ParserTest.php: -------------------------------------------------------------------------------- 1 | parse($doc); 25 | 26 | $expectedHeaders = new Header(array( 27 | 'Project-Id-Version: value 1', 28 | 'Report-Msgid-Bugs-To: value 2', 29 | )); 30 | $this->assertEquals( 31 | $expectedHeaders, 32 | $catalog->getHeader() 33 | ); 34 | } 35 | 36 | /** 37 | * @param string $doc 38 | * @return \Sepia\PoParser\Catalog\Catalog|\Sepia\PoParser\Catalog\CatalogArray 39 | * @throws \Exception 40 | */ 41 | public function parse(string $doc) 42 | { 43 | $parser = new Parser(new StringSource($doc)); 44 | return $parser->parse(); 45 | } 46 | } -------------------------------------------------------------------------------- /tests/UnitTest/PoCompilerTest.php: -------------------------------------------------------------------------------- 1 | withId('a-message') 18 | ->withTranslation('hello fellow ant') 19 | ->withContext('context 1') 20 | ->withReference(['src/views/forms.php:44']) 21 | ->withTranslatorComment(['translator comment']) 22 | ->withDeveloperComment(['developer comment']) 23 | ->withFlags(['1','2','3']) 24 | ->withPreviousEntry( 25 | EntryBuilder::anEntry() 26 | ->withId('previous.string.1') 27 | ->withContext('previous context') 28 | ->build() 29 | ) 30 | ->build(), 31 | 32 | EntryBuilder::anEntry() 33 | ->withId('second message') 34 | ->withTranslation('segón missatge') 35 | ->build() 36 | ]); 37 | 38 | $compiler = new PoCompiler(); 39 | $output = $compiler->compile($catalog); 40 | 41 | $this->assertEquals( 42 | <<withId('a-message') 65 | ->withTranslation('hello fellow ant') 66 | ->obsolete() 67 | ->build() 68 | ]); 69 | 70 | $compiler = new PoCompiler(); 71 | $output = $compiler->compile($catalog); 72 | 73 | $this->assertEquals( 74 | <<withId('a-message') 88 | ->withTranslation('hello fellow ant') 89 | ->build() 90 | ]); 91 | 92 | $compiler = new PoCompiler(); 93 | $output = $compiler->compile($catalog); 94 | 95 | $this->assertEquals( 96 | <<withId('a-message') 110 | ->withPluralId('a-message %d') 111 | ->withTranslation('hello fellow ant') 112 | ->withPluralTranslation(0, 'translation plural 0') 113 | ->withPluralTranslation(1, 'translation plural 1') 114 | ->withPluralTranslation(2, 'translation plural 2') 115 | ->build() 116 | ]); 117 | 118 | $compiler = new PoCompiler(); 119 | $output = $compiler->compile($catalog); 120 | 121 | $this->assertEquals( 122 | <<withId('a-message') 139 | ->withPluralId('%d obsolete strings') 140 | ->withTranslation('hello fellow ant') 141 | ->withPluralTranslation(0, 'translation plural 0') 142 | ->withPluralTranslation(1, 'translation plural 1') 143 | ->withPluralTranslation(2, 'translation plural 2') 144 | ->obsolete() 145 | ->build() 146 | ]); 147 | 148 | $compiler = new PoCompiler(); 149 | $output = $compiler->compile($catalog); 150 | 151 | $this->assertEquals( 152 | <<withId('a\"b\"c') 169 | ->withTranslation('quotes') 170 | ->build(), 171 | 172 | EntryBuilder::anEntry() 173 | ->withId('a\nb\nc') 174 | ->withTranslation('slashes') 175 | ->build(), 176 | 177 | EntryBuilder::anEntry() 178 | ->withId("a\nb\nc") 179 | ->withTranslation("proper\nlinebreaks") 180 | ->build(), 181 | ]); 182 | 183 | $compiler = new PoCompiler(); 184 | $output = $compiler->compile($catalog); 185 | 186 | $this->assertEquals( 187 | 'msgid "a\\\\\"b\\\\\"c" 188 | msgstr "quotes" 189 | 190 | msgid "a\\\\nb\\\\nc" 191 | msgstr "slashes" 192 | 193 | msgid "a\nb\nc" 194 | msgstr "proper\nlinebreaks" 195 | ', $output); 196 | } 197 | 198 | /** 199 | * @test 200 | * @dataProvider wrappingDataProvider 201 | */ 202 | public function should_compile_translation_with_wrapping_long_lines(string $value, int $wrappingColumn, bool $shouldWrapLines, array $assert) 203 | { 204 | // Make sure that encoding is set to UTF-8 for this test 205 | \mb_internal_encoding(); 206 | \mb_internal_encoding('UTF-8'); 207 | 208 | $catalog = new CatalogArray([ 209 | EntryBuilder::anEntry() 210 | ->withId('a-message') 211 | ->withTranslation($value) 212 | ->build() 213 | ]); 214 | 215 | $compiler = new PoCompiler($wrappingColumn); 216 | $output = $compiler->compile($catalog); 217 | 218 | $expected = 'msgid "a-message"'."\n"; 219 | if ($shouldWrapLines) { 220 | $expected .= 'msgstr ""' . "\n"; 221 | } else { 222 | $expected.= 'msgstr '; 223 | } 224 | foreach ($assert as $line) { 225 | $expected.= '"'.$line.'"'."\n"; 226 | } 227 | 228 | $this->assertEquals($expected, $output); 229 | } 230 | 231 | public function wrappingDataProvider(): array 232 | { 233 | return array( 234 | 'Multibyte Wrap (char 81)' => array( 235 | 'value' => 'Hello everybody, Hello ladies and gentlemen.... this is a multibyte translation á with a multibyte beginning at char 81.', 236 | 'wrappingColumn' => 80, 237 | 'shouldWrap' => true, 238 | 'assert' => array( 239 | 'Hello everybody, Hello ladies and gentlemen.... this is a multibyte translation ', 240 | 'á with a multibyte beginning at char 81.' 241 | ), 242 | ), 243 | 'Multibyte Wrap (char 80)' => array( 244 | 'value' => 'Hello everybody, Hello ladies and gentlemen... this is a multibyte translation á with a multibyte beginning at char 80.', 245 | 'wrappingColumn' => 80, 246 | 'shouldWrap' => true, 247 | 'assert' => array( 248 | 'Hello everybody, Hello ladies and gentlemen... this is a multibyte translation á', 249 | ' with a multibyte beginning at char 80.' 250 | ), 251 | ), 252 | 'Multibyte Wrap (char 79)' => array( 253 | 'value' => 'Hello everybody, Hello ladies and gentlemen.. this is a multibyte translation á with multibytes beginning at char 79.', 254 | 'wrappingColumn' => 80, 255 | 'shouldWrap' => true, 256 | 'assert' => array( 257 | 'Hello everybody, Hello ladies and gentlemen.. this is a multibyte translation á ', 258 | 'with multibytes beginning at char 79.' 259 | ), 260 | ), 261 | 'Escape-Sequence Wrap (char 80+81)' => array( 262 | 'value' => 'Hello everybody, Hello ladies and gentlemen..... this is a line with more than \"eighty\" chars. And char 80+81 is an escaped double quote.', 263 | 'wrappingColumn' => 80, 264 | 'shouldWrap' => true, 265 | 'assert' => array( 266 | 'Hello everybody, Hello ladies and gentlemen..... this is a line with more than ', 267 | '\\\\\"eighty\\\\\" chars. And char 80+81 is an escaped double quote.' 268 | ), 269 | ), 270 | 'Escape-Sequence Wrap (char 79+80)' => array( 271 | 'value' => 'Hello everybody, Hello ladies and gentlemen.... this is a line with more than \"eighty\" chars. And char 79+80 is an escaped double quote.', 272 | 'wrappingColumn' => 80, 273 | 'shouldWrap' => true, 274 | 'assert' => array( 275 | 'Hello everybody, Hello ladies and gentlemen.... this is a line with more than ', 276 | '\\\\\"eighty\\\\\" chars. And char 79+80 is an escaped double quote.' 277 | ), 278 | ), 279 | /* 'Escaped Line-break' => array( 280 | 'value' => 'Hello everybody, \\nHello ladies and gentlemen.', 281 | 'wrappingColumn' => 80, 282 | 'assert' => array( 283 | 'Hello everybody, \\\\nHello ladies and gentlemen.' 284 | ), 285 | ), 286 | */ 'String with a lot of multibyte characters should not break when wrappingColumn is at its mb_strlen' => array( 287 | 'value' => 'kategóriáját kötelező', 288 | 'wrappingColumn' => 21, 289 | 'shouldWrap' => false, 290 | 'assert' => array( 291 | 'kategóriáját kötelező' 292 | ), 293 | ), 294 | ); 295 | } 296 | } -------------------------------------------------------------------------------- /tests/UnitTest/ReadPoTest.php: -------------------------------------------------------------------------------- 1 | parseFile('basic.po'); 13 | 14 | $entry = $catalog->getEntry('string.1'); 15 | 16 | $this->assertNotNull($entry); 17 | $this->assertEquals('string.1', $entry->getMsgId()); 18 | $this->assertEquals('translation.1', $entry->getMsgStr()); 19 | 20 | $entry = $catalog->getEntry('string.2'); 21 | $this->assertNotNull($entry); 22 | $this->assertEquals('string.2', $entry->getMsgId()); 23 | $this->assertEquals('translation "quoted"', $entry->getMsgStr()); 24 | } 25 | 26 | public function testBasicMultiline() 27 | { 28 | $catalog = $this->parseFile('basicMultiline.po'); 29 | 30 | $entry = $catalog->getEntry('string.1'); 31 | 32 | $this->assertNotNull($entry); 33 | $this->assertEquals('string.1', $entry->getMsgId()); 34 | $this->assertEquals('translation line 1 translation line 2', $entry->getMsgStr()); 35 | } 36 | 37 | public function testBasicCollection() 38 | { 39 | $catalog = $this->parseFile('basicCollection.po'); 40 | 41 | $this->assertCount(2, $catalog->getEntries()); 42 | 43 | $entry = $catalog->getEntry('string.1'); 44 | $this->assertNotNull($entry); 45 | $this->assertEquals('string.1', $entry->getMsgId()); 46 | $this->assertEquals('translation.1', $entry->getMsgStr()); 47 | 48 | $entry = $catalog->getEntry('string.2'); 49 | $this->assertNotNull($entry); 50 | $this->assertEquals('string.2', $entry->getMsgId()); 51 | $this->assertEquals('translation.2', $entry->getMsgStr()); 52 | } 53 | 54 | public function testEntriesWithContext() 55 | { 56 | $catalog = $this->parseFile('context.po'); 57 | 58 | $withContext = $catalog->getEntry('string.1', 'register'); 59 | $this->assertNotNull($withContext); 60 | $this->assertEquals('register', $withContext->getMsgCtxt()); 61 | 62 | $withoutContext = $catalog->getEntry('string.1'); 63 | $this->assertNotNull($withoutContext); 64 | $this->assertEmpty($withoutContext->getMsgCtxt()); 65 | $this->assertNotEquals($withContext, $withoutContext); 66 | } 67 | 68 | public function testPlurals() 69 | { 70 | $catalog = $this->parseFile('plurals.po'); 71 | 72 | $entry = $catalog->getEntry('%s post not updated, somebody is editing it.'); 73 | $this->assertNotNull($entry); 74 | $this->assertNotEmpty($entry->getMsgStrPlurals()); 75 | $this->assertEquals( 76 | array( 77 | '%s entrada no actualizada, alguien la está editando.', 78 | '%s entradas no actualizadas, alguien las está editando.', 79 | ), 80 | $entry->getMsgStrPlurals() 81 | ); 82 | } 83 | 84 | public function testPluralsMultiline() 85 | { 86 | $catalog = $this->parseFile('pluralsMultiline.po'); 87 | $entry = $catalog->getEntry('%s post not updated,somebody is editing it.'); 88 | 89 | $this->assertNotNull($entry); 90 | $this->assertNotEmpty($entry->getMsgStrPlurals()); 91 | $this->assertEquals( 92 | array( 93 | '%s entrada no actualizada,alguien la está editando.', 94 | '%s entradas no actualizadas,alguien las está editando.', 95 | ), 96 | $entry->getMsgStrPlurals() 97 | ); 98 | } 99 | 100 | public function testEmptyPlurals() 101 | { 102 | $catalog = $this->parseFile('plurals.po'); 103 | 104 | $entry = $catalog->getEntry('Light'); 105 | $this->assertNotNull($entry); 106 | $this->assertNull($entry->getMsgIdPlural()); 107 | $this->assertEmpty($entry->getMsgStrPlurals()); 108 | } 109 | 110 | public function testFlags() 111 | { 112 | $catalog = $this->parseFile('multiflags.po'); 113 | 114 | $this->assertCount(1, $catalog->getEntries()); 115 | $entry = $catalog->getEntry('Attachment', 'Background Attachment'); 116 | 117 | $this->assertNotNull($entry); 118 | $this->assertCount(2, $entry->getFlags()); 119 | $this->assertEquals(array('php-format', 'fuzzy'), $entry->getFlags()); 120 | } 121 | 122 | public function testTranslatorComment() 123 | { 124 | $catalog = $this->parseFile('translatorComments.po'); 125 | $entry = $catalog->getEntry('string.1'); 126 | 127 | $this->assertNotNull($entry); 128 | $this->assertEquals( 129 | array('translator comment', 'second translator comment'), 130 | $entry->getTranslatorComments() 131 | ); 132 | } 133 | 134 | public function testDeveloperComment() 135 | { 136 | $catalog = $this->parseFile('codeComments.po'); 137 | $entry = $catalog->getEntry('string.1'); 138 | 139 | $this->assertNotNull($entry); 140 | $this->assertEquals(array('code comment', 'code translator comment'), $entry->getDeveloperComments()); 141 | } 142 | 143 | public function testReferences() 144 | { 145 | $catalog = $this->parseFile('basicReference.po'); 146 | 147 | $entry = $catalog->getEntry('string.1'); 148 | $this->assertNotNull($entry); 149 | $this->assertEquals(array('src/views/forms.php:44'), $entry->getReference()); 150 | } 151 | 152 | public function testPreviousString() 153 | { 154 | $catalog = $this->parseFile('previousString.po'); 155 | 156 | $this->assertCount(1, $catalog->getEntries()); 157 | 158 | $entry = new Entry('this is a string', 'this is a translation'); 159 | $entry->setPreviousEntry(new Entry('this is a previous string', 'this is a previous translation string')); 160 | $this->assertEquals( 161 | $entry, 162 | $catalog->getEntry('this is a string') 163 | ); 164 | } 165 | 166 | public function testPreviousStringMultiline() 167 | { 168 | $catalog = $this->parseFile('previousStringMultiline.po'); 169 | 170 | $entry = $catalog->getEntry('this is a string'); 171 | $this->assertNotNull($entry); 172 | 173 | $previous = $entry->getPreviousEntry(); 174 | $this->assertNotNull($previous); 175 | $this->assertEquals('this is a previous string', $previous->getMsgId()); 176 | $this->assertEquals('Doloribus nulla odit et aut est. Rerum molestiae pariatur suscipit unde in quidem alias alias. Ut ea omnis placeat rerum quae asperiores. Et recusandae praesentium ea.', $previous->getMsgStr()); 177 | } 178 | 179 | public function testHeaders() 180 | { 181 | $catalog = $this->parseFile('basicHeader.po'); 182 | $this->assertCount(1, $catalog->getEntries()); 183 | } 184 | 185 | public function testOnlyCustomHeaders() 186 | { 187 | $catalog = $this->parseFile('basicCustomHeaders.po'); 188 | $this->assertCount(1, $catalog->getEntries()); 189 | $this->assertGreaterThanOrEqual(1, \count($catalog->getHeaders())); 190 | } 191 | 192 | public function testHeadersMultiline() 193 | { 194 | $catalog = $this->parseFile('basicHeadersMultiline.po'); 195 | $this->assertCount(1, $catalog->getEntries()); 196 | $this->assertCount(3,$catalog->getHeaders()); 197 | } 198 | 199 | public function testFileWithOnlyHeaders() 200 | { 201 | $catalog = $this->parseFile('basicOnlyHeader.po'); 202 | $this->assertCount(0, $catalog->getEntries()); 203 | $this->assertGreaterThanOrEqual(1, \count($catalog->getHeaders())); 204 | } 205 | 206 | public function testNoBlankLinesSeparatingEntries() 207 | { 208 | $catalog = $this->parseFile('noblankline.po'); 209 | 210 | $this->assertCount(2, $catalog->getEntries()); 211 | } 212 | 213 | public function testProperQuotesEscaping() 214 | { 215 | $catalog = $this->parseFile('quotes.po'); 216 | 217 | $this->assertCount(2, $catalog->getEntries()); 218 | $this->assertNotNull($catalog->getEntry("a\nb\nc")); 219 | 220 | $entryWithQuotes = $catalog->getEntry('a"b"c'); 221 | $this->assertEquals('a"b"c', $entryWithQuotes->getMsgId()); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /tests/WriteTest.php: -------------------------------------------------------------------------------- 1 | 'string.1', 25 | 'msgstr' => 'translation.1', 26 | 'msgctxt' => 'context.1', 27 | 'reference' => array('src/views/forms.php:44'), 28 | 'tcomment' => array('translator comment'), 29 | 'ccomment' => array('code comment'), 30 | 'flags' => array('1', '2', '3') 31 | )); 32 | $previousEntry = EntryFactory::createFromArray(array( 33 | 'msgid' => 'previous.string.1', 34 | 'msgctxt' => 'previous.context.1' 35 | )); 36 | $entry->setPreviousEntry($previousEntry); 37 | $catalogSource->addEntry($entry); 38 | 39 | // Obsolete entry 40 | $entry = EntryFactory::createFromArray(array( 41 | 'msgid' => 'obsolete.1', 42 | 'msgstr' => $faker->paragraph(5), 43 | 'msgctxt' => 'obsolete.context', 44 | 'obsolete' => true 45 | )); 46 | $catalogSource->addEntry($entry); 47 | 48 | try { 49 | $this->saveCatalog($catalogSource); 50 | } catch (Exception $e) { 51 | $this->fail('Cannot save catalog.'); 52 | } 53 | 54 | $catalog = $this->parseFile('temp.po'); 55 | $this->assertPoFile($catalogSource, $catalog); 56 | } 57 | 58 | public function testWritePlurals() 59 | { 60 | $catalogSource = new CatalogArray(); 61 | // Normal Entry 62 | $entry = EntryFactory::createFromArray(array( 63 | 'msgid' => 'string.1', 64 | 'msgstr' => 'translation.1', 65 | 'msgstr[0]' => 'translation.plural.0', 66 | 'msgstr[1]' => 'translation.plural.1', 67 | 'msgstr[2]' => 'translation.plural.2', 68 | 'reference' => array('src/views/forms.php:44'), 69 | 'tcomment' => array('translator comment'), 70 | 'ccomment' => array('code comment'), 71 | 'flags' => array('1', '2', '3') 72 | )); 73 | 74 | $catalogSource->addEntry($entry); 75 | 76 | try { 77 | $this->saveCatalog($catalogSource); 78 | } catch (Exception $e) { 79 | $this->fail('Cannot save catalog.'); 80 | } 81 | $catalog = $this->parseFile('temp.po'); 82 | $entry = $catalog->getEntry('string.1'); 83 | $this->assertCount(3, $entry->getMsgStrPlurals()); 84 | } 85 | 86 | public function testDoubleEscaped() 87 | { 88 | $catalogSource = new CatalogArray(); 89 | // Normal Entry 90 | $entry = EntryFactory::createFromArray(array( 91 | 'msgid' => 'a\"b\"c', 92 | 'msgstr' => 'quotes' 93 | )); 94 | $catalogSource->addEntry($entry); 95 | 96 | $entry = EntryFactory::createFromArray(array( 97 | 'msgid' => 'a\nb\nc', 98 | 'msgstr' => 'slashes' 99 | )); 100 | $catalogSource->addEntry($entry); 101 | 102 | // Entry with line breaks 103 | $entry = EntryFactory::createFromArray(array( 104 | 'msgid' => "a\nb\nc", 105 | 'msgstr' => "proper\nlinebreaks" 106 | )); 107 | $catalogSource->addEntry($entry); 108 | 109 | try { 110 | $this->saveCatalog($catalogSource); 111 | } catch (Exception $e) { 112 | $this->fail('Cannot save catalog.'); 113 | } 114 | 115 | $catalog = $this->parseFile('temp.po'); 116 | $this->assertCount(3, $catalog->getEntries()); 117 | $this->assertNotNull($catalog->getEntry('a\"b\"c')); 118 | $this->assertNotNull($catalog->getEntry('a\nb\nc')); 119 | $this->assertNotNull($catalog->getEntry("a\nb\nc")); 120 | } 121 | 122 | public function wrappingDataProvider() 123 | { 124 | return array( 125 | 'Multibyte Wrap (char 81)' => array( 126 | 'value' => 'Hello everybody, Hello ladies and gentlemen.... this is a multibyte translation á with a multibyte beginning at char 81.', 127 | 'wrappingColumn' => 80, 128 | 'assert' => array( 129 | 'Hello everybody, Hello ladies and gentlemen.... this is a multibyte translation ', 130 | 'á with a multibyte beginning at char 81.' 131 | ), 132 | ), 133 | 'Multibyte Wrap (char 80)' => array( 134 | 'value' => 'Hello everybody, Hello ladies and gentlemen... this is a multibyte translation á with a multibyte beginning at char 80.', 135 | 'wrappingColumn' => 80, 136 | 'assert' => array( 137 | 'Hello everybody, Hello ladies and gentlemen... this is a multibyte translation á', 138 | ' with a multibyte beginning at char 80.' 139 | ), 140 | ), 141 | 'Multibyte Wrap (char 79)' => array( 142 | 'value' => 'Hello everybody, Hello ladies and gentlemen.. this is a multibyte translation á with multibytes beginning at char 79.', 143 | 'wrappingColumn' => 80, 144 | 'assert' => array( 145 | 'Hello everybody, Hello ladies and gentlemen.. this is a multibyte translation á ', 146 | 'with multibytes beginning at char 79.' 147 | ), 148 | ), 149 | 'Escape-Sequence Wrap (char 80+81)' => array( 150 | 'value' => 'Hello everybody, Hello ladies and gentlemen..... this is a line with more than \"eighty\" chars. And char 80+81 is an escaped double quote.', 151 | 'wrappingColumn' => 80, 152 | 'assert' => array( 153 | 'Hello everybody, Hello ladies and gentlemen..... this is a line with more than ', 154 | '\"eighty\" chars. And char 80+81 is an escaped double quote.' 155 | ), 156 | ), 157 | 'Escape-Sequence Wrap (char 79+80)' => array( 158 | 'value' => 'Hello everybody, Hello ladies and gentlemen.... this is a line with more than \"eighty\" chars. And char 79+80 is an escaped double quote.', 159 | 'wrappingColumn' => 80, 160 | 'assert' => array( 161 | 'Hello everybody, Hello ladies and gentlemen.... this is a line with more than ', 162 | '\"eighty\" chars. And char 79+80 is an escaped double quote.' 163 | ), 164 | ), 165 | 'Escaped Line-break' => array( 166 | 'value' => 'Hello everybody, \\nHello ladies and gentlemen.', 167 | 'wrappingColumn' => 80, 168 | 'assert' => array( 169 | 'Hello everybody, \\nHello ladies and gentlemen.' 170 | ), 171 | ), 172 | 'String with a lot of multibyte characters should not break when wrappingColumn is at its mb_strlen' => array( 173 | 'value' => 'kategóriáját kötelező', 174 | 'wrappingColumn' => 21, 175 | 'assert' => array( 176 | 'kategóriáját kötelező' 177 | ), 178 | ), 179 | ); 180 | } 181 | 182 | /** 183 | * @dataProvider wrappingDataProvider 184 | * 185 | * @param string $value 186 | * @param int $wrappingColumn 187 | * @param array $assert 188 | */ 189 | public function testWrapping($value, $wrappingColumn, array $assert) 190 | { 191 | 192 | // Make sure that encoding is set to UTF-8 for this test 193 | $mbEncoding = \mb_internal_encoding(); 194 | \mb_internal_encoding('UTF-8'); 195 | 196 | $class = new ReflectionClass('\Sepia\PoParser\PoCompiler'); 197 | try { 198 | // Use Reflection and make private method accessible... 199 | $method = $class->getMethod('wrapString'); 200 | $method->setAccessible(true); 201 | $compiler = new PoCompiler($wrappingColumn); 202 | 203 | } catch (ReflectionException $e) { 204 | $this->fail('Method wrapString not found in PoCompiler'); 205 | return; 206 | } 207 | 208 | // Test the private method 209 | $res = $method->invokeArgs($compiler, array($value)); 210 | $this->assertEquals($assert, $res); 211 | 212 | 213 | // Create a po-file with all the test-values as msgid and a fake translation as msgstr 214 | // And test if the entry could be fetched and the translation equals the msgstr. 215 | 216 | $faker = Factory::create(); 217 | $catalogSource = new CatalogArray(); 218 | 219 | $translation = $faker->paragraph(5); 220 | 221 | $entry = EntryFactory::createFromArray(array( 222 | 'msgid' => $value, 223 | 'msgstr' => $translation 224 | )); 225 | 226 | $catalogSource->addEntry($entry); 227 | try { 228 | $this->saveCatalog($catalogSource); 229 | } catch (Exception $e) { 230 | $this->fail('Cannot save catalog'); 231 | } 232 | 233 | $catalog = $this->parseFile('temp.po'); 234 | $entry = $catalog->getEntry($value); 235 | 236 | $this->assertNotNull($entry); 237 | $this->assertEquals($translation, $entry->getMsgStr()); 238 | 239 | 240 | // Revert encoding to previous setting 241 | \mb_internal_encoding($mbEncoding); 242 | } 243 | 244 | 245 | public function testWriteObsoletePlural() 246 | { 247 | 248 | $catalogSource = new CatalogArray(); 249 | 250 | // Obsolete entry 251 | $entry = EntryFactory::createFromArray(array( 252 | 'msgid' => '%d obsolete string', 253 | 'msgid_plural' => '%d obsolete strings', 254 | 'msgstr' => 'translation.2', 255 | 'msgstr[0]' => 'translation.plural.0', 256 | 'msgstr[1]' => 'translation.plural.1', 257 | 'msgstr[2]' => 'translation.plural.2', 258 | 'reference' => array('src/views/forms.php:45'), 259 | 'tcomment' => array('translator comment'), 260 | 'ccomment' => array('code comment'), 261 | 'flags' => array('fuzzy'), 262 | 'obsolete' => true 263 | )); 264 | 265 | $catalogSource->addEntry($entry); 266 | 267 | try { 268 | $this->saveCatalog($catalogSource); 269 | } catch (Exception $e) { 270 | $this->fail('Cannot save catalog'); 271 | } 272 | 273 | $written_contents = \file_get_contents($this->resourcesPath.'temp.po'); 274 | 275 | $eol = "\n"; 276 | 277 | $expected_contents = '' . 278 | '#, fuzzy' . $eol . 279 | '#~ msgid "%d obsolete string"' . $eol . 280 | '#~ msgid_plural "%d obsolete strings"' . $eol . 281 | '#~ msgstr[0] "translation.plural.0"' . $eol . 282 | '#~ msgstr[1] "translation.plural.1"' . $eol . 283 | '#~ msgstr[2] "translation.plural.2"' . $eol; 284 | 285 | $this->assertEquals($expected_contents, $written_contents); 286 | 287 | } 288 | 289 | /** 290 | * @param Catalog $catalog 291 | * @param int $wrappingColumn 292 | * @throws Exception 293 | */ 294 | protected function saveCatalog(Catalog $catalog, $wrappingColumn = 80) 295 | { 296 | $fileHandler = new FileSystem($this->resourcesPath.'temp.po'); 297 | $compiler = new PoCompiler($wrappingColumn); 298 | $fileHandler->save($compiler->compile($catalog)); 299 | } 300 | 301 | private function assertPoFile(CatalogArray $catalogSource, Catalog $catalogNew) 302 | { 303 | foreach ($catalogSource->getEntries() as $entry) { 304 | $entryWritten = $catalogNew->getEntry($entry->getMsgId(), $entry->getMsgCtxt()); 305 | 306 | $this->assertNotNull($entryWritten, 'Entry not found:'.$entry->getMsgId().','.$entry->getMsgCtxt()); 307 | 308 | $this->assertEquals($entry->getMsgStr(), $entryWritten->getMsgStr()); 309 | $this->assertEquals($entry->getMsgCtxt(), $entryWritten->getMsgCtxt()); 310 | $this->assertEquals($entry->getFlags(), $entryWritten->getFlags()); 311 | $this->assertEquals($entry->isObsolete(), $entryWritten->isObsolete()); 312 | 313 | if ($entry->isObsolete() === true) { 314 | $this->assertEmpty($entryWritten->getReference()); 315 | $this->assertEmpty($entryWritten->getTranslatorComments()); 316 | $this->assertEmpty($entryWritten->getDeveloperComments()); 317 | } else { 318 | $this->assertEquals($entry->getReference(), $entryWritten->getReference()); 319 | $this->assertEquals($entry->getDeveloperComments(), $entryWritten->getDeveloperComments()); 320 | $this->assertEquals($entry->getTranslatorComments(), $entryWritten->getTranslatorComments()); 321 | } 322 | } 323 | } 324 | 325 | public function tearDown() 326 | { 327 | parent::tearDown(); 328 | 329 | //if (file_exists($this->resourcesPath.'temp.po')) { 330 | // unlink($this->resourcesPath.'temp.po'); 331 | //} 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /tests/pofiles/context.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: \n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2013-09-25 15:55+0100\n" 6 | "PO-Revision-Date: \n" 7 | "Last-Translator: Raúl Ferràs \n" 8 | "Language-Team: \n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 13 | "X-Poedit-SourceCharset: UTF-8\n" 14 | "X-Poedit-KeywordsList: __;_e;_n;_t\n" 15 | "X-Textdomain-Support: yes\n" 16 | "X-Poedit-Basepath: .\n" 17 | "X-Generator: Poedit 1.5.7\n" 18 | 19 | #: wp-admin/custom-background.php:305 20 | msgctxt "Background Attachment" 21 | msgid "Attachment" 22 | msgstr "Adjunto" 23 | 24 | #: wp-admin/includes/schema.php:369 25 | msgctxt "start of week" 26 | msgid "1" 27 | msgstr "1" 28 | 29 | msgid "1" 30 | msgstr "1" 31 | 32 | #: wp-admin/includes/screen.php:956 33 | msgctxt "Welcome panel" 34 | msgid "Welcome" 35 | msgstr "Hola" 36 | 37 | #: wp-admin/includes/schema.php:355 38 | msgctxt "default GMT offset or timezone string" 39 | msgid "0" 40 | msgstr "0" 41 | 42 | #: wp-admin/includes/template.php:662 43 | #, fuzzy 44 | msgid "%1$s-%2$s" 45 | msgstr "%1$s-%2$s" 46 | 47 | #: wp-admin/custom-header.php:506 48 | msgid "Images should be at least %1$d pixels wide." 49 | msgstr "Las imágenes deben ser de al menos %1$d pixels de ancho." 50 | 51 | #: wp-admin/custom-header.php:515 52 | msgid "Suggested height is %1$d pixels." 53 | msgstr "La altura sugerida es de %1$d pixels." 54 | 55 | #: wp-admin/custom-header.php:509 56 | msgid "Images should be at least %1$d pixels tall." 57 | msgstr "Las imágenes deben ser de al menos %1$d pixels de altura." 58 | 59 | #: wp-admin/install.php:177 60 | msgctxt "Howdy" 61 | msgid "Welcome" 62 | msgstr "Hola" 63 | -------------------------------------------------------------------------------- /tests/pofiles/flags-phpformat-fuzzy.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: \n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2013-09-25 15:55+0100\n" 6 | "PO-Revision-Date: \n" 7 | "Last-Translator: Raúl Ferràs \n" 8 | "Language-Team: \n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 13 | "X-Poedit-SourceCharset: UTF-8\n" 14 | "X-Poedit-KeywordsList: __;_e;_n;_t\n" 15 | "X-Textdomain-Support: yes\n" 16 | "X-Poedit-Basepath: .\n" 17 | "X-Generator: Poedit 1.5.7\n" 18 | 19 | #: wp-admin/custom-background.php:305 20 | #, php-format, fuzzy 21 | msgctxt "Background Attachment" 22 | msgid "Attachment" 23 | msgstr "Adjunto" 24 | -------------------------------------------------------------------------------- /tests/pofiles/flags-phpformat.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: \n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2013-09-25 15:55+0100\n" 6 | "PO-Revision-Date: \n" 7 | "Last-Translator: Raúl Ferràs \n" 8 | "Language-Team: \n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 13 | "X-Poedit-SourceCharset: UTF-8\n" 14 | "X-Poedit-KeywordsList: __;_e;_n;_t\n" 15 | "X-Textdomain-Support: yes\n" 16 | "X-Poedit-Basepath: .\n" 17 | "X-Generator: Poedit 1.5.7\n" 18 | 19 | #: wp-admin/custom-background.php:305 20 | #, php-format 21 | msgctxt "Background Attachment" 22 | msgid "Attachment" 23 | msgstr "Adjunto" 24 | -------------------------------------------------------------------------------- /tests/pofiles/healthy.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: \n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2013-09-25 15:55+0100\n" 6 | "PO-Revision-Date: \n" 7 | "Last-Translator: Raúl Ferràs \n" 8 | "Language-Team: \n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 13 | "X-Poedit-SourceCharset: UTF-8\n" 14 | "X-Poedit-KeywordsList: __;_e;_n;_t\n" 15 | "X-Textdomain-Support: yes\n" 16 | "X-Poedit-Basepath: .\n" 17 | "X-Generator: Poedit 1.5.7\n" 18 | "X-Poedit-SearchPath-0: .\n" 19 | "X-Poedit-SearchPath-1: ../..\n" 20 | "X-Poedit-SearchPath-2: ../../../modules\n" 21 | 22 | #: ../../classes/dddddd.php:33 23 | msgid "string.1" 24 | msgstr "translation.1" 25 | 26 | # Translator comment 27 | #. Code comment 28 | #: ../../classes/xxxxx.php:96 ../../classes/controller/iiiiiii.php:107 29 | #: ../../classes/controller/yyyyyyy/zzzzzz.php:288 30 | msgid "string.2" 31 | msgstr "Has d'indicar un nom." 32 | 33 | 34 | #| msgid "%d php-файло" 35 | #| msgid_plural "%d php-файлины" 36 | msgid "%d php-файл" 37 | msgid_plural "%d php-файлов" 38 | msgstr "translation" 39 | -------------------------------------------------------------------------------- /tests/pofiles/multilines.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: \n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2013-09-25 15:55+0100\n" 6 | "PO-Revision-Date: \n" 7 | "Last-Translator: Raúl Ferràs \n" 8 | "Language-Team: \n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 13 | "X-Poedit-SourceCharset: UTF-8\n" 14 | "X-Poedit-KeywordsList: __;_e;_n;_t\n" 15 | "X-Textdomain-Support: yes\n" 16 | "X-Poedit-Basepath: .\n" 17 | "X-Generator: Poedit 1.5.7\n" 18 | "X-Poedit-SearchPath-0: .\n" 19 | "X-Poedit-SearchPath-1: ../..\n" 20 | "X-Poedit-SearchPath-2: ../../../modules\n" 21 | 22 | # @ default 23 | #: ../../classes/dddddd.php:33 24 | msgid "Lo sentimos, ha ocurrido un error..." 25 | msgstr "Ho sentim, s'ha produït un error" 26 | 27 | #: ../../classes/xxxxx.php:96 ../../classes/controller/iiiiiii.php:107 28 | #: ../../classes/controller/yyyyyyy/zzzzzz.php:288 29 | msgid "Debes indicar un nombre." 30 | msgstr "Has d'indicar un nom." 31 | 32 | #: ../../classes/ccccc.php:100 ../../classes/cccc.php:104 33 | #: ../../classes/controller/cccc/mmmmm.php:295 34 | #: ../../classes/controller/cccc/mmmmm.php:513 35 | msgid "Ya existe otro usuario con este mismo nombre." 36 | msgstr "Ja existeix un altre usuari amb aquest nom." 37 | 38 | #: ../../classes/ccccc.php:108 ../../classes/controller/cccc/mmmmm.php:305 39 | #: ../../classes/controller/ccccc/mmmmm.php:518 40 | msgid "Debes indicar una dirección web!" 41 | msgstr "Has d'indicar una direcció web!" 42 | 43 | # @ default 44 | #: ../../classes/uuuuuuu.php:175 45 | #, fuzzy 46 | msgid "El archivo supera el tamaño máximo permitido: %size%MB" 47 | msgstr "" 48 | "El fitxer {file} es massa gran, el tamany máxim de fitxer es {sizeLimit}." 49 | 50 | #: ../../classes/controller/ccccc.php:361 51 | msgid "" 52 | "%user% acaba de responder tu comentario.
Consulta que te ha " 53 | "dicho %link%aquí." 54 | msgstr "" 55 | "%user% acaba de respondre el teu comentari.
Consulta que t'ha " 56 | "dit %link%aquí." 57 | 58 | #: ../../classes/controller/eeee.php:786 59 | msgid "" 60 | "Atención, para finalizar el proceso debes iniciar sesión con tu usuario " 61 | ".
No sufras, hemos guardado los datos introducidos, una vez " 62 | "te autentifiques te redirigiremos al paso final." 63 | msgstr "" 64 | "Atenció, per finalitzar el procés has de iniciar sessió amb el teu usuari " 65 | ".
No pateixis, hem desat les teves dades, un cop " 66 | "t'autentifiquis finalitzarem el procés." 67 | 68 | # @ default 69 | #~ msgid "Arrastra imagenes aquí para subirlas." 70 | #~ msgstr "Arrossega les teves imatges aquí per pujarles." 71 | 72 | # @ default 73 | #~ msgid "" 74 | #~ "El archivo {file} es demasiado pequeño, el tamaño mínimo de archivo es " 75 | #~ "{minSizeLimit}." 76 | #~ msgstr "" 77 | #~ "El fitxer {file} es massa petit, el tamany mínim de fitxer es " 78 | #~ "{minSizeLimit}." 79 | -------------------------------------------------------------------------------- /tests/pofiles/noheader.po: -------------------------------------------------------------------------------- 1 | #: tmp/cache/00/78/1d67cd3f64c5469c21cb05e6fd0a.php:32 2 | #: tmp/cache/07/0a/9136032d279c7c9bb536e108fced.php:58 3 | #: tmp/cache/0b/da/a019bff51aa8a932ec179398dc43.php:65 4 | #: tmp/cache/18/41/43e62ed075451a77574351278bf2.php:58 5 | #: tmp/cache/1d/81/2af439d1ee9aac469ff66b2173d0.php:58 6 | #: tmp/cache/32/46/d22400dcca2bfe7f4e4afd39fd01.php:65 7 | #: tmp/cache/32/da/138149be12bf58b7fa8b621abcf4.php:35 8 | #: tmp/cache/35/77/88dcd5f3a90a7842ea85959569c9.php:58 9 | #: tmp/cache/69/1c/68c94bb5f661f65ef796afc069a9.php:58 10 | #: tmp/cache/95/14/b34e21f518a52d77109ed535cb5f.php:59 11 | #: tmp/cache/9e/6d/48f3a91d014c76613583be955d63.php:58 12 | #: tmp/cache/ac/e0/356f9dc6499cc1b18453093a79f3.php:58 13 | #: tmp/cache/b1/10/16e1bfb77387f1ca7d0ff4c67a64.php:57 14 | #: tmp/cache/fe/91/b339a580bcf69b609d15988a1b42.php:58 15 | msgid "Membership" 16 | msgstr "" 17 | 18 | #: tmp/cache/00/78/1d67cd3f64c5469c21cb05e6fd0a.php:45 19 | #: tmp/cache/18/05/1489b493d73a1596584807221eb9.php:40 20 | #: tmp/cache/27/73/8503d0d9f20aabc08a6372c67dba.php:49 21 | #: tmp/cache/7a/e7/9b68911bacf2b1ef75790f07bff8.php:23 22 | msgid "New membership type" 23 | msgstr "" 24 | -------------------------------------------------------------------------------- /tests/pofiles/pluralsMultiline.po: -------------------------------------------------------------------------------- 1 | # Translation of Administration in Spanish (Spain) 2 | # This file is distributed under the same license as the Administration package. 3 | msgid "" 4 | msgstr "" 5 | "PO-Revision-Date: 2013-10-23 09:51:48+0000\n" 6 | "MIME-Version: 1.0\n" 7 | "Content-Type: text/plain; charset=UTF-8\n" 8 | "Content-Transfer-Encoding: 8bit\n" 9 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 10 | "X-Generator: GlotPress/0.1\n" 11 | "Project-Id-Version: Administration\n" 12 | 13 | 14 | #: wp-admin/edit.php:238 15 | msgid "%s post not updated," 16 | "somebody is editing it." 17 | msgid_plural "%s posts not updated," 18 | "somebody is editing them." 19 | msgstr[0] "%s entrada no actualizada," 20 | "alguien la está editando." 21 | msgstr[1] "%s entradas no actualizadas," 22 | "alguien las está editando." 23 | 24 | #: wp-admin/edit.php:239 25 | msgid "" 26 | "%s post permanently deleted." 27 | msgid_plural "%s posts permanently deleted." 28 | msgstr[0] "" 29 | "%s entrada borrada" 30 | "permanentemente." 31 | msgstr[1] "" 32 | "%s entradas borradas" 33 | "permanentemente." 34 | -------------------------------------------------------------------------------- /tests/pofiles/previous_unstranslated.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: volga-volga.local\n" 4 | "Report-Msgid-Bugs-To: pm@101media.ru\n" 5 | "POT-Creation-Date: 2014-07-08 07:51+0400\n" 6 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 7 | "Last-Translator: FULL NAME \n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 12 | 13 | #| msgid "this is a previous string" 14 | #| msgstr "this is a previous translation string" 15 | msgid "this is a string" 16 | msgstr "this is a translation" 17 | 18 | --------------------------------------------------------------------------------