diff --git a/imgboard.php b/imgboard.php index 8445e89..4f93e0d 100644 --- a/imgboard.php +++ b/imgboard.php @@ -25,6 +25,8 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +use Gettext\Translator; +use Gettext\Translations; error_reporting(E_ALL); ini_set("display_errors", 1); @@ -60,24 +62,24 @@ if (!file_exists('settings.php')) { } require 'settings.php'; -if (function_exists('_')) { - if (defined('TINYIB_LOCALE')) { - setlocale(LC_ALL, TINYIB_LOCALE); - } - bindtextdomain('tinyib', 'locale'); - textdomain('tinyib'); -} else { - function _($string) { +if (TINYIB_LOCALE == '') { + function __($string) { return $string; } +} else { + require 'inc/gettext/src/autoloader.php'; + $translations = Translations::fromPoFile('locale/' . TINYIB_LOCALE . '/LC_MESSAGES/tinyib.po'); + $translator = new Translator(); + $translator->loadTranslations($translations); + $translator->register(); } if (TINYIB_TRIPSEED == '' || TINYIB_ADMINPASS == '') { - fancyDie(_('TINYIB_TRIPSEED and TINYIB_ADMINPASS must be configured.')); + fancyDie(__('TINYIB_TRIPSEED and TINYIB_ADMINPASS must be configured.')); } if (TINYIB_CAPTCHA === 'recaptcha' && (TINYIB_RECAPTCHA_SITE == '' || TINYIB_RECAPTCHA_SECRET == '')) { - fancyDie(_('TINYIB_RECAPTCHA_SITE and TINYIB_RECAPTCHA_SECRET must be configured.')); + fancyDie(__('TINYIB_RECAPTCHA_SITE and TINYIB_RECAPTCHA_SECRET must be configured.')); } // Check directories are writable by the script @@ -87,7 +89,7 @@ if (TINYIB_DBMODE == 'flatfile') { } foreach ($writedirs as $dir) { if (!is_writable($dir)) { - fancyDie(sprintf(_("Directory '%s' can not be written to. Please modify its permissions."), $dir)); + fancyDie(sprintf(__("Directory '%s' can not be written to. Please modify its permissions."), $dir)); } } @@ -95,7 +97,7 @@ $includes = array("inc/defines.php", "inc/functions.php", "inc/html.php"); if (in_array(TINYIB_DBMODE, array('flatfile', 'mysql', 'mysqli', 'sqlite', 'sqlite3', 'pdo'))) { $includes[] = 'inc/database_' . TINYIB_DBMODE . '.php'; } else { - fancyDie(_('Unknown database mode specified.')); + fancyDie(__('Unknown database mode specified.')); } foreach ($includes as $include) { @@ -110,7 +112,7 @@ $redirect = true; // Check if the request is to make a post if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) || isset($_POST['email']) || isset($_POST['subject']) || isset($_POST['message']) || isset($_POST['file']) || isset($_POST['embed']) || isset($_POST['password']))) { if (TINYIB_DBMIGRATE) { - fancyDie(_('Posting is currently disabled.
Please try again in a few moments.')); + fancyDie(__('Posting is currently disabled.
Please try again in a few moments.')); } list($loggedin, $isadmin) = manageCheckLogIn(); @@ -129,9 +131,9 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) if ($post['parent'] != TINYIB_NEWTHREAD && !$loggedin) { $parent = postByID($post['parent']); if (!isset($parent['locked'])) { - fancyDie(_('Invalid parent thread ID supplied, unable to create post.')); + fancyDie(__('Invalid parent thread ID supplied, unable to create post.')); } else if ($parent['locked'] == 1) { - fancyDie(_('Replies are not allowed to locked threads.')); + fancyDie(__('Replies are not allowed to locked threads.')); } } @@ -168,12 +170,12 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) if (isset($_POST['embed']) && trim($_POST['embed']) != '' && ($rawpost || !in_array('embed', $hide_fields))) { if (isset($_FILES['file']) && $_FILES['file']['name'] != "") { - fancyDie(_('Embedding a URL and uploading a file at the same time is not supported.')); + fancyDie(__('Embedding a URL and uploading a file at the same time is not supported.')); } list($service, $embed) = getEmbed(trim($_POST['embed'])); if (empty($embed) || !isset($embed['html']) || !isset($embed['title']) || !isset($embed['thumbnail_url'])) { - fancyDie(sprintf(_('Invalid embed URL. Only %s URLs are supported.'), implode('/', array_keys($tinyib_embeds)))); + fancyDie(sprintf(__('Invalid embed URL. Only %s URLs are supported.'), implode('/', array_keys($tinyib_embeds)))); } $post['file_hex'] = $service; @@ -193,14 +195,14 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) } else if ($file_mime == "image/png") { $post['thumb'] = $temp_file . '.png'; } else { - fancyDie(_('Error while processing audio/video.')); + fancyDie(__('Error while processing audio/video.')); } $thumb_location = "thumb/" . $post['thumb']; list($thumb_maxwidth, $thumb_maxheight) = thumbnailDimensions($post); if (!createThumbnail($file_location, $thumb_location, $thumb_maxwidth, $thumb_maxheight)) { - fancyDie(_('Could not create thumbnail.')); + fancyDie(__('Could not create thumbnail.')); } addVideoOverlay($thumb_location); @@ -216,11 +218,11 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) validateFileUpload(); if (!is_file($_FILES['file']['tmp_name']) || !is_readable($_FILES['file']['tmp_name'])) { - fancyDie(_('File transfer failure. Please retry the submission.')); + fancyDie(__('File transfer failure. Please retry the submission.')); } if ((TINYIB_MAXKB > 0) && (filesize($_FILES['file']['tmp_name']) > (TINYIB_MAXKB * 1024))) { - fancyDie(sprintf(_('That file is larger than %s.'), TINYIB_MAXKBDESC)); + fancyDie(sprintf(__('That file is larger than %s.'), TINYIB_MAXKBDESC)); } $post['file_original'] = trim(htmlentities(substr($_FILES['file']['name'], 0, 50), ENT_QUOTES)); @@ -235,7 +237,7 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) $file_mime = strtolower(array_pop($file_mime_split)); } else { if (!@getimagesize($_FILES['file']['tmp_name'])) { - fancyDie(_('Failed to read the MIME type and size of the uploaded file. Please retry the submission.')); + fancyDie(__('Failed to read the MIME type and size of the uploaded file. Please retry the submission.')); } $file_info = getimagesize($_FILES['file']['tmp_name']); @@ -251,12 +253,12 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) $file_location = "src/" . $post['file']; if (!move_uploaded_file($_FILES['file']['tmp_name'], $file_location)) { - fancyDie(_('Could not copy uploaded file.')); + fancyDie(__('Could not copy uploaded file.')); } if ($_FILES['file']['size'] != filesize($file_location)) { @unlink($file_location); - fancyDie(_('File transfer failure. Please go back and try again.')); + fancyDie(__('File transfer failure. Please go back and try again.')); } if ($file_mime == "audio/webm" || $file_mime == "video/webm" || $file_mime == "audio/mp4" || $file_mime == "video/mp4") { @@ -275,7 +277,7 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) if ($post['thumb_width'] <= 0 || $post['thumb_height'] <= 0) { @unlink($file_location); @unlink("thumb/" . $post['thumb']); - fancyDie(_('Sorry, your video appears to be corrupt.')); + fancyDie(__('Sorry, your video appears to be corrupt.')); } addVideoOverlay("thumb/" . $post['thumb']); @@ -300,7 +302,7 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) $post['thumb'] = $file_name . "s." . array_pop($thumbfile_split); if (!copy($tinyib_uploads[$file_mime][1], "thumb/" . $post['thumb'])) { @unlink($file_location); - fancyDie(_('Could not create thumbnail.')); + fancyDie(__('Could not create thumbnail.')); } if ($file_mime == "application/x-shockwave-flash") { addVideoOverlay("thumb/" . $post['thumb']); @@ -311,7 +313,7 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) if (!createThumbnail($file_location, "thumb/" . $post['thumb'], $thumb_maxwidth, $thumb_maxheight)) { @unlink($file_location); - fancyDie(_('Could not create thumbnail.')); + fancyDie(__('Could not create thumbnail.')); } } @@ -335,7 +337,7 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) $allowed .= "embed URL"; } if ($post['parent'] == TINYIB_NEWTHREAD && $allowed != "" && !TINYIB_NOFILEOK) { - fancyDie(sprintf(_('A %s is required to start a thread.'), $allowed)); + fancyDie(sprintf(__('A %s is required to start a thread.'), $allowed)); } if (!$rawpost && str_replace('
', '', $post['message']) == "") { $die_msg = ""; @@ -348,12 +350,12 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) fancyDie("Please $die_msg."); } } else { - echo sprintf(_('%s uploaded.'), $post['file_original']) . '
'; + echo sprintf(__('%s uploaded.'), $post['file_original']) . '
'; } if (!$loggedin && (($post['file'] != '' && TINYIB_REQMOD == 'files') || TINYIB_REQMOD == 'all')) { $post['moderated'] = '0'; - echo sprintf(_('Your %s will be shown once it has been approved.'), $post['parent'] == TINYIB_NEWTHREAD ? 'thread' : 'post') . '
'; + echo sprintf(__('Your %s will be shown once it has been approved.'), $post['parent'] == TINYIB_NEWTHREAD ? 'thread' : 'post') . '
'; $slow_redirect = true; } @@ -366,7 +368,7 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) trimThreads(); - echo _('Updating thread...') . '
'; + echo __('Updating thread...') . '
'; if ($post['parent'] != TINYIB_NEWTHREAD) { rebuildThread($post['parent']); @@ -379,17 +381,17 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) rebuildThread($post['id']); } - echo _('Updating index...') . '
'; + echo __('Updating index...') . '
'; rebuildIndexes(); } // Check if the request is to delete a post and/or its associated image } elseif (isset($_GET['delete']) && !isset($_GET['manage'])) { if (!isset($_POST['delete'])) { - fancyDie(_('Tick the box next to a post and click "Delete" to delete it.')); + fancyDie(__('Tick the box next to a post and click "Delete" to delete it.')); } if (TINYIB_DBMIGRATE) { - fancyDie(_('Post deletion is currently disabled.
Please try again in a few moments.')); + fancyDie(__('Post deletion is currently disabled.
Please try again in a few moments.')); } $post = postByID($_POST['delete']); @@ -406,12 +408,12 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) } else { threadUpdated($post['parent']); } - fancyDie(_('Post deleted.')); + fancyDie(__('Post deleted.')); } else { - fancyDie(_('Invalid password.')); + fancyDie(__('Invalid password.')); } } else { - fancyDie(_('Sorry, an invalid post identifier was sent. Please go back, refresh the page, and try again.')); + fancyDie(__('Sorry, an invalid post identifier was sent. Please go back, refresh the page, and try again.')); } $redirect = false; @@ -435,7 +437,7 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) rebuildThread($thread['id']); } rebuildIndexes(); - $text .= manageInfo(_('Rebuilt board.')); + $text .= manageInfo(__('Rebuilt board.')); } elseif (isset($_GET['bans'])) { clearExpiredBans(); @@ -443,7 +445,7 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) if ($_POST['ip'] != '') { $banexists = banByIP($_POST['ip']); if ($banexists) { - fancyDie(_('Sorry, there is already a ban on record for that IP address.')); + fancyDie(__('Sorry, there is already a ban on record for that IP address.')); } $ban = array(); @@ -452,13 +454,13 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) $ban['reason'] = $_POST['reason']; insertBan($ban); - $text .= manageInfo(sprintf(_('Ban record added for %s'), $ban['ip'])); + $text .= manageInfo(sprintf(__('Ban record added for %s'), $ban['ip'])); } } elseif (isset($_GET['lift'])) { $ban = banByID($_GET['lift']); if ($ban) { deleteBanByID($_GET['lift']); - $text .= manageInfo(sprintf(_('Ban record lifted for %s'), $ban['ip'])); + $text .= manageInfo(sprintf(__('Ban record lifted for %s'), $ban['ip'])); } } @@ -550,9 +552,9 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) if ($post['parent'] != TINYIB_NEWTHREAD) { rebuildThread($post['parent']); } - $text .= manageInfo(sprintf(_('Post No.%d deleted.'), $post['id'])); + $text .= manageInfo(sprintf(__('Post No.%d deleted.'), $post['id'])); } else { - fancyDie(_("Sorry, there doesn't appear to be a post with that ID.")); + fancyDie(__("Sorry, there doesn't appear to be a post with that ID.")); } } elseif (isset($_GET['approve'])) { if ($_GET['approve'] > 0) { @@ -566,9 +568,9 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) } threadUpdated($thread_id); - $text .= manageInfo(sprintf(_('Post No.%d approved.'), $post['id'])); + $text .= manageInfo(sprintf(__('Post No.%d approved.'), $post['id'])); } else { - fancyDie(_("Sorry, there doesn't appear to be a post with that ID.")); + fancyDie(__("Sorry, there doesn't appear to be a post with that ID.")); } } } elseif (isset($_GET['moderate'])) { @@ -577,7 +579,7 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) if ($post) { $text .= manageModeratePost($post); } else { - fancyDie(_("Sorry, there doesn't appear to be a post with that ID.")); + fancyDie(__("Sorry, there doesn't appear to be a post with that ID.")); } } else { $onload = manageOnLoad('moderate'); @@ -592,10 +594,10 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) $text .= manageInfo('Thread No.' . $post['id'] . ' ' . (intval($_GET['setsticky']) == 1 ? 'stickied' : 'un-stickied') . '.'); } else { - fancyDie(_("Sorry, there doesn't appear to be a post with that ID.")); + fancyDie(__("Sorry, there doesn't appear to be a post with that ID.")); } } else { - fancyDie(_('Form data was lost. Please go back and try again.')); + fancyDie(__('Form data was lost. Please go back and try again.')); } } elseif (isset($_GET['lock']) && isset($_GET['setlock'])) { if ($_GET['lock'] > 0) { @@ -606,10 +608,10 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name']) $text .= manageInfo('Thread No.' . $post['id'] . ' ' . (intval($_GET['setlock']) == 1 ? 'locked' : 'unlocked') . '.'); } else { - fancyDie(_("Sorry, there doesn't appear to be a post with that ID.")); + fancyDie(__("Sorry, there doesn't appear to be a post with that ID.")); } } else { - fancyDie(_('Form data was lost. Please go back and try again.')); + fancyDie(__('Form data was lost. Please go back and try again.')); } } elseif (isset($_GET["rawpost"])) { $onload = manageOnLoad("rawpost"); diff --git a/inc/functions.php b/inc/functions.php index bf1f4e6..77c53ad 100644 --- a/inc/functions.php +++ b/inc/functions.php @@ -211,7 +211,7 @@ function nameAndTripcode($name) { function nameBlock($name, $tripcode, $email, $timestamp, $rawposttext) { $output = ''; - $output .= ($name == '' && $tripcode == '') ? _('Anonymous') : $name; + $output .= ($name == '' && $tripcode == '') ? __('Anonymous') : $name; if ($tripcode != '') { $output .= '!' . $tripcode; @@ -320,9 +320,9 @@ function checkCAPTCHA() { $captcha_solution = isset($_SESSION['tinyibcaptcha']) ? strtolower(trim($_SESSION['tinyibcaptcha'])) : ''; if ($captcha == '') { - fancyDie(_('Please enter the CAPTCHA text.')); + fancyDie(__('Please enter the CAPTCHA text.')); } else if ($captcha != $captcha_solution) { - fancyDie(_('Incorrect CAPTCHA text entered. Please try again.
Click the image to retrieve a new CAPTCHA.')); + fancyDie(__('Incorrect CAPTCHA text entered. Please try again.
Click the image to retrieve a new CAPTCHA.')); } } } @@ -353,7 +353,7 @@ function checkFlood() { function checkMessageSize() { if (strlen($_POST["message"]) > 8000) { - fancyDie(sprintf(_('Please shorten your message, or post it in multiple parts. Your message is %1$d characters long, and the maximum allowed is %2$d.'), strlen($_POST["message"]), 8000)); + fancyDie(sprintf(__('Please shorten your message, or post it in multiple parts. Your message is %1$d characters long, and the maximum allowed is %2$d.'), strlen($_POST["message"]), 8000)); } } @@ -384,7 +384,7 @@ function setParent() { if (isset($_POST["parent"])) { if ($_POST["parent"] != TINYIB_NEWTHREAD) { if (!threadExistsByID($_POST['parent'])) { - fancyDie(_('Invalid parent thread ID supplied, unable to create post.')); + fancyDie(__('Invalid parent thread ID supplied, unable to create post.')); } return $_POST["parent"]; @@ -410,25 +410,25 @@ function validateFileUpload() { case UPLOAD_ERR_OK: break; case UPLOAD_ERR_FORM_SIZE: - fancyDie(sprintf(_('That file is larger than %s.'), TINYIB_MAXKBDESC)); + fancyDie(sprintf(__('That file is larger than %s.'), TINYIB_MAXKBDESC)); break; case UPLOAD_ERR_INI_SIZE: - fancyDie(sprintf(_('The uploaded file exceeds the upload_max_filesize directive (%s) in php.ini.'), ini_get('upload_max_filesize'))); + fancyDie(sprintf(__('The uploaded file exceeds the upload_max_filesize directive (%s) in php.ini.'), ini_get('upload_max_filesize'))); break; case UPLOAD_ERR_PARTIAL: - fancyDie(_('The uploaded file was only partially uploaded.')); + fancyDie(__('The uploaded file was only partially uploaded.')); break; case UPLOAD_ERR_NO_FILE: - fancyDie(_('No file was uploaded.')); + fancyDie(__('No file was uploaded.')); break; case UPLOAD_ERR_NO_TMP_DIR: - fancyDie(_('Missing a temporary folder.')); + fancyDie(__('Missing a temporary folder.')); break; case UPLOAD_ERR_CANT_WRITE: - fancyDie(_('Failed to write file to disk')); + fancyDie(__('Failed to write file to disk')); break; default: - fancyDie(_('Unable to save the uploaded file.')); + fancyDie(__('Unable to save the uploaded file.')); } } @@ -436,7 +436,7 @@ function checkDuplicateFile($hex) { $hexmatches = postsByHex($hex); if (count($hexmatches) > 0) { foreach ($hexmatches as $hexmatch) { - fancyDie(sprintf(_('Duplicate file uploaded. That file has already been posted here.'), 'res/' . (($hexmatch['parent'] == TINYIB_NEWTHREAD) ? $hexmatch['id'] : $hexmatch['parent'])) . '.html#' . $hexmatch['id']); + fancyDie(sprintf(__('Duplicate file uploaded. That file has already been posted here.'), 'res/' . (($hexmatch['parent'] == TINYIB_NEWTHREAD) ? $hexmatch['id'] : $hexmatch['parent'])) . '.html#' . $hexmatch['id']); } } } @@ -467,7 +467,7 @@ function createThumbnail($file_location, $thumb_location, $new_w, $new_h) { } if (!$src_img) { - fancyDie(_('Unable to read the uploaded file while creating its thumbnail. A common cause for this is an incorrect extension when the file is actually of a different type.')); + fancyDie(__('Unable to read the uploaded file while creating its thumbnail. A common cause for this is an incorrect extension when the file is actually of a different type.')); } $old_x = imageSX($src_img); diff --git a/inc/gettext/CHANGELOG.md b/inc/gettext/CHANGELOG.md new file mode 100644 index 0000000..d270cd8 --- /dev/null +++ b/inc/gettext/CHANGELOG.md @@ -0,0 +1,172 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + +Previous releases are documented in [github releases](https://github.com/oscarotero/Gettext/releases) + +## [4.8.2] - 2019-12-02 +### Fixed +- UTF-8 handling for VueJs extractor [#242] + +## [4.8.1] - 2019-11-15 +### Fixed +- Php error when scanning for a single domain but other string found [#238] + +## [4.8.0] - 2019-11-04 +### Changed +- Many `private` properties and methods were changed to `protected` in order to improve the extensibility [#231] + +### Fixed +- PHP 7.4 support [#230] + +## [4.7.0] - 2019-10-07 +### Added +- Support for UnitID in Xliff [#221] [#224] [#225] +- Support for scan multiple domains at the same time [#223] + +### Fixed +- New lines in windows [#218] [#226] + +## [4.6.3] - 2019-07-15 +### Added +- Some VueJs extraction improvements and additions [#205], [#213] + +### Fixed +- Multiline extractions in jsCode [#200] +- Support for js template literals [#214] +- Fixed tabs in PHP comments [#215] + +## [4.6.2] - 2019-01-12 +### Added +- New option `facade` in blade extractor to use a facade instead create a blade compiler [#197], [#198] + +### Fixed +- Added php-7.3 to travis +- Added VueJS extractor method docblocks for IDEs [#191] + +## [4.6.1] - 2018-08-27 +### Fixed +- VueJS DOM parsing [#188] +- Javascript parser was unable to extract some functions [#187] + +## [4.6.0] - 2018-06-26 +### Added +- New extractor for VueJs [#178] + +### Fixed +- Do not include empty translations containing the headers in the translator [#182] +- Test enhancement [#177] + +## [4.5.0] - 2018-04-23 +### Added +- Support for disabled translations + +### Fixed +- Added php-7.2 to travis +- Fixed po tests on bigendian [#159] +- Improved comment estraction [#166] +- Fixed incorrect docs to dn__ function [#170] +- Ignored phpcs.xml file on export [#168] +- Improved `@method` docs in `Translations` [#175] + +## [4.4.4] - 2018-02-21 +### Fixed +- Changed the comment extraction to be compatible with gettext behaviour: the comment must be placed in the line preceding the function [#161] + +### Security +- Validate eval input from plural forms [#156] + +## [4.4.3] - 2017-08-09 +### Fixed +- Handle `NULL` arguments on extract entries in php. For example `dn__(null, 'singular', 'plural')`. +- Fixed the `PhpCode` and `JsCode` extractors that didn't extract `dn__` and `dngettext` entries [#155]. +- Fixed the `PhpCode` and `JsCode` extractors that didn't extract `dnpgettext` correctly. + +## [4.4.2] - 2017-07-27 +### Fixed +- Clone the translations in `Translations::mergeWith` to prevent that the translation is referenced in both places. [#152] +- Fixed escaped quotes in the javascript extractor [#154] + +## [4.4.1] - 2017-05-20 +### Fixed +- Fixed a bug where the options was not passed correctly to the merging Translations object [#147] +- Unified the plural behaviours between PHP gettext and Translator when the plural translation is unknown [#148] +- Removed the deprecated function `create_function()` and use `eval()` instead + +## [4.4.0] - 2017-05-10 +### Added +- New option `noLocation` to po generator, to omit the references [#143] +- New options `delimiter`, `enclosure` and `escape_char` to Csv and CsvDictionary extractors and generators [#145] +- Added the missing `dn__()` function [#146] + +### Fixed +- Improved the code style including php_codesniffer in development + +## 4.3.0 - 2017-03-04 +### Added +- Added support for named placeholders (using `strtr`). For example: + ```php + __('Hello :name', [':name' => 'World']); + ``` +- Added support for Twig v2 +- New function `BaseTranslator::includeFunctions()` to include the functions file without register any translator + +### Fixed +- Fixed a bug related with the javascript source extraction with single quotes + +[#143]: https://github.com/oscarotero/Gettext/issues/143 +[#145]: https://github.com/oscarotero/Gettext/issues/145 +[#146]: https://github.com/oscarotero/Gettext/issues/146 +[#147]: https://github.com/oscarotero/Gettext/issues/147 +[#148]: https://github.com/oscarotero/Gettext/issues/148 +[#152]: https://github.com/oscarotero/Gettext/issues/152 +[#154]: https://github.com/oscarotero/Gettext/issues/154 +[#155]: https://github.com/oscarotero/Gettext/issues/155 +[#156]: https://github.com/oscarotero/Gettext/issues/156 +[#159]: https://github.com/oscarotero/Gettext/issues/159 +[#161]: https://github.com/oscarotero/Gettext/issues/161 +[#166]: https://github.com/oscarotero/Gettext/issues/166 +[#168]: https://github.com/oscarotero/Gettext/issues/168 +[#170]: https://github.com/oscarotero/Gettext/issues/170 +[#175]: https://github.com/oscarotero/Gettext/issues/175 +[#177]: https://github.com/oscarotero/Gettext/issues/177 +[#178]: https://github.com/oscarotero/Gettext/issues/178 +[#182]: https://github.com/oscarotero/Gettext/issues/182 +[#187]: https://github.com/oscarotero/Gettext/issues/187 +[#188]: https://github.com/oscarotero/Gettext/issues/188 +[#191]: https://github.com/oscarotero/Gettext/issues/191 +[#197]: https://github.com/oscarotero/Gettext/issues/197 +[#198]: https://github.com/oscarotero/Gettext/issues/198 +[#200]: https://github.com/oscarotero/Gettext/issues/200 +[#205]: https://github.com/oscarotero/Gettext/issues/205 +[#213]: https://github.com/oscarotero/Gettext/issues/213 +[#214]: https://github.com/oscarotero/Gettext/issues/214 +[#215]: https://github.com/oscarotero/Gettext/issues/215 +[#218]: https://github.com/oscarotero/Gettext/issues/218 +[#221]: https://github.com/oscarotero/Gettext/issues/221 +[#223]: https://github.com/oscarotero/Gettext/issues/223 +[#224]: https://github.com/oscarotero/Gettext/issues/224 +[#225]: https://github.com/oscarotero/Gettext/issues/225 +[#226]: https://github.com/oscarotero/Gettext/issues/226 +[#230]: https://github.com/oscarotero/Gettext/issues/230 +[#231]: https://github.com/oscarotero/Gettext/issues/231 +[#238]: https://github.com/oscarotero/Gettext/issues/238 +[#242]: https://github.com/oscarotero/Gettext/issues/242 + +[4.8.2]: https://github.com/oscarotero/Gettext/compare/v4.8.1...v4.8.2 +[4.8.1]: https://github.com/oscarotero/Gettext/compare/v4.8.0...v4.8.1 +[4.8.0]: https://github.com/oscarotero/Gettext/compare/v4.7.0...v4.8.0 +[4.7.0]: https://github.com/oscarotero/Gettext/compare/v4.6.3...v4.7.0 +[4.6.3]: https://github.com/oscarotero/Gettext/compare/v4.6.2...v4.6.3 +[4.6.2]: https://github.com/oscarotero/Gettext/compare/v4.6.1...v4.6.2 +[4.6.1]: https://github.com/oscarotero/Gettext/compare/v4.6.0...v4.6.1 +[4.6.0]: https://github.com/oscarotero/Gettext/compare/v4.5.0...v4.6.0 +[4.5.0]: https://github.com/oscarotero/Gettext/compare/v4.4.4...v4.5.0 +[4.4.4]: https://github.com/oscarotero/Gettext/compare/v4.4.3...v4.4.4 +[4.4.3]: https://github.com/oscarotero/Gettext/compare/v4.4.2...v4.4.3 +[4.4.2]: https://github.com/oscarotero/Gettext/compare/v4.4.1...v4.4.2 +[4.4.1]: https://github.com/oscarotero/Gettext/compare/v4.4.0...v4.4.1 +[4.4.0]: https://github.com/oscarotero/Gettext/compare/v4.3.0...v4.4.0 diff --git a/inc/gettext/CONTRIBUTING.md b/inc/gettext/CONTRIBUTING.md new file mode 100644 index 0000000..eda824f --- /dev/null +++ b/inc/gettext/CONTRIBUTING.md @@ -0,0 +1,17 @@ +Contributing to Gettext +======================= + +Looking to contribute something to this library? Here's how you can help. + +## Bugs + +A bug is a demonstrable problem that is caused by the code in the repository. Good bug reports are extremely helpful – thank you! + +Please try to be as detailed as possible in your report. Include specific information about the environment – version of PHP, version of gettext, etc, and steps required to reproduce the issue. + +## Pull Requests + +Good pull requests – patches, improvements, new features – are a fantastic help. New extractors or generator are welcome. Before create a pull request, please follow these instructions: + +* The code must be PSR-2 compliant +* Write some tests diff --git a/inc/gettext/LICENSE b/inc/gettext/LICENSE new file mode 100644 index 0000000..2385321 --- /dev/null +++ b/inc/gettext/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Oscar Otero Marzoa + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/inc/gettext/README.md b/inc/gettext/README.md new file mode 100644 index 0000000..4911afb --- /dev/null +++ b/inc/gettext/README.md @@ -0,0 +1,425 @@ +Gettext +======= + +[![Build Status](https://travis-ci.org/oscarotero/Gettext.png?branch=master)](https://travis-ci.org/oscarotero/Gettext) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/oscarotero/Gettext/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/oscarotero/Gettext/?branch=master) +[![Latest Stable Version](https://poser.pugx.org/gettext/gettext/v/stable.svg)](https://packagist.org/packages/gettext/gettext) +[![Total Downloads](https://poser.pugx.org/gettext/gettext/downloads.svg)](https://packagist.org/packages/gettext/gettext) +[![Monthly Downloads](https://poser.pugx.org/gettext/gettext/d/monthly.png)](https://packagist.org/packages/gettext/gettext) +[![License](https://poser.pugx.org/gettext/gettext/license.svg)](https://packagist.org/packages/gettext/gettext) + +[![SensioLabsInsight](https://insight.sensiolabs.com/projects/496dc2a6-43be-4046-a283-f8370239dd47/big.png)](https://insight.sensiolabs.com/projects/496dc2a6-43be-4046-a283-f8370239dd47) + +Created by Oscar Otero (MIT License) + +Gettext is a PHP (>=5.4) library to import/export/edit gettext from PO, MO, PHP, JS files, etc. + +## Installation + +With composer (recomended): + +``` +composer require gettext/gettext +``` + +If you don't use composer in your project, you have to download and place this package in a directory of your project. You need to install also [gettext/languages](https://github.com/mlocati/cldr-to-gettext-plural-rules). Then, include the autoloaders of both projects in any place of your php code: + +```php +include_once "libs/gettext/src/autoloader.php"; +include_once "libs/cldr-to-gettext-plural-rules/src/autoloader.php"; +``` + +## Classes and functions + +This package contains the following classes: + +* `Gettext\Translation` - A translation definition +* `Gettext\Translations` - A collection of translations +* `Gettext\Extractors\*` - Import translations from various sources (po, mo, php, js, etc) +* `Gettext\Generators\*` - Export translations to various formats (po, mo, php, json, etc) +* `Gettext\Translator` - To use the translations in your php templates instead the [gettext extension](http://php.net/gettext) +* `Gettext\GettextTranslator` - To use the [gettext extension](http://php.net/gettext) + +## Usage example + +```php +use Gettext\Translations; + +//import from a .po file: +$translations = Translations::fromPoFile('locales/gl.po'); + +//edit some translations: +$translation = $translations->find(null, 'apple'); + +if ($translation) { + $translation->setTranslation('Mazá'); +} + +//export to a php array: +$translations->toPhpArrayFile('locales/gl.php'); + +//and to a .mo file +$translations->toMoFile('Locale/gl/LC_MESSAGES/messages.mo'); +``` + +If you want use this translations in your php templates without using the gettext extension: + +```php +use Gettext\Translator; + +//Create the translator instance +$t = new Translator(); + +//Load your translations (exported as PhpArray): +$t->loadTranslations('locales/gl.php'); + +//Use it: +echo $t->gettext('apple'); // "Mazá" + +//If you want use global functions: +$t->register(); + +echo __('apple'); // "Mazá" +``` + +To use this translations with the gettext extension: + +```php +use Gettext\GettextTranslator; + +//Create the translator instance +$t = new GettextTranslator(); + +//Set the language and load the domain +$t->setLanguage('gl'); +$t->loadDomain('messages', 'Locale'); + +//Use it: +echo $t->gettext('apple'); // "Mazá" + +//Or use the gettext functions +echo gettext('apple'); // "Mazá" + +//If you want use the global functions +$t->register(); + +echo __('apple'); // "Mazá" + +//And use sprintf/strtr placeholders +echo __('Hello %s', 'world'); //Hello world +echo __('Hello {name}', ['{name}' => 'world']); //Hello world +``` + +The benefits of using the functions provided by this library (`__()` instead `_()` or `gettext()`) are: + +* You are using the same functions, no matter whether the translations are provided by gettext extension or any other method. +* You can use variables easier because `sprintf` functionality is included. For example: `__('Hello %s', 'world')` instead `sprintf(_('Hello %s'), 'world')`. +* You can also use named placeholders if the second argument is an array. For example: `__('Hello %name%', ['%name%' => 'world'])` instead of `strtr(_('Hello %name%'), ['%name%' => 'world'])`. + +## Translation + +The `Gettext\Translation` class stores all information about a translation: the original text, the translated text, source references, comments, etc. + +```php +// __construct($context, $original, $plural) +$translation = new Gettext\Translation('comments', 'One comment', '%s comments'); + +$translation->setTranslation('Un comentario'); +$translation->setPluralTranslation('%s comentarios'); + +$translation->addReference('templates/comments/comment.php', 34); +$translation->addComment('To display the amount of comments in a post'); + +echo $translation->getContext(); // comments +echo $translation->getOriginal(); // One comment +echo $translation->getTranslation(); // Un comentario + +// etc... +``` + +## Translations + +The `Gettext\Translations` class stores a collection of translations: + +```php +$translations = new Gettext\Translations(); + +//You can add new translations using the array syntax +$translations[] = new Gettext\Translation('comments', 'One comment', '%s comments'); + +//Or using the "insert" method +$insertedTranslation = $translations->insert('comments', 'One comment', '%s comments'); + +//Find a specific translation +$translation = $translations->find('comments', 'One comment'); + +//Edit headers, domain, etc +$translations->setHeader('Last-Translator', 'Oscar Otero'); +$translations->setDomain('my-blog'); +``` + +## Extractors + +The extrators allows to fetch gettext values from any source. For example, to scan a .po file: + +```php +$translations = new Gettext\Translations(); + +//From a file +Gettext\Extractors\Po::fromFile('locales/en.po', $translations); + +//From a string +$string = file_get_contents('locales2/en.po'); +Gettext\Extractors\Po::fromString($string, $translations); +``` + +The better way to use extractors is using the magic methods of `Gettext\Translations`: + +```php +//Create a Translations instance using a po file +$translations = Gettext\Translations::fromPoFile('locales/en.po'); + +//Add more messages from other files +$translations->addFromPoFile('locales2/en.po'); +``` + +The available extractors are the following: + +Name | Description | Example +---- | ----------- | -------- +**Blade** | Scans a Blade template (For laravel users). | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/blade/input.php) +**Csv** | Gets the messages from csv. | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/Csv.csv) +**CsvDictionary** | Gets the messages from csv (without plurals and context). | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/CsvDictionary.csv) +**Jed** | Gets the messages from a json compatible with [Jed](http://slexaxton.github.com/Jed/). | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/Jed.json) +**JsCode** | Scans javascript code looking for all gettext functions (the same than PhpCode but for javascript). You can use [the javascript gettext-translator library](https://github.com/oscarotero/gettext-translator) | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/jscode/input.js) +**Json** | Gets the messages from json compatible with [gettext-translator](https://github.com/oscarotero/gettext-translator). | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/Json.json) +**JsonDictionary** | Gets the messages from a json (without plurals and context). | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/JsonDictionary.json) +**Mo** | Gets the messages from MO. | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/Mo.mo) +**PhpArray** | Gets the messages from a php file that returns an array. | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/PhpArray.php) +**PhpCode** | Scans php code looking for all gettext functions (see `translator_functions.php`). | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/phpcode/input.php) +**Po** | Gets the messages from PO. | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/Po.po) +**Twig** | To scan a Twig template. | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/twig/input.php) +**Xliff** | Gets the messages from [xliff (2.0)](http://docs.oasis-open.org/xliff/xliff-core/v2.0/os/xliff-core-v2.0-os.html). | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/Xliff.xlf) +**Yaml** | Gets the messages from yaml. | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/Yaml.yml) +**YamlDictionary** | Gets the messages from a yaml (without plurals and context). | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/YamlDictionary.yml) +**VueJs** | Gets the messages from a VueJs template. | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/vuejs/input.vue) + +## Generators + +The generators export a `Gettext\Translations` instance to any format (po, mo, array, etc). + +```php +//Save to a file +Gettext\Generators\Po::toFile($translations, 'locales/en.po'); + +//Return as a string +$content = Gettext\Generators\Po::toString($translations); +file_put_contents('locales/en.po', $content); +``` + +Like extractors, the better way to use generators is using the magic methods of `Gettext\Translations`: + +```php +//Extract messages from a php code file +$translations = Gettext\Translations::fromPhpCodeFile('templates/index.php'); + +//Export to a po file +$translations->toPoFile('locales/en.po'); + +//Export to a po string +$content = $translations->toPoString(); +file_put_contents('locales/en.po', $content); +``` + +The available generators are the following: + +Name | Description | Example +---- | ----------- | -------- +**Csv** | Exports to csv. | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/Csv.csv) +**CsvDictionary** | Exports to csv (without plurals and context). | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/CsvDictionary.csv) +**Json** | Exports to json, compatible with [gettext-translator](https://github.com/oscarotero/gettext-translator). | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/Json.json) +**JsonDictionary** | Exports to json (without plurals and context). | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/JsonDictionary.json) +**Mo** | Exports to Mo. | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/Mo.mo) +**PhpArray** | Exports to php code that returns an array. | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/PhpArray.php) +**Po** | Exports to Po. | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/Po.po) +**Jed** | Exports to json format compatible with [Jed](http://slexaxton.github.com/Jed/). | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/Jed.json) +**Xliff** | Exports to [xliff (2.0)](http://docs.oasis-open.org/xliff/xliff-core/v2.0/os/xliff-core-v2.0-os.html). | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/Xliff.xlf) +**Yaml** | Exports to yaml. | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/Yaml.yml) +**YamlDictionary** | Exports to yaml (without plurals and context). | [example](https://github.com/oscarotero/Gettext/blob/master/tests/assets/po/YamlDictionary.yml) + +## Translator + +The class `Gettext\Translator` implements the gettext functions in php. Useful if you don't have the native gettext extension for php or want to avoid problems with it. You can load the translations from a php array file or using a `Gettext\Translations` instance: + +```php +use Gettext\Translator; + +//Create a new instance of the translator +$t = new Translator(); + +//Load the translations using any of the following ways: + +// 1. from php files (generated by Gettext\Extractors\PhpArray) +$t->loadTranslations('locales/gl.php'); + +// 2. using the array directly +$array = include 'locales/gl.php'; +$t->loadTranslations($array); + +// 3. using a Gettext\Translations instance (slower) +$translations = Gettext\Translations::fromPoFile('locales/gl.po'); +$t->loadTranslations($translations); + +//Now you can use it in your templates +echo $t->gettext('apple'); +``` + +## GettextTranslator + +The class `Gettext\GettextTranslator` uses the gettext extension. It's useful because combines the performance of using real gettext functions but with the same API than `Translator` class, so you can switch to one or other translator deppending of the environment without change code of your app. + +```php +use Gettext\GettextTranslator; + +//Create a new instance +$t = new GettextTranslator(); + +//It detects the environment variables to set the locale, but you can change it: +$t->setLanguage('gl'); + +//Load the domains: +$t->loadDomain('messages', 'project/Locale'); +//this means you have the file "project/Locale/gl/LC_MESSAGES/messages.po" + +//Now you can use it in your templates +echo $t->gettext('apple'); +``` + +## Global functions + +To ease the use of translations in your php templates, you can use the provided functions: + +```php +//Register the translator to use the global functions +$t->register(); + +echo __('apple'); // it's the same than $t->gettext('apple'); +``` + +You can scan the php files containing these functions and extract the values with the PhpCode extractor: + +```html + + + + + + +``` + + +## Merge translations + +To work with different translations you may want merge them in an unique file. There are two ways to do this: + +The simplest way is adding new translations: + +```php +use Gettext\Translations; + +$translations = Translations::fromPoFile('my-file1.po'); +$translations->addFromPoFile('my-file2.po'); +``` + +A more advanced way is merge two `Translations` instances: + +```php +use Gettext\Translations; + +//Create a new Translations instances with our translations. + +$translations1 = Translations::fromPoFile('my-file1.po'); +$translations2 = Translations::fromPoFile('my-file2.po'); + +//Merge one inside other: +$translations1->mergeWith($translations2); + +//Now translations1 has all values +``` + +The second argument of `mergeWith` defines how the merge will be done. Use the `Gettext\Merge` constants to configure the merging: + +Constant | Description +--------- | ----------- +`Merge::ADD` | Adds the translations from `$translations2` that are missing +`Merge::REMOVE` | Removes the translations missing in `$translations2` +`Merge::HEADERS_ADD` | Adds the headers from `$translations2` that are missing +`Merge::HEADERS_REMOVE` | Removes the headers missing in `$translations2` +`Merge::HEADERS_OVERRIDE` | Overrides the headers with the values of `$translations2` +`Merge::LANGUAGE_OVERRIDE` | Set the language defined in `$translations2` +`Merge::DOMAIN_OVERRIDE` | Set the domain defined in `$translations2` +`Merge::TRANSLATION_OVERRIDE` | Override the translation and plural translations with the value of `$translation2` +`Merge::COMMENTS_OURS` | Use only the comments of `$translation1` +`Merge::COMMENTS_THEIRS` | Use only the comments of `$translation2` +`Merge::EXTRACTED_COMMENTS_OURS` | Use only the extracted comments of `$translation1` +`Merge::EXTRACTED_COMMENTS_THEIRS` | Use only the extracted comments of `$translation2` +`Merge::FLAGS_OURS` | Use only the flags of `$translation1` +`Merge::FLAGS_THEIRS` | Use only the flags of `$translation2` +`Merge::REFERENCES_OURS` | Use only the references of `$translation1` +`Merge::REFERENCES_THEIRS` | Use only the references of `$translation2` + +Example: + +```php +use Gettext\Translations; +use Gettext\Merge; + +//Scan the php code to find the latest gettext translations +$phpTranslations = Translations::fromPhpCodeFile('my-templates.php'); + +//Get the translations of the code that are stored in a po file +$poTranslations = Translations::fromPoFile('locale.po'); + +//Merge the translations from the po file using the references from `$phpTranslations`: +$translations->mergeWith($poTranslations, Merge::REFERENCES_OURS); + +//Now save a po file with the result +$translations->toPoFile('locale.po'); +``` + +Note, if the second argument is not defined, the default value is `Merge::DEFAULTS` that's equivalent to `Merge::ADD | Merge::HEADERS_ADD`. + +## Use from CLI + +There's a Robo task to use this library from the command line interface: https://github.com/oscarotero/GettextRobo + +## Use in the browser + +If you want to use your translations in the browser, there's a javascript translator: https://github.com/oscarotero/gettext-translator + +## Third party packages + +Twig integration: + +* [jaimeperez/twig-configurable-i18n](https://packagist.org/packages/jaimeperez/twig-configurable-i18n) +* [cemerson/translator-twig-extension](https://packagist.org/packages/cemerson/translator-twig-extension) + +Framework integration: + +* [Laravel 5](https://packagist.org/packages/eusonlito/laravel-gettext) +* [CakePHP 3](https://packagist.org/packages/k1low/po) +* [Symfony 2](https://packagist.org/packages/mablae/gettext-bundle) + +[add your package](https://github.com/oscarotero/Gettext/issues/new) + +## Contributors + +Thanks to all [contributors](https://github.com/oscarotero/Gettext/graphs/contributors) specially to [@mlocati](https://github.com/mlocati). + +## Donations + +If this library is useful for you, consider to donate to the author. + +[Buy me a beer :beer:](https://www.paypal.me/oscarotero) + +Thanks in advance! diff --git a/inc/gettext/composer.json b/inc/gettext/composer.json new file mode 100644 index 0000000..24b5083 --- /dev/null +++ b/inc/gettext/composer.json @@ -0,0 +1,54 @@ +{ + "name": "gettext/gettext", + "type": "library", + "description": "PHP gettext manager", + "keywords": ["js", "gettext", "i18n", "translation", "po", "mo"], + "homepage": "https://github.com/oscarotero/Gettext", + "license": "MIT", + "authors": [ + { + "name": "Oscar Otero", + "email": "oom@oscarotero.com", + "homepage": "http://oscarotero.com", + "role": "Developer" + } + ], + "support": { + "email": "oom@oscarotero.com", + "issues": "https://github.com/oscarotero/Gettext/issues" + }, + "require": { + "php": ">=5.4.0", + "gettext/languages": "^2.3" + }, + "require-dev": { + "illuminate/view": "*", + "twig/twig": "^1.31|^2.0", + "twig/extensions": "*", + "symfony/yaml": "~2", + "phpunit/phpunit": "^4.8|^5.7|^6.5", + "squizlabs/php_codesniffer": "^3.0" + }, + "suggest": { + "illuminate/view": "Is necessary if you want to use the Blade extractor", + "twig/twig": "Is necessary if you want to use the Twig extractor", + "twig/extensions": "Is necessary if you want to use the Twig extractor", + "symfony/yaml": "Is necessary if you want to use the Yaml extractor/generator" + }, + "autoload": { + "psr-4": { + "Gettext\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Gettext\\Tests\\": "tests" + } + }, + "scripts": { + "test": [ + "phpunit", + "phpcs" + ] + } +} diff --git a/inc/gettext/src/BaseTranslator.php b/inc/gettext/src/BaseTranslator.php new file mode 100644 index 0000000..c52038f --- /dev/null +++ b/inc/gettext/src/BaseTranslator.php @@ -0,0 +1,39 @@ +compileString($string); + } else { + $string = $options['facade']::compileString($string); + } + + PhpCode::fromString($string, $translations, $options); + } +} diff --git a/inc/gettext/src/Extractors/Csv.php b/inc/gettext/src/Extractors/Csv.php new file mode 100644 index 0000000..fba3a98 --- /dev/null +++ b/inc/gettext/src/Extractors/Csv.php @@ -0,0 +1,53 @@ + ",", + 'enclosure' => '"', + 'escape_char' => "\\" + ]; + + /** + * {@inheritdoc} + */ + public static function fromString($string, Translations $translations, array $options = []) + { + $options += static::$options; + $handle = fopen('php://memory', 'w'); + + fputs($handle, $string); + rewind($handle); + + while ($row = static::fgetcsv($handle, $options)) { + $context = array_shift($row); + $original = array_shift($row); + + if ($context === '' && $original === '') { + static::extractHeaders(array_shift($row), $translations); + continue; + } + + $translation = $translations->insert($context, $original); + + if (!empty($row)) { + $translation->setTranslation(array_shift($row)); + $translation->setPluralTranslations($row); + } + } + + fclose($handle); + } +} diff --git a/inc/gettext/src/Extractors/CsvDictionary.php b/inc/gettext/src/Extractors/CsvDictionary.php new file mode 100644 index 0000000..807357c --- /dev/null +++ b/inc/gettext/src/Extractors/CsvDictionary.php @@ -0,0 +1,47 @@ + ",", + 'enclosure' => '"', + 'escape_char' => "\\" + ]; + + /** + * {@inheritdoc} + */ + public static function fromString($string, Translations $translations, array $options = []) + { + $options += static::$options; + $handle = fopen('php://memory', 'w'); + + fputs($handle, $string); + rewind($handle); + + while ($row = static::fgetcsv($handle, $options)) { + list($original, $translation) = $row + ['', '']; + + if ($original === '') { + static::extractHeaders($translation, $translations); + continue; + } + + $translations->insert(null, $original)->setTranslation($translation); + } + + fclose($handle); + } +} diff --git a/inc/gettext/src/Extractors/Extractor.php b/inc/gettext/src/Extractors/Extractor.php new file mode 100644 index 0000000..59974aa --- /dev/null +++ b/inc/gettext/src/Extractors/Extractor.php @@ -0,0 +1,80 @@ +setDomain($headers['domain']); + } + + if (!empty($headers['lang'])) { + $translations->setLanguage($headers['lang']); + } + + if (!empty($headers['plural-forms'])) { + $translations->setHeader(Translations::HEADER_PLURAL, $headers['plural-forms']); + } + + $context_glue = '\u0004'; + + foreach ($messages as $key => $translation) { + $key = explode($context_glue, $key); + $context = isset($key[1]) ? array_shift($key) : ''; + + $translations->insert($context, array_shift($key)) + ->setTranslation(array_shift($translation)) + ->setPluralTranslations($translation); + } + } +} diff --git a/inc/gettext/src/Extractors/JsCode.php b/inc/gettext/src/Extractors/JsCode.php new file mode 100644 index 0000000..0a88d4f --- /dev/null +++ b/inc/gettext/src/Extractors/JsCode.php @@ -0,0 +1,74 @@ + [], + + 'functions' => [ + 'gettext' => 'gettext', + '__' => 'gettext', + 'ngettext' => 'ngettext', + 'n__' => 'ngettext', + 'pgettext' => 'pgettext', + 'p__' => 'pgettext', + 'dgettext' => 'dgettext', + 'd__' => 'dgettext', + 'dngettext' => 'dngettext', + 'dn__' => 'dngettext', + 'dpgettext' => 'dpgettext', + 'dp__' => 'dpgettext', + 'npgettext' => 'npgettext', + 'np__' => 'npgettext', + 'dnpgettext' => 'dnpgettext', + 'dnp__' => 'dnpgettext', + 'noop' => 'noop', + 'noop__' => 'noop', + ], + ]; + + protected static $functionsScannerClass = 'Gettext\Utils\JsFunctionsScanner'; + + /** + * @inheritdoc + * @throws Exception + */ + public static function fromString($string, Translations $translations, array $options = []) + { + static::fromStringMultiple($string, [$translations], $options); + } + + /** + * @inheritDoc + * @throws Exception + */ + public static function fromStringMultiple($string, array $translations, array $options = []) + { + $options += static::$options; + + /** @var FunctionsScanner $functions */ + $functions = new static::$functionsScannerClass($string); + $functions->saveGettextFunctions($translations, $options); + } + + /** + * @inheritDoc + * @throws Exception + */ + public static function fromFileMultiple($file, array $translations, array $options = []) + { + foreach (static::getFiles($file) as $file) { + $options['file'] = $file; + static::fromStringMultiple(static::readFile($file), $translations, $options); + } + } +} diff --git a/inc/gettext/src/Extractors/Json.php b/inc/gettext/src/Extractors/Json.php new file mode 100644 index 0000000..3aaea29 --- /dev/null +++ b/inc/gettext/src/Extractors/Json.php @@ -0,0 +1,26 @@ +seekto($originals); + $table_originals = static::readIntArray($stream, $byteOrder, $total * 2); + + $stream->seekto($tran); + $table_translations = static::readIntArray($stream, $byteOrder, $total * 2); + + for ($i = 0; $i < $total; ++$i) { + $next = $i * 2; + + $stream->seekto($table_originals[$next + 2]); + $original = $stream->read($table_originals[$next + 1]); + + $stream->seekto($table_translations[$next + 2]); + $translated = $stream->read($table_translations[$next + 1]); + + if ($original === '') { + // Headers + foreach (explode("\n", $translated) as $headerLine) { + if ($headerLine === '') { + continue; + } + + $headerChunks = preg_split('/:\s*/', $headerLine, 2); + $translations->setHeader($headerChunks[0], isset($headerChunks[1]) ? $headerChunks[1] : ''); + } + + continue; + } + + $chunks = explode("\x04", $original, 2); + + if (isset($chunks[1])) { + $context = $chunks[0]; + $original = $chunks[1]; + } else { + $context = ''; + } + + $chunks = explode("\x00", $original, 2); + + if (isset($chunks[1])) { + $original = $chunks[0]; + $plural = $chunks[1]; + } else { + $plural = ''; + } + + $translation = $translations->insert($context, $original, $plural); + + if ($translated === '') { + continue; + } + + if ($plural === '') { + $translation->setTranslation($translated); + continue; + } + + $v = explode("\x00", $translated); + $translation->setTranslation(array_shift($v)); + $translation->setPluralTranslations($v); + } + } + + /** + * @param StringReader $stream + * @param string $byteOrder + */ + protected static function readInt(StringReader $stream, $byteOrder) + { + if (($read = $stream->read(4)) === false) { + return false; + } + + $read = unpack($byteOrder, $read); + + return array_shift($read); + } + + /** + * @param StringReader $stream + * @param string $byteOrder + * @param int $count + */ + protected static function readIntArray(StringReader $stream, $byteOrder, $count) + { + return unpack($byteOrder.$count, $stream->read(4 * $count)); + } +} diff --git a/inc/gettext/src/Extractors/PhpArray.php b/inc/gettext/src/Extractors/PhpArray.php new file mode 100644 index 0000000..3e4b262 --- /dev/null +++ b/inc/gettext/src/Extractors/PhpArray.php @@ -0,0 +1,33 @@ + false, + + 'constants' => [], + + 'functions' => [ + 'gettext' => 'gettext', + '__' => 'gettext', + 'ngettext' => 'ngettext', + 'n__' => 'ngettext', + 'pgettext' => 'pgettext', + 'p__' => 'pgettext', + 'dgettext' => 'dgettext', + 'd__' => 'dgettext', + 'dngettext' => 'dngettext', + 'dn__' => 'dngettext', + 'dpgettext' => 'dpgettext', + 'dp__' => 'dpgettext', + 'npgettext' => 'npgettext', + 'np__' => 'npgettext', + 'dnpgettext' => 'dnpgettext', + 'dnp__' => 'dnpgettext', + 'noop' => 'noop', + 'noop__' => 'noop', + ], + ]; + + protected static $functionsScannerClass = 'Gettext\Utils\PhpFunctionsScanner'; + + /** + * {@inheritdoc} + * @throws Exception + */ + public static function fromString($string, Translations $translations, array $options = []) + { + static::fromStringMultiple($string, [$translations], $options); + } + + /** + * @inheritDoc + * @throws Exception + */ + public static function fromStringMultiple($string, array $translations, array $options = []) + { + $options += static::$options; + + /** @var FunctionsScanner $functions */ + $functions = new static::$functionsScannerClass($string); + + if ($options['extractComments'] !== false) { + $functions->enableCommentsExtraction($options['extractComments']); + } + + $functions->saveGettextFunctions($translations, $options); + } + + /** + * @inheritDoc + */ + public static function fromFileMultiple($file, array $translations, array $options = []) + { + foreach (static::getFiles($file) as $file) { + $options['file'] = $file; + static::fromStringMultiple(static::readFile($file), $translations, $options); + } + } + + + /** + * Decodes a T_CONSTANT_ENCAPSED_STRING string. + * + * @param string $value + * + * @return string + */ + public static function convertString($value) + { + if (strpos($value, '\\') === false) { + return substr($value, 1, -1); + } + + if ($value[0] === "'") { + return strtr(substr($value, 1, -1), ['\\\\' => '\\', '\\\'' => '\'']); + } + + $value = substr($value, 1, -1); + + return preg_replace_callback( + '/\\\(n|r|t|v|e|f|\$|"|\\\|x[0-9A-Fa-f]{1,2}|u{[0-9a-f]{1,6}}|[0-7]{1,3})/', + function ($match) { + switch ($match[1][0]) { + case 'n': + return "\n"; + case 'r': + return "\r"; + case 't': + return "\t"; + case 'v': + return "\v"; + case 'e': + return "\e"; + case 'f': + return "\f"; + case '$': + return '$'; + case '"': + return '"'; + case '\\': + return '\\'; + case 'x': + return chr(hexdec(substr($match[1], 1))); + case 'u': + return static::unicodeChar(hexdec(substr($match[1], 1))); + default: + return chr(octdec($match[1])); + } + }, + $value + ); + } + + /** + * @param $dec + * @return string|null + * @see http://php.net/manual/en/function.chr.php#118804 + */ + protected static function unicodeChar($dec) + { + if ($dec < 0x80) { + return chr($dec); + } + + if ($dec < 0x0800) { + return chr(0xC0 + ($dec >> 6)) + . chr(0x80 + ($dec & 0x3f)); + } + + if ($dec < 0x010000) { + return chr(0xE0 + ($dec >> 12)) + . chr(0x80 + (($dec >> 6) & 0x3f)) + . chr(0x80 + ($dec & 0x3f)); + } + + if ($dec < 0x200000) { + return chr(0xF0 + ($dec >> 18)) + . chr(0x80 + (($dec >> 12) & 0x3f)) + . chr(0x80 + (($dec >> 6) & 0x3f)) + . chr(0x80 + ($dec & 0x3f)); + } + + return null; + } +} diff --git a/inc/gettext/src/Extractors/Po.php b/inc/gettext/src/Extractors/Po.php new file mode 100644 index 0000000..a5ee56b --- /dev/null +++ b/inc/gettext/src/Extractors/Po.php @@ -0,0 +1,211 @@ +createNewTranslation('', ''); + + for ($n = count($lines); $i < $n; ++$i) { + $line = trim($lines[$i]); + $line = static::fixMultiLines($line, $lines, $i); + + if ($line === '') { + if ($translation->is('', '')) { + static::extractHeaders($translation->getTranslation(), $translations); + } elseif ($translation->hasOriginal()) { + $translations[] = $translation; + } + + $translation = $translations->createNewTranslation('', ''); + continue; + } + + $splitLine = preg_split('/\s+/', $line, 2); + $key = $splitLine[0]; + $data = isset($splitLine[1]) ? $splitLine[1] : ''; + + if ($key === '#~') { + $translation->setDisabled(true); + + $splitLine = preg_split('/\s+/', $data, 2); + $key = $splitLine[0]; + $data = isset($splitLine[1]) ? $splitLine[1] : ''; + } + + switch ($key) { + case '#': + $translation->addComment($data); + $append = null; + break; + + case '#.': + $translation->addExtractedComment($data); + $append = null; + break; + + case '#,': + foreach (array_map('trim', explode(',', trim($data))) as $value) { + $translation->addFlag($value); + } + $append = null; + break; + + case '#:': + foreach (preg_split('/\s+/', trim($data)) as $value) { + if (preg_match('/^(.+)(:(\d*))?$/U', $value, $matches)) { + $translation->addReference($matches[1], isset($matches[3]) ? $matches[3] : null); + } + } + $append = null; + break; + + case 'msgctxt': + $translation = $translation->getClone(static::convertString($data)); + $append = 'Context'; + break; + + case 'msgid': + $translation = $translation->getClone(null, static::convertString($data)); + $append = 'Original'; + break; + + case 'msgid_plural': + $translation->setPlural(static::convertString($data)); + $append = 'Plural'; + break; + + case 'msgstr': + case 'msgstr[0]': + $translation->setTranslation(static::convertString($data)); + $append = 'Translation'; + break; + + case 'msgstr[1]': + $translation->setPluralTranslations([static::convertString($data)]); + $append = 'PluralTranslation'; + break; + + default: + if (strpos($key, 'msgstr[') === 0) { + $p = $translation->getPluralTranslations(); + $p[] = static::convertString($data); + + $translation->setPluralTranslations($p); + $append = 'PluralTranslation'; + break; + } + + if (isset($append)) { + if ($append === 'Context') { + $translation = $translation->getClone($translation->getContext() + ."\n" + .static::convertString($data)); + break; + } + + if ($append === 'Original') { + $translation = $translation->getClone(null, $translation->getOriginal() + ."\n" + .static::convertString($data)); + break; + } + + if ($append === 'PluralTranslation') { + $p = $translation->getPluralTranslations(); + $p[] = array_pop($p)."\n".static::convertString($data); + $translation->setPluralTranslations($p); + break; + } + + $getMethod = 'get'.$append; + $setMethod = 'set'.$append; + $translation->$setMethod($translation->$getMethod()."\n".static::convertString($data)); + } + break; + } + } + + if ($translation->hasOriginal() && !in_array($translation, iterator_to_array($translations))) { + $translations[] = $translation; + } + } + + /** + * Gets one string from multiline strings. + * + * @param string $line + * @param array $lines + * @param int &$i + * + * @return string + */ + protected static function fixMultiLines($line, array $lines, &$i) + { + for ($j = $i, $t = count($lines); $j < $t; ++$j) { + if (substr($line, -1, 1) == '"' + && isset($lines[$j + 1]) + && substr(trim($lines[$j + 1]), 0, 1) == '"' + ) { + $line = substr($line, 0, -1).substr(trim($lines[$j + 1]), 1); + } else { + $i = $j; + break; + } + } + + return $line; + } + + /** + * Convert a string from its PO representation. + * + * @param string $value + * + * @return string + */ + public static function convertString($value) + { + if (!$value) { + return ''; + } + + if ($value[0] === '"') { + $value = substr($value, 1, -1); + } + + return strtr( + $value, + [ + '\\\\' => '\\', + '\\a' => "\x07", + '\\b' => "\x08", + '\\t' => "\t", + '\\n' => "\n", + '\\v' => "\x0b", + '\\f' => "\x0c", + '\\r' => "\r", + '\\"' => '"', + ] + ); + } +} diff --git a/inc/gettext/src/Extractors/Twig.php b/inc/gettext/src/Extractors/Twig.php new file mode 100644 index 0000000..2060d08 --- /dev/null +++ b/inc/gettext/src/Extractors/Twig.php @@ -0,0 +1,45 @@ + 'notes:', + 'twig' => null, + ]; + + /** + * {@inheritdoc} + */ + public static function fromString($string, Translations $translations, array $options = []) + { + $options += static::$options; + + $twig = $options['twig'] ?: static::createTwig(); + + PhpCode::fromString($twig->compileSource(new Twig_Source($string, '')), $translations, $options); + } + + /** + * Returns a Twig instance. + * + * @return Twig_Environment + */ + protected static function createTwig() + { + $twig = new Twig_Environment(new Twig_Loader_Array(['' => ''])); + $twig->addExtension(new Twig_Extensions_Extension_I18n()); + + return static::$options['twig'] = $twig; + } +} diff --git a/inc/gettext/src/Extractors/VueJs.php b/inc/gettext/src/Extractors/VueJs.php new file mode 100644 index 0000000..0d29f45 --- /dev/null +++ b/inc/gettext/src/Extractors/VueJs.php @@ -0,0 +1,423 @@ + [], + + 'functions' => [ + 'gettext' => 'gettext', + '__' => 'gettext', + 'ngettext' => 'ngettext', + 'n__' => 'ngettext', + 'pgettext' => 'pgettext', + 'p__' => 'pgettext', + 'dgettext' => 'dgettext', + 'd__' => 'dgettext', + 'dngettext' => 'dngettext', + 'dn__' => 'dngettext', + 'dpgettext' => 'dpgettext', + 'dp__' => 'dpgettext', + 'npgettext' => 'npgettext', + 'np__' => 'npgettext', + 'dnpgettext' => 'dnpgettext', + 'dnp__' => 'dnpgettext', + 'noop' => 'noop', + 'noop__' => 'noop', + ], + ]; + + protected static $functionsScannerClass = 'Gettext\Utils\JsFunctionsScanner'; + + /** + * @inheritDoc + * @throws Exception + */ + public static function fromFileMultiple($file, array $translations, array $options = []) + { + foreach (static::getFiles($file) as $file) { + $options['file'] = $file; + static::fromStringMultiple(static::readFile($file), $translations, $options); + } + } + + /** + * @inheritdoc + * @throws Exception + */ + public static function fromString($string, Translations $translations, array $options = []) + { + static::fromStringMultiple($string, [$translations], $options); + } + + /** + * @inheritDoc + * @throws Exception + */ + public static function fromStringMultiple($string, array $translations, array $options = []) + { + $options += static::$options; + $options += [ + // HTML attribute prefixes we parse as JS which could contain translations (are JS expressions) + 'attributePrefixes' => [ + ':', + 'v-bind:', + 'v-on:', + 'v-text', + ], + // HTML Tags to parse + 'tagNames' => [ + 'translate', + ], + // HTML tags to parse when attribute exists + 'tagAttributes' => [ + 'v-translate', + ], + // Comments + 'commentAttributes' => [ + 'translate-comment', + ], + 'contextAttributes' => [ + 'translate-context', + ], + // Attribute with plural content + 'pluralAttributes' => [ + 'translate-plural', + ], + ]; + + // Ok, this is the weirdest hack, but let me explain: + // On Linux (Mac is fine), when converting HTML to DOM, new lines get trimmed after the first tag. + // So if there are new lines between