Compare commits

..

23 Commits

Author SHA1 Message Date
Trevor Slocum
9db98b0ebe Fix migrating to PostgreSQL database 2025-04-26 00:06:00 -07:00
Trevor Slocum
90b1bcea8a Migrate to codeberg.org 2025-03-31 19:54:31 -07:00
Trevor Slocum
17873befbd Update translations 2025-03-31 19:43:09 -07:00
Trevor Slocum
36ce4a065f Migrate online translation to Codeberg 2024-11-19 18:35:23 -08:00
Trevor Slocum
af956ff048 Update translations 2024-07-10 21:02:15 -07:00
Trevor Slocum
08fc167f7b Update translations 2024-01-02 10:40:11 -08:00
Trevor Slocum
12dcade4a8 Add missing semicolons 2023-10-11 21:37:08 -07:00
Trevor Slocum
a35c046b1d Add EditorConfig file 2023-10-06 13:26:42 -07:00
Trevor Slocum
8a6244c0bc Fix maximum field length settings 2023-10-06 13:25:08 -07:00
Trevor Slocum
2f357d6418 Hide deprecated warnings in CAPTCHA script 2023-09-24 23:38:31 -07:00
Trevor Slocum
e0bddde850 Update translations 2023-09-24 23:13:46 -07:00
Trevor Slocum
334359e31f Fix expanding thumbnail causing page content to jump
Resolves #281.
2023-09-24 23:04:08 -07:00
Trevor Slocum
2aa8a8efa0 Migrate to code.rocket9labs.com
Run the following command to ensure continued updates:
  git remote set-url origin https://code.rocket9labs.com/tslocum/tinyib.git
2023-09-24 16:30:53 -07:00
Trevor Slocum
5f42b82cc0 Operate on text using multibyte functions
TinyIB now fully supports UTF-8 encoded text input.

Resolves #255.
Resolves #273.
2023-09-24 14:17:24 -07:00
Trevor Slocum
0405fe9577 Update translations 2023-09-09 09:56:08 -07:00
Trevor Slocum
b22ff54f12 Switch gettext library from v5.6.1 to v4.8.11
This restores compatibility with PHP 5+.

Relates to #273.
2023-09-09 09:13:00 -07:00
Trevor Slocum
2de03eb42b Update translations 2023-03-19 21:48:48 -07:00
Trevor Slocum
7c6cf64d4d Add missing addLogs function
This function is required when migrating between databases.
2023-03-19 19:18:17 -07:00
Trevor Slocum
57704531cf Use newer syntax when including variables in strings
Resolves #271.
2023-01-05 10:27:06 -08:00
Trevor Slocum
baba988f0a Fix translation issue when moderating post
Resolves #270.
2023-01-05 10:22:07 -08:00
Trevor Slocum
2befc70988 Update translations 2023-01-05 10:13:17 -08:00
Trevor Slocum
3ba8e3045b Merge pull request 'Fix audit log for flat file database' (#257) from averageLukas/tinyib:fix/flatfile-audit-log into master
Reviewed-on: https://code.rocketnine.space/tslocum/tinyib/pulls/257
2022-06-23 22:31:16 -07:00
averageLukas
c5af7a0ef7
Fix flatfile database audit log 2022-06-23 07:15:38 +02:00
125 changed files with 8637 additions and 4497 deletions

10
.editorconfig Normal file
View File

@ -0,0 +1,10 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = tab
insert_final_newline = true
max_line_length = 120
tab_width = 4

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2020 Trevor Slocum <trevor@rocketnine.space> Copyright (c) 2023 Trevor Slocum <trevor@rocket9labs.com>
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,9 +1,8 @@
# TinyIB - Lightweight and efficient [imageboard](https://en.wikipedia.org/wiki/Imageboard) # TinyIB - Lightweight and efficient [imageboard](https://en.wikipedia.org/wiki/Imageboard)
[![Translate](https://hosted.weblate.org/widgets/tinyib/-/tinyib/svg-badge.svg)](https://hosted.weblate.org/projects/tinyib/tinyib/) [![Translate](https://translate.codeberg.org/widget/tinyib/tinyib/svg-badge.svg)](https://translate.codeberg.org/projects/tinyib/tinyib/)
[![Donate via LiberaPay](https://img.shields.io/liberapay/receives/rocketnine.space.svg?logo=liberapay)](https://liberapay.com/rocketnine.space) [![Donate](https://img.shields.io/liberapay/receives/rocket9labs.com.svg?logo=liberapay)](https://liberapay.com/rocket9labs.com)
[![Donate via Patreon](https://img.shields.io/badge/dynamic/json?color=%23e85b46&label=Patreon&query=data.attributes.patron_count&suffix=%20patrons&url=https%3A%2F%2Fwww.patreon.com%2Fapi%2Fcampaigns%2F5252223)](https://www.patreon.com/rocketnine)
A [**read-only demo**](https://tinyib.rocketnine.space) is available. A [**read-only demo**](https://tinyib.rocket9labs.com) is available.
## Features ## Features
@ -32,7 +31,7 @@ A [**read-only demo**](https://tinyib.rocketnine.space) is available.
- Ban offensive/abusive posters across all boards. - Ban offensive/abusive posters across all boards.
- Post using raw HTML. - Post using raw HTML.
- Upgrade automatically when installed via git. (Tested on Linux only) - Upgrade automatically when installed via git. (Tested on Linux only)
- [Translations:](https://hosted.weblate.org/projects/tinyib/tinyib/) - [Translations:](https://translate.codeberg.org/projects/tinyib/tinyib/)
- Catalan, Chinese, Dutch, Finnish, French, German, Indonesian, Italian, Japanese, Korean, Norwegian, Polish, Portuguese, Romanian, Russian, Spanish (Mexico) and Turkish - Catalan, Chinese, Dutch, Finnish, French, German, Indonesian, Italian, Japanese, Korean, Norwegian, Polish, Portuguese, Romanian, Russian, Spanish (Mexico) and Turkish
## Donate ## Donate
@ -40,27 +39,25 @@ A [**read-only demo**](https://tinyib.rocketnine.space) is available.
Please consider supporting the continued development of TinyIB. Please consider supporting the continued development of TinyIB.
If you make a donation and there is a certain feature you'd like to see added to If you make a donation and there is a certain feature you'd like to see added to
TinyIB, <a href="mailto:trevor@rocketnine.space">send me an email</a>. I can't TinyIB, <a href="mailto:trevor@rocket9labs.com">send me an email</a>. I can't
promise that I will implement the feature right away, however I will keep your promise that I will implement the feature right away, however I will keep your
support in mind. support in mind.
- [LiberaPay](https://liberapay.com/rocketnine.space) (anonymous, no added fees) - [LiberaPay](https://liberapay.com/rocket9labs.com) (anonymous, no added fees)
- [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=TEP9HT98XK7QA) - [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=TEP9HT98XK7QA)
## Install ## Install
1. Verify the following are installed: 1. Verify the following are installed:
- [PHP 5.5+](https://php.net), or PHP 8.2.0+ for avif support - [PHP 5.5+](https://php.net)
- [GD Image Processing Library](https://php.net/gd) - [GD Image Processing Library](https://php.net/gd)
- This library is usually installed by default. - This library is usually installed by default.
- If you plan on disabling image uploads to use TinyIB as a text board only, this library is not required. - If you plan on disabling image uploads to use TinyIB as a text board only, this library is not required.
- [cURL Library](https://www.php.net/manual/en/book.curl.php) - [cURL Library](https://www.php.net/manual/en/book.curl.php)
- This is recommended, but is not strictly required except when `TINYIB_CAPTCHA` is set to `hcaptcha` or `recaptcha`. - This is recommended, but is not strictly required except when `TINYIB_CAPTCHA` is set to `hcaptcha` or `recaptcha`.
- AVIF Library
- Compile PHP using `--with-avif`
2. CD to the directory you wish to install TinyIB. 2. CD to the directory you wish to install TinyIB.
3. Run the command: 3. Run the command:
- `git clone https://code.rocketnine.space/tslocum/tinyib.git ./` - `git clone https://codeberg.org/tslocum/tinyib.git ./`
4. Copy **settings.default.php** to **settings.php** 4. Copy **settings.default.php** to **settings.php**
5. Configure **settings.php** 5. Configure **settings.php**
- When setting ``TINYIB_DBMODE`` to ``flatfile``, note that all post, report and ban data are exposed as the database is composed of standard text files. Access to ./inc/database/flatfile/ should be denied. - When setting ``TINYIB_DBMODE`` to ``flatfile``, note that all post, report and ban data are exposed as the database is composed of standard text files. Access to ./inc/database/flatfile/ should be denied.
@ -83,6 +80,7 @@ support in mind.
- Set ``TINYIB_THUMBNAIL`` to ``imagemagick``. - Set ``TINYIB_THUMBNAIL`` to ``imagemagick``.
- **Note:** GIF files will have animated thumbnails, which will often have large file sizes. - **Note:** GIF files will have animated thumbnails, which will often have large file sizes.
- To use TINYIB in another language, set ``TINYIB_LOCALE`` to a language code found in `locale/`. - To use TINYIB in another language, set ``TINYIB_LOCALE`` to a language code found in `locale/`.
- **Note:** The [mbstring](https://www.php.net/manual/en/book.mbstring.php) PHP extension must be installed and enabled for TinyIB to properly support operating on and rendering text in any language other than English.
6. [CHMOD](https://en.wikipedia.org/wiki/Chmod) write permissions to these directories: 6. [CHMOD](https://en.wikipedia.org/wiki/Chmod) write permissions to these directories:
- ./ (the directory containing TinyIB) - ./ (the directory containing TinyIB)
- ./src/ - ./src/
@ -108,12 +106,12 @@ support in mind.
1. Obtain the latest release. 1. Obtain the latest release.
- If you installed via Git, run the following command in TinyIB's directory: - If you installed via Git, run the following command in TinyIB's directory:
- `git pull` - `git pull`
- Otherwise, [download](https://code.rocketnine.space/tslocum/tinyib/archive/master.zip) and extract a zipped archive. - Otherwise, [download](https://codeberg.org/tslocum/tinyib/archive/master.zip) and extract a zipped archive.
2. Note which files were modified. 2. Note which files were modified.
- If **settings.default.php** was updated, migrate the changes to **settings.php** - If **settings.default.php** was updated, migrate the changes to **settings.php**
- Take care to not change the value of `TINYIB_TRIPSEED`, as it is used to generate secure tripcodes, hash passwords and hash IP addresses. - Take care to not change the value of `TINYIB_TRIPSEED`, as it is used to generate secure tripcodes, hash passwords and hash IP addresses.
- If other files were updated, and you have made changes yourself: - If other files were updated, and you have made changes yourself:
- Visit [code.rocketnine.space](https://code.rocketnine.space/tslocum/tinyib) and review the changes made in the update. - Visit [codeberg.org](https://codeberg.org/tslocum/tinyib) and review the changes made in the update.
- Ensure the update does not interfere with your changes. - Ensure the update does not interfere with your changes.
## Migrate ## Migrate
@ -137,17 +135,17 @@ While the migration is in progress, visitors will not be able to create or delet
## Support ## Support
1. Ensure you are running the latest version of TinyIB. 1. Ensure you are running the latest version of TinyIB.
2. Review the [open issues](https://code.rocketnine.space/tslocum/tinyib/issues). 2. Review the [open issues](https://codeberg.org/tslocum/tinyib/issues).
3. Open a [new issue](https://code.rocketnine.space/tslocum/tinyib/issues/new). 3. Open a [new issue](https://codeberg.org/tslocum/tinyib/issues/new).
## Translate ## Translate
Translation is handled [online](https://hosted.weblate.org/projects/tinyib/tinyib/). Translation is handled [online](https://translate.codeberg.org/projects/tinyib/tinyib/).
## Contribute ## Contribute
**Note:** Please do not submit translations via pull requests. See above. **Note:** Please do not submit translations via pull requests. See above.
1. [Fork TinyIB.](https://code.rocketnine.space/repo/fork/6) 1. [Fork TinyIB.](https://codeberg.org/tslocum/tinyib/fork)
2. Commit code changes to your forked repository. 2. Commit code changes to your forked repository.
3. [Submit a pull request.](https://code.rocketnine.space/tslocum/tinyib/pulls) 3. [Submit a pull request.](https://codeberg.org/tslocum/tinyib/pulls)

View File

View File

@ -1,11 +1,11 @@
<?php <?php
/* /*
TinyIB TinyIB
https://code.rocketnine.space/tslocum/tinyib https://codeberg.org/tslocum/tinyib
MIT License MIT License
Copyright (c) 2020 Trevor Slocum <trevor@rocketnine.space> Copyright (c) 2020 Trevor Slocum <trevor@rocket9labs.com>
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -81,7 +81,8 @@ if (!in_array(TINYIB_DBMODE, $database_modes)) {
fancyDie(__('Unknown database mode specified.')); fancyDie(__('Unknown database mode specified.'));
} }
if (TINYIB_DBMODE == 'pdo' && TINYIB_DBDRIVER == 'pgsql') { $schema_mode = (TINYIB_DBMIGRATE) ? TINYIB_DBMIGRATE : TINYIB_DBMODE;
if ($schema_mode == 'pdo' && TINYIB_DBDRIVER == 'pgsql') {
$accounts_sql = 'CREATE TABLE "' . TINYIB_DBACCOUNTS . '" ( $accounts_sql = 'CREATE TABLE "' . TINYIB_DBACCOUNTS . '" (
"id" bigserial NOT NULL, "id" bigserial NOT NULL,
"username" varchar(255) NOT NULL, "username" varchar(255) NOT NULL,
@ -306,25 +307,27 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name'])
if ($staffpost || !in_array('name', $hide_fields)) { if ($staffpost || !in_array('name', $hide_fields)) {
list($post['name'], $post['tripcode']) = nameAndTripcode($_POST['name']); list($post['name'], $post['tripcode']) = nameAndTripcode($_POST['name']);
$post['name'] = cleanString(substr($post['name'], 0, 75)); if (TINYIB_MAXNAME > 0) {
if (!$staffpost && TINYIB_MAXNAME > 0) { $post['name'] = _substr($post['name'], 0, TINYIB_MAXNAME);
$post['name'] = substr($post['name'], 0, TINYIB_MAXNAME);
} }
$post['name'] = cleanString($post['name']);
} }
if ($staffpost || !in_array('email', $hide_fields)) { if ($staffpost || !in_array('email', $hide_fields)) {
$post['email'] = cleanString(str_replace('"', '&quot;', substr($_POST['email'], 0, 75))); $post['email'] = $_POST['email'];
if (!$staffpost && TINYIB_MAXEMAIL > 0) { if (TINYIB_MAXEMAIL > 0) {
$post['email'] = substr($post['email'], 0, TINYIB_MAXEMAIL); $post['email'] = _substr($post['email'], 0, TINYIB_MAXEMAIL);
} }
$post['email'] = cleanString(str_replace('"', '&quot;', $post['email']));
} }
if ($staffpost) { if ($staffpost) {
$capcode = ($isadmin) ? ' <span style="color: ' . $tinyib_capcodes[0][1] . ' ;">## ' . $tinyib_capcodes[0][0] . '</span>' : ' <span style="color: ' . $tinyib_capcodes[1][1] . ';">## ' . $tinyib_capcodes[1][0] . '</span>'; $capcode = ($isadmin) ? ' <span style="color: ' . $tinyib_capcodes[0][1] . ' ;">## ' . $tinyib_capcodes[0][0] . '</span>' : ' <span style="color: ' . $tinyib_capcodes[1][1] . ';">## ' . $tinyib_capcodes[1][0] . '</span>';
} }
if ($staffpost || !in_array('subject', $hide_fields)) { if ($staffpost || !in_array('subject', $hide_fields)) {
$post['subject'] = cleanString(substr($_POST['subject'], 0, 75)); $post['subject'] = $_POST['subject'];
if (!$staffpost && TINYIB_MAXSUBJECT > 0) { if (TINYIB_MAXSUBJECT > 0) {
$post['subject'] = substr($post['subject'], 0, TINYIB_MAXSUBJECT); $post['subject'] = _substr($post['subject'], 0, TINYIB_MAXSUBJECT);
} }
$post['subject'] = cleanString($post['subject']);
} }
if ($staffpost || !in_array('message', $hide_fields)) { if ($staffpost || !in_array('message', $hide_fields)) {
$post['message'] = $_POST['message']; $post['message'] = $_POST['message'];
@ -332,7 +335,7 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name'])
// Treat message as raw HTML // Treat message as raw HTML
} else { } else {
if (TINYIB_WORDBREAK > 0) { if (TINYIB_WORDBREAK > 0) {
$post['message'] = preg_replace('/([^\s]{' . TINYIB_WORDBREAK . '})(?=[^\s])/', '$1' . TINYIB_WORDBREAK_IDENTIFIER, $post['message']); $post['message'] = preg_replace('/([^\s]{' . TINYIB_WORDBREAK . '})(?=[^\s])/u', '$1' . TINYIB_WORDBREAK_IDENTIFIER, $post['message']);
} }
$post['message'] = str_replace("\n", '<br>', makeLinksClickable(colorQuote(postLink(cleanString(rtrim($post['message'])))))); $post['message'] = str_replace("\n", '<br>', makeLinksClickable(colorQuote(postLink(cleanString(rtrim($post['message']))))));
@ -462,10 +465,6 @@ if (!isset($_GET['delete']) && !isset($_GET['manage']) && (isset($_POST['name'])
$post['thumb'] = $temp_file . '.gif'; $post['thumb'] = $temp_file . '.gif';
} else if ($file_mime == "image/png") { } else if ($file_mime == "image/png") {
$post['thumb'] = $temp_file . '.png'; $post['thumb'] = $temp_file . '.png';
} else if ($file_mime == "image/avif") {
$post['thumb'] = $temp_file . '.avif';
} else if ($file_mime == "image/heif") {
$post['thumb'] = $temp_file . '.avif';
} else { } else {
fancyDie(__('Error while processing audio/video.')); fancyDie(__('Error while processing audio/video.'));
} }
@ -972,12 +971,12 @@ EOF;
$text .= '<blockquote class="reply" style="padding: 7px;font-size: 1.25em;"> $text .= '<blockquote class="reply" style="padding: 7px;font-size: 1.25em;">
<pre style="margin: 0;padding: 0;">Attempting update...' . "\n\n" . $git_output . '</pre> <pre style="margin: 0;padding: 0;">Attempting update...' . "\n\n" . $git_output . '</pre>
</blockquote> </blockquote>
<p><b>Note:</b> If TinyIB updates and you have made custom modifications, <a href="https://code.rocketnine.space/tslocum/tinyib/commits/master" target="_blank">review the changes</a> which have been merged into your installation. <p><b>Note:</b> If TinyIB updates and you have made custom modifications, <a href="https://codeberg.org/tslocum/tinyib/commits/master" target="_blank">review the changes</a> which have been merged into your installation.
Ensure that your modifications do not interfere with any new/modified files. Ensure that your modifications do not interfere with any new/modified files.
See the <a href="https://code.rocketnine.space/tslocum/tinyib/src/branch/master/README.md">README</a> <small>(<a href="README.md" target="_blank">alternate link</a>)</small> for instructions.</p>'; See the <a href="https://codeberg.org/tslocum/tinyib/src/branch/master/README.md">README</a> <small>(<a href="README.md" target="_blank">alternate link</a>)</small> for instructions.</p>';
} else { } else {
$text .= '<p><b>TinyIB was not installed via Git.</b></p> $text .= '<p><b>TinyIB was not installed via Git.</b></p>
<p>If you installed TinyIB without Git, you must <a href="https://code.rocketnine.space/tslocum/tinyib">update manually</a>. If you did install with Git, ensure the script has read and write access to the <b>.git</b> folder.</p>'; <p>If you installed TinyIB without Git, you must <a href="https://codeberg.org/tslocum/tinyib">update manually</a>. If you did install with Git, ensure the script has read and write access to the <b>.git</b> folder.</p>';
} }
} elseif (isset($_GET['dbmigrate'])) { } elseif (isset($_GET['dbmigrate'])) {
if (TINYIB_DBMIGRATE !== '' && TINYIB_DBMIGRATE !== false && TINYIB_DBMODE != TINYIB_DBMIGRATE) { if (TINYIB_DBMIGRATE !== '' && TINYIB_DBMIGRATE !== false && TINYIB_DBMODE != TINYIB_DBMIGRATE) {
@ -1039,7 +1038,7 @@ EOF;
echo '<p><b>Database migration complete</b>. Set TINYIB_DBMODE to the new database mode and TINYIB_DBMIGRATE to false, then click <b>Rebuild All</b> above and ensure everything looks and works as it should.</p>'; echo '<p><b>Database migration complete</b>. Set TINYIB_DBMODE to the new database mode and TINYIB_DBMIGRATE to false, then click <b>Rebuild All</b> above and ensure everything looks and works as it should.</p>';
} else { } else {
$text .= '<p>Your original database will not be deleted. If the migration fails, disable the tool and your board will be unaffected. See the <a href="https://code.rocketnine.space/tslocum/tinyib/src/branch/master/README.md" target="_blank">README</a> <small>(<a href="README.md" target="_blank">alternate link</a>)</small> for instructions.</a><br><br><a href="?manage&dbmigrate&go"><b>Start the migration</b></a></p>'; $text .= '<p>Your original database will not be deleted. If the migration fails, disable the tool and your board will be unaffected. See the <a href="https://codeberg.org/tslocum/tinyib/src/branch/master/README.md" target="_blank">README</a> <small>(<a href="README.md" target="_blank">alternate link</a>)</small> for instructions.</a><br><br><a href="?manage&dbmigrate&go"><b>Start the migration</b></a></p>';
} }
} else { } else {
fancyDie('Set TINYIB_DBMIGRATE to the desired TINYIB_DBMODE and enter in any database related settings in settings.php before migrating.'); fancyDie('Set TINYIB_DBMIGRATE to the desired TINYIB_DBMODE and enter in any database related settings in settings.php before migrating.');
@ -1070,8 +1069,8 @@ EOF;
$action = sprintf(__('Deleted %s'),'&gt;&gt;' . $post['id']) . ' - ' . hashData($post['ip']); $action = sprintf(__('Deleted %s'),'&gt;&gt;' . $post['id']) . ' - ' . hashData($post['ip']);
$stripped = strip_tags($post['message']); $stripped = strip_tags($post['message']);
if ($stripped != '') { if ($stripped != '') {
$action .= ' - ' . htmlentities(substr($stripped, 0, 32)); $action .= ' - ' . htmlentities(_substr($stripped, 0, 32));
if (strlen($stripped) > 32) { if (_strlen($stripped) > 32) {
$action .= '...'; $action .= '...';
} }
} }

View File

@ -11,6 +11,9 @@
session_start(); session_start();
putenv('GDFONTPATH=' . realpath(dirname(__FILE__)) . '/fonts/'); putenv('GDFONTPATH=' . realpath(dirname(__FILE__)) . '/fonts/');
error_reporting(E_ALL ^ (E_NOTICE | E_WARNING | E_DEPRECATED));
$captcha = new SimpleCaptcha(); $captcha = new SimpleCaptcha();
$captcha->CreateImage(); $captcha->CreateImage();

View File

@ -184,6 +184,11 @@ function getLogs($offset, $limit) {
return convertLogsToSQLStyle($rows); return convertLogsToSQLStyle($rows);
} }
function allLogs() {
$rows = $GLOBALS['db']->selectWhere(LOGS_FILE, NULL, -1, new OrderBy(LOG_TIMESTAMP, ASCENDING, INTEGER_COMPARISON));
return convertLogsToSQLStyle($rows);
}
function convertLogsToSQLStyle($logs, $single = false) { function convertLogsToSQLStyle($logs, $single = false) {
$newlogs = array(); $newlogs = array();
foreach ($logs as $l) { foreach ($logs as $l) {
@ -203,10 +208,10 @@ function convertLogsToSQLStyle($logs, $single = false) {
function insertLog($l) { function insertLog($l) {
$log = array(); $log = array();
$log['id'] = '0'; $log[LOG_ID] = '0';
$log['timestamp'] = $l[LOG_TIMESTAMP]; $log[LOG_TIMESTAMP] = $l['timestamp'];
$log['account'] = $l[LOG_ACCOUNT]; $log[LOG_ACCOUNT] = $l['account'];
$log['message'] = $l[LOG_MESSAGE]; $log[LOG_MESSAGE] = $l['message'];
$GLOBALS['db']->insertWithAutoId(LOGS_FILE, LOG_ID, $log); $GLOBALS['db']->insertWithAutoId(LOGS_FILE, LOG_ID, $log);
} }

View File

@ -147,6 +147,17 @@ function getLogs($offset, $limit) {
return $logs; return $logs;
} }
function allLogs() {
$logs = array();
$result = mysql_query("SELECT * FROM `" . TINYIB_DBLOGS . "` ORDER BY `timestamp` ASC");
if ($result) {
while ($log = mysql_fetch_assoc($result)) {
$logs[] = $log;
}
}
return $logs;
}
function insertLog($log) { function insertLog($log) {
mysql_query("INSERT INTO `" . TINYIB_DBLOGS . "` (`timestamp`, `account`, `message`) VALUES ('" . mysql_real_escape_string($log['timestamp']) . "', '" . mysql_real_escape_string($log['account']) . "', '" . mysql_real_escape_string($log['message']) . "')"); mysql_query("INSERT INTO `" . TINYIB_DBLOGS . "` (`timestamp`, `account`, `message`) VALUES ('" . mysql_real_escape_string($log['timestamp']) . "', '" . mysql_real_escape_string($log['account']) . "', '" . mysql_real_escape_string($log['message']) . "')");
} }

View File

@ -168,6 +168,18 @@ function getLogs($offset, $limit) {
return $logs; return $logs;
} }
function allLogs() {
global $link;
$logs = array();
$result = mysqli_query($link, "SELECT * FROM `" . TINYIB_DBLOGS . "` ORDER BY `timestamp` ASC");
if ($result) {
while ($log = mysqli_fetch_assoc($result)) {
$logs[] = $log;
}
}
return $logs;
}
function insertLog($log) { function insertLog($log) {
global $link; global $link;
mysqli_query($link, "INSERT INTO `" . TINYIB_DBLOGS . "` (`timestamp`, `account`, `message`) VALUES ('" . mysqli_real_escape_string($link, $log['timestamp']) . "', '" . mysqli_real_escape_string($link, $log['account']) . "', '" . mysqli_real_escape_string($link, $log['message']) . "')"); mysqli_query($link, "INSERT INTO `" . TINYIB_DBLOGS . "` (`timestamp`, `account`, `message`) VALUES ('" . mysqli_real_escape_string($link, $log['timestamp']) . "', '" . mysqli_real_escape_string($link, $log['account']) . "', '" . mysqli_real_escape_string($link, $log['message']) . "')");

View File

@ -124,6 +124,15 @@ function getLogs($offset, $limit) {
return $logs; return $logs;
} }
function allLogs() {
$logs = array();
$results = pdoQuery("SELECT * FROM " . TINYIB_DBLOGS . " ORDER BY timestamp ASC");
while ($row = $results->fetch(PDO::FETCH_ASSOC)) {
$logs[] = $row;
}
return $logs;
}
function insertLog($log) { function insertLog($log) {
global $dbh; global $dbh;
$stm = $dbh->prepare("INSERT INTO " . TINYIB_DBLOGS . " (timestamp, account, message) VALUES (?, ?, ?)"); $stm = $dbh->prepare("INSERT INTO " . TINYIB_DBLOGS . " (timestamp, account, message) VALUES (?, ?, ?)");

View File

@ -99,45 +99,45 @@ if (TINYIB_DBDRIVER === 'pgsql') {
$query = "SELECT column_name FROM information_schema.columns WHERE table_name='" . TINYIB_DBPOSTS . "' and column_name='moderated'"; $query = "SELECT column_name FROM information_schema.columns WHERE table_name='" . TINYIB_DBPOSTS . "' and column_name='moderated'";
$moderated_exists = $dbh->query($query)->fetchColumn() != 0; $moderated_exists = $dbh->query($query)->fetchColumn() != 0;
} else { } else {
$dbh->query("SHOW COLUMNS FROM `" . TINYIB_DBPOSTS . "` LIKE 'moderated'"); $dbh->query("SHOW COLUMNS FROM " . TINYIB_DBPOSTS . " LIKE 'moderated'");
$moderated_exists = $dbh->query("SELECT FOUND_ROWS()")->fetchColumn() != 0; $moderated_exists = $dbh->query("SELECT FOUND_ROWS()")->fetchColumn() != 0;
} }
if (!$moderated_exists) { if (!$moderated_exists) {
$dbh->exec("ALTER TABLE `" . TINYIB_DBPOSTS . "` ADD COLUMN moderated TINYINT(1) NOT NULL DEFAULT '1'"); $dbh->exec("ALTER TABLE " . TINYIB_DBPOSTS . " ADD COLUMN moderated TINYINT(1) NOT NULL DEFAULT '1'");
} }
if (TINYIB_DBDRIVER === 'pgsql') { if (TINYIB_DBDRIVER === 'pgsql') {
$query = "SELECT column_name FROM information_schema.columns WHERE table_name='" . TINYIB_DBPOSTS . "' and column_name='stickied'"; $query = "SELECT column_name FROM information_schema.columns WHERE table_name='" . TINYIB_DBPOSTS . "' and column_name='stickied'";
$stickied_exists = $dbh->query($query)->fetchColumn() != 0; $stickied_exists = $dbh->query($query)->fetchColumn() != 0;
} else { } else {
$dbh->query("SHOW COLUMNS FROM `" . TINYIB_DBPOSTS . "` LIKE 'stickied'"); $dbh->query("SHOW COLUMNS FROM " . TINYIB_DBPOSTS . " LIKE 'stickied'");
$stickied_exists = $dbh->query("SELECT FOUND_ROWS()")->fetchColumn() != 0; $stickied_exists = $dbh->query("SELECT FOUND_ROWS()")->fetchColumn() != 0;
} }
if (!$stickied_exists) { if (!$stickied_exists) {
$dbh->exec("ALTER TABLE `" . TINYIB_DBPOSTS . "` ADD COLUMN stickied TINYINT(1) NOT NULL DEFAULT '0'"); $dbh->exec("ALTER TABLE " . TINYIB_DBPOSTS . " ADD COLUMN stickied TINYINT(1) NOT NULL DEFAULT '0'");
} }
if (TINYIB_DBDRIVER === 'pgsql') { if (TINYIB_DBDRIVER === 'pgsql') {
$query = "SELECT column_name FROM information_schema.columns WHERE table_name='" . TINYIB_DBPOSTS . "' and column_name='locked'"; $query = "SELECT column_name FROM information_schema.columns WHERE table_name='" . TINYIB_DBPOSTS . "' and column_name='locked'";
$locked_exists = $dbh->query($query)->fetchColumn() != 0; $locked_exists = $dbh->query($query)->fetchColumn() != 0;
} else { } else {
$dbh->query("SHOW COLUMNS FROM `" . TINYIB_DBPOSTS . "` LIKE 'locked'"); $dbh->query("SHOW COLUMNS FROM " . TINYIB_DBPOSTS . " LIKE 'locked'");
$locked_exists = $dbh->query("SELECT FOUND_ROWS()")->fetchColumn() != 0; $locked_exists = $dbh->query("SELECT FOUND_ROWS()")->fetchColumn() != 0;
} }
if (!$locked_exists) { if (!$locked_exists) {
$dbh->exec("ALTER TABLE `" . TINYIB_DBPOSTS . "` ADD COLUMN locked TINYINT(1) NOT NULL DEFAULT '0'"); $dbh->exec("ALTER TABLE " . TINYIB_DBPOSTS . " ADD COLUMN locked TINYINT(1) NOT NULL DEFAULT '0'");
} }
if (TINYIB_DBDRIVER === 'pgsql') { if (TINYIB_DBDRIVER === 'pgsql') {
$dbh->query("ALTER TABLE `" . TINYIB_DBPOSTS . "` ALTER COLUMN tripcode VARCHAR(24) NOT NULL DEFAULT ''"); $dbh->query("ALTER TABLE " . TINYIB_DBPOSTS . " ALTER COLUMN tripcode TYPE VARCHAR(24)");
$dbh->query("ALTER TABLE `" . TINYIB_DBPOSTS . "` ALTER COLUMN ip VARCHAR(255) NOT NULL DEFAULT ''"); $dbh->query("ALTER TABLE " . TINYIB_DBPOSTS . " ALTER COLUMN ip TYPE VARCHAR(255)");
$dbh->query("ALTER TABLE `" . TINYIB_DBBANS . "` ALTER COLUMN ip VARCHAR(255) NOT NULL DEFAULT ''"); $dbh->query("ALTER TABLE " . TINYIB_DBBANS . " ALTER COLUMN ip TYPE VARCHAR(255)");
} else { } else {
$dbh->query("ALTER TABLE `" . TINYIB_DBPOSTS . "` MODIFY tripcode VARCHAR(24) NOT NULL DEFAULT ''"); $dbh->query("ALTER TABLE " . TINYIB_DBPOSTS . " MODIFY tripcode VARCHAR(24) NOT NULL DEFAULT ''");
$dbh->query("ALTER TABLE `" . TINYIB_DBPOSTS . "` MODIFY ip VARCHAR(255) NOT NULL DEFAULT ''"); $dbh->query("ALTER TABLE " . TINYIB_DBPOSTS . " MODIFY ip VARCHAR(255) NOT NULL DEFAULT ''");
$dbh->query("ALTER TABLE `" . TINYIB_DBBANS . "` MODIFY ip VARCHAR(255) NOT NULL DEFAULT ''"); $dbh->query("ALTER TABLE " . TINYIB_DBBANS . " MODIFY ip VARCHAR(255) NOT NULL DEFAULT ''");
} }
function pdoQuery($sql, $params = false) { function pdoQuery($sql, $params = false) {

View File

@ -128,6 +128,15 @@ function getLogs($offset, $limit) {
return $logs; return $logs;
} }
function allLogs() {
$logs = array();
$result = sqlite_fetch_all(sqlite_query($GLOBALS["db"], "SELECT * FROM " . TINYIB_DBLOGS . " ORDER BY timestamp ASC"), SQLITE_ASSOC);
foreach ($result as $log) {
$logs[] = $log;
}
return $logs;
}
function insertLog($log) { function insertLog($log) {
sqlite_query($GLOBALS["db"], "INSERT INTO " . TINYIB_DBLOGS . " (timestamp, account, message) VALUES ('" . sqlite_escape_string($log['timestamp']) . "', '" . sqlite_escape_string($log['account']) . "', '" . sqlite_escape_string($log['message']) . "')"); sqlite_query($GLOBALS["db"], "INSERT INTO " . TINYIB_DBLOGS . " (timestamp, account, message) VALUES ('" . sqlite_escape_string($log['timestamp']) . "', '" . sqlite_escape_string($log['account']) . "', '" . sqlite_escape_string($log['message']) . "')");
} }

View File

@ -147,6 +147,16 @@ function getLogs($offset, $limit) {
return $logs; return $logs;
} }
function allLogs() {
global $db;
$logs = array();
$result = $db->query("SELECT * FROM " . TINYIB_DBLOGS . " ORDER BY timestamp ASC");
while ($log = $result->fetchArray()) {
$logs[] = $log;
}
return $logs;
}
function insertLog($log) { function insertLog($log) {
global $db; global $db;
$db->exec("INSERT INTO " . TINYIB_DBLOGS . " (timestamp, account, message) VALUES ('" . $db->escapeString($log['timestamp']) . "', '" . $db->escapeString($log['account']) . "', '" . $db->escapeString($log['message']) . "')"); $db->exec("INSERT INTO " . TINYIB_DBLOGS . " (timestamp, account, message) VALUES ('" . $db->escapeString($log['timestamp']) . "', '" . $db->escapeString($log['account']) . "', '" . $db->escapeString($log['message']) . "')");

View File

@ -108,7 +108,7 @@ if (!$result->fetchArray()) {
if (function_exists('insertPost')) { if (function_exists('insertPost')) {
function migrateAccount($account) { function migrateAccount($account) {
global $db; global $db;
$db->exec("INSERT INTO " . TINYIB_DBACCOUNTS . " (id, username, password, role, lastactive) VALUES (" . $db->escapeString($account['id']) . ", '" . $db->escapeString($account['username']) . "', " . $db->escapeString($account['password']) . ", " . $db->escapeString($account['role']) . ", '" . $db->escapeString($account['lastactive']) . "')"); $db->exec("INSERT INTO " . TINYIB_DBACCOUNTS . " (id, username, password, role, lastactive) VALUES (" . $db->escapeString($account['id']) . ", '" . $db->escapeString($account['username']) . "', '" . $db->escapeString($account['password']) . "', " . $db->escapeString($account['role']) . ", '" . $db->escapeString($account['lastactive']) . "')");
} }
function migrateBan($ban) { function migrateBan($ban) {

View File

@ -3,6 +3,8 @@ if (!defined('TINYIB_BOARD')) {
die(''); die('');
} }
$multibyte_enabled = function_exists('mb_strlen');
if (!function_exists('array_column')) { if (!function_exists('array_column')) {
function array_column($array, $column_name) { function array_column($array, $column_name) {
return array_map(function ($element) use ($column_name) { return array_map(function ($element) use ($column_name) {
@ -24,6 +26,38 @@ function lockDatabase() {
return $fp; return $fp;
} }
function _strlen($string) {
global $multibyte_enabled;
if ($multibyte_enabled) {
return mb_strlen($string);
}
return strlen($string);
}
function _strpos($haystack, $needle, $offset=0) {
global $multibyte_enabled;
if ($multibyte_enabled) {
return mb_strpos($haystack, $needle, $offset);
}
return strpos($haystack, $needle, $offset);
}
function _substr($string, $start, $length=null) {
global $multibyte_enabled;
if ($multibyte_enabled) {
return mb_substr($string, $start, $length);
}
return substr($string, $start, $length);
}
function _substr_count($haystack, $needle) {
global $multibyte_enabled;
if ($multibyte_enabled) {
return mb_substr_count($haystack, $needle);
}
return substr_count($haystack, $needle);
}
function hashData($data, $force = false) { function hashData($data, $force = false) {
global $bcrypt_salt; global $bcrypt_salt;
if (substr($data, 0, 4) == '$2y$' && !$force) { if (substr($data, 0, 4) == '$2y$' && !$force) {
@ -388,8 +422,8 @@ function checkFlood() {
} }
function checkMessageSize() { function checkMessageSize() {
if (TINYIB_MAXMESSAGE > 0 && strlen($_POST['message']) > TINYIB_MAXMESSAGE) { if (TINYIB_MAXMESSAGE > 0 && _strlen($_POST['message']) > TINYIB_MAXMESSAGE) {
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']), TINYIB_MAXMESSAGE)); 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']), TINYIB_MAXMESSAGE));
} }
} }
@ -613,72 +647,63 @@ function ffmpegThumbnail($file_location, $thumb_location, $new_w, $new_h) {
} }
} }
// TODO: Check whether I missed error handling somewhere during refactoring. function createThumbnail($file_location, $thumb_location, $new_w, $new_h, $spoiler) {
function readImageFromFile($path, $error_message) { $system = explode(".", $thumb_location);
switch (pathinfo($path)["extension"]) { $system = array_reverse($system);
case "jpg": if (TINYIB_THUMBNAIL == 'gd' || (TINYIB_THUMBNAIL == 'ffmpeg' && preg_match("/jpg|jpeg/", $system[0]))) {
case "jpeg": return imagecreatefromjpeg($path); if (preg_match("/jpg|jpeg/", $system[0])) {
case "png": return imagecreatefrompng($path); $src_img = imagecreatefromjpeg($file_location);
case "gif": return imagecreatefromgif($path); } else if (preg_match("/png/", $system[0])) {
case "avif": return imagecreatefromavif($path); $src_img = imagecreatefrompng($file_location);
} } else if (preg_match("/gif/", $system[0])) {
fancyDie(__($error_message)); $src_img = imagecreatefromgif($file_location);
} else {
return false; return false;
}
function saveImageToFile($image, $path) {
switch (pathinfo($path)["extension"]) {
case "jpg":
case "jpeg": return imagejpeg($image, $path, 90); // why only set quality for jpegs?
case "png": return imagepng($image, $path);
case "gif": return imagegif($image, $path);
case "avif": return imageavif($image, $path);
} }
return false;
}
function resizeImage($src_img, $max_width, $max_height, $file_extension) { if (!$src_img) {
$src_width = imageSX($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.'));
$src_height = imageSY($src_img); }
$ratio = ($src_width > $src_height) ? ($max_width / $src_width) : ($max_height / $src_height); $old_x = imageSX($src_img);
$old_y = imageSY($src_img);
$percent = ($old_x > $old_y) ? ($new_w / $old_x) : ($new_h / $old_y);
$thumb_w = round($old_x * $percent);
$thumb_h = round($old_y * $percent);
$target_width = round($src_width * $ratio); $dst_img = imagecreatetruecolor($thumb_w, $thumb_h);
$target_height = round($src_height * $ratio); if (preg_match("/png/", $system[0]) && imagepng($src_img, $thumb_location)) {
// Why don't we use imagescale (PHP 5 >= 5.5.0, PHP 7, PHP 8) here?
$dst_img = imagecreatetruecolor($target_width, $target_height);
if ($file_extension === 'png') {
imagealphablending($dst_img, false); imagealphablending($dst_img, false);
imagesavealpha($dst_img, true); imagesavealpha($dst_img, true);
$color = imagecolorallocatealpha($dst_img, 0, 0, 0, 0); $color = imagecolorallocatealpha($dst_img, 0, 0, 0, 0);
imagefilledrectangle($dst_img, 0, 0, $target_width, $target_height, $color); imagefilledrectangle($dst_img, 0, 0, $thumb_w, $thumb_h, $color);
imagecolortransparent($dst_img, $color); imagecolortransparent($dst_img, $color);
imagecopyresampled($dst_img, $src_img, 0, 0, 0, 0, $target_width, $target_height, $src_width, $src_height); imagecopyresampled($dst_img, $src_img, 0, 0, 0, 0, $thumb_w, $thumb_h, $old_x, $old_y);
} else { } else {
fastimagecopyresampled($dst_img, $src_img, 0, 0, 0, 0, $target_width, $target_height, $src_width, $src_height); fastimagecopyresampled($dst_img, $src_img, 0, 0, 0, 0, $thumb_w, $thumb_h, $old_x, $old_y);
} }
return $dst_img; if (preg_match("/png/", $system[0])) {
} if (!imagepng($dst_img, $thumb_location)) {
return false;
function blurImage($image) { }
$gaussian = array(array(1.0, 2.0, 1.0), array(2.0, 4.0, 2.0), array(1.0, 2.0, 1.0)); } else if (preg_match("/jpg|jpeg/", $system[0])) {
if (!imagejpeg($dst_img, $thumb_location, 70)) {
for ($x = 1; $x <= 149; $x++) { return false;
imageconvolution($image, $gaussian, 16, 0); }
} else if (preg_match("/gif/", $system[0])) {
if (!imagegif($dst_img, $thumb_location)) {
return false;
}
} }
}
function createFallbackImage($source, $destination) { imagedestroy($dst_img);
$image = readImageFromFile($source, '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.'); imagedestroy($src_img);
saveImageToFile($image, $destination); } else if (TINYIB_THUMBNAIL == 'ffmpeg') {
} ffmpegThumbnail($file_location, $thumb_location, $new_w, $new_h);
} else { // ImageMagick
function imageMagickThumbnail($file_location, $thumb_location, $new_w, $new_h) {
$discard = ''; $discard = '';
$exit_status = 1; $exit_status = 1;
@ -693,45 +718,45 @@ function imageMagickThumbnail($file_location, $thumb_location, $new_w, $new_h) {
if ($exit_status != 0) { if ($exit_status != 0) {
return false; return false;
} }
} }
function gdThumbnail($file_location, $file_extension, $thumb_location, $new_w, $new_h) { if (!$spoiler) {
$src_img = readImageFromFile($file_location, '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.'); return true;
$dst_img = resizeImage($src_img, $new_w, $new_h, $file_extension); }
saveImageToFile($dst_img, $thumb_location);
imagedestroy($dst_img);
imagedestroy($src_img);
}
function spoilerThumbnail($file_location, $thumb_location) { if (preg_match("/jpg|jpeg/", $system[0])) {
// Why don't we resize images here? $src_img = imagecreatefromjpeg($thumb_location);
$src_img = readImageFromFile($file_location, '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.'); } else if (preg_match("/png/", $system[0])) {
$src_img = imagecreatefrompng($thumb_location);
blurImage($src_img); } else if (preg_match("/gif/", $system[0])) {
saveImageToFile($src_img, $thumb_location); $src_img = imagecreatefromgif($thumb_location);
imagedestroy($src_img);
}
function createThumbnail($file_location, $thumb_location, $new_w, $new_h, $spoiler) {
$path = pathinfo($file_location);
$file_extension = $path['extension'];
if (TINYIB_THUMBNAIL == 'gd' || (TINYIB_THUMBNAIL == 'ffmpeg' && preg_match("/jpg|jpeg/", $file_extension))) {
gdThumbnail($file_location, $file_extension, $thumb_location, $new_w, $new_h);
} else if (TINYIB_THUMBNAIL == 'ffmpeg') {
ffmpegThumbnail($file_location, $thumb_location, $new_w, $new_h);
} else { } else {
imageMagickThumbnail($file_location, $thumb_location, $new_w, $new_h); return true;
} }
if ($spoiler) { if (!$src_img) {
spoilerThumbnail($file_location, $thumb_location); 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.'));
} }
if ($file_extension === 'avif') { $gaussian = array(array(1.0, 2.0, 1.0), array(2.0, 4.0, 2.0), array(1.0, 2.0, 1.0));
createFallbackImage($thumb_location, 'fallback/' . $path['filename'] . 's.jpg'); for ($x = 1; $x <= 149; $x++) {
imageconvolution($src_img, $gaussian, 16, 0);
} }
if (preg_match("/png/", $system[0])) {
if (!imagepng($src_img, $thumb_location)) {
return false;
}
} else if (preg_match("/jpg|jpeg/", $system[0])) {
if (!imagejpeg($src_img, $thumb_location, 70)) {
return false;
}
} else if (preg_match("/gif/", $system[0])) {
if (!imagegif($src_img, $thumb_location)) {
return false;
}
}
imagedestroy($src_img);
return true; return true;
} }
@ -802,8 +827,8 @@ function addVideoOverlay($thumb_location) {
function strallpos($haystack, $needle, $offset = 0) { function strallpos($haystack, $needle, $offset = 0) {
$result = array(); $result = array();
for ($i = $offset; $i < strlen($haystack); $i++) { for ($i = $offset; $i < _strlen($haystack); $i++) {
$pos = strpos($haystack, $needle, $i); $pos = _strpos($haystack, $needle, $i);
if ($pos !== False) { if ($pos !== False) {
$offset = $pos; $offset = $pos;
if ($offset >= $i) { if ($offset >= $i) {
@ -909,13 +934,13 @@ function attachFile($post, $filepath, $filename, $uploaded, $spoiler) {
} }
$post['file'] = $file_name; $post['file'] = $file_name;
$post['file_original'] = trim(htmlentities(substr($filename, 0, 50), ENT_QUOTES)); $post['file_original'] = trim(htmlentities(_substr($filename, 0, 50), ENT_QUOTES));
$post['file_hex'] = md5_file($filepath); $post['file_hex'] = md5_file($filepath);
$post['file_size'] = $filesize; $post['file_size'] = $filesize;
$post['file_size_formatted'] = convertBytes($post['file_size']); $post['file_size_formatted'] = convertBytes($post['file_size']);
checkDuplicateFile($post['file_hex']); checkDuplicateFile($post['file_hex']);
if (in_array($file_mime, array('image/jpeg', 'image/pjpeg', 'image/png', 'image/gif', 'application/x-shockwave-flash', 'image/avif', 'image/heif'))) { if (in_array($file_mime, array('image/jpeg', 'image/pjpeg', 'image/png', 'image/gif', 'application/x-shockwave-flash'))) {
$file_info = getimagesize($file_src); $file_info = getimagesize($file_src);
$post['image_width'] = $file_info[0] != '' ? $file_info[0] : 0; $post['image_width'] = $file_info[0] != '' ? $file_info[0] : 0;
$post['image_height'] = $file_info[1] != '' ? $file_info[1] : 0; $post['image_height'] = $file_info[1] != '' ? $file_info[1] : 0;
@ -931,7 +956,7 @@ function attachFile($post, $filepath, $filename, $uploaded, $spoiler) {
if ($file_mime == 'application/x-shockwave-flash') { if ($file_mime == 'application/x-shockwave-flash') {
addVideoOverlay('thumb/' . $post['thumb']); addVideoOverlay('thumb/' . $post['thumb']);
} }
} else if (in_array($file_mime, array('image/jpeg', 'image/pjpeg', 'image/png', 'image/gif', 'image/avif', 'image/heif'))) { } else if (in_array($file_mime, array('image/jpeg', 'image/pjpeg', 'image/png', 'image/gif'))) {
$post['thumb'] = $file_name_pre . 's.' . $tinyib_uploads[$file_mime][0]; $post['thumb'] = $file_name_pre . 's.' . $tinyib_uploads[$file_mime][0];
list($thumb_maxwidth, $thumb_maxheight) = thumbnailDimensions($post); list($thumb_maxwidth, $thumb_maxheight) = thumbnailDimensions($post);
@ -939,10 +964,6 @@ function attachFile($post, $filepath, $filename, $uploaded, $spoiler) {
@unlink($file_src); @unlink($file_src);
fancyDie(__('Could not create thumbnail.')); fancyDie(__('Could not create thumbnail.'));
} }
if (in_array($file_mime, array('image/avif', 'image/heif'))) {
createFallbackImage($file_src, "fallback/$file_name_pre.jpg");
}
} else if ($file_mime == 'audio/webm' || $file_mime == 'video/webm' || $file_mime == 'audio/mp4' || $file_mime == 'video/mp4') { } else if ($file_mime == 'audio/webm' || $file_mime == 'video/webm' || $file_mime == 'audio/mp4' || $file_mime == 'video/mp4') {
list($post['image_width'], $post['image_height']) = videoDimensions($file_src); list($post['image_width'], $post['image_height']) = videoDimensions($file_src);

View File

@ -1,18 +1,13 @@
<?php <?php
use Gettext\Loader\PoLoader; use Gettext\Translator;
use Gettext\Translations;
setlocale(LC_ALL, TINYIB_LOCALE);
require 'inc/gettext/src/autoloader.php'; require 'inc/gettext/src/autoloader.php';
setlocale(LC_ALL, TINYIB_LOCALE); $translations = Translations::fromPoFile('locale/' . TINYIB_LOCALE . '/tinyib.po');
$loader = new PoLoader(); $translator = new Translator();
$translations = $loader->loadFile('locale/' . TINYIB_LOCALE . '/tinyib.po'); $translator->loadTranslations($translations);
$translator->register();
function __($string) {
global $translations;
$translation = $translations->find(null, $string)->getTranslation();
if ($translation == '') {
return $string;
}
return $translation;
}

View File

@ -1,5 +1,4 @@
# Change Log # Change Log
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/) The format is based on [Keep a Changelog](http://keepachangelog.com/)
@ -7,123 +6,220 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
Previous releases are documented in [github releases](https://github.com/oscarotero/Gettext/releases) Previous releases are documented in [github releases](https://github.com/oscarotero/Gettext/releases)
## [5.6.1] - 2021-12-04 ## [4.8.11] - 2023-08-14
### Fixed ### Fixed
- PHP 8.1 support [#278]. - PHP 5.4 support [#289]
## [5.6.0] - 2021-11-05
### Added
- New method `addFlag` to `ParsedFunction`, that allows to assign flags by scanners.
- The `FunctionsHandlersTrait` has an abstract `addFlags` method.
## [4.8.10] - 2023-08-10
### Fixed ### Fixed
- Subsequent load file fails [#257] [#276] - Previous version was tagged with the incorrect branch.
- Upgraded some dependencies in `dev`.
## [5.5.4] - 2020-12-20 ## [4.8.9] - 2023-08-10
### Fixed ### Fixed
- TypeError in which numeric entries were converted to integers [#265] - PHP 8.1 deprecation warning [#289]
## [5.5.3] - 2020-12-01 ## [4.8.8] - 2022-12-08
### Fixed ### Fixed
- Add PHP 8 to composer.json - PHP functions prefixed with a slash are being ignored [#284], [#288]
## [5.5.2] - 2020-11-17 ## [4.8.7] - 2022-08-02
### Fixed ### Fixed
- Parse of multiline disabled translations [#262] [#263] - Suppress deprecation error on PHP 8.1 [#280]
## [5.5.1] - 2020-06-08 ## [4.8.6] - 2021-10-19
### Fixed ### Fixed
- Type error in which numeric filenames were converted to integers [#260] - Parse PO files with multiline disabled entries [#274]
## [5.5.0] - 2020-05-23 ## [4.8.5] - 2021-07-13
### Added ### Fixed
- New option `addReferences()` to configure the code scanners whether add or not references [#258] - Prevent adding the same translator comment to multiple functions [#271]
## [4.8.4] - 2021-03-10
### Fixed
- PHP 8 compatibilty [#266]
## [4.8.3] - 2020-11-18
### Fixed
- Blade extractor for Laravel8/Jetstream [#261]
## [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 ### Changed
- BREAKING: Moved some code from `CodeScanner` to the new `FunctionsHandlersTrait` in order to better reuse. - Many `private` properties and methods were changed to `protected` in order to improve the extensibility [#231]
## [5.4.1] - 2020-03-15
### Fixed
- PoGenerator includes the description and flags of the translations [#253]
## [5.4.0] - 2020-03-07
### Added
- Added `_` function to the list of functions scanned by default
- Added `Translations::setDescription()` and `Translations::getDescription()` methods [#251]
- Added `Translations::getFlags()` that returns a `Flags` object to assign flags to the entire po file [#251]
## [5.3.0] - 2020-02-18
### Added
- `Comments::delete()` and `Flags::delete()` methods [#247]
## [5.2.2] - 2020-02-09
### Fixed
- MoLoader with plurals [#246]
## [5.2.1] - 2019-12-08
### Fixed
- Multiline string in PoGenerator [#244]
## [5.2.0] - 2019-11-25
### Added
- New function `CodeScanner::extractCommentsStartingWith()` to extract comments from the code.
## [5.1.0] - 2019-11-11
### Added
- New function `CodeScanner::ignoreInvalidFunctions()` to ignore invalid functions instead throw an exception
## 5.0.0 - 2019-11-04
### Added
- New interfaces: `ScannerInterface` and `FunctionsScannerInterface`.
### Changed
- Moved the package and dependencies to [php-gettext](https://github.com/php-gettext) organization
- Minimum PHP version supported is 7.2
- Added php7 strict typing
- Extractors have been split into two different types of classes to import translations:
- Scanners: To scan code files (like php, javascript, twig, etc) in order to collect gettext entries from many domains at the same time.
- Loaders: To load a translation format such po, mo, json, xliff, etc
- Split the `Translation` and `Translations` classes in different sub-classes to handle comments, flags, references, etc. For example, instead `$translation->addComment('foo')` now it's `$translation->getComments()->add('foo')`.
- Simplified the options to merge translations with pre-configured options like `Merged::SCAN_AND_LOAD`.
- The headers of translations are always sorted alphabetically.
- Changed the signature of all classes and interfaces.
### Removed
- Extractors (now scanners and loaders), generators and translators were removed from this package and published as external packages, allowing to install only those that you need. Only Po and Mo formats are included by default.
- Removed magic classes like `Translations::fromPoFile` or `$translation->toMoFile()`. Now, the scanners, loaders and generators are independent classes that have to be instantiated.
- Removed `Merge::LANGUAGE_OVERRIDE` and `Merge::DOMAIN_OVERRIDE` contants
### Fixed ### Fixed
- Improved code quality - PHP 7.4 support [#230]
- The library is easier to extend
- Translation id can be independent of the context + original values, in order to be more compatible with Xliff format.
[#244]: https://github.com/php-gettext/Gettext/issues/244 ## [4.7.0] - 2019-10-07
[#246]: https://github.com/php-gettext/Gettext/issues/246 ### Added
[#247]: https://github.com/php-gettext/Gettext/issues/247 - Support for UnitID in Xliff [#221] [#224] [#225]
[#251]: https://github.com/php-gettext/Gettext/issues/251 - Support for scan multiple domains at the same time [#223]
[#253]: https://github.com/php-gettext/Gettext/issues/253
[#257]: https://github.com/php-gettext/Gettext/issues/257
[#258]: https://github.com/php-gettext/Gettext/issues/258
[#260]: https://github.com/php-gettext/Gettext/issues/260
[#262]: https://github.com/php-gettext/Gettext/issues/262
[#263]: https://github.com/php-gettext/Gettext/issues/263
[#265]: https://github.com/php-gettext/Gettext/issues/265
[#276]: https://github.com/php-gettext/Gettext/issues/276
[#278]: https://github.com/php-gettext/Gettext/issues/278
[5.6.1]: https://github.com/php-gettext/Gettext/compare/v5.6.0...v5.6.1 ### Fixed
[5.6.0]: https://github.com/php-gettext/Gettext/compare/v5.5.4...v5.6.0 - New lines in windows [#218] [#226]
[5.5.4]: https://github.com/php-gettext/Gettext/compare/v5.5.3...v5.5.4
[5.5.3]: https://github.com/php-gettext/Gettext/compare/v5.5.2...v5.5.3 ## [4.6.3] - 2019-07-15
[5.5.2]: https://github.com/php-gettext/Gettext/compare/v5.5.1...v5.5.2 ### Added
[5.5.1]: https://github.com/php-gettext/Gettext/compare/v5.5.0...v5.5.1 - Some VueJs extraction improvements and additions [#205], [#213]
[5.5.0]: https://github.com/php-gettext/Gettext/compare/v5.4.1...v5.5.0
[5.4.1]: https://github.com/php-gettext/Gettext/compare/v5.4.0...v5.4.1 ### Fixed
[5.4.0]: https://github.com/php-gettext/Gettext/compare/v5.3.0...v5.4.0 - Multiline extractions in jsCode [#200]
[5.3.0]: https://github.com/php-gettext/Gettext/compare/v5.2.2...v5.3.0 - Support for js template literals [#214]
[5.2.2]: https://github.com/php-gettext/Gettext/compare/v5.2.1...v5.2.2 - Fixed tabs in PHP comments [#215]
[5.2.1]: https://github.com/php-gettext/Gettext/compare/v5.2.0...v5.2.1
[5.2.0]: https://github.com/php-gettext/Gettext/compare/v5.1.0...v5.2.0 ## [4.6.2] - 2019-01-12
[5.1.0]: https://github.com/php-gettext/Gettext/compare/v5.0.0...v5.1.0 ### 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
[#261]: https://github.com/oscarotero/Gettext/issues/261
[#266]: https://github.com/oscarotero/Gettext/issues/266
[#271]: https://github.com/oscarotero/Gettext/issues/271
[#274]: https://github.com/oscarotero/Gettext/issues/274
[#280]: https://github.com/oscarotero/Gettext/issues/280
[#284]: https://github.com/oscarotero/Gettext/issues/284
[#288]: https://github.com/oscarotero/Gettext/issues/288
[#289]: https://github.com/oscarotero/Gettext/issues/289
[4.8.11]: https://github.com/oscarotero/Gettext/compare/v4.8.10...v4.8.11
[4.8.10]: https://github.com/oscarotero/Gettext/compare/v4.8.9...v4.8.10
[4.8.9]: https://github.com/oscarotero/Gettext/compare/v4.8.8...v4.8.9
[4.8.8]: https://github.com/oscarotero/Gettext/compare/v4.8.7...v4.8.8
[4.8.7]: https://github.com/oscarotero/Gettext/compare/v4.8.6...v4.8.7
[4.8.6]: https://github.com/oscarotero/Gettext/compare/v4.8.5...v4.8.6
[4.8.5]: https://github.com/oscarotero/Gettext/compare/v4.8.4...v4.8.5
[4.8.4]: https://github.com/oscarotero/Gettext/compare/v4.8.3...v4.8.4
[4.8.3]: https://github.com/oscarotero/Gettext/compare/v4.8.2...v4.8.3
[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
[4.3.0]: https://github.com/oscarotero/Gettext/releases/tag/v4.3.0

View File

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2019 Oscar Otero Marzoa Copyright (c) 2017 Oscar Otero Marzoa
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,68 +1,134 @@
# Gettext Gettext
=======
[![Latest Version on Packagist][ico-version]][link-packagist] [![Build Status](https://travis-ci.org/oscarotero/Gettext.png?branch=master)](https://travis-ci.org/oscarotero/Gettext)
[![Software License][ico-license]](LICENSE) [![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)
![ico-ga] [![Latest Stable Version](https://poser.pugx.org/gettext/gettext/v/stable.svg)](https://packagist.org/packages/gettext/gettext)
[![Total Downloads][ico-downloads]][link-downloads] [![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)
> Note: this is the documentation of the new 5.x version. Go to [4.x branch](https://github.com/php-gettext/Gettext/tree/4.x) if you're looking for the old 4.x version [![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 <http://oscarotero.com> <oom@oscarotero.com> (MIT License) Created by Oscar Otero <http://oscarotero.com> <oom@oscarotero.com> (MIT License)
Gettext is a PHP (^7.2) library to import/export/edit gettext from PO, MO, PHP, JS files, etc. Gettext is a PHP (>=5.4) library to import/export/edit gettext from PO, MO, PHP, JS files, etc.
## Installation ## Installation
With composer (recomended):
``` ```
composer require gettext/gettext 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 ## Classes and functions
This package contains the following classes: This package contains the following classes:
* `Gettext\Translation` - A translation definition * `Gettext\Translation` - A translation definition
* `Gettext\Translations` - A collection of translations (under the same domain) * `Gettext\Translations` - A collection of translations
* `Gettext\Scanner\*` - Scan files to extract translations (php, js, twig templates, ...) * `Gettext\Extractors\*` - Import translations from various sources (po, mo, php, js, etc)
* `Gettext\Loader\*` - Load translations from different formats (po, mo, json, ...) * `Gettext\Generators\*` - Export translations to various formats (po, mo, php, json, etc)
* `Gettext\Generator\*` - Export translations to various formats (po, mo, json, ...) * `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 ## Usage example
```php ```php
use Gettext\Loader\PoLoader; use Gettext\Translations;
use Gettext\Generator\MoGenerator;
//import from a .po file: //import from a .po file:
$loader = new PoLoader(); $translations = Translations::fromPoFile('locales/gl.po');
$translations = $loader->loadFile('locales/gl.po');
//edit some translations: //edit some translations:
$translation = $translations->find(null, 'apple'); $translation = $translations->find(null, 'apple');
if ($translation) { if ($translation) {
$translation->translate('Mazá'); $translation->setTranslation('Mazá');
} }
//export to a .mo file: //export to a php array:
$generator = new MoGenerator(); $translations->toPhpArrayFile('locales/gl.php');
$generator->generateFile($translations, 'Locale/gl/LC_MESSAGES/messages.mo');
//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 ## Translation
The `Gettext\Translation` class stores all information about a translation: the original text, the translated text, source references, comments, etc. The `Gettext\Translation` class stores all information about a translation: the original text, the translated text, source references, comments, etc.
```php ```php
use Gettext\Translation; // __construct($context, $original, $plural)
$translation = new Gettext\Translation('comments', 'One comment', '%s comments');
$translation = Translation::create('comments', 'One comment', '%s comments'); $translation->setTranslation('Un comentario');
$translation->setPluralTranslation('%s comentarios');
$translation->translate('Un comentario'); $translation->addReference('templates/comments/comment.php', 34);
$translation->translatePlural('%s comentarios'); $translation->addComment('To display the amount of comments in a post');
$translation->getReferences()->add('templates/comments/comment.php', 34);
$translation->getComments()->add('To display the amount of comments in a post');
echo $translation->getContext(); // comments echo $translation->getContext(); // comments
echo $translation->getOriginal(); // One comment echo $translation->getOriginal(); // One comment
@ -76,134 +142,223 @@ echo $translation->getTranslation(); // Un comentario
The `Gettext\Translations` class stores a collection of translations: The `Gettext\Translations` class stores a collection of translations:
```php ```php
use Gettext\Translations; $translations = new Gettext\Translations();
$translations = Translations::create('my-domain'); //You can add new translations using the array syntax
$translations[] = new Gettext\Translation('comments', 'One comment', '%s comments');
//You can add new translations: //Or using the "insert" method
$translation = Translation::create('comments', 'One comment', '%s comments'); $insertedTranslation = $translations->insert('comments', 'One comment', '%s comments');
$translations->add($translation);
//Find a specific translation //Find a specific translation
$translation = $translations->find('comments', 'One comment'); $translation = $translations->find('comments', 'One comment');
//Edit headers, domain, etc //Edit headers, domain, etc
$translations->getHeaders()->set('Last-Translator', 'Oscar Otero'); $translations->setHeader('Last-Translator', 'Oscar Otero');
$translations->setDomain('my-blog'); $translations->setDomain('my-blog');
``` ```
## Loaders ## Extractors
The loaders allows to get gettext values from any format. For example, to load a .po file: The extrators allows to fetch gettext values from any source. For example, to scan a .po file:
```php ```php
use Gettext\Loader\PoLoader; $translations = new Gettext\Translations();
$loader = new PoLoader();
//From a file //From a file
$translations = $loader->loadFile('locales/en.po'); Gettext\Extractors\Po::fromFile('locales/en.po', $translations);
//From a string //From a string
$string = file_get_contents('locales2/en.po'); $string = file_get_contents('locales2/en.po');
$translations = $loader->loadString($string); Gettext\Extractors\Po::fromString($string, $translations);
``` ```
This package includes the following loaders: The better way to use extractors is using the magic methods of `Gettext\Translations`:
- `MoLoader` ```php
- `PoLoader` //Create a Translations instance using a po file
$translations = Gettext\Translations::fromPoFile('locales/en.po');
And you can install other formats with loaders and generators: //Add more messages from other files
$translations->addFromPoFile('locales2/en.po');
```
- [Json](https://github.com/php-gettext/Json) 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 ## Generators
The generators export a `Gettext\Translations` instance to any format (po, mo, etc). The generators export a `Gettext\Translations` instance to any format (po, mo, array, etc).
```php ```php
use Gettext\Loader\PoLoader; //Save to a file
use Gettext\Generator\MoGenerator; Gettext\Generators\Po::toFile($translations, 'locales/en.po');
//Load a PO file //Return as a string
$poLoader = new PoLoader(); $content = Gettext\Generators\Po::toString($translations);
file_put_contents('locales/en.po', $content);
$translations = $poLoader->loadFile('locales/en.po');
//Save to MO file
$moGenerator = new MoGenerator();
$moGenerator->generateFile($translations, 'locales/en.mo');
//Or return as a string
$content = $moGenerator->generateString($translations);
file_put_contents('locales/en.mo', $content);
``` ```
This package includes the following generators: Like extractors, the better way to use generators is using the magic methods of `Gettext\Translations`:
- `MoGenerator` ```php
- `PoGenerator` //Extract messages from a php code file
$translations = Gettext\Translations::fromPhpCodeFile('templates/index.php');
And you can install other formats with loaders and generators:
//Export to a po file
- [Json](https://github.com/php-gettext/Json) $translations->toPoFile('locales/en.po');
//Export to a po string
## Scanners $content = $translations->toPoString();
file_put_contents('locales/en.po', $content);
Scanners allow to search and extract new gettext entries from different sources like php files, twig templates, blade templates, etc. Unlike loaders, scanners allows to extract gettext entries with different domains at the same time: ```
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
<!-- index.php -->
<html>
<body>
<?= __('Hello world'); ?>
</body>
</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 ```php
use Gettext\Scanner\PhpScanner;
use Gettext\Translations; use Gettext\Translations;
//Create a new scanner, adding a translation for each domain we want to get: $translations = Translations::fromPoFile('my-file1.po');
$phpScanner = new PhpScanner( $translations->addFromPoFile('my-file2.po');
Translations::create('domain1'),
Translations::create('domain2'),
Translations::create('domain3')
);
//Set a default domain, so any translations with no domain specified, will be added to that domain
$phpScanner->setDefaultDomain('domain1');
//Extract all comments starting with 'i18n:' and 'Translators:'
$phpScanner->extractCommentsStartingWith('i18n:', 'Translators:');
//Scan files
foreach (glob('*.php') as $file) {
$phpScanner->scanFile($file);
}
//Get the translations
list('domain1' => $domain1, 'domain2' => $domain2, 'domain3' => $domain3) = $phpScanner->getTranslations();
``` ```
This package does not include any scanner by default. But there are some that you can install: A more advanced way is merge two `Translations` instances:
- [PHP Scanner](https://github.com/php-gettext/PHP-Scanner)
- [JS Scanner](https://github.com/php-gettext/JS-Scanner)
## Merging translations
You will want to update or merge translations. The function `mergeWith` create a new `Translations` instance with other translations merged:
```php ```php
$translations3 = $translations1->mergeWith($translations2); 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
``` ```
But sometimes this is not enough, and this is why we have merging options, allowing to configure how two translations will be merged. These options are defined as constants in the `Gettext\Merge` class, and are the following: The second argument of `mergeWith` defines how the merge will be done. Use the `Gettext\Merge` constants to configure the merging:
Constant | Description Constant | Description
--------- | ----------- --------- | -----------
`Merge::TRANSLATIONS_OURS` | Use only the translations present in `$translations1` `Merge::ADD` | Adds the translations from `$translations2` that are missing
`Merge::TRANSLATIONS_THEIRS` | Use only the translations present in `$translations2` `Merge::REMOVE` | Removes the translations missing in `$translations2`
`Merge::TRANSLATION_OVERRIDE` | Override the translation and plural translations with the value of `$translation2` `Merge::HEADERS_ADD` | Adds the headers from `$translations2` that are missing
`Merge::HEADERS_OURS` | Use only the headers of `$translations1` `Merge::HEADERS_REMOVE` | Removes the headers missing in `$translations2`
`Merge::HEADERS_REMOVE` | Use only the headers of `$translations2`
`Merge::HEADERS_OVERRIDE` | Overrides the headers with the values of `$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_OURS` | Use only the comments of `$translation1`
`Merge::COMMENTS_THEIRS` | Use only the comments of `$translation2` `Merge::COMMENTS_THEIRS` | Use only the comments of `$translation2`
`Merge::EXTRACTED_COMMENTS_OURS` | Use only the extracted comments of `$translation1` `Merge::EXTRACTED_COMMENTS_OURS` | Use only the extracted comments of `$translation1`
@ -213,48 +368,58 @@ Constant | Description
`Merge::REFERENCES_OURS` | Use only the references of `$translation1` `Merge::REFERENCES_OURS` | Use only the references of `$translation1`
`Merge::REFERENCES_THEIRS` | Use only the references of `$translation2` `Merge::REFERENCES_THEIRS` | Use only the references of `$translation2`
Use the second argument to configure the merging strategy: Example:
```php ```php
$strategy = Merge::TRANSLATIONS_OURS | Merge::HEADERS_OURS; use Gettext\Translations;
use Gettext\Merge;
$translations3 = $translations1->mergeWith($translations2, $strategy); //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');
``` ```
There are some typical scenarios, one of the most common: Note, if the second argument is not defined, the default value is `Merge::DEFAULTS` that's equivalent to `Merge::ADD | Merge::HEADERS_ADD`.
- Scan php templates searching for entries to translate ## Use from CLI
- Complete these entries with the translations stored in a .po file
- You may want to add new entries to the .po file
- And also remove those entries present in the .po file but not in the templates (because they were removed)
- But you want to update some translations with new references and extracted comments
- And keep the translations, comments and flags defined in .po file
For this scenario, you can use the option `Merge::SCAN_AND_LOAD` with the combination of options to fit this needs (SCAN new entries and LOAD a .po file). There's a Robo task to use this library from the command line interface: https://github.com/oscarotero/GettextRobo
```php ## Use in the browser
$newEntries = $scanner->scanFile('template.php');
$previousEntries = $loader->loadFile('translations.po');
$updatedEntries = $newEntries->mergeWith($previousEntries); If you want to use your translations in the browser, there's a javascript translator: https://github.com/oscarotero/gettext-translator
```
More common scenarios may be added in a future. ## 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 ## Contributors
Thanks to all [contributors](https://github.com/oscarotero/Gettext/graphs/contributors) specially to [@mlocati](https://github.com/mlocati). Thanks to all [contributors](https://github.com/oscarotero/Gettext/graphs/contributors) specially to [@mlocati](https://github.com/mlocati).
--- ## Donations
Please see [CHANGELOG](CHANGELOG.md) for more information about recent changes and [CONTRIBUTING](CONTRIBUTING.md) for contributing details. If this library is useful for you, consider to donate to the author.
The MIT License (MIT). Please see [LICENSE](LICENSE) for more information. [Buy me a beer :beer:](https://www.paypal.me/oscarotero)
[ico-version]: https://img.shields.io/packagist/v/gettext/gettext.svg?style=flat-square Thanks in advance!
[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square
[ico-ga]: https://github.com/php-gettext/Gettext/workflows/testing/badge.svg
[ico-downloads]: https://img.shields.io/packagist/dt/gettext/gettext.svg?style=flat-square
[link-packagist]: https://packagist.org/packages/gettext/gettext
[link-downloads]: https://packagist.org/packages/gettext/gettext

View File

@ -3,7 +3,7 @@
"type": "library", "type": "library",
"description": "PHP gettext manager", "description": "PHP gettext manager",
"keywords": ["js", "gettext", "i18n", "translation", "po", "mo"], "keywords": ["js", "gettext", "i18n", "translation", "po", "mo"],
"homepage": "https://github.com/php-gettext/Gettext", "homepage": "https://github.com/oscarotero/Gettext",
"license": "MIT", "license": "MIT",
"authors": [ "authors": [
{ {
@ -15,18 +15,25 @@
], ],
"support": { "support": {
"email": "oom@oscarotero.com", "email": "oom@oscarotero.com",
"issues": "https://github.com/php-gettext/Gettext/issues" "issues": "https://github.com/oscarotero/Gettext/issues"
}, },
"require": { "require": {
"php": "^7.2|^8.0", "php": ">=5.4.0",
"gettext/languages": "^2.3" "gettext/languages": "^2.3"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^8.0|^9.0", "illuminate/view": "^5.0.x-dev",
"squizlabs/php_codesniffer": "^3.0", "twig/twig": "^1.31|^2.0",
"brick/varexporter": "^0.3.5", "twig/extensions": "*",
"friendsofphp/php-cs-fixer": "^3.2", "symfony/yaml": "~2",
"oscarotero/php-cs-fixer-config": "^2.0" "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": { "autoload": {
"psr-4": { "psr-4": {
@ -42,7 +49,11 @@
"test": [ "test": [
"phpunit", "phpunit",
"phpcs" "phpcs"
], ]
"cs-fix": "php-cs-fixer fix" },
"config": {
"allow-plugins": {
"kylekatarnls/update-helper": false
}
} }
} }

View File

@ -0,0 +1,39 @@
<?php
namespace Gettext;
abstract class BaseTranslator implements TranslatorInterface
{
/** @var TranslatorInterface */
public static $current;
/**
* @see TranslatorInterface
*/
public function noop($original)
{
return $original;
}
/**
* @see TranslatorInterface
*/
public function register()
{
$previous = static::$current;
static::$current = $this;
static::includeFunctions();
return $previous;
}
/**
* Include the gettext functions
*/
public static function includeFunctions()
{
include_once __DIR__.'/translator_functions.php';
}
}

View File

@ -1,89 +0,0 @@
<?php
declare(strict_types = 1);
namespace Gettext;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use JsonSerializable;
use ReturnTypeWillChange;
/**
* Class to manage the comments of a translation.
*/
class Comments implements JsonSerializable, Countable, IteratorAggregate
{
protected $comments = [];
public static function __set_state(array $state): Comments
{
return new static(...$state['comments']);
}
public function __construct(string ...$comments)
{
if (!empty($comments)) {
$this->add(...$comments);
}
}
public function __debugInfo()
{
return $this->toArray();
}
public function add(string ...$comments): self
{
foreach ($comments as $comment) {
if (!in_array($comment, $this->comments)) {
$this->comments[] = $comment;
}
}
return $this;
}
public function delete(string ...$comments): self
{
foreach ($comments as $comment) {
$key = array_search($comment, $this->comments);
if (is_int($key)) {
array_splice($this->comments, $key, 1);
}
}
return $this;
}
#[ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->toArray();
}
#[ReturnTypeWillChange]
public function getIterator()
{
return new ArrayIterator($this->comments);
}
public function count(): int
{
return count($this->comments);
}
public function toArray(): array
{
return $this->comments;
}
public function mergeWith(Comments $comments): Comments
{
$merged = clone $this;
$merged->add(...$comments->comments);
return $merged;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Gettext\Extractors;
use Gettext\Translations;
use Illuminate\Filesystem\Filesystem;
use Illuminate\View\Compilers\BladeCompiler;
/**
* Class to get gettext strings from blade.php files returning arrays.
*/
class Blade extends Extractor implements ExtractorInterface
{
/**
* {@inheritdoc}
*/
public static function fromString($string, Translations $translations, array $options = [])
{
if (empty($options['facade'])) {
$cachePath = empty($options['cachePath']) ? sys_get_temp_dir() : $options['cachePath'];
$bladeCompiler = new BladeCompiler(new Filesystem(), $cachePath);
if (method_exists($bladeCompiler, 'withoutComponentTags')) {
$bladeCompiler->withoutComponentTags();
}
$string = $bladeCompiler->compileString($string);
} else {
$string = $options['facade']::compileString($string);
}
PhpCode::fromString($string, $translations, $options);
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace Gettext\Extractors;
use Gettext\Translations;
use Gettext\Utils\HeadersExtractorTrait;
use Gettext\Utils\CsvTrait;
/**
* Class to get gettext strings from csv.
*/
class Csv extends Extractor implements ExtractorInterface
{
use HeadersExtractorTrait;
use CsvTrait;
public static $options = [
'delimiter' => ",",
'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);
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace Gettext\Extractors;
use Gettext\Translations;
use Gettext\Utils\HeadersExtractorTrait;
use Gettext\Utils\CsvTrait;
/**
* Class to get gettext strings from csv.
*/
class CsvDictionary extends Extractor implements ExtractorInterface
{
use HeadersExtractorTrait;
use CsvTrait;
public static $options = [
'delimiter' => ",",
'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);
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace Gettext\Extractors;
use Exception;
use InvalidArgumentException;
use Gettext\Translations;
abstract class Extractor implements ExtractorInterface
{
/**
* {@inheritdoc}
*/
public static function fromFile($file, Translations $translations, array $options = [])
{
foreach (static::getFiles($file) as $file) {
$options['file'] = $file;
static::fromString(static::readFile($file), $translations, $options);
}
}
/**
* Checks and returns all files.
*
* @param string|array $file The file/s
*
* @return array The file paths
*/
protected static function getFiles($file)
{
if (empty($file)) {
throw new InvalidArgumentException('There is not any file defined');
}
if (is_string($file)) {
if (!is_file($file)) {
throw new InvalidArgumentException("'$file' is not a valid file");
}
if (!is_readable($file)) {
throw new InvalidArgumentException("'$file' is not a readable file");
}
return [$file];
}
if (is_array($file)) {
$files = [];
foreach ($file as $f) {
$files = array_merge($files, static::getFiles($f));
}
return $files;
}
throw new InvalidArgumentException('The first argument must be string or array');
}
/**
* Reads and returns the content of a file.
*
* @param string $file
*
* @return string
*/
protected static function readFile($file)
{
$length = filesize($file);
if (!($fd = fopen($file, 'rb'))) {
throw new Exception("Cannot read the file '$file', probably permissions");
}
$content = $length ? fread($fd, $length) : '';
fclose($fd);
return $content;
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Gettext\Extractors;
use Gettext\Translations;
interface ExtractorInterface
{
/**
* Extract the translations from a file.
*
* @param array|string $file A path of a file or files
* @param Translations $translations The translations instance to append the new translations.
* @param array $options
*/
public static function fromFile($file, Translations $translations, array $options = []);
/**
* Parses a string and append the translations found in the Translations instance.
*
* @param string $string
* @param Translations $translations
* @param array $options
*/
public static function fromString($string, Translations $translations, array $options = []);
}

View File

@ -0,0 +1,28 @@
<?php
namespace Gettext\Extractors;
use Gettext\Translations;
interface ExtractorMultiInterface
{
/**
* Parses a string and append the translations found in the Translations instance.
* Allows scanning for multiple domains at a time (each Translation has to have a different domain)
*
* @param string $string
* @param Translations[] $translations
* @param array $options
*/
public static function fromStringMultiple($string, array $translations, array $options = []);
/**
* Parses a string and append the translations found in the Translations instance.
* Allows scanning for multiple domains at a time (each Translation has to have a different domain)
*
* @param $file
* @param Translations[] $translations
* @param array $options
*/
public static function fromFileMultiple($file, array $translations, array $options = []);
}

View File

@ -0,0 +1,55 @@
<?php
namespace Gettext\Extractors;
use Gettext\Translations;
/**
* Class to get gettext strings from json files.
*/
class Jed extends Extractor implements ExtractorInterface
{
/**
* {@inheritdoc}
*/
public static function fromString($string, Translations $translations, array $options = [])
{
static::extract(json_decode($string, true), $translations);
}
/**
* Handle an array of translations and append to the Translations instance.
*
* @param array $content
* @param Translations $translations
*/
public static function extract(array $content, Translations $translations)
{
$messages = current($content);
$headers = isset($messages['']) ? $messages[''] : null;
unset($messages['']);
if (!empty($headers['domain'])) {
$translations->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);
}
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace Gettext\Extractors;
use Exception;
use Gettext\Translations;
use Gettext\Utils\FunctionsScanner;
/**
* Class to get gettext strings from javascript files.
*/
class JsCode extends Extractor implements ExtractorInterface, ExtractorMultiInterface
{
public static $options = [
'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\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);
}
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Gettext\Extractors;
use Gettext\Translations;
use Gettext\Utils\MultidimensionalArrayTrait;
/**
* Class to get gettext strings from json.
*/
class Json extends Extractor implements ExtractorInterface
{
use MultidimensionalArrayTrait;
/**
* {@inheritdoc}
*/
public static function fromString($string, Translations $translations, array $options = [])
{
$messages = json_decode($string, true);
if (is_array($messages)) {
static::fromArray($messages, $translations);
}
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Gettext\Extractors;
use Gettext\Translations;
use Gettext\Utils\DictionaryTrait;
/**
* Class to get gettext strings from plain json.
*/
class JsonDictionary extends Extractor implements ExtractorInterface
{
use DictionaryTrait;
/**
* {@inheritdoc}
*/
public static function fromString($string, Translations $translations, array $options = [])
{
$messages = json_decode($string, true);
if (is_array($messages)) {
static::fromArray($messages, $translations);
}
}
}

View File

@ -0,0 +1,131 @@
<?php
namespace Gettext\Extractors;
use Exception;
use Gettext\Translations;
use Gettext\Utils\StringReader;
/**
* Class to get gettext strings from .mo files.
*/
class Mo extends Extractor implements ExtractorInterface
{
const MAGIC1 = -1794895138;
const MAGIC2 = -569244523;
const MAGIC3 = 2500072158;
protected static $stringReaderClass = 'Gettext\Utils\StringReader';
/**
* {@inheritdoc}
*/
public static function fromString($string, Translations $translations, array $options = [])
{
/** @var StringReader $stream */
$stream = new static::$stringReaderClass($string);
$magic = static::readInt($stream, 'V');
if (($magic === static::MAGIC1) || ($magic === static::MAGIC3)) { //to make sure it works for 64-bit platforms
$byteOrder = 'V'; //low endian
} elseif ($magic === (static::MAGIC2 & 0xFFFFFFFF)) {
$byteOrder = 'N'; //big endian
} else {
throw new Exception('Not MO file');
}
static::readInt($stream, $byteOrder);
$total = static::readInt($stream, $byteOrder); //total string count
$originals = static::readInt($stream, $byteOrder); //offset of original table
$tran = static::readInt($stream, $byteOrder); //offset of translation table
$stream->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));
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Gettext\Extractors;
use BadMethodCallException;
use Gettext\Translations;
use Gettext\Utils\MultidimensionalArrayTrait;
/**
* Class to get gettext strings from php files returning arrays.
*/
class PhpArray extends Extractor implements ExtractorInterface
{
use MultidimensionalArrayTrait;
/**
* {@inheritdoc}
*/
public static function fromFile($file, Translations $translations, array $options = [])
{
foreach (static::getFiles($file) as $file) {
static::fromArray(include($file), $translations);
}
}
/**
* {@inheritdoc}
*/
public static function fromString($string, Translations $translations, array $options = [])
{
throw new BadMethodCallException('PhpArray::fromString() cannot be called. Use PhpArray::fromFile()');
}
}

View File

@ -0,0 +1,170 @@
<?php
namespace Gettext\Extractors;
use Exception;
use Gettext\Translations;
use Gettext\Utils\FunctionsScanner;
/**
* Class to get gettext strings from php files returning arrays.
*/
class PhpCode extends Extractor implements ExtractorInterface, ExtractorMultiInterface
{
public static $options = [
// - false: to not extract comments
// - empty string: to extract all comments
// - non-empty string: to extract comments that start with that string
// - array with strings to extract comments format.
'extractComments' => 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;
}
}

View File

@ -0,0 +1,215 @@
<?php
namespace Gettext\Extractors;
use Gettext\Translations;
use Gettext\Translation;
use Gettext\Utils\HeadersExtractorTrait;
/**
* Class to get gettext strings from php files returning arrays.
*/
class Po extends Extractor implements ExtractorInterface
{
use HeadersExtractorTrait;
/**
* Parses a .po file and append the translations found in the Translations instance.
*
* {@inheritdoc}
*/
public static function fromString($string, Translations $translations, array $options = [])
{
$lines = explode("\n", $string);
$i = 0;
$translation = $translations->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])) {
$nextLine = trim($lines[$j + 1]);
if (substr($nextLine, 0, 1) == '"') {
$line = substr($line, 0, -1).substr($nextLine, 1);
continue;
}
if (substr($nextLine, 0, 4) == '#~ "') {
$line = substr($line, 0, -1).substr($nextLine, 4);
continue;
}
}
$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",
'\\"' => '"',
]
);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace Gettext\Extractors;
use Gettext\Translations;
use Twig_Loader_Array;
use Twig_Environment;
use Twig_Source;
use Twig_Extensions_Extension_I18n;
/**
* Class to get gettext strings from twig files returning arrays.
*/
class Twig extends Extractor implements ExtractorInterface
{
public static $options = [
'extractComments' => '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;
}
}

View File

@ -0,0 +1,423 @@
<?php
/** @noinspection PhpComposerExtensionStubsInspection */
namespace Gettext\Extractors;
use DOMAttr;
use DOMDocument;
use DOMElement;
use DOMNode;
use Exception;
use Gettext\Translations;
use Gettext\Utils\FunctionsScanner;
/**
* Class to get gettext strings from VueJS template files.
*/
class VueJs extends Extractor implements ExtractorInterface, ExtractorMultiInterface
{
public static $options = [
'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\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 <template> and next element, they are lost
// So we insert a "." which is a text node, and it will prevent that newlines are stripped between elements.
// Same thing happens between template and script tag.
$string = str_replace('<template>', '<template>.', $string);
$string = str_replace('</template>', '</template>.', $string);
// Normalize newlines
$string = str_replace(["\r\n", "\n\r", "\r"], "\n", $string);
// VueJS files are valid HTML files, we will operate with the DOM here
$dom = static::convertHtmlToDom($string);
$script = static::extractScriptTag($string);
// Parse the script part as a regular JS code
if ($script) {
$scriptLineNumber = $dom->getElementsByTagName('script')->item(0)->getLineNo();
static::getScriptTranslationsFromString(
$script,
$translations,
$options,
$scriptLineNumber - 1
);
}
// Template part is parsed separately, all variables will be extracted
// and handled as a regular JS code
$template = $dom->getElementsByTagName('template')->item(0);
if ($template) {
static::getTemplateTranslations(
$template,
$translations,
$options,
$template->getLineNo() - 1
);
}
}
/**
* Extracts script tag contents using regex instead of DOM operations.
* If we parse using DOM, some contents may change, for example, tags within strings will be stripped
*
* @param $string
* @return bool|string
*/
protected static function extractScriptTag($string)
{
if (preg_match('#<\s*?script\b[^>]*>(.*?)</script\b[^>]*>#s', $string, $matches)) {
return $matches[1];
}
return '';
}
/**
* @param string $html
* @return DOMDocument
*/
protected static function convertHtmlToDom($html)
{
$dom = new DOMDocument;
libxml_use_internal_errors(true);
// Prepend xml encoding so DOMDocument document handles UTF8 correctly.
// Assuming that vue template files will not have any xml encoding tags, because duplicate tags may be ignored.
$dom->loadHTML('<?xml encoding="utf-8"?>' . $html);
libxml_clear_errors();
return $dom;
}
/**
* Extract translations from script part
*
* @param string $scriptContents Only script tag contents, not the whole template
* @param Translations|Translations[] $translations One or multiple domain Translation objects
* @param array $options
* @param int $lineOffset Number of lines the script is offset in the vue template file
* @throws Exception
*/
protected static function getScriptTranslationsFromString(
$scriptContents,
$translations,
array $options = [],
$lineOffset = 0
) {
/** @var FunctionsScanner $functions */
$functions = new static::$functionsScannerClass($scriptContents);
$options['lineOffset'] = $lineOffset;
$functions->saveGettextFunctions($translations, $options);
}
/**
* Parse template to extract all translations (element content and dynamic element attributes)
*
* @param DOMNode $dom
* @param Translations|Translations[] $translations One or multiple domain Translation objects
* @param array $options
* @param int $lineOffset Line number where the template part starts in the vue file
* @throws Exception
*/
protected static function getTemplateTranslations(
DOMNode $dom,
$translations,
array $options,
$lineOffset = 0
) {
// Build a JS string from all template attribute expressions
$fakeAttributeJs = static::getTemplateAttributeFakeJs($options, $dom);
// 1 line offset is necessary because parent template element was ignored when converting to DOM
static::getScriptTranslationsFromString($fakeAttributeJs, $translations, $options, $lineOffset);
// Build a JS string from template element content expressions
$fakeTemplateJs = static::getTemplateFakeJs($dom);
static::getScriptTranslationsFromString($fakeTemplateJs, $translations, $options, $lineOffset);
static::getTagTranslations($options, $dom, $translations);
}
/**
* @param array $options
* @param DOMNode $dom
* @param Translations|Translations[] $translations
*/
protected static function getTagTranslations(array $options, DOMNode $dom, $translations)
{
// Since tag scanning does not support domains, we always use the first translation given
$translations = is_array($translations) ? reset($translations) : $translations;
$children = $dom->childNodes;
for ($i = 0; $i < $children->length; $i++) {
$node = $children->item($i);
if (!($node instanceof DOMElement)) {
continue;
}
$translatable = false;
if (in_array($node->tagName, $options['tagNames'], true)) {
$translatable = true;
}
$attrList = $node->attributes;
$context = null;
$plural = "";
$comment = null;
for ($j = 0; $j < $attrList->length; $j++) {
/** @var DOMAttr $domAttr */
$domAttr = $attrList->item($j);
// Check if this is a dynamic vue attribute
if (in_array($domAttr->name, $options['tagAttributes'])) {
$translatable = true;
}
if (in_array($domAttr->name, $options['contextAttributes'])) {
$context = $domAttr->value;
}
if (in_array($domAttr->name, $options['pluralAttributes'])) {
$plural = $domAttr->value;
}
if (in_array($domAttr->name, $options['commentAttributes'])) {
$comment = $domAttr->value;
}
}
if ($translatable) {
$translation = $translations->insert($context, trim($node->textContent), $plural);
$translation->addReference($options['file'], $node->getLineNo());
if ($comment) {
$translation->addExtractedComment($comment);
}
}
if ($node->hasChildNodes()) {
static::getTagTranslations($options, $node, $translations);
}
}
}
/**
* Extract JS expressions from element attribute bindings (excluding text within elements)
* For example: <span :title="__('extract this')"> skip element content </span>
*
* @param array $options
* @param DOMNode $dom
* @return string JS code
*/
protected static function getTemplateAttributeFakeJs(array $options, DOMNode $dom)
{
$expressionsByLine = static::getVueAttributeExpressions($options['attributePrefixes'], $dom);
if (empty($expressionsByLine)) {
return '';
}
$maxLines = max(array_keys($expressionsByLine));
$fakeJs = '';
for ($line = 1; $line <= $maxLines; $line++) {
if (isset($expressionsByLine[$line])) {
$fakeJs .= implode("; ", $expressionsByLine[$line]);
}
$fakeJs .= "\n";
}
return $fakeJs;
}
/**
* Loop DOM element recursively and parse out all dynamic vue attributes which are basically JS expressions
*
* @param array $attributePrefixes List of attribute prefixes we parse as JS (may contain translations)
* @param DOMNode $dom
* @param array $expressionByLine [lineNumber => [jsExpression, ..], ..]
* @return array [lineNumber => [jsExpression, ..], ..]
*/
protected static function getVueAttributeExpressions(
array $attributePrefixes,
DOMNode $dom,
array &$expressionByLine = []
) {
$children = $dom->childNodes;
for ($i = 0; $i < $children->length; $i++) {
$node = $children->item($i);
if (!($node instanceof DOMElement)) {
continue;
}
$attrList = $node->attributes;
for ($j = 0; $j < $attrList->length; $j++) {
/** @var DOMAttr $domAttr */
$domAttr = $attrList->item($j);
// Check if this is a dynamic vue attribute
if (static::isAttributeMatching($domAttr->name, $attributePrefixes)) {
$line = $domAttr->getLineNo();
$expressionByLine += [$line => []];
$expressionByLine[$line][] = $domAttr->value;
}
}
if ($node->hasChildNodes()) {
$expressionByLine = static::getVueAttributeExpressions($attributePrefixes, $node, $expressionByLine);
}
}
return $expressionByLine;
}
/**
* Check if this attribute name should be parsed for translations
*
* @param string $attributeName
* @param string[] $attributePrefixes
* @return bool
*/
protected static function isAttributeMatching($attributeName, $attributePrefixes)
{
foreach ($attributePrefixes as $prefix) {
if (strpos($attributeName, $prefix) === 0) {
return true;
}
}
return false;
}
/**
* Extract JS expressions from within template elements (excluding attributes)
* For example: <span :title="skip attributes"> {{__("extract element content")}} </span>
*
* @param DOMNode $dom
* @return string JS code
*/
protected static function getTemplateFakeJs(DOMNode $dom)
{
$fakeJs = '';
$lines = explode("\n", $dom->textContent);
// Build a fake JS file from template by extracting JS expressions within each template line
foreach ($lines as $line) {
$expressionMatched = static::parseOneTemplateLine($line);
$fakeJs .= implode("; ", $expressionMatched) . "\n";
}
return $fakeJs;
}
/**
* Match JS expressions in a template line
*
* @param string $line
* @return string[]
*/
protected static function parseOneTemplateLine($line)
{
$line = trim($line);
if (!$line) {
return [];
}
$regex = '#\{\{(.*?)\}\}#';
preg_match_all($regex, $line, $matches);
$matched = array_map(function ($v) {
return trim($v, '\'"{}');
}, $matches[1]);
return $matched;
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace Gettext\Extractors;
use Gettext\Translations;
use Gettext\Translation;
use SimpleXMLElement;
/**
* Class to get gettext strings from xliff format.
*/
class Xliff extends Extractor implements ExtractorInterface
{
public static $options = [
'unitid_as_id' => false
];
/**
* {@inheritdoc}
*/
public static function fromString($string, Translations $translations, array $options = [])
{
$options += static::$options;
$xml = new SimpleXMLElement($string, null, false);
foreach ($xml->file as $file) {
if (isset($file->notes)) {
foreach ($file->notes->note as $note) {
$translations->setHeader($note['id'], (string) $note);
}
}
foreach ($file->unit as $unit) {
foreach ($unit->segment as $segment) {
$targets = [];
foreach ($segment->target as $target) {
$targets[] = (string) $target;
}
$translation = $translations->createNewTranslation(null, (string) $segment->source);
if (isset($unit['id'])) {
$unitId = (string) $unit['id'];
$translation->addComment("XLIFF_UNIT_ID: $unitId");
if ($options['unitid_as_id']) {
$translation->setId($unitId);
}
}
$translation->setTranslation(array_shift($targets));
$translation->setPluralTranslations($targets);
if (isset($unit->notes)) {
foreach ($unit->notes->note as $note) {
switch ($note['category']) {
case 'context':
$translation = $translation->getClone((string) $note);
break;
case 'extracted-comment':
$translation->addExtractedComment((string) $note);
break;
case 'flag':
$translation->addFlag((string) $note);
break;
case 'reference':
$ref = explode(':', (string) $note, 2);
$translation->addReference($ref[0], isset($ref[1]) ? $ref[1] : null);
break;
default:
$translation->addComment((string) $note);
break;
}
}
}
$translations[] = $translation;
}
}
}
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Gettext\Extractors;
use Gettext\Translations;
use Gettext\Utils\MultidimensionalArrayTrait;
use Symfony\Component\Yaml\Yaml as YamlParser;
/**
* Class to get gettext strings from yaml.
*/
class Yaml extends Extractor implements ExtractorInterface
{
use MultidimensionalArrayTrait;
/**
* {@inheritdoc}
*/
public static function fromString($string, Translations $translations, array $options = [])
{
$messages = YamlParser::parse($string);
if (is_array($messages)) {
static::fromArray($messages, $translations);
}
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Gettext\Extractors;
use Gettext\Translations;
use Gettext\Utils\DictionaryTrait;
use Symfony\Component\Yaml\Yaml as YamlParser;
/**
* Class to get gettext strings from yaml.
*/
class YamlDictionary extends Extractor implements ExtractorInterface
{
use DictionaryTrait;
/**
* {@inheritdoc}
*/
public static function fromString($string, Translations $translations, array $options = [])
{
$messages = YamlParser::parse($string);
if (is_array($messages)) {
static::fromArray($messages, $translations);
}
}
}

View File

@ -1,96 +0,0 @@
<?php
declare(strict_types = 1);
namespace Gettext;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use JsonSerializable;
use ReturnTypeWillChange;
/**
* Class to manage the flags of a translation.
*/
class Flags implements JsonSerializable, Countable, IteratorAggregate
{
protected $flags = [];
public static function __set_state(array $state): Flags
{
return new static(...$state['flags']);
}
public function __construct(string ...$flags)
{
if (!empty($flags)) {
$this->add(...$flags);
}
}
public function __debugInfo()
{
return $this->toArray();
}
public function add(string ...$flags): self
{
foreach ($flags as $flag) {
if (!$this->has($flag)) {
$this->flags[] = $flag;
}
}
sort($this->flags);
return $this;
}
public function delete(string ...$flags): self
{
foreach ($flags as $flag) {
$key = array_search($flag, $this->flags);
if (is_int($key)) {
array_splice($this->flags, $key, 1);
}
}
return $this;
}
public function has(string $flag): bool
{
return in_array($flag, $this->flags, true);
}
#[ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->toArray();
}
#[ReturnTypeWillChange]
public function getIterator()
{
return new ArrayIterator($this->flags);
}
public function count(): int
{
return count($this->flags);
}
public function toArray(): array
{
return $this->flags;
}
public function mergeWith(Flags $flags): Flags
{
$merged = clone $this;
$merged->add(...$flags->flags);
return $merged;
}
}

View File

@ -1,18 +0,0 @@
<?php
declare(strict_types = 1);
namespace Gettext\Generator;
use Gettext\Translations;
abstract class Generator implements GeneratorInterface
{
public function generateFile(Translations $translations, string $filename): bool
{
$content = $this->generateString($translations);
return file_put_contents($filename, $content) !== false;
}
abstract public function generateString(Translations $translations): string;
}

View File

@ -1,13 +0,0 @@
<?php
declare(strict_types = 1);
namespace Gettext\Generator;
use Gettext\Translations;
interface GeneratorInterface
{
public function generateFile(Translations $translations, string $filename): bool;
public function generateString(Translations $translations): string;
}

View File

@ -1,134 +0,0 @@
<?php
declare(strict_types = 1);
namespace Gettext\Generator;
use Gettext\Translations;
final class PoGenerator extends Generator
{
public function generateString(Translations $translations): string
{
$pluralForm = $translations->getHeaders()->getPluralForm();
$pluralSize = is_array($pluralForm) ? ($pluralForm[0] - 1) : null;
$lines = [];
//Description and flags
if ($translations->getDescription()) {
$description = explode("\n", $translations->getDescription());
foreach ($description as $line) {
$lines[] = sprintf('# %s', $line);
}
$lines[] = '#';
}
if (count($translations->getFlags())) {
$lines[] = sprintf('#, %s', implode(',', $translations->getFlags()->toArray()));
}
//Headers
$lines[] = 'msgid ""';
$lines[] = 'msgstr ""';
foreach ($translations->getHeaders() as $name => $value) {
$lines[] = sprintf('"%s: %s\\n"', $name, $value);
}
$lines[] = '';
//Translations
foreach ($translations as $translation) {
foreach ($translation->getComments() as $comment) {
$lines[] = sprintf('# %s', $comment);
}
foreach ($translation->getExtractedComments() as $comment) {
$lines[] = sprintf('#. %s', $comment);
}
foreach ($translation->getReferences() as $filename => $lineNumbers) {
if (empty($lineNumbers)) {
$lines[] = sprintf('#: %s', $filename);
continue;
}
foreach ($lineNumbers as $number) {
$lines[] = sprintf('#: %s:%d', $filename, $number);
}
}
if (count($translation->getFlags())) {
$lines[] = sprintf('#, %s', implode(',', $translation->getFlags()->toArray()));
}
$prefix = $translation->isDisabled() ? '#~ ' : '';
if ($context = $translation->getContext()) {
$lines[] = sprintf('%smsgctxt %s', $prefix, self::encode($context));
}
self::appendLines($lines, $prefix, 'msgid', $translation->getOriginal());
if ($plural = $translation->getPlural()) {
self::appendLines($lines, $prefix, 'msgid_plural', $plural);
self::appendLines($lines, $prefix, 'msgstr[0]', $translation->getTranslation() ?: '');
foreach ($translation->getPluralTranslations($pluralSize) as $k => $v) {
self::appendLines($lines, $prefix, sprintf('msgstr[%d]', $k + 1), $v);
}
} else {
self::appendLines($lines, $prefix, 'msgstr', $translation->getTranslation() ?: '');
}
$lines[] = '';
}
return implode("\n", $lines);
}
/**
* Add one or more lines depending whether the string is multiline or not.
*/
private static function appendLines(array &$lines, string $prefix, string $name, string $value): void
{
$newLines = explode("\n", $value);
$total = count($newLines);
if ($total === 1) {
$lines[] = sprintf('%s%s %s', $prefix, $name, self::encode($newLines[0]));
return;
}
$lines[] = sprintf('%s%s ""', $prefix, $name);
$last = $total - 1;
foreach ($newLines as $k => $line) {
if ($k < $last) {
$line .= "\n";
}
$lines[] = self::encode($line);
}
}
/**
* Convert a string to its PO representation.
*/
public static function encode(string $value): string
{
return '"'.strtr(
$value,
[
"\x00" => '',
'\\' => '\\\\',
"\t" => '\t',
"\r" => '\r',
"\n" => '\n',
'"' => '\\"',
]
).'"';
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace Gettext\Generators;
use Gettext\Translations;
use Gettext\Utils\HeadersGeneratorTrait;
use Gettext\Utils\CsvTrait;
/**
* Class to export translations to csv.
*/
class Csv extends Generator implements GeneratorInterface
{
use HeadersGeneratorTrait;
use CsvTrait;
public static $options = [
'includeHeaders' => false,
'delimiter' => ",",
'enclosure' => '"',
'escape_char' => "\\"
];
/**
* {@parentDoc}.
*/
public static function toString(Translations $translations, array $options = [])
{
$options += static::$options;
$handle = fopen('php://memory', 'w');
if ($options['includeHeaders']) {
static::fputcsv($handle, ['', '', static::generateHeaders($translations)], $options);
}
foreach ($translations as $translation) {
if ($translation->isDisabled()) {
continue;
}
$line = [$translation->getContext(), $translation->getOriginal(), $translation->getTranslation()];
if ($translation->hasPluralTranslations(true)) {
$line = array_merge($line, $translation->getPluralTranslations());
}
static::fputcsv($handle, $line, $options);
}
rewind($handle);
$csv = stream_get_contents($handle);
fclose($handle);
return $csv;
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Gettext\Generators;
use Gettext\Translations;
use Gettext\Utils\DictionaryTrait;
use Gettext\Utils\CsvTrait;
class CsvDictionary extends Generator implements GeneratorInterface
{
use DictionaryTrait;
use CsvTrait;
public static $options = [
'includeHeaders' => false,
'delimiter' => ",",
'enclosure' => '"',
'escape_char' => "\\"
];
/**
* {@parentDoc}.
*/
public static function toString(Translations $translations, array $options = [])
{
$options += static::$options;
$handle = fopen('php://memory', 'w');
foreach (static::toArray($translations, $options['includeHeaders']) as $original => $translation) {
static::fputcsv($handle, [$original, $translation], $options);
}
rewind($handle);
$csv = stream_get_contents($handle);
fclose($handle);
return $csv;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Gettext\Generators;
use Gettext\Translations;
abstract class Generator implements GeneratorInterface
{
/**
* {@inheritdoc}
*/
public static function toFile(Translations $translations, $file, array $options = [])
{
$content = static::toString($translations, $options);
if (file_put_contents($file, $content) === false) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Gettext\Generators;
use Gettext\Translations;
interface GeneratorInterface
{
/**
* Saves the translations in a file.
*
* @param Translations $translations
* @param string $file
* @param array $options
*
* @return bool
*/
public static function toFile(Translations $translations, $file, array $options = []);
/**
* Generates a string with the translations ready to save in a file.
*
* @param Translations $translations
* @param array $options
*
* @return string
*/
public static function toString(Translations $translations, array $options = []);
}

View File

@ -0,0 +1,66 @@
<?php
namespace Gettext\Generators;
use Gettext\Translations;
class Jed extends Generator implements GeneratorInterface
{
public static $options = [
'json' => 0,
];
/**
* {@parentDoc}.
*/
public static function toString(Translations $translations, array $options = [])
{
$domain = $translations->getDomain() ?: 'messages';
$options += static::$options;
return json_encode([
$domain => [
'' => [
'domain' => $domain,
'lang' => $translations->getLanguage() ?: 'en',
'plural-forms' => $translations->getHeader('Plural-Forms') ?: 'nplurals=2; plural=(n != 1);',
],
] + static::buildMessages($translations),
], $options['json']);
}
/**
* Generates an array with all translations.
*
* @param Translations $translations
*
* @return array
*/
protected static function buildMessages(Translations $translations)
{
$pluralForm = $translations->getPluralForms();
$pluralSize = is_array($pluralForm) ? ($pluralForm[0] - 1) : null;
$messages = [];
$context_glue = '\u0004';
foreach ($translations as $translation) {
if ($translation->isDisabled()) {
continue;
}
$key = ($translation->hasContext() ? $translation->getContext().$context_glue : '')
.$translation->getOriginal();
if ($translation->hasPluralTranslations(true)) {
$message = $translation->getPluralTranslations($pluralSize);
array_unshift($message, $translation->getTranslation());
} else {
$message = [$translation->getTranslation()];
}
$messages[$key] = $message;
}
return $messages;
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Gettext\Generators;
use Gettext\Translations;
use Gettext\Utils\MultidimensionalArrayTrait;
class Json extends Generator implements GeneratorInterface
{
use MultidimensionalArrayTrait;
public static $options = [
'json' => 0,
'includeHeaders' => false,
];
/**
* {@inheritdoc}
*/
public static function toString(Translations $translations, array $options = [])
{
$options += static::$options;
return json_encode(static::toArray($translations, $options['includeHeaders'], true), $options['json']);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Gettext\Generators;
use Gettext\Translations;
use Gettext\Utils\DictionaryTrait;
class JsonDictionary extends Generator implements GeneratorInterface
{
use DictionaryTrait;
public static $options = [
'json' => 0,
'includeHeaders' => false,
];
/**
* {@parentDoc}.
*/
public static function toString(Translations $translations, array $options = [])
{
$options += static::$options;
return json_encode(static::toArray($translations, $options['includeHeaders']), $options['json']);
}
}

View File

@ -1,43 +1,37 @@
<?php <?php
declare(strict_types = 1);
namespace Gettext\Generator; namespace Gettext\Generators;
use Gettext\Translation;
use Gettext\Translations; use Gettext\Translations;
use Gettext\Utils\HeadersGeneratorTrait;
final class MoGenerator extends Generator class Mo extends Generator implements GeneratorInterface
{ {
private $includeHeaders = false; use HeadersGeneratorTrait;
public function includeHeaders(bool $includeHeaders = true): self public static $options = [
{ 'includeHeaders' => true,
$this->includeHeaders = $includeHeaders; ];
return $this; /**
} * {@parentDoc}.
*/
public function generateString(Translations $translations): string public static function toString(Translations $translations, array $options = [])
{ {
$options += static::$options;
$messages = []; $messages = [];
if ($this->includeHeaders) { if ($options['includeHeaders']) {
$lines = []; $messages[''] = static::generateHeaders($translations);
foreach ($translations->getHeaders() as $name => $value) {
$lines[] = sprintf('%s: %s', $name, $value);
}
$messages[''] = implode("\n", $lines);
} }
foreach ($translations as $translation) { foreach ($translations as $translation) {
if (!$translation->getTranslation() || $translation->isDisabled()) { if (!$translation->hasTranslation() || $translation->isDisabled()) {
continue; continue;
} }
if ($context = $translation->getContext()) { if ($translation->hasContext()) {
$originalString = "{$context}\x04{$translation->getOriginal()}"; $originalString = $translation->getContext()."\x04".$translation->getOriginal();
} else { } else {
$originalString = $translation->getOriginal(); $originalString = $translation->getOriginal();
} }
@ -51,28 +45,32 @@ final class MoGenerator extends Generator
$translationsTable = ''; $translationsTable = '';
$originalsIndex = []; $originalsIndex = [];
$translationsIndex = []; $translationsIndex = [];
$pluralForm = $translations->getHeaders()->getPluralForm(); $pluralForm = $translations->getPluralForms();
$pluralSize = is_array($pluralForm) ? ($pluralForm[0] - 1) : null; $pluralSize = is_array($pluralForm) ? ($pluralForm[0] - 1) : null;
foreach ($messages as $originalString => $translation) { foreach ($messages as $originalString => $translation) {
if (is_string($translation)) { if (is_string($translation)) {
// Headers
$translationString = $translation; $translationString = $translation;
} elseif (self::hasPluralTranslations($translation)) { } else {
$originalString .= "\x00{$translation->getPlural()}"; /* @var $translation \Gettext\Translation */
$translationString = "{$translation->getTranslation()}\x00" if ($translation->hasPlural() && $translation->hasPluralTranslations(true)) {
.implode("\x00", $translation->getPluralTranslations($pluralSize)); $originalString .= "\x00".$translation->getPlural();
$translationString = $translation->getTranslation();
$translationString .= "\x00".implode("\x00", $translation->getPluralTranslations($pluralSize));
} else { } else {
$translationString = $translation->getTranslation(); $translationString = $translation->getTranslation();
} }
}
$originalsIndex[] = [ $originalsIndex[] = [
'relativeOffset' => strlen($originalsTable), 'relativeOffset' => strlen($originalsTable),
'length' => strlen((string) $originalString), 'length' => strlen($originalString)
]; ];
$originalsTable .= $originalString."\x00"; $originalsTable .= $originalString."\x00";
$translationsIndex[] = [ $translationsIndex[] = [
'relativeOffset' => strlen($translationsTable), 'relativeOffset' => strlen($translationsTable),
'length' => strlen($translationString), 'length' => strlen($translationString)
]; ];
$translationsTable .= $translationString."\x00"; $translationsTable .= $translationString."\x00";
} }
@ -139,13 +137,4 @@ final class MoGenerator extends Generator
return $mo; return $mo;
} }
private static function hasPluralTranslations(Translation $translation): bool
{
if (!$translation->getPlural()) {
return false;
}
return implode('', $translation->getPluralTranslations()) !== '';
}
} }

View File

@ -0,0 +1,40 @@
<?php
namespace Gettext\Generators;
use Gettext\Translations;
use Gettext\Utils\MultidimensionalArrayTrait;
class PhpArray extends Generator implements GeneratorInterface
{
use MultidimensionalArrayTrait;
public static $options = [
'includeHeaders' => true,
];
/**
* {@inheritdoc}
*/
public static function toString(Translations $translations, array $options = [])
{
$array = static::generate($translations, $options);
return '<?php return '.var_export($array, true).';';
}
/**
* Generates an array with the translations.
*
* @param Translations $translations
* @param array $options
*
* @return array
*/
public static function generate(Translations $translations, array $options = [])
{
$options += static::$options;
return static::toArray($translations, $options['includeHeaders'], true);
}
}

View File

@ -0,0 +1,145 @@
<?php
namespace Gettext\Generators;
use Gettext\Translations;
class Po extends Generator implements GeneratorInterface
{
public static $options = [
'noLocation' => false,
];
/**
* {@parentDoc}.
*/
public static function toString(Translations $translations, array $options = [])
{
$options += static::$options;
$pluralForm = $translations->getPluralForms();
$pluralSize = is_array($pluralForm) ? ($pluralForm[0] - 1) : null;
$lines = ['msgid ""', 'msgstr ""'];
foreach ($translations->getHeaders() as $name => $value) {
$lines[] = sprintf('"%s: %s\\n"', $name, $value);
}
$lines[] = '';
//Translations
foreach ($translations as $translation) {
if ($translation->hasComments()) {
foreach ($translation->getComments() as $comment) {
$lines[] = '# '.$comment;
}
}
if ($translation->hasExtractedComments()) {
foreach ($translation->getExtractedComments() as $comment) {
$lines[] = '#. '.$comment;
}
}
if (!$options['noLocation'] && $translation->hasReferences()) {
foreach ($translation->getReferences() as $reference) {
$lines[] = '#: '.$reference[0].(!is_null($reference[1]) ? ':'.$reference[1] : null);
}
}
if ($translation->hasFlags()) {
$lines[] = '#, '.implode(',', $translation->getFlags());
}
$prefix = $translation->isDisabled() ? '#~ ' : '';
if ($translation->hasContext()) {
$lines[] = $prefix.'msgctxt '.static::convertString($translation->getContext());
}
static::addLines($lines, $prefix.'msgid', $translation->getOriginal());
if ($translation->hasPlural()) {
static::addLines($lines, $prefix.'msgid_plural', $translation->getPlural());
static::addLines($lines, $prefix.'msgstr[0]', $translation->getTranslation());
foreach ($translation->getPluralTranslations($pluralSize) as $k => $v) {
static::addLines($lines, $prefix.'msgstr['.($k + 1).']', $v);
}
} else {
static::addLines($lines, $prefix.'msgstr', $translation->getTranslation());
}
$lines[] = '';
}
return implode("\n", $lines);
}
/**
* Escapes and adds double quotes to a string.
*
* @param string $string
*
* @return string
*/
protected static function multilineQuote($string)
{
$lines = explode("\n", $string);
$last = count($lines) - 1;
foreach ($lines as $k => $line) {
if ($k === $last) {
$lines[$k] = static::convertString($line);
} else {
$lines[$k] = static::convertString($line."\n");
}
}
return $lines;
}
/**
* Add one or more lines depending whether the string is multiline or not.
*
* @param array &$lines
* @param string $name
* @param string $value
*/
protected static function addLines(array &$lines, $name, $value)
{
$newLines = static::multilineQuote($value);
if (count($newLines) === 1) {
$lines[] = $name.' '.$newLines[0];
} else {
$lines[] = $name.' ""';
foreach ($newLines as $line) {
$lines[] = $line;
}
}
}
/**
* Convert a string to its PO representation.
*
* @param string $value
*
* @return string
*/
public static function convertString($value)
{
return '"'.strtr(
$value,
[
"\x00" => '',
'\\' => '\\\\',
"\t" => '\t',
"\r" => '\r',
"\n" => '\n',
'"' => '\\"',
]
).'"';
}
}

View File

@ -0,0 +1,122 @@
<?php
namespace Gettext\Generators;
use Gettext\Translation;
use Gettext\Translations;
use DOMDocument;
class Xliff extends Generator implements GeneratorInterface
{
const UNIT_ID_REGEXP = '/^XLIFF_UNIT_ID: (.*)$/';
/**
* {@inheritdoc}
*/
public static function toString(Translations $translations, array $options = [])
{
$dom = new DOMDocument('1.0', 'utf-8');
$dom->formatOutput = true;
$xliff = $dom->appendChild($dom->createElement('xliff'));
$xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:2.0');
$xliff->setAttribute('version', '2.0');
$xliff->setAttribute('srcLang', $translations->getLanguage());
$xliff->setAttribute('trgLang', $translations->getLanguage());
$file = $xliff->appendChild($dom->createElement('file'));
$file->setAttribute('id', $translations->getDomain().'.'.$translations->getLanguage());
//Save headers as notes
$notes = $dom->createElement('notes');
foreach ($translations->getHeaders() as $name => $value) {
$notes->appendChild(static::createTextNode($dom, 'note', $value))->setAttribute('id', $name);
}
if ($notes->hasChildNodes()) {
$file->appendChild($notes);
}
foreach ($translations as $translation) {
//Find an XLIFF unit ID, if one is available; otherwise generate
$unitId = static::getUnitID($translation)?:md5($translation->getContext().$translation->getOriginal());
$unit = $dom->createElement('unit');
$unit->setAttribute('id', $unitId);
//Save comments as notes
$notes = $dom->createElement('notes');
$notes->appendChild(static::createTextNode($dom, 'note', $translation->getContext()))
->setAttribute('category', 'context');
foreach ($translation->getComments() as $comment) {
//Skip XLIFF unit ID comments.
if (preg_match(static::UNIT_ID_REGEXP, $comment)) {
continue;
}
$notes->appendChild(static::createTextNode($dom, 'note', $comment))
->setAttribute('category', 'comment');
}
foreach ($translation->getExtractedComments() as $comment) {
$notes->appendChild(static::createTextNode($dom, 'note', $comment))
->setAttribute('category', 'extracted-comment');
}
foreach ($translation->getFlags() as $flag) {
$notes->appendChild(static::createTextNode($dom, 'note', $flag))
->setAttribute('category', 'flag');
}
foreach ($translation->getReferences() as $reference) {
$notes->appendChild(static::createTextNode($dom, 'note', $reference[0].':'.$reference[1]))
->setAttribute('category', 'reference');
}
$unit->appendChild($notes);
$segment = $unit->appendChild($dom->createElement('segment'));
$segment->appendChild(static::createTextNode($dom, 'source', $translation->getOriginal()));
$segment->appendChild(static::createTextNode($dom, 'target', $translation->getTranslation()));
foreach ($translation->getPluralTranslations() as $plural) {
if ($plural !== '') {
$segment->appendChild(static::createTextNode($dom, 'target', $plural));
}
}
$file->appendChild($unit);
}
return $dom->saveXML();
}
protected static function createTextNode(DOMDocument $dom, $name, $string)
{
$node = $dom->createElement($name);
$text = (preg_match('/[&<>]/', $string) === 1)
? $dom->createCDATASection($string)
: $dom->createTextNode($string);
$node->appendChild($text);
return $node;
}
/**
* Gets the translation's unit ID, if one is available.
*
* @param Translation $translation
*
* @return string|null
*/
public static function getUnitID(Translation $translation)
{
foreach ($translation->getComments() as $comment) {
if (preg_match(static::UNIT_ID_REGEXP, $comment, $matches)) {
return $matches[1];
}
}
return null;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Gettext\Generators;
use Gettext\Translations;
use Gettext\Utils\MultidimensionalArrayTrait;
use Symfony\Component\Yaml\Yaml as YamlDumper;
class Yaml extends Generator implements GeneratorInterface
{
use MultidimensionalArrayTrait;
public static $options = [
'includeHeaders' => false,
'indent' => 2,
'inline' => 4,
];
/**
* {@inheritdoc}
*/
public static function toString(Translations $translations, array $options = [])
{
$options += static::$options;
return YamlDumper::dump(
static::toArray($translations, $options['includeHeaders']),
$options['inline'],
$options['indent']
);
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Gettext\Generators;
use Gettext\Translations;
use Gettext\Utils\DictionaryTrait;
use Symfony\Component\Yaml\Yaml as YamlDumper;
class YamlDictionary extends Generator implements GeneratorInterface
{
use DictionaryTrait;
public static $options = [
'includeHeaders' => false,
'indent' => 2,
'inline' => 3,
];
/**
* {@inheritdoc}
*/
public static function toString(Translations $translations, array $options = [])
{
$options += static::$options;
return YamlDumper::dump(
static::toArray($translations, $options['includeHeaders']),
$options['inline'],
$options['indent']
);
}
}

View File

@ -0,0 +1,161 @@
<?php
namespace Gettext;
class GettextTranslator extends BaseTranslator implements TranslatorInterface
{
/**
* Constructor. Detects the current language using the environment variables.
*
* @param string $language
*/
public function __construct($language = null)
{
if (!function_exists('gettext')) {
throw new \RuntimeException('This class require the gettext extension for PHP');
}
//detects the language environment respecting the priority order
//http://php.net/manual/en/function.gettext.php#114062
if (empty($language)) {
$language = getenv('LANGUAGE') ?: getenv('LC_ALL') ?: getenv('LC_MESSAGES') ?: getenv('LANG');
}
if (!empty($language)) {
$this->setLanguage($language);
}
}
/**
* Define the current locale.
*
* @param string $language
* @param int|null $category
*
* @return self
*/
public function setLanguage($language, $category = null)
{
if ($category === null) {
$category = defined('LC_MESSAGES') ? LC_MESSAGES : LC_ALL;
}
setlocale($category, $language);
putenv('LANGUAGE='.$language);
return $this;
}
/**
* Loads a gettext domain.
*
* @param string $domain
* @param string $path
* @param bool $default
*
* @return self
*/
public function loadDomain($domain, $path = null, $default = true)
{
bindtextdomain($domain, $path);
bind_textdomain_codeset($domain, 'UTF-8');
if ($default) {
textdomain($domain);
}
return $this;
}
/**
* @see TranslatorInterface
*
* {@inheritdoc}
*/
public function gettext($original)
{
return gettext($original);
}
/**
* @see TranslatorInterface
*
* {@inheritdoc}
*/
public function ngettext($original, $plural, $value)
{
return ngettext($original, $plural, $value);
}
/**
* @see TranslatorInterface
*
* {@inheritdoc}
*/
public function dngettext($domain, $original, $plural, $value)
{
return dngettext($domain, $original, $plural, $value);
}
/**
* @see TranslatorInterface
*
* {@inheritdoc}
*/
public function npgettext($context, $original, $plural, $value)
{
$message = $context."\x04".$original;
$translation = ngettext($message, $plural, $value);
return ($translation === $message) ? $original : $translation;
}
/**
* @see TranslatorInterface
*
* {@inheritdoc}
*/
public function pgettext($context, $original)
{
$message = $context."\x04".$original;
$translation = gettext($message);
return ($translation === $message) ? $original : $translation;
}
/**
* @see TranslatorInterface
*
* {@inheritdoc}
*/
public function dgettext($domain, $original)
{
return dgettext($domain, $original);
}
/**
* @see TranslatorInterface
*
* {@inheritdoc}
*/
public function dpgettext($domain, $context, $original)
{
$message = $context."\x04".$original;
$translation = dgettext($domain, $message);
return ($translation === $message) ? $original : $translation;
}
/**
* @see TranslatorInterface
*
* {@inheritdoc}
*/
public function dnpgettext($domain, $context, $original, $plural, $value)
{
$message = $context."\x04".$original;
$translation = dngettext($domain, $message, $plural, $value);
return ($translation === $message) ? $original : $translation;
}
}

View File

@ -1,144 +0,0 @@
<?php
declare(strict_types = 1);
namespace Gettext;
use ArrayIterator;
use Countable;
use InvalidArgumentException;
use IteratorAggregate;
use JsonSerializable;
use ReturnTypeWillChange;
/**
* Class to manage the headers of translations.
*/
class Headers implements JsonSerializable, Countable, IteratorAggregate
{
public const HEADER_LANGUAGE = 'Language';
public const HEADER_PLURAL = 'Plural-Forms';
public const HEADER_DOMAIN = 'X-Domain';
protected $headers = [];
public static function __set_state(array $state): Headers
{
return new static($state['headers']);
}
public function __construct(array $headers = [])
{
$this->headers = $headers;
ksort($this->headers);
}
public function __debugInfo()
{
return $this->toArray();
}
public function set(string $name, string $value): self
{
$this->headers[$name] = trim($value);
ksort($this->headers);
return $this;
}
public function get(string $name): ?string
{
return $this->headers[$name] ?? null;
}
public function delete(string $name): self
{
unset($this->headers[$name]);
return $this;
}
public function clear(): self
{
$this->headers = [];
return $this;
}
#[ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->toArray();
}
#[ReturnTypeWillChange]
public function getIterator()
{
return new ArrayIterator($this->toArray());
}
public function count(): int
{
return count($this->headers);
}
public function setLanguage(string $language): self
{
return $this->set(self::HEADER_LANGUAGE, $language);
}
public function getLanguage(): ?string
{
return $this->get(self::HEADER_LANGUAGE);
}
public function setDomain(string $domain): self
{
return $this->set(self::HEADER_DOMAIN, $domain);
}
public function getDomain(): ?string
{
return $this->get(self::HEADER_DOMAIN);
}
public function setPluralForm(int $count, string $rule): self
{
if (preg_match('/[a-z]/i', str_replace('n', '', $rule))) {
throw new InvalidArgumentException(sprintf('Invalid Plural form: "%s"', $rule));
}
return $this->set(self::HEADER_PLURAL, sprintf('nplurals=%d; plural=%s;', $count, $rule));
}
/**
* Returns the parsed plural definition.
*
* @return array|null [count, rule]
*/
public function getPluralForm(): ?array
{
$header = $this->get(self::HEADER_PLURAL);
if (!empty($header) &&
preg_match('/^nplurals\s*=\s*(\d+)\s*;\s*plural\s*=\s*([^;]+)\s*;$/', $header, $matches)
) {
return [intval($matches[1]), $matches[2]];
}
return null;
}
public function toArray(): array
{
return $this->headers;
}
public function mergeWith(Headers $headers): Headers
{
$merged = clone $this;
$merged->headers = $headers->headers + $merged->headers;
ksort($merged->headers);
return $merged;
}
}

View File

@ -1,56 +0,0 @@
<?php
declare(strict_types = 1);
namespace Gettext\Loader;
use Exception;
use Gettext\Translation;
use Gettext\Translations;
/**
* Base class with common funtions for all loaders.
*/
abstract class Loader implements LoaderInterface
{
public function loadFile(string $filename, Translations $translations = null): Translations
{
$string = static::readFile($filename);
return $this->loadString($string, $translations);
}
public function loadString(string $string, Translations $translations = null): Translations
{
return $translations ?: $this->createTranslations();
}
protected function createTranslations(): Translations
{
return Translations::create();
}
protected function createTranslation(?string $context, string $original, string $plural = null): ?Translation
{
$translation = Translation::create($context, $original);
if (isset($plural)) {
$translation->setPlural($plural);
}
return $translation;
}
/**
* Reads and returns the content of a file.
*/
protected static function readFile(string $file): string
{
$content = @file_get_contents($file);
if (false === $content) {
throw new Exception("Cannot read the file '$file', probably permissions");
}
return $content;
}
}

View File

@ -1,13 +0,0 @@
<?php
declare(strict_types = 1);
namespace Gettext\Loader;
use Gettext\Translations;
interface LoaderInterface
{
public function loadFile(string $filename, Translations $translations = null): Translations;
public function loadString(string $string, Translations $translations = null): Translations;
}

View File

@ -1,142 +0,0 @@
<?php
declare(strict_types = 1);
namespace Gettext\Loader;
use Exception;
use Gettext\Translation;
use Gettext\Translations;
/**
* Class to load a MO file.
*/
final class MoLoader extends Loader
{
private $string;
private $position;
private $length;
private const MAGIC1 = -1794895138;
private const MAGIC2 = -569244523;
private const MAGIC3 = 2500072158;
public function loadString(string $string, Translations $translations = null): Translations
{
$translations = parent::loadString($string, $translations);
$this->init($string);
$magic = $this->readInt('V');
if (($magic === self::MAGIC1) || ($magic === self::MAGIC3)) { //to make sure it works for 64-bit platforms
$byteOrder = 'V'; //low endian
} elseif ($magic === (self::MAGIC2 & 0xFFFFFFFF)) {
$byteOrder = 'N'; //big endian
} else {
throw new Exception('Not MO file');
}
$this->readInt($byteOrder);
$total = $this->readInt($byteOrder); //total string count
$originals = $this->readInt($byteOrder); //offset of original table
$tran = $this->readInt($byteOrder); //offset of translation table
$this->seekto($originals);
$table_originals = $this->readIntArray($byteOrder, $total * 2);
$this->seekto($tran);
$table_translations = $this->readIntArray($byteOrder, $total * 2);
for ($i = 0; $i < $total; ++$i) {
$next = $i * 2;
$this->seekto($table_originals[$next + 2]);
$original = $this->read($table_originals[$next + 1]);
$this->seekto($table_translations[$next + 2]);
$translated = $this->read($table_translations[$next + 1]);
// Headers
if ($original === '') {
foreach (explode("\n", $translated) as $headerLine) {
if ($headerLine === '') {
continue;
}
$headerChunks = preg_split('/:\s*/', $headerLine, 2);
$translations->getHeaders()->set($headerChunks[0], isset($headerChunks[1]) ? $headerChunks[1] : '');
}
continue;
}
$context = $plural = null;
$chunks = explode("\x04", $original, 2);
if (isset($chunks[1])) {
list($context, $original) = $chunks;
}
$chunks = explode("\x00", $original, 2);
if (isset($chunks[1])) {
list($original, $plural) = $chunks;
}
$translation = $this->createTranslation($context, $original, $plural);
$translations->add($translation);
if ($translated === '') {
continue;
}
if ($plural === null) {
$translation->translate($translated);
continue;
}
$v = explode("\x00", $translated);
$translation->translate(array_shift($v));
$translation->translatePlural(...array_filter($v));
}
return $translations;
}
private function init(string $string): void
{
$this->string = $string;
$this->position = 0;
$this->length = strlen($string);
}
private function read(int $bytes): string
{
$data = substr($this->string, $this->position, $bytes);
$this->seekTo($this->position + $bytes);
return $data;
}
private function seekTo(int $position): void
{
$this->position = ($this->length < $position) ? $this->length : $position;
}
private function readInt(string $byteOrder): int
{
if (($read = $this->read(4)) === false) {
return 0;
}
$read = (array) unpack($byteOrder, $read);
return (int) array_shift($read);
}
private function readIntArray(string $byteOrder, int $count): array
{
return unpack($byteOrder.$count, $this->read(4 * $count)) ?: [];
}
}

View File

@ -1,226 +0,0 @@
<?php
declare(strict_types = 1);
namespace Gettext\Loader;
use Gettext\Translation;
use Gettext\Translations;
/**
* Class to load a PO file.
*/
final class PoLoader extends Loader
{
public function loadString(string $string, Translations $translations = null): Translations
{
$translations = parent::loadString($string, $translations);
$lines = explode("\n", $string);
$line = current($lines);
$translation = $this->createTranslation(null, '');
while ($line !== false) {
$line = trim($line);
$nextLine = next($lines);
//Multiline
while (substr($line, -1, 1) === '"'
&& $nextLine !== false
&& (substr(trim($nextLine), 0, 1) === '"' || substr(trim($nextLine), 0, 4) === '#~ "')
) {
if (substr(trim($nextLine), 0, 1) === '"') { // Normal multiline
$line = substr($line, 0, -1).substr(trim($nextLine), 1);
} elseif (substr(trim($nextLine), 0, 4) === '#~ "') { // Disabled multiline
$line = substr($line, 0, -1).substr(trim($nextLine), 4);
}
$nextLine = next($lines);
}
//End of translation
if ($line === '') {
if (!self::isEmpty($translation)) {
$translations->add($translation);
}
$translation = $this->createTranslation(null, '');
$line = $nextLine;
continue;
}
$splitLine = preg_split('/\s+/', $line, 2);
$key = $splitLine[0];
$data = $splitLine[1] ?? '';
if ($key === '#~') {
$translation->disable();
$splitLine = preg_split('/\s+/', $data, 2);
$key = $splitLine[0];
$data = $splitLine[1] ?? '';
}
if ($data === '') {
$line = $nextLine;
continue;
}
switch ($key) {
case '#':
$translation->getComments()->add($data);
break;
case '#.':
$translation->getExtractedComments()->add($data);
break;
case '#,':
foreach (array_map('trim', explode(',', trim($data))) as $value) {
$translation->getFlags()->add($value);
}
break;
case '#:':
foreach (preg_split('/\s+/', trim($data)) as $value) {
if (preg_match('/^(.+)(:(\d*))?$/U', $value, $matches)) {
$line = isset($matches[3]) ? intval($matches[3]) : null;
$translation->getReferences()->add($matches[1], $line);
}
}
break;
case 'msgctxt':
$translation = $translation->withContext(self::decode($data));
break;
case 'msgid':
$translation = $translation->withOriginal(self::decode($data));
break;
case 'msgid_plural':
$translation->setPlural(self::decode($data));
break;
case 'msgstr':
case 'msgstr[0]':
$translation->translate(self::decode($data));
break;
case 'msgstr[1]':
$translation->translatePlural(self::decode($data));
break;
default:
if (strpos($key, 'msgstr[') === 0) {
$p = $translation->getPluralTranslations();
$p[] = self::decode($data);
$translation->translatePlural(...$p);
break;
}
break;
}
$line = $nextLine;
}
if (!self::isEmpty($translation)) {
$translations->add($translation);
}
//Headers
$translation = $translations->find(null, '');
if (!$translation) {
return $translations;
}
$translations->remove($translation);
$description = $translation->getComments()->toArray();
if (!empty($description)) {
$translations->setDescription(implode("\n", $description));
}
$flags = $translation->getFlags()->toArray();
if (!empty($flags)) {
$translations->getFlags()->add(...$flags);
}
$headers = $translations->getHeaders();
foreach (self::parseHeaders($translation->getTranslation()) as $name => $value) {
$headers->set($name, $value);
}
return $translations;
}
private static function parseHeaders(?string $string): array
{
if (empty($string)) {
return [];
}
$headers = [];
$lines = explode("\n", $string);
$name = null;
foreach ($lines as $line) {
$line = self::decode($line);
if ($line === '') {
continue;
}
// Checks if it is a header definition line.
// Useful for distinguishing between header definitions and possible continuations of a header entry.
if (preg_match('/^[\w-]+:/', $line)) {
$pieces = array_map('trim', explode(':', $line, 2));
list($name, $value) = $pieces;
$headers[$name] = $value;
continue;
}
$value = $headers[$name] ?? '';
$headers[$name] = $value.$line;
}
return $headers;
}
/**
* Convert a string from its PO representation.
*/
public static function decode(string $value): string
{
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",
'\\"' => '"',
]
);
}
private static function isEmpty(Translation $translation): bool
{
if (!empty($translation->getOriginal())) {
return false;
}
if (!empty($translation->getTranslation())) {
return false;
}
return true;
}
}

View File

@ -1,40 +1,221 @@
<?php <?php
declare(strict_types = 1);
namespace Gettext; namespace Gettext;
/** /**
* Merge contants. * Static class with merge contants.
*/ */
final class Merge final class Merge
{ {
public const TRANSLATIONS_OURS = 1 << 0; const ADD = 1;
public const TRANSLATIONS_THEIRS = 1 << 1; const REMOVE = 2;
public const TRANSLATIONS_OVERRIDE = 1 << 2;
public const HEADERS_OURS = 1 << 3; const HEADERS_ADD = 4;
public const HEADERS_THEIRS = 1 << 4; const HEADERS_REMOVE = 8;
public const HEADERS_OVERRIDE = 1 << 5; const HEADERS_OVERRIDE = 16;
public const COMMENTS_OURS = 1 << 6; const LANGUAGE_OVERRIDE = 32;
public const COMMENTS_THEIRS = 1 << 7; const DOMAIN_OVERRIDE = 64;
const TRANSLATION_OVERRIDE = 128;
public const EXTRACTED_COMMENTS_OURS = 1 << 8; const COMMENTS_OURS = 256;
public const EXTRACTED_COMMENTS_THEIRS = 1 << 9; const COMMENTS_THEIRS = 512;
public const FLAGS_OURS = 1 << 10; const EXTRACTED_COMMENTS_OURS = 1024;
public const FLAGS_THEIRS = 1 << 11; const EXTRACTED_COMMENTS_THEIRS = 2048;
public const REFERENCES_OURS = 1 << 12; const FLAGS_OURS = 4096;
public const REFERENCES_THEIRS = 1 << 13; const FLAGS_THEIRS = 8192;
//Merge strategies const REFERENCES_OURS = 16384;
public const SCAN_AND_LOAD = const REFERENCES_THEIRS = 32768;
Merge::HEADERS_OVERRIDE
| Merge::TRANSLATIONS_OURS const DEFAULTS = 5; //1 + 4
| Merge::TRANSLATIONS_OVERRIDE
| Merge::EXTRACTED_COMMENTS_OURS /**
| Merge::REFERENCES_OURS * Merge the flags of two translations.
| Merge::FLAGS_THEIRS *
| Merge::COMMENTS_THEIRS; * @param Translation $from
* @param Translation $to
* @param int $options
*/
public static function mergeFlags(Translation $from, Translation $to, $options = self::DEFAULTS)
{
if ($options & self::FLAGS_THEIRS) {
$to->deleteFlags();
}
if (!($options & self::FLAGS_OURS)) {
foreach ($from->getFlags() as $flag) {
$to->addFlag($flag);
}
}
}
/**
* Merge the extracted comments of two translations.
*
* @param Translation $from
* @param Translation $to
* @param int $options
*/
public static function mergeExtractedComments(Translation $from, Translation $to, $options = self::DEFAULTS)
{
if ($options & self::EXTRACTED_COMMENTS_THEIRS) {
$to->deleteExtractedComments();
}
if (!($options & self::EXTRACTED_COMMENTS_OURS)) {
foreach ($from->getExtractedComments() as $comment) {
$to->addExtractedComment($comment);
}
}
}
/**
* Merge the comments of two translations.
*
* @param Translation $from
* @param Translation $to
* @param int $options
*/
public static function mergeComments(Translation $from, Translation $to, $options = self::DEFAULTS)
{
if ($options & self::COMMENTS_THEIRS) {
$to->deleteComments();
}
if (!($options & self::COMMENTS_OURS)) {
foreach ($from->getComments() as $comment) {
$to->addComment($comment);
}
}
}
/**
* Merge the references of two translations.
*
* @param Translation $from
* @param Translation $to
* @param int $options
*/
public static function mergeReferences(Translation $from, Translation $to, $options = self::DEFAULTS)
{
if ($options & self::REFERENCES_THEIRS) {
$to->deleteReferences();
}
if (!($options & self::REFERENCES_OURS)) {
foreach ($from->getReferences() as $reference) {
$to->addReference($reference[0], $reference[1]);
}
}
}
/**
* Merge the translations of two translations.
*
* @param Translation $from
* @param Translation $to
* @param int $options
*/
public static function mergeTranslation(Translation $from, Translation $to, $options = self::DEFAULTS)
{
$override = (boolean) ($options & self::TRANSLATION_OVERRIDE);
if (!$to->hasTranslation() || ($from->hasTranslation() && $override)) {
$to->setTranslation($from->getTranslation());
}
if (!$to->hasPlural() || ($from->hasPlural() && $override)) {
$to->setPlural($from->getPlural());
}
if (!$to->hasPluralTranslations() || ($from->hasPluralTranslations() && $override)) {
$to->setPluralTranslations($from->getPluralTranslations());
}
}
/**
* Merge the translations of two translations.
*
* @param Translations $from
* @param Translations $to
* @param int $options
*/
public static function mergeTranslations(Translations $from, Translations $to, $options = self::DEFAULTS)
{
if ($options & self::REMOVE) {
$filtered = [];
foreach ($to as $entry) {
if ($from->find($entry)) {
$filtered[$entry->getId()] = $entry;
}
}
$to->exchangeArray($filtered);
}
foreach ($from as $entry) {
if (($existing = $to->find($entry))) {
$existing->mergeWith($entry, $options);
} elseif ($options & self::ADD) {
$to[] = $entry->getClone();
}
}
}
/**
* Merge the headers of two translations.
*
* @param Translations $from
* @param Translations $to
* @param int $options
*/
public static function mergeHeaders(Translations $from, Translations $to, $options = self::DEFAULTS)
{
if ($options & self::HEADERS_REMOVE) {
foreach (array_keys($to->getHeaders()) as $name) {
if ($from->getHeader($name) === null) {
$to->deleteHeader($name);
}
}
}
foreach ($from->getHeaders() as $name => $value) {
$current = $to->getHeader($name);
if (empty($current)) {
if ($options & self::HEADERS_ADD) {
$to->setHeader($name, $value);
}
continue;
}
if (empty($value)) {
continue;
}
switch ($name) {
case Translations::HEADER_LANGUAGE:
case Translations::HEADER_PLURAL:
if ($options & self::LANGUAGE_OVERRIDE) {
$to->setHeader($name, $value);
}
break;
case Translations::HEADER_DOMAIN:
if ($options & self::DOMAIN_OVERRIDE) {
$to->setHeader($name, $value);
}
break;
default:
if ($options & self::HEADERS_OVERRIDE) {
$to->setHeader($name, $value);
}
}
}
}
} }

View File

@ -1,89 +0,0 @@
<?php
declare(strict_types = 1);
namespace Gettext;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use JsonSerializable;
use ReturnTypeWillChange;
/**
* Class to manage the references of a translation.
*/
class References implements JsonSerializable, Countable, IteratorAggregate
{
protected $references = [];
public static function __set_state(array $state): References
{
$references = new static();
$references->references = $state['references'];
return $references;
}
public function __debugInfo()
{
return $this->toArray();
}
public function add(string $filename, int $line = null): self
{
$fileReferences = $this->references[$filename] ?? [];
if (isset($line) && !in_array($line, $fileReferences)) {
$fileReferences[] = $line;
}
$this->references[$filename] = $fileReferences;
return $this;
}
#[ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->toArray();
}
#[ReturnTypeWillChange]
public function getIterator()
{
return new ArrayIterator($this->references);
}
public function count(): int
{
return array_reduce($this->references, function ($carry, $item) {
return $carry + (count($item) ?: 1);
}, 0);
}
public function toArray(): array
{
return $this->references;
}
public function mergeWith(References $references): References
{
$merged = clone $this;
foreach ($references as $filename => $lines) {
//Set filename always to string
$filename = (string) $filename;
if (empty($lines)) {
$merged->add($filename);
continue;
}
foreach ($lines as $line) {
$merged->add($filename, $line);
}
}
return $merged;
}
}

View File

@ -1,171 +0,0 @@
<?php
declare(strict_types = 1);
namespace Gettext\Scanner;
use Exception;
use Gettext\Translation;
use Gettext\Translations;
/**
* Base class with common functions to scan files with code and get gettext translations.
*/
abstract class CodeScanner extends Scanner
{
protected $ignoreInvalidFunctions = false;
protected $addReferences = true;
protected $commentsPrefixes = [];
protected $functions = [];
/**
* @param array $functions [fnName => handler]
*/
public function setFunctions(array $functions): self
{
$this->functions = $functions;
return $this;
}
/**
* @return array [fnName => handler]
*/
public function getFunctions(): array
{
return $this->functions;
}
public function ignoreInvalidFunctions($ignore = true): self
{
$this->ignoreInvalidFunctions = $ignore;
return $this;
}
public function addReferences($enabled = true): self
{
$this->addReferences = $enabled;
return $this;
}
public function extractCommentsStartingWith(string ...$prefixes): self
{
$this->commentsPrefixes = $prefixes;
return $this;
}
public function scanString(string $string, string $filename): void
{
$functionsScanner = $this->getFunctionsScanner();
$functions = $functionsScanner->scan($string, $filename);
foreach ($functions as $function) {
$this->handleFunction($function);
}
}
abstract public function getFunctionsScanner(): FunctionsScannerInterface;
protected function handleFunction(ParsedFunction $function)
{
$handler = $this->getFunctionHandler($function);
if (is_null($handler)) {
return;
}
$translation = call_user_func($handler, $function);
if ($translation && $this->addReferences) {
$translation->getReferences()->add($function->getFilename(), $function->getLine());
}
}
protected function getFunctionHandler(ParsedFunction $function): ?callable
{
$name = $function->getName();
$handler = $this->functions[$name] ?? null;
return is_null($handler) ? null : [$this, $handler];
}
protected function addComments(ParsedFunction $function, ?Translation $translation): ?Translation
{
if (empty($this->commentsPrefixes) || empty($translation)) {
return $translation;
}
foreach ($function->getComments() as $comment) {
if ($this->checkComment($comment)) {
$translation->getExtractedComments()->add($comment);
}
}
return $translation;
}
protected function addFlags(ParsedFunction $function, ?Translation $translation): ?Translation
{
if (empty($translation)) {
return $translation;
}
foreach ($function->getFlags() as $flag) {
$translation->getFlags()->add($flag);
}
return $translation;
}
protected function checkFunction(ParsedFunction $function, int $minLength): bool
{
if ($function->countArguments() < $minLength) {
if ($this->ignoreInvalidFunctions) {
return false;
}
throw new Exception(
sprintf(
'Invalid gettext function in %s:%d. At least %d arguments are required',
$function->getFilename(),
$function->getLine(),
$minLength
)
);
}
$arguments = array_slice($function->getArguments(), 0, $minLength);
if (in_array(null, $arguments, true)) {
if ($this->ignoreInvalidFunctions) {
return false;
}
throw new Exception(
sprintf(
'Invalid gettext function in %s:%d. Some required arguments are not valid',
$function->getFilename(),
$function->getLine()
)
);
}
return true;
}
protected function checkComment(string $comment): bool
{
foreach ($this->commentsPrefixes as $prefix) {
if ($prefix === '' || strpos($comment, $prefix) === 0) {
return true;
}
}
return false;
}
}

View File

@ -1,145 +0,0 @@
<?php
declare(strict_types = 1);
namespace Gettext\Scanner;
use Gettext\Translation;
/**
* Trait with common gettext function handlers
*/
trait FunctionsHandlersTrait
{
protected function gettext(ParsedFunction $function): ?Translation
{
if (!$this->checkFunction($function, 1)) {
return null;
}
list($original) = $function->getArguments();
$translation = $this->addComments(
$function,
$this->saveTranslation(null, null, $original)
);
return $this->addFlags($function, $translation);
}
protected function ngettext(ParsedFunction $function): ?Translation
{
if (!$this->checkFunction($function, 2)) {
return null;
}
list($original, $plural) = $function->getArguments();
$translation = $this->addComments(
$function,
$this->saveTranslation(null, null, $original, $plural)
);
return $this->addFlags($function, $translation);
}
protected function pgettext(ParsedFunction $function): ?Translation
{
if (!$this->checkFunction($function, 2)) {
return null;
}
list($context, $original) = $function->getArguments();
$translation = $this->addComments(
$function,
$this->saveTranslation(null, $context, $original)
);
return $this->addFlags($function, $translation);
}
protected function dgettext(ParsedFunction $function): ?Translation
{
if (!$this->checkFunction($function, 2)) {
return null;
}
list($domain, $original) = $function->getArguments();
$translation = $this->addComments(
$function,
$this->saveTranslation($domain, null, $original)
);
return $this->addFlags($function, $translation);
}
protected function dpgettext(ParsedFunction $function): ?Translation
{
if (!$this->checkFunction($function, 3)) {
return null;
}
list($domain, $context, $original) = $function->getArguments();
$translation = $this->addComments(
$function,
$this->saveTranslation($domain, $context, $original)
);
return $this->addFlags($function, $translation);
}
protected function npgettext(ParsedFunction $function): ?Translation
{
if (!$this->checkFunction($function, 3)) {
return null;
}
list($context, $original, $plural) = $function->getArguments();
$translation = $this->addComments(
$function,
$this->saveTranslation(null, $context, $original, $plural)
);
return $this->addFlags($function, $translation);
}
protected function dngettext(ParsedFunction $function): ?Translation
{
if (!$this->checkFunction($function, 3)) {
return null;
}
list($domain, $original, $plural) = $function->getArguments();
$translation = $this->addComments(
$function,
$this->saveTranslation($domain, null, $original, $plural)
);
return $this->addFlags($function, $translation);
}
protected function dnpgettext(ParsedFunction $function): ?Translation
{
if (!$this->checkFunction($function, 4)) {
return null;
}
list($domain, $context, $original, $plural) = $function->getArguments();
$translation = $this->addComments(
$function,
$this->saveTranslation($domain, $context, $original, $plural)
);
return $this->addFlags($function, $translation);
}
abstract protected function addComments(ParsedFunction $function, ?Translation $translation): ?Translation;
abstract protected function addFlags(ParsedFunction $function, ?Translation $translation): ?Translation;
abstract protected function checkFunction(ParsedFunction $function, int $minLength): bool;
abstract protected function saveTranslation(
?string $domain,
?string $context,
string $original,
string $plural = null
): ?Translation;
}

View File

@ -1,12 +0,0 @@
<?php
declare(strict_types = 1);
namespace Gettext\Scanner;
interface FunctionsScannerInterface
{
/**
* @return ParsedFunction[]
*/
public function scan(string $code, string $filename): array;
}

View File

@ -1,105 +0,0 @@
<?php
declare(strict_types = 1);
namespace Gettext\Scanner;
/**
* Class to handle the info of a parsed function.
*/
final class ParsedFunction
{
private $name;
private $filename;
private $line;
private $lastLine;
private $arguments = [];
private $comments = [];
private $flags = [];
public function __construct(string $name, string $filename, int $line, int $lastLine = null)
{
$this->name = $name;
$this->filename = $filename;
$this->line = $line;
$this->lastLine = isset($lastLine) ? $lastLine : $line;
}
public function __debugInfo()
{
return $this->toArray();
}
public function toArray(): array
{
return [
'name' => $this->name,
'filename' => $this->filename,
'line' => $this->line,
'lastLine' => $this->lastLine,
'arguments' => $this->arguments,
'comments' => $this->comments,
'flags' => $this->flags,
];
}
public function getName(): string
{
return $this->name;
}
public function getLine(): int
{
return $this->line;
}
public function getLastLine(): int
{
return $this->lastLine;
}
public function getFilename(): string
{
return $this->filename;
}
public function getArguments(): array
{
return $this->arguments;
}
public function countArguments(): int
{
return count($this->arguments);
}
public function getComments(): array
{
return $this->comments;
}
public function getFlags(): array
{
return $this->flags;
}
public function addArgument($argument = null): self
{
$this->arguments[] = $argument;
return $this;
}
public function addComment(string $comment): self
{
$this->comments[] = $comment;
return $this;
}
public function addFlag(string $flag): self
{
$this->flags[] = $flag;
return $this;
}
}

View File

@ -1,88 +0,0 @@
<?php
declare(strict_types = 1);
namespace Gettext\Scanner;
use Exception;
use Gettext\Translation;
use Gettext\Translations;
/**
* Base class with common funtions for all scanners.
*/
abstract class Scanner implements ScannerInterface
{
protected $translations;
protected $defaultDomain;
public function __construct(Translations ...$allTranslations)
{
foreach ($allTranslations as $translations) {
$domain = $translations->getDomain();
$this->translations[$domain] = $translations;
}
}
public function setDefaultDomain(string $defaultDomain): void
{
$this->defaultDomain = $defaultDomain;
}
public function getDefaultDomain(): string
{
return $this->defaultDomain;
}
public function getTranslations(): array
{
return $this->translations;
}
public function scanFile(string $filename): void
{
$string = static::readFile($filename);
$this->scanString($string, $filename);
}
abstract public function scanString(string $string, string $filename): void;
protected function saveTranslation(
?string $domain,
?string $context,
string $original,
string $plural = null
): ?Translation {
if (is_null($domain)) {
$domain = $this->defaultDomain;
}
if (!isset($this->translations[$domain])) {
return null;
}
$translation = $this->translations[$domain]->addOrMerge(
Translation::create($context, $original)
);
if (isset($plural)) {
$translation->setPlural($plural);
}
return $translation;
}
/**
* Reads and returns the content of a file.
*/
protected static function readFile(string $file): string
{
$content = @file_get_contents($file);
if (false === $content) {
throw new Exception("Cannot read the file '$file', probably permissions");
}
return $content;
}
}

View File

@ -1,22 +0,0 @@
<?php
declare(strict_types = 1);
namespace Gettext\Scanner;
use Gettext\Translations;
interface ScannerInterface
{
public function setDefaultDomain(string $domain): void;
public function getDefaultDomain(): string;
/**
* @return Translations[]
*/
public function getTranslations(): array;
public function scanFile(string $filename): void;
public function scanString(string $string, string $filename): void;
}

View File

@ -1,236 +1,537 @@
<?php <?php
declare(strict_types = 1);
namespace Gettext; namespace Gettext;
/** /**
* Class to manage an individual translation. * Class to manage a translation string.
*/ */
class Translation class Translation
{ {
protected $id; protected $id;
protected $context; protected $context;
protected $original; protected $original;
protected $translation = '';
protected $plural; protected $plural;
protected $translation; protected $pluralTranslation = [];
protected $pluralTranslations = []; protected $references = [];
protected $comments = [];
protected $extractedComments = [];
protected $flags = [];
protected $disabled = false; protected $disabled = false;
protected $references;
protected $flags;
protected $comments;
protected $extractedComments;
public static function create(?string $context, string $original): Translation /**
{ * Generates the id of a translation (context + glue + original).
$id = static::generateId($context, $original); *
* @param string $context
$translation = new static($id); * @param string $original
$translation->context = $context; *
$translation->original = $original; * @return string
*/
return $translation; public static function generateId($context, $original)
}
protected static function generateId(?string $context, string $original): string
{ {
return "{$context}\004{$original}"; return "{$context}\004{$original}";
} }
protected function __construct(string $id) /**
* Create a new instance of a Translation object.
*
* This is a factory method that will work even when Translation is extended.
*
* @param string $context The context of the translation
* @param string $original The original string
* @param string $plural The original plural string
* @return static New Translation instance
*/
public static function create($context, $original, $plural = '')
{
return new static($context, $original, $plural);
}
/**
* Construct.
*
* @param string $context The context of the translation
* @param string $original The original string
* @param string $plural The original plural string
*/
public function __construct($context, $original, $plural = '')
{
$this->context = (string) $context;
$this->original = (string) $original;
$this->setPlural($plural);
}
/**
* Clones this translation.
*
* @param null|string $context Optional new context
* @param null|string $original Optional new original
*
* @return Translation
*/
public function getClone($context = null, $original = null)
{
$new = clone $this;
if ($context !== null) {
$new->context = (string) $context;
}
if ($original !== null) {
$new->original = (string) $original;
}
return $new;
}
/**
* Sets the id of this translation.
* @warning The use of this function to set a custom ID will prevent
* Translations::find from matching this translation.
*
* @param string $id
*/
public function setId($id)
{ {
$this->id = $id; $this->id = $id;
$this->references = new References();
$this->flags = new Flags();
$this->comments = new Comments();
$this->extractedComments = new Comments();
} }
public function __clone()
{
$this->references = clone $this->references;
$this->flags = clone $this->flags;
$this->comments = clone $this->comments;
$this->extractedComments = clone $this->extractedComments;
}
public function toArray(): array /**
* Returns the id of this translation.
*
* @return string
*/
public function getId()
{ {
return [ if ($this->id === null) {
'id' => $this->id, return static::generateId($this->context, $this->original);
'context' => $this->context,
'original' => $this->original,
'translation' => $this->translation,
'plural' => $this->plural,
'pluralTranslations' => $this->pluralTranslations,
'disabled' => $this->disabled,
'references' => $this->getReferences()->toArray(),
'flags' => $this->getFlags()->toArray(),
'comments' => $this->getComments()->toArray(),
'extractedComments' => $this->getExtractedComments()->toArray(),
];
} }
public function getId(): string
{
return $this->id; return $this->id;
} }
public function getContext(): ?string /**
* Checks whether the translation matches with the arguments.
*
* @param string $context
* @param string $original
*
* @return bool
*/
public function is($context, $original = '')
{ {
return $this->context; return (($this->context === $context) && ($this->original === $original)) ? true : false;
} }
public function withContext(?string $context): Translation /**
* Enable or disable the translation
*
* @param bool $disabled
*
* @return self
*/
public function setDisabled($disabled)
{ {
$clone = clone $this; $this->disabled = (bool) $disabled;
$clone->context = $context;
$clone->id = static::generateId($clone->getContext(), $clone->getOriginal());
return $clone;
}
public function getOriginal(): string
{
return $this->original;
}
public function withOriginal(string $original): Translation
{
$clone = clone $this;
$clone->original = $original;
$clone->id = static::generateId($clone->getContext(), $clone->getOriginal());
return $clone;
}
public function setPlural(string $plural): self
{
$this->plural = $plural;
return $this; return $this;
} }
public function getPlural(): ?string /**
{ * Returns whether the translation is disabled
return $this->plural; *
} * @return bool
*/
public function disable(bool $disabled = true): self public function isDisabled()
{
$this->disabled = $disabled;
return $this;
}
public function isDisabled(): bool
{ {
return $this->disabled; return $this->disabled;
} }
public function translate(string $translation): self /**
* Gets the original string.
*
* @return string
*/
public function getOriginal()
{ {
$this->translation = $translation; return $this->original;
}
/**
* Checks if the original string is empty or not.
*
* @return bool
*/
public function hasOriginal()
{
return ($this->original !== '') ? true : false;
}
/**
* Sets the translation string.
*
* @param string $translation
*
* @return self
*/
public function setTranslation($translation)
{
$this->translation = (string) $translation;
return $this; return $this;
} }
public function getTranslation(): ?string /**
* Gets the translation string.
*
* @return string
*/
public function getTranslation()
{ {
return $this->translation; return $this->translation;
} }
public function isTranslated(): bool /**
* Checks if the translation string is empty or not.
*
* @return bool
*/
public function hasTranslation()
{ {
return isset($this->translation) && $this->translation !== ''; return ($this->translation !== '') ? true : false;
} }
public function translatePlural(string ...$translations): self /**
* Sets the plural translation string.
*
* @param string $plural
*
* @return self
*/
public function setPlural($plural)
{ {
$this->pluralTranslations = $translations; $this->plural = (string) $plural;
return $this; return $this;
} }
public function getPluralTranslations(int $size = null): array /**
* Gets the plural translation string.
*
* @return string
*/
public function getPlural()
{
return $this->plural;
}
/**
* Checks if the plural translation string is empty or not.
*
* @return bool
*/
public function hasPlural()
{
return ($this->plural !== '') ? true : false;
}
/**
* Set a new plural translation.
*
* @param array $plural
*
* @return self
*/
public function setPluralTranslations(array $plural)
{
$this->pluralTranslation = $plural;
return $this;
}
/**
* Gets all plural translations.
*
* @param int $size
*
* @return array
*/
public function getPluralTranslations($size = null)
{ {
if ($size === null) { if ($size === null) {
return $this->pluralTranslations; return $this->pluralTranslation;
} }
$length = count($this->pluralTranslations); $current = count($this->pluralTranslation);
if ($size > $length) { if ($size > $current) {
return $this->pluralTranslations + array_fill(0, $size, ''); return $this->pluralTranslation + array_fill(0, $size, '');
} }
return array_slice($this->pluralTranslations, 0, $size); if ($size < $current) {
return array_slice($this->pluralTranslation, 0, $size);
} }
public function getReferences(): References return $this->pluralTranslation;
}
/**
* Checks if there are any plural translation.
*
* @param bool $checkContent
*
* @return bool
*/
public function hasPluralTranslations($checkContent = false)
{ {
return $this->references; if ($checkContent) {
return implode('', $this->pluralTranslation) !== '';
} }
public function getFlags(): Flags return !empty($this->pluralTranslation);
}
/**
* Removes all plural translations.
*
* @return self
*/
public function deletePluralTranslation()
{ {
return $this->flags; $this->pluralTranslation = [];
return $this;
} }
public function getComments(): Comments /**
* Gets the context of this translation.
*
* @return string
*/
public function getContext()
{
return $this->context;
}
/**
* Checks if the context is empty or not.
*
* @return bool
*/
public function hasContext()
{
return (isset($this->context) && ($this->context !== '')) ? true : false;
}
/**
* Adds a new reference for this translation.
*
* @param string $filename The file path where the translation has been found
* @param null|int $line The line number where the translation has been found
*
* @return self
*/
public function addReference($filename, $line = null)
{
$key = "{$filename}:{$line}";
$this->references[$key] = [$filename, $line];
return $this;
}
/**
* Checks if the translation has any reference.
*
* @return bool
*/
public function hasReferences()
{
return !empty($this->references);
}
/**
* Return all references for this translation.
*
* @return array
*/
public function getReferences()
{
return array_values($this->references);
}
/**
* Removes all references.
*
* @return self
*/
public function deleteReferences()
{
$this->references = [];
return $this;
}
/**
* Adds a new comment for this translation.
*
* @param string $comment
*
* @return self
*/
public function addComment($comment)
{
if (!in_array($comment, $this->comments, true)) {
$this->comments[] = $comment;
}
return $this;
}
/**
* Checks if the translation has any comment.
*
* @return bool
*/
public function hasComments()
{
return isset($this->comments[0]);
}
/**
* Returns all comments for this translation.
*
* @return array
*/
public function getComments()
{ {
return $this->comments; return $this->comments;
} }
public function getExtractedComments(): Comments /**
* Removes all comments.
*
* @return self
*/
public function deleteComments()
{
$this->comments = [];
return $this;
}
/**
* Adds a new extracted comment for this translation.
*
* @param string $comment
*
* @return self
*/
public function addExtractedComment($comment)
{
if (!in_array($comment, $this->extractedComments, true)) {
$this->extractedComments[] = $comment;
}
return $this;
}
/**
* Checks if the translation has any extracted comment.
*
* @return bool
*/
public function hasExtractedComments()
{
return isset($this->extractedComments[0]);
}
/**
* Returns all extracted comments for this translation.
*
* @return array
*/
public function getExtractedComments()
{ {
return $this->extractedComments; return $this->extractedComments;
} }
public function mergeWith(Translation $translation, int $strategy = 0): Translation /**
* Removes all extracted comments.
*
* @return self
*/
public function deleteExtractedComments()
{ {
$merged = clone $this; $this->extractedComments = [];
if ($strategy & Merge::COMMENTS_THEIRS) { return $this;
$merged->comments = clone $translation->comments;
} elseif (!($strategy & Merge::COMMENTS_OURS)) {
$merged->comments = $merged->comments->mergeWith($translation->comments);
} }
if ($strategy & Merge::EXTRACTED_COMMENTS_THEIRS) { /**
$merged->extractedComments = clone $translation->extractedComments; * Adds a new flag for this translation.
} elseif (!($strategy & Merge::EXTRACTED_COMMENTS_OURS)) { *
$merged->extractedComments = $merged->extractedComments->mergeWith($translation->extractedComments); * @param string $flag
*
* @return self
*/
public function addFlag($flag)
{
if (!in_array($flag, $this->flags, true)) {
$this->flags[] = $flag;
} }
if ($strategy & Merge::REFERENCES_THEIRS) { return $this;
$merged->references = clone $translation->references;
} elseif (!($strategy & Merge::REFERENCES_OURS)) {
$merged->references = $merged->references->mergeWith($translation->references);
} }
if ($strategy & Merge::FLAGS_THEIRS) { /**
$merged->flags = clone $translation->flags; * Checks if the translation has any flag.
} elseif (!($strategy & Merge::FLAGS_OURS)) { *
$merged->flags = $merged->flags->mergeWith($translation->flags); * @return bool
*/
public function hasFlags()
{
return isset($this->flags[0]);
} }
$override = (bool) ($strategy & Merge::TRANSLATIONS_OVERRIDE); /**
* Returns all extracted flags for this translation.
if (!$merged->translation || ($translation->translation && $override)) { *
$merged->translation = $translation->translation; * @return array
*/
public function getFlags()
{
return $this->flags;
} }
if (!$merged->plural || ($translation->plural && $override)) { /**
$merged->plural = $translation->plural; * Removes all flags.
*
* @return self
*/
public function deleteFlags()
{
$this->flags = [];
return $this;
} }
if (empty($merged->pluralTranslations) || (!empty($translation->pluralTranslations) && $override)) { /**
$merged->pluralTranslations = $translation->pluralTranslations; * Merges this translation with other translation.
} *
* @param Translation $translation The translation to merge with
* @param int $options
*
* @return self
*/
public function mergeWith(Translation $translation, $options = Merge::DEFAULTS)
{
Merge::mergeTranslation($translation, $this, $options);
Merge::mergeReferences($translation, $this, $options);
Merge::mergeComments($translation, $this, $options);
Merge::mergeExtractedComments($translation, $this, $options);
Merge::mergeFlags($translation, $this, $options);
$merged->disable($translation->isDisabled()); return $this;
return $merged;
} }
} }

View File

@ -1,216 +1,499 @@
<?php <?php
declare(strict_types = 1);
namespace Gettext; namespace Gettext;
use ArrayIterator;
use Countable;
use Gettext\Languages\Language; use Gettext\Languages\Language;
use BadMethodCallException;
use InvalidArgumentException; use InvalidArgumentException;
use IteratorAggregate; use ArrayObject;
use ReturnTypeWillChange;
/** /**
* Class to manage a collection of translations under the same domain. * Class to manage a collection of translations.
*
* @method static $this fromBladeFile(string $filename, array $options = [])
* @method static $this fromBladeString(string $string, array $options = [])
* @method $this addFromBladeFile(string $filename, array $options = [])
* @method $this addFromBladeString(string $string, array $options = [])
* @method static $this fromCsvFile(string $filename, array $options = [])
* @method static $this fromCsvString(string $string, array $options = [])
* @method $this addFromCsvFile(string $filename, array $options = [])
* @method $this addFromCsvString(string $string, array $options = [])
* @method bool toCsvFile(string $filename, array $options = [])
* @method string toCsvString(array $options = [])
* @method static $this fromCsvDictionaryFile(string $filename, array $options = [])
* @method static $this fromCsvDictionaryString(string $string, array $options = [])
* @method $this addFromCsvDictionaryFile(string $filename, array $options = [])
* @method $this addFromCsvDictionaryString(string $string, array $options = [])
* @method bool toCsvDictionaryFile(string $filename, array $options = [])
* @method string toCsvDictionaryString(array $options = [])
* @method static $this fromJedFile(string $filename, array $options = [])
* @method static $this fromJedString(string $string, array $options = [])
* @method $this addFromJedFile(string $filename, array $options = [])
* @method $this addFromJedString(string $string, array $options = [])
* @method bool toJedFile(string $filename, array $options = [])
* @method string toJedString(array $options = [])
* @method static $this fromJsCodeFile(string $filename, array $options = [])
* @method static $this fromJsCodeString(string $string, array $options = [])
* @method $this addFromJsCodeFile(string $filename, array $options = [])
* @method $this addFromJsCodeString(string $string, array $options = [])
* @method static $this fromJsonFile(string $filename, array $options = [])
* @method static $this fromJsonString(string $string, array $options = [])
* @method $this addFromJsonFile(string $filename, array $options = [])
* @method $this addFromJsonString(string $string, array $options = [])
* @method bool toJsonFile(string $filename, array $options = [])
* @method string toJsonString(array $options = [])
* @method static $this fromJsonDictionaryFile(string $filename, array $options = [])
* @method static $this fromJsonDictionaryString(string $string, array $options = [])
* @method $this addFromJsonDictionaryFile(string $filename, array $options = [])
* @method $this addFromJsonDictionaryString(string $string, array $options = [])
* @method bool toJsonDictionaryFile(string $filename, array $options = [])
* @method string toJsonDictionaryString(array $options = [])
* @method static $this fromMoFile(string $filename, array $options = [])
* @method static $this fromMoString(string $string, array $options = [])
* @method $this addFromMoFile(string $filename, array $options = [])
* @method $this addFromMoString(string $string, array $options = [])
* @method bool toMoFile(string $filename, array $options = [])
* @method string toMoString(array $options = [])
* @method static $this fromPhpArrayFile(string $filename, array $options = [])
* @method static $this fromPhpArrayString(string $string, array $options = [])
* @method $this addFromPhpArrayFile(string $filename, array $options = [])
* @method $this addFromPhpArrayString(string $string, array $options = [])
* @method bool toPhpArrayFile(string $filename, array $options = [])
* @method string toPhpArrayString(array $options = [])
* @method static $this fromPhpCodeFile(string $filename, array $options = [])
* @method static $this fromPhpCodeString(string $string, array $options = [])
* @method $this addFromPhpCodeFile(string $filename, array $options = [])
* @method $this addFromPhpCodeString(string $string, array $options = [])
* @method static $this fromPoFile(string $filename, array $options = [])
* @method static $this fromPoString(string $string, array $options = [])
* @method $this addFromPoFile(string $filename, array $options = [])
* @method $this addFromPoString(string $string, array $options = [])
* @method bool toPoFile(string $filename, array $options = [])
* @method string toPoString(array $options = [])
* @method static $this fromTwigFile(string $filename, array $options = [])
* @method static $this fromTwigString(string $string, array $options = [])
* @method $this addFromTwigFile(string $filename, array $options = [])
* @method $this addFromTwigString(string $string, array $options = [])
* @method static $this fromVueJsFile(string $filename, array $options = [])
* @method static $this fromVueJsString(string $filename, array $options = [])
* @method $this addFromVueJsFile(string $filename, array $options = [])
* @method $this addFromVueJsString(string $filename, array $options = [])
* @method static $this fromXliffFile(string $filename, array $options = [])
* @method static $this fromXliffString(string $string, array $options = [])
* @method $this addFromXliffFile(string $filename, array $options = [])
* @method $this addFromXliffString(string $string, array $options = [])
* @method bool toXliffFile(string $filename, array $options = [])
* @method string toXliffString(array $options = [])
* @method static $this fromYamlFile(string $filename, array $options = [])
* @method static $this fromYamlString(string $string, array $options = [])
* @method $this addFromYamlFile(string $filename, array $options = [])
* @method $this addFromYamlString(string $string, array $options = [])
* @method bool toYamlFile(string $filename, array $options = [])
* @method string toYamlString(array $options = [])
* @method static $this fromYamlDictionaryFile(string $filename, array $options = [])
* @method static $this fromYamlDictionaryString(string $string, array $options = [])
* @method $this addFromYamlDictionaryFile(string $filename, array $options = [])
* @method $this addFromYamlDictionaryString(string $string, array $options = [])
* @method bool toYamlDictionaryFile(string $filename, array $options = [])
* @method string toYamlDictionaryString(array $options = [])
*/ */
class Translations implements Countable, IteratorAggregate class Translations extends ArrayObject
{ {
protected $description; const HEADER_LANGUAGE = 'Language';
protected $translations = []; const HEADER_PLURAL = 'Plural-Forms';
const HEADER_DOMAIN = 'X-Domain';
public static $options = [
'defaultHeaders' => [
'Project-Id-Version' => '',
'Report-Msgid-Bugs-To' => '',
'Last-Translator' => '',
'Language-Team' => '',
'MIME-Version' => '1.0',
'Content-Type' => 'text/plain; charset=UTF-8',
'Content-Transfer-Encoding' => '8bit',
],
'headersSorting' => false,
'defaultDateHeaders' => [
'POT-Creation-Date',
'PO-Revision-Date',
],
];
protected $headers; protected $headers;
protected $flags;
public static function create(string $domain = null, string $language = null): Translations protected $translationClass;
/**
* @see ArrayObject::__construct()
*/
public function __construct(
$input = [],
$flags = 0,
$iterator_class = 'ArrayIterator',
$translationClass = 'Gettext\Translation'
) {
$this->headers = static::$options['defaultHeaders'];
foreach (static::$options['defaultDateHeaders'] as $header) {
$this->headers[$header] = date('c');
}
$this->headers[self::HEADER_LANGUAGE] = '';
$this->translationClass = $translationClass;
parent::__construct($input, $flags, $iterator_class);
}
/**
* Magic method to create new instances using extractors
* For example: Translations::fromMoFile($filename, $options);.
*
* @return Translations
*/
public static function __callStatic($name, $arguments)
{ {
$translations = new static(); if (!preg_match('/^from(\w+)(File|String)$/i', $name, $matches)) {
throw new BadMethodCallException("The method $name does not exists");
if (isset($domain)) {
$translations->setDomain($domain);
} }
if (isset($language)) { return call_user_func_array([new static(), 'add'.ucfirst($name)], $arguments);
$translations->setLanguage($language);
} }
return $translations; /**
} * Magic method to import/export the translations to a specific format
* For example: $translations->toMoFile($filename, $options);
protected function __construct() * For example: $translations->addFromMoFile($filename, $options);.
*
* @return self|bool
*/
public function __call($name, $arguments)
{ {
$this->headers = new Headers(); if (!preg_match('/^(addFrom|to)(\w+)(File|String)$/i', $name, $matches)) {
$this->flags = new Flags(); throw new BadMethodCallException("The method $name does not exists");
} }
public function __clone() if ($matches[1] === 'addFrom') {
{ $extractor = 'Gettext\\Extractors\\'.$matches[2].'::from'.$matches[3];
foreach ($this->translations as $id => $translation) { $source = array_shift($arguments);
$this->translations[$id] = clone $translation; $options = array_shift($arguments) ?: [];
}
$this->headers = clone $this->headers; call_user_func($extractor, $source, $this, $options);
}
public function setDescription(?string $description): self
{
$this->description = $description;
return $this; return $this;
} }
public function getDescription(): ?string $generator = 'Gettext\\Generators\\'.$matches[2].'::to'.$matches[3];
{
return $this->description; array_unshift($arguments, $this);
return call_user_func_array($generator, $arguments);
} }
public function getFlags(): Flags /**
* Magic method to clone each translation on clone the translations object.
*/
public function __clone()
{ {
return $this->flags; $array = [];
foreach ($this as $key => $translation) {
$array[$key] = clone $translation;
} }
public function toArray(): array $this->exchangeArray($array);
{
return [
'description' => $this->description,
'headers' => $this->headers->toArray(),
'flags' => $this->flags->toArray(),
'translations' => array_map(
function ($translation) {
return $translation->toArray();
},
array_values($this->translations)
),
];
} }
#[ReturnTypeWillChange] /**
public function getIterator() * Control the new translations added.
*
* @param mixed $index
* @param Translation $value
*
* @throws InvalidArgumentException If the value is not an instance of Gettext\Translation
*
* @return Translation
*/
#[\ReturnTypeWillChange]
public function offsetSet($index, $value)
{ {
return new ArrayIterator($this->translations); if (!($value instanceof Translation)) {
throw new InvalidArgumentException(
'Only instances of Gettext\\Translation must be added to a Gettext\\Translations'
);
} }
public function getTranslations(): array $id = $value->getId();
{
return $this->translations; if ($this->offsetExists($id)) {
$this[$id]->mergeWith($value);
return $this[$id];
} }
public function count(): int parent::offsetSet($id, $value);
{
return count($this->translations); return $value;
} }
public function getHeaders(): Headers /**
* Set the plural definition.
*
* @param int $count
* @param string $rule
*
* @return self
*/
public function setPluralForms($count, $rule)
{ {
if (preg_match('/[a-z]/i', str_replace('n', '', $rule))) {
throw new \InvalidArgumentException('Invalid Plural form: ' . $rule);
}
$this->setHeader(self::HEADER_PLURAL, "nplurals={$count}; plural={$rule};");
return $this;
}
/**
* Returns the parsed plural definition.
*
* @param null|array [count, rule]
*/
public function getPluralForms()
{
$header = $this->getHeader(self::HEADER_PLURAL);
if (!empty($header)
&& preg_match('/^nplurals\s*=\s*(\d+)\s*;\s*plural\s*=\s*([^;]+)\s*;$/', $header, $matches)
) {
return [intval($matches[1]), $matches[2]];
}
}
/**
* Set a new header.
*
* @param string $name
* @param string $value
*
* @return self
*/
public function setHeader($name, $value)
{
$name = trim($name);
$this->headers[$name] = trim(isset($value) ? $value : '');
return $this;
}
/**
* Returns a header value.
*
* @param string $name
*
* @return null|string
*/
public function getHeader($name)
{
return isset($this->headers[$name]) ? $this->headers[$name] : null;
}
/**
* Returns all header for this translations (in alphabetic order).
*
* @return array
*/
public function getHeaders()
{
if (static::$options['headersSorting']) {
ksort($this->headers);
}
return $this->headers; return $this->headers;
} }
public function add(Translation $translation): self /**
* Removes all headers.
*
* @return self
*/
public function deleteHeaders()
{ {
$id = $translation->getId(); $this->headers = [];
$this->translations[$id] = $translation;
return $this; return $this;
} }
public function addOrMerge(Translation $translation, int $mergeStrategy = 0): Translation /**
* Removes one header.
*
* @param string $name
*
* @return self
*/
public function deleteHeader($name)
{ {
$id = $translation->getId(); unset($this->headers[$name]);
if (isset($this->translations[$id])) {
return $this->translations[$id] = $this->translations[$id]->mergeWith($translation, $mergeStrategy);
}
return $this->translations[$id] = $translation;
}
public function remove(Translation $translation): self
{
$key = array_search($translation, $this->translations);
if ($key !== false) {
unset($this->translations[$key]);
}
return $this; return $this;
} }
public function setDomain(string $domain): self /**
* Returns the language value.
*
* @return string $language
*/
public function getLanguage()
{ {
$this->getHeaders()->setDomain($domain); return $this->getHeader(self::HEADER_LANGUAGE);
return $this;
} }
public function getDomain(): ?string /**
* Sets the language and the plural forms.
*
* @param string $language
*
* @throws InvalidArgumentException if the language hasn't been recognized
*
* @return self
*/
public function setLanguage($language)
{ {
return $this->getHeaders()->getDomain(); $this->setHeader(self::HEADER_LANGUAGE, trim($language));
if (($info = Language::getById($language))) {
return $this->setPluralForms(count($info->categories), $info->formula);
} }
public function setLanguage(string $language): self
{
$info = Language::getById($language);
if (empty($info)) {
throw new InvalidArgumentException(sprintf('The language "%s" is not valid', $language)); throw new InvalidArgumentException(sprintf('The language "%s" is not valid', $language));
} }
$this->getHeaders() /**
->setLanguage($language) * Checks whether the language is empty or not.
->setPluralForm(count($info->categories), $info->formula); *
* @return bool
*/
public function hasLanguage()
{
$language = $this->getLanguage();
return (is_string($language) && ($language !== '')) ? true : false;
}
/**
* Set a new domain for this translations.
*
* @param string $domain
*
* @return self
*/
public function setDomain($domain)
{
$this->setHeader(self::HEADER_DOMAIN, trim($domain));
return $this; return $this;
} }
public function getLanguage(): ?string /**
* Returns the domain.
*
* @return string
*/
public function getDomain()
{ {
return $this->getHeaders()->getLanguage(); return $this->getHeader(self::HEADER_DOMAIN);
} }
public function find(?string $context, string $original): ?Translation /**
* Checks whether the domain is empty or not.
*
* @return bool
*/
public function hasDomain()
{ {
foreach ($this->translations as $translation) { $domain = $this->getDomain();
if ($translation->getContext() === $context && $translation->getOriginal() === $original) {
return $translation; return (is_string($domain) && ($domain !== '')) ? true : false;
}
} }
return null; /**
} * Search for a specific translation.
*
public function mergeWith(Translations $translations, int $strategy = 0): Translations * @param string|Translation $context The context of the translation or a translation instance
* @param string $original The original string
* @warning Translations with custom identifiers (e.g. XLIFF unit IDs) cannot be found using this function.
*
* @return Translation|false
*/
public function find($context, $original = '')
{ {
$merged = clone $this; if ($context instanceof Translation) {
$id = $context->getId();
if ($strategy & Merge::HEADERS_THEIRS) { } else {
$merged->headers = clone $translations->headers; $id = Translation::generateId($context, $original);
} elseif (!($strategy & Merge::HEADERS_OURS)) {
$merged->headers = $merged->headers->mergeWith($translations->headers);
} }
if ($strategy & Merge::FLAGS_THEIRS) { return $this->offsetExists($id) ? $this[$id] : false;
$merged->flags = clone $translations->flags;
} elseif (!($strategy & Merge::FLAGS_OURS)) {
$merged->flags = $merged->flags->mergeWith($translations->flags);
} }
if (!$merged->description) { /**
$merged->description = $translations->description; * Count all elements translated
*
* @return integer
*/
public function countTranslated()
{
$c = 0;
foreach ($this as $v) {
if ($v->hasTranslation()) {
$c++;
}
}
return $c;
} }
foreach ($translations as $id => $translation) { /**
if (isset($merged->translations[$id])) { * Creates and insert/merges a new translation.
$translation = $merged->translations[$id]->mergeWith($translation, $strategy); *
* @param string $context The translation context
* @param string $original The translation original string
* @param string $plural The translation original plural string
*
* @return Translation The translation created
*/
public function insert($context, $original, $plural = '')
{
return $this->offsetSet(null, $this->createNewTranslation($context, $original, $plural));
} }
$merged->add($translation); /**
* Merges this translations with other translations.
*
* @param Translations $translations The translations instance to merge with
* @param int $options
*
* @return self
*/
public function mergeWith(Translations $translations, $options = Merge::DEFAULTS)
{
Merge::mergeHeaders($translations, $this, $options);
Merge::mergeTranslations($translations, $this, $options);
return $this;
} }
if ($strategy & Merge::TRANSLATIONS_THEIRS) { /**
$merged->translations = array_intersect_key($merged->translations, $translations->translations); * Create a new instance of a Translation object.
} elseif ($strategy & Merge::TRANSLATIONS_OURS) { *
$merged->translations = array_intersect_key($merged->translations, $this->translations); * @param string $context The context of the translation
} * @param string $original The original string
* @param string $plural The original plural string
return $merged; * @return Translation New Translation instance
*/
public function createNewTranslation($context, $original, $plural = '')
{
$class = $this->translationClass;
return $class::create($context, $original, $plural);
} }
} }

View File

@ -0,0 +1,270 @@
<?php
namespace Gettext;
use Gettext\Generators\PhpArray;
class Translator extends BaseTranslator implements TranslatorInterface
{
protected $domain;
protected $dictionary = [];
protected $plurals = [];
/**
* Loads translation from a Translations instance, a file on an array.
*
* @param Translations|string|array $translations
*
* @return static
*/
public function loadTranslations($translations)
{
if (is_object($translations) && $translations instanceof Translations) {
$translations = PhpArray::generate($translations, ['includeHeaders' => false]);
} elseif (is_string($translations) && is_file($translations)) {
$translations = include $translations;
} elseif (!is_array($translations)) {
throw new \InvalidArgumentException(
'Invalid Translator: only arrays, files or instance of Translations are allowed'
);
}
$this->addTranslations($translations);
return $this;
}
/**
* Set the default domain.
*
* @param string $domain
*
* @return static
*/
public function defaultDomain($domain)
{
$this->domain = $domain;
return $this;
}
/**
* @see TranslatorInterface
*
* {@inheritdoc}
*/
public function gettext($original)
{
return $this->dpgettext($this->domain, null, $original);
}
/**
* @see TranslatorInterface
*
* {@inheritdoc}
*/
public function ngettext($original, $plural, $value)
{
return $this->dnpgettext($this->domain, null, $original, $plural, $value);
}
/**
* @see TranslatorInterface
*
* {@inheritdoc}
*/
public function dngettext($domain, $original, $plural, $value)
{
return $this->dnpgettext($domain, null, $original, $plural, $value);
}
/**
* @see TranslatorInterface
*
* {@inheritdoc}
*/
public function npgettext($context, $original, $plural, $value)
{
return $this->dnpgettext($this->domain, $context, $original, $plural, $value);
}
/**
* @see TranslatorInterface
*
* {@inheritdoc}
*/
public function pgettext($context, $original)
{
return $this->dpgettext($this->domain, $context, $original);
}
/**
* @see TranslatorInterface
*
* {@inheritdoc}
*/
public function dgettext($domain, $original)
{
return $this->dpgettext($domain, null, $original);
}
/**
* @see TranslatorInterface
*
* {@inheritdoc}
*/
public function dpgettext($domain, $context, $original)
{
$translation = $this->getTranslation($domain, $context, $original);
if (isset($translation[0]) && $translation[0] !== '') {
return $translation[0];
}
return $original;
}
/**
* @see TranslatorInterface
*
* {@inheritdoc}
*/
public function dnpgettext($domain, $context, $original, $plural, $value)
{
$translation = $this->getTranslation($domain, $context, $original);
$key = $this->getPluralIndex($domain, $value, $translation === false);
if (isset($translation[$key]) && $translation[$key] !== '') {
return $translation[$key];
}
return ($key === 0) ? $original : $plural;
}
/**
* Set new translations to the dictionary.
*
* @param array $translations
*/
protected function addTranslations(array $translations)
{
$domain = isset($translations['domain']) ? $translations['domain'] : '';
//Set the first domain loaded as default domain
if ($this->domain === null) {
$this->domain = $domain;
}
if (isset($this->dictionary[$domain])) {
$this->dictionary[$domain] = array_replace_recursive($this->dictionary[$domain], $translations['messages']);
return;
}
if (!empty($translations['plural-forms'])) {
list($count, $code) = array_map('trim', explode(';', $translations['plural-forms'], 2));
// extract just the expression turn 'n' into a php variable '$n'.
// Slap on a return keyword and semicolon at the end.
$this->plurals[$domain] = [
'count' => (int) str_replace('nplurals=', '', $count),
'code' => str_replace('plural=', 'return ', str_replace('n', '$n', $code)).';',
];
}
$this->dictionary[$domain] = $translations['messages'];
}
/**
* Search and returns a translation.
*
* @param string $domain
* @param string $context
* @param string $original
*
* @return string|false
*/
protected function getTranslation($domain, $context, $original)
{
return isset($this->dictionary[$domain][$context][$original])
? $this->dictionary[$domain][$context][$original]
: false;
}
/**
* Executes the plural decision code given the number to decide which
* plural version to take.
*
* @param string $domain
* @param string $n
* @param bool $fallback set to true to get fallback plural index
*
* @return int
*/
protected function getPluralIndex($domain, $n, $fallback)
{
//Not loaded domain or translation, use a fallback
if (!isset($this->plurals[$domain]) || $fallback === true) {
return $n == 1 ? 0 : 1;
}
if (!isset($this->plurals[$domain]['function'])) {
$code = static::fixTerseIfs($this->plurals[$domain]['code']);
$this->plurals[$domain]['function'] = eval("return function (\$n) { $code };");
}
if ($this->plurals[$domain]['count'] <= 2) {
return call_user_func($this->plurals[$domain]['function'], $n) ? 1 : 0;
}
return call_user_func($this->plurals[$domain]['function'], $n);
}
/**
* This function will recursively wrap failure states in brackets if they contain a nested terse if.
*
* This because PHP can not handle nested terse if's unless they are wrapped in brackets.
*
* This code probably only works for the gettext plural decision codes.
*
* return ($n==1 ? 0 : $n%10>=2 && $n%10<=4 && ($n%100<10 || $n%100>=20) ? 1 : 2);
* becomes
* return ($n==1 ? 0 : ($n%10>=2 && $n%10<=4 && ($n%100<10 || $n%100>=20) ? 1 : 2));
*
* @param string $code the terse if string
* @param bool $inner If inner is true we wrap it in brackets
*
* @return string A formatted terse If that PHP can work with.
*/
private static function fixTerseIfs($code, $inner = false)
{
/*
* (?P<expression>[^?]+) Capture everything up to ? as 'expression'
* \? ?
* (?P<success>[^:]+) Capture everything up to : as 'success'
* : :
* (?P<failure>[^;]+) Capture everything up to ; as 'failure'
*/
preg_match('/(?P<expression>[^?]+)\?(?P<success>[^:]+):(?P<failure>[^;]+)/', $code, $matches);
// If no match was found then no terse if was present
if (!isset($matches[0])) {
return $code;
}
$expression = $matches['expression'];
$success = $matches['success'];
$failure = $matches['failure'];
// Go look for another terse if in the failure state.
$failure = static::fixTerseIfs($failure, true);
$code = $expression.' ? '.$success.' : '.$failure;
if ($inner) {
return "($code)";
}
// note the semicolon. We need that for executing the code.
return "$code;";
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace Gettext;
/**
* Interface used by all translators.
*/
interface TranslatorInterface
{
/**
* Register this translator as global, to use with the gettext functions __(), p__(), etc.
* Returns the previous translator if exists.
*
* @return TranslatorInterface|null
*/
public function register();
/**
* Noop, marks the string for translation but returns it unchanged.
*
* @param string $original
*
* @return string
*/
public function noop($original);
/**
* Gets a translation using the original string.
*
* @param string $original
*
* @return string
*/
public function gettext($original);
/**
* Gets a translation checking the plural form.
*
* @param string $original
* @param string $plural
* @param string $value
*
* @return string
*/
public function ngettext($original, $plural, $value);
/**
* Gets a translation checking the domain and the plural form.
*
* @param string $domain
* @param string $original
* @param string $plural
* @param string $value
*
* @return string
*/
public function dngettext($domain, $original, $plural, $value);
/**
* Gets a translation checking the context and the plural form.
*
* @param string $context
* @param string $original
* @param string $plural
* @param string $value
*
* @return string
*/
public function npgettext($context, $original, $plural, $value);
/**
* Gets a translation checking the context.
*
* @param string $context
* @param string $original
*
* @return string
*/
public function pgettext($context, $original);
/**
* Gets a translation checking the domain.
*
* @param string $domain
* @param string $original
*
* @return string
*/
public function dgettext($domain, $original);
/**
* Gets a translation checking the domain and context.
*
* @param string $domain
* @param string $context
* @param string $original
*
* @return string
*/
public function dpgettext($domain, $context, $original);
/**
* Gets a translation checking the domain, the context and the plural form.
*
* @param string $domain
* @param string $context
* @param string $original
* @param string $plural
* @param string $value
*/
public function dnpgettext($domain, $context, $original, $plural, $value);
}

View File

@ -0,0 +1,56 @@
<?php
namespace Gettext\Utils;
/*
* Trait to provide the functionality of read/write csv.
*/
trait CsvTrait
{
protected static $csvEscapeChar;
/**
* Check whether support the escape_char argument to fgetcsv/fputcsv or not
*
* @return bool
*/
protected static function supportsCsvEscapeChar()
{
if (static::$csvEscapeChar === null) {
static::$csvEscapeChar = version_compare(PHP_VERSION, '5.5.4') >= 0;
}
return static::$csvEscapeChar;
}
/**
* @param resource $handle
* @param array $options
*
* @return array
*/
protected static function fgetcsv($handle, $options)
{
if (static::supportsCsvEscapeChar()) {
return fgetcsv($handle, 0, $options['delimiter'], $options['enclosure'], $options['escape_char']);
}
return fgetcsv($handle, 0, $options['delimiter'], $options['enclosure']);
}
/**
* @param resource $handle
* @param array $fields
* @param array $options
*
* @return bool|int
*/
protected static function fputcsv($handle, $fields, $options)
{
if (static::supportsCsvEscapeChar()) {
return fputcsv($handle, $fields, $options['delimiter'], $options['enclosure'], $options['escape_char']);
}
return fputcsv($handle, $fields, $options['delimiter'], $options['enclosure']);
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace Gettext\Utils;
use Gettext\Translations;
/**
* Trait used by all generators that exports the translations to plain dictionary (original => singular-translation).
*/
trait DictionaryTrait
{
use HeadersGeneratorTrait;
use HeadersExtractorTrait;
/**
* Returns a plain dictionary with the format [original => translation].
*
* @param Translations $translations
* @param bool $includeHeaders
*
* @return array
*/
protected static function toArray(Translations $translations, $includeHeaders)
{
$messages = [];
if ($includeHeaders) {
$messages[''] = static::generateHeaders($translations);
}
foreach ($translations as $translation) {
if ($translation->isDisabled()) {
continue;
}
$messages[$translation->getOriginal()] = $translation->getTranslation();
}
return $messages;
}
/**
* Extract the entries from a dictionary.
*
* @param array $messages
* @param Translations $translations
*/
protected static function fromArray(array $messages, Translations $translations)
{
foreach ($messages as $original => $translation) {
if ($original === '') {
static::extractHeaders($translation, $translations);
continue;
}
$translations->insert(null, $original)->setTranslation($translation);
}
}
}

View File

@ -0,0 +1,181 @@
<?php
namespace Gettext\Utils;
use Exception;
use Gettext\Translations;
abstract class FunctionsScanner
{
/**
* Scan and returns the functions and the arguments.
*
* @param array $constants Constants used in the code to replace
*
* @return array
*/
abstract public function getFunctions(array $constants = []);
/**
* Search for specific functions and create translations.
*
* You can pass multiple translation with different domains and value found will be sorted respectively.
*
* @param Translations|Translations[] $translations Multiple domain translations instances where to save the values
* @param array $options The extractor options
* @throws Exception
*/
public function saveGettextFunctions($translations, array $options)
{
$translations = is_array($translations) ? $translations : [$translations];
/** @var Translations[] $translationByDomain [domain => translations, ..] */
$translationByDomain = array_reduce($translations, function ($carry, Translations $translations) {
$carry[$translations->getDomain()] = $translations;
return $carry;
}, []);
$functions = $options['functions'];
$file = $options['file'];
/**
* List of source code comments already associated with a function.
*
* Prevents associating the same comment to multiple functions.
*
* @var ParsedComment[] $commentsCache
*/
$commentsCache = [];
foreach ($this->getFunctions($options['constants']) as $function) {
list($name, $line, $args) = $function;
if (isset($options['lineOffset'])) {
$line += $options['lineOffset'];
}
if (!isset($functions[$name])) {
continue;
}
$deconstructed = $this->deconstructArgs($functions[$name], $args);
if (!$deconstructed) {
continue;
}
list($domain, $context, $original, $plural) = $deconstructed;
if ((string)$original === '') {
continue;
}
$isDefaultDomain = $domain === null;
$domainTranslations = isset($translationByDomain[$domain]) ? $translationByDomain[$domain] : false;
if (!empty($options['domainOnly']) && $isDefaultDomain) {
// If we want to find translations for a specific domain, skip default domain messages
continue;
}
if (!$domainTranslations) {
continue;
}
$translation = $domainTranslations->insert($context, $original, $plural);
$translation->addReference($file, $line);
if (isset($function[3])) {
/* @var ParsedComment $extractedComment */
foreach ($function[3] as $extractedComment) {
if (in_array($extractedComment, $commentsCache, true)) {
continue;
}
$translation->addExtractedComment($extractedComment->getComment());
$commentsCache[] = $extractedComment;
}
}
}
}
/**
* Deconstruct arguments to translation values
*
* @param $function
* @param $args
* @return array|null
* @throws Exception
*/
protected function deconstructArgs($function, $args)
{
$domain = null;
$context = null;
$original = null;
$plural = null;
switch ($function) {
case 'noop':
case 'gettext':
if (!isset($args[0])) {
return null;
}
$original = $args[0];
break;
case 'ngettext':
if (!isset($args[1])) {
return null;
}
list($original, $plural) = $args;
break;
case 'pgettext':
if (!isset($args[1])) {
return null;
}
list($context, $original) = $args;
break;
case 'dgettext':
if (!isset($args[1])) {
return null;
}
list($domain, $original) = $args;
break;
case 'dpgettext':
if (!isset($args[2])) {
return null;
}
list($domain, $context, $original) = $args;
break;
case 'npgettext':
if (!isset($args[2])) {
return null;
}
list($context, $original, $plural) = $args;
break;
case 'dnpgettext':
if (!isset($args[3])) {
return null;
}
list($domain, $context, $original, $plural) = $args;
break;
case 'dngettext':
if (!isset($args[2])) {
return null;
}
list($domain, $original, $plural) = $args;
break;
default:
throw new Exception(sprintf('Not valid function %s', $function));
}
return [$domain, $context, $original, $plural];
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace Gettext\Utils;
use Gettext\Translations;
/**
* Trait to provide the functionality of extracting headers.
*/
trait HeadersExtractorTrait
{
/**
* Add the headers found to the translations instance.
*
* @param string $headers
* @param Translations $translations
*
* @return array
*/
protected static function extractHeaders($headers, Translations $translations)
{
$headers = explode("\n", $headers);
$currentHeader = null;
foreach ($headers as $line) {
$line = static::convertString($line);
if ($line === '') {
continue;
}
if (static::isHeaderDefinition($line)) {
$header = array_map('trim', explode(':', $line, 2));
$currentHeader = $header[0];
$translations->setHeader($currentHeader, $header[1]);
} else {
$entry = $translations->getHeader($currentHeader);
$translations->setHeader($currentHeader, $entry.$line);
}
}
}
/**
* Checks if it is a header definition line. Useful for distguishing between header definitions
* and possible continuations of a header entry.
*
* @param string $line Line to parse
*
* @return bool
*/
protected static function isHeaderDefinition($line)
{
return (bool) preg_match('/^[\w-]+:/', $line);
}
/**
* Normalize a string.
*
* @param string $value
*
* @return string
*/
public static function convertString($value)
{
return $value;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Gettext\Utils;
use Gettext\Translations;
/**
* Trait to provide the functionality of extracting headers.
*/
trait HeadersGeneratorTrait
{
/**
* Returns the headers as a string.
*
* @param Translations $translations
*
* @return string
*/
protected static function generateHeaders(Translations $translations)
{
$headers = '';
foreach ($translations->getHeaders() as $name => $value) {
$headers .= sprintf("%s: %s\n", $name, $value);
}
return $headers;
}
}

View File

@ -0,0 +1,320 @@
<?php
namespace Gettext\Utils;
class JsFunctionsScanner extends FunctionsScanner
{
protected $code;
protected $status = [];
/**
* Constructor.
*
* @param string $code The php code to scan
*/
public function __construct($code)
{
// Normalize newline characters
$this->code = str_replace(["\r\n", "\n\r", "\r"], "\n", $code);
}
/**
* {@inheritdoc}
*/
public function getFunctions(array $constants = [])
{
$length = strlen($this->code);
$line = 1;
$buffer = '';
$functions = [];
$bufferFunctions = [];
$char = null;
for ($pos = 0; $pos < $length; ++$pos) {
$prev = $char;
$char = $this->code[$pos];
$next = isset($this->code[$pos + 1]) ? $this->code[$pos + 1] : null;
switch ($char) {
case '\\':
switch ($this->status()) {
case 'simple-quote':
if ($next !== "'") {
break 2;
}
break;
case 'double-quote':
if ($next !== '"') {
break 2;
}
break;
case 'back-tick':
if ($next !== '`') {
break 2;
}
break;
}
$prev = $char;
$char = $next;
$pos++;
$next = isset($this->code[$pos]) ? $this->code[$pos] : null;
break;
case "\n":
++$line;
if ($this->status('line-comment')) {
$this->upStatus();
}
break;
case '/':
switch ($this->status()) {
case 'simple-quote':
case 'double-quote':
case 'back-tick':
case 'line-comment':
break;
case 'block-comment':
if ($prev === '*') {
$this->upStatus();
}
break;
default:
if ($next === '/') {
$this->downStatus('line-comment');
} elseif ($next === '*') {
$this->downStatus('block-comment');
}
break;
}
break;
case "'":
switch ($this->status()) {
case 'simple-quote':
$this->upStatus();
break;
case 'line-comment':
case 'block-comment':
case 'double-quote':
case 'back-tick':
break;
default:
$this->downStatus('simple-quote');
break;
}
break;
case '"':
switch ($this->status()) {
case 'double-quote':
$this->upStatus();
break;
case 'line-comment':
case 'block-comment':
case 'simple-quote':
case 'back-tick':
break;
default:
$this->downStatus('double-quote');
break;
}
break;
case '`':
switch ($this->status()) {
case 'back-tick':
$this->upStatus();
break;
case 'line-comment':
case 'block-comment':
case 'simple-quote':
case 'double-quote':
break;
default:
$this->downStatus('back-tick');
break;
}
break;
case '(':
switch ($this->status()) {
case 'simple-quote':
case 'double-quote':
case 'back-tick':
case 'line-comment':
case 'block-comment':
break;
default:
if ($buffer && preg_match('/(\w+)$/', $buffer, $matches)) {
$this->downStatus('function');
array_unshift($bufferFunctions, [$matches[1], $line, []]);
$buffer = '';
continue 3;
}
break;
}
break;
case ')':
switch ($this->status()) {
case 'function':
if (($argument = static::prepareArgument($buffer))) {
$bufferFunctions[0][2][] = $argument;
}
if (!empty($bufferFunctions)) {
$functions[] = array_shift($bufferFunctions);
}
$this->upStatus();
$buffer = '';
continue 3;
}
break;
case ',':
switch ($this->status()) {
case 'function':
if (($argument = static::prepareArgument($buffer))) {
$bufferFunctions[0][2][] = $argument;
}
$buffer = '';
continue 3;
}
break;
case ' ':
case '\t':
switch ($this->status()) {
case 'double-quote':
case 'simple-quote':
case 'back-tick':
break;
default:
$buffer = '';
continue 3;
}
break;
}
switch ($this->status()) {
case 'line-comment':
case 'block-comment':
break;
default:
$buffer .= $char;
break;
}
}
return $functions;
}
/**
* Get the current context of the scan.
*
* @param null|string $match To check whether the current status is this value
*
* @return string|bool
*/
protected function status($match = null)
{
$status = isset($this->status[0]) ? $this->status[0] : null;
if ($match !== null) {
return $status === $match;
}
return $status;
}
/**
* Add a new status to the stack.
*
* @param string $status
*/
protected function downStatus($status)
{
array_unshift($this->status, $status);
}
/**
* Removes and return the current status.
*
* @return string|null
*/
protected function upStatus()
{
return array_shift($this->status);
}
/**
* Prepares the arguments found in functions.
*
* @param string $argument
*
* @return string
*/
protected static function prepareArgument($argument)
{
if ($argument && in_array($argument[0], ['"', "'", '`'], true)) {
return static::convertString(substr($argument, 1, -1));
}
}
/**
* Decodes a string with an argument.
*
* @param string $value
*
* @return string
*/
protected static function convertString($value)
{
if (strpos($value, '\\') === false) {
return $value;
}
return preg_replace_callback(
'/\\\(n|r|t|v|e|f|"|\\\)/',
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 '\\';
}
},
$value
);
}
}

View File

@ -0,0 +1,100 @@
<?php
namespace Gettext\Utils;
use Gettext\Translations;
/**
* Trait used by all generators that exports the translations to multidimensional arrays
* (context => [original => [translation, plural1, pluraln...]]).
*/
trait MultidimensionalArrayTrait
{
use HeadersGeneratorTrait;
use HeadersExtractorTrait;
/**
* Returns a multidimensional array.
*
* @param Translations $translations
* @param bool $includeHeaders
* @param bool $forceArray
*
* @return array
*/
protected static function toArray(Translations $translations, $includeHeaders, $forceArray = false)
{
$pluralForm = $translations->getPluralForms();
$pluralSize = is_array($pluralForm) ? ($pluralForm[0] - 1) : null;
$messages = [];
if ($includeHeaders) {
$messages[''] = [
'' => [static::generateHeaders($translations)],
];
}
foreach ($translations as $translation) {
if ($translation->isDisabled()) {
continue;
}
$context = $translation->getContext();
$original = $translation->getOriginal();
if (!isset($messages[$context])) {
$messages[$context] = [];
}
if ($translation->hasPluralTranslations(true)) {
$messages[$context][$original] = $translation->getPluralTranslations($pluralSize);
array_unshift($messages[$context][$original], $translation->getTranslation());
} elseif ($forceArray) {
$messages[$context][$original] = [$translation->getTranslation()];
} else {
$messages[$context][$original] = $translation->getTranslation();
}
}
return [
'domain' => $translations->getDomain(),
'plural-forms' => $translations->getHeader('Plural-Forms'),
'messages' => $messages,
];
}
/**
* Extract the entries from a multidimensional array.
*
* @param array $messages
* @param Translations $translations
*/
protected static function fromArray(array $messages, Translations $translations)
{
if (!empty($messages['domain'])) {
$translations->setDomain($messages['domain']);
}
if (!empty($messages['plural-forms'])) {
$translations->setHeader(Translations::HEADER_PLURAL, $messages['plural-forms']);
}
foreach ($messages['messages'] as $context => $contextTranslations) {
foreach ($contextTranslations as $original => $value) {
if ($context === '' && $original === '') {
static::extractHeaders(is_array($value) ? array_shift($value) : $value, $translations);
continue;
}
$translation = $translations->insert($context, $original);
if (is_array($value)) {
$translation->setTranslation(array_shift($value));
$translation->setPluralTranslations($value);
} else {
$translation->setTranslation($value);
}
}
}
}
}

View File

@ -0,0 +1,140 @@
<?php
namespace Gettext\Utils;
/**
* Comment parsed by PhpFunctionsScanner.
*/
class ParsedComment
{
/**
* The comment itself.
*
* @var string
*/
protected $comment;
/**
* The line where the comment starts.
*
* @var int
*/
protected $firstLine;
/**
* The line where the comment ends.
*
* @var int
*/
protected $lastLine;
/**
* Initializes the instance.
*
* @param string $comment The comment itself.
* @param int $firstLine The line where the comment starts.
* @param int $lastLine The line where the comment ends.
*/
public function __construct($comment, $firstLine, $lastLine)
{
$this->comment = $comment;
$this->firstLine = $firstLine;
$this->lastLine = $lastLine;
}
/**
* Create new object from raw comment data.
*
* @param string $value The PHP comment string.
* @param int $line The line where the comment starts.
*
* @return static The parsed comment.
*/
public static function create($value, $line)
{
$lastLine = $line + substr_count($value, "\n");
$lines = array_map(function ($line) {
if ('' === trim($line)) {
return null;
}
$line = ltrim($line, "#*/ \t");
$line = rtrim($line, "#*/ \t");
return trim($line);
}, explode("\n", $value));
// Remove empty lines.
$lines = array_filter($lines);
$value = implode(' ', $lines);
return new static($value, $line, $lastLine);
}
/**
* Return the line where the comment starts.
*
* @return int Line number.
*/
public function getFirstLine()
{
return $this->firstLine;
}
/**
* Return the line where the comment ends.
*
* @return int Line number.
*/
public function getLastLine()
{
return $this->lastLine;
}
/**
* Return the actual comment string.
*
* @return string The comment.
*/
public function getComment()
{
return $this->comment;
}
/**
* Whether this comment is related with a given function.
*
* @param ParsedFunction $function The function to check.
* @return bool Whether the comment is related or not.
*/
public function isRelatedWith(ParsedFunction $function)
{
return $this->getLastLine() === $function->getLine() || $this->getLastLine() === $function->getLine() - 1;
}
/**
* Whether the comment matches the required prefixes.
*
* @param array $prefixes An array of prefixes to check.
* @return bool Whether the comment matches the prefixes or not.
*/
public function checkPrefixes(array $prefixes)
{
if ('' === $this->comment) {
return false;
}
if (empty($prefixes)) {
return true;
}
foreach ($prefixes as $prefix) {
if (strpos($this->comment, $prefix) === 0) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,159 @@
<?php
namespace Gettext\Utils;
/**
* Function parsed by PhpFunctionsScanner.
*/
class ParsedFunction
{
/**
* The function name.
*
* @var string
*/
protected $name;
/**
* The line where the function starts.
*
* @var int
*/
protected $line;
/**
* The strings extracted from the function arguments.
*
* @var string[]
*/
protected $arguments;
/**
* The current index of the function (-1 if no arguments).
*
* @var int|null
*/
protected $argumentIndex;
/**
* Shall we stop adding string chunks to the current argument?
*
* @var bool
*/
protected $argumentStopped;
/**
* Extracted comments.
*
* @var ParsedComment[]|null
*/
protected $comments;
/**
* Initializes the instance.
*
* @param string $name The function name.
* @param int $line The line where the function starts.
*/
public function __construct($name, $line)
{
$this->name = $name;
$this->line = $line;
$this->arguments = [];
$this->argumentIndex = -1;
$this->argumentStopped = false;
$this->comments = null;
}
/**
* Stop extracting strings from the current argument (because we found something that's not a string).
*/
public function stopArgument()
{
if ($this->argumentIndex === -1) {
$this->argumentIndex = 0;
}
$this->argumentStopped = true;
}
/**
* Go to the next argument because we a comma was found.
*/
public function nextArgument()
{
if ($this->argumentIndex === -1) {
// This should neve occur, but let's stay safe - During test/development an Exception should be thrown.
$this->argumentIndex = 1;
} else {
++$this->argumentIndex;
}
$this->argumentStopped = false;
}
/**
* Add a string to the current argument.
*
* @param string|null $chunk
*/
public function addArgumentChunk($chunk)
{
if ($this->argumentStopped === false) {
if ($this->argumentIndex === -1) {
$this->argumentIndex = 0;
}
if (isset($this->arguments[$this->argumentIndex])) {
$this->arguments[$this->argumentIndex] .= $chunk;
} else {
$this->arguments[$this->argumentIndex] = $chunk;
}
}
}
/**
* Add a comment associated to this function.
*
* @param ParsedComment $comment
*/
public function addComment($comment)
{
if ($this->comments === null) {
$this->comments = [];
}
$this->comments[] = $comment;
}
/**
* Return the line the function starts.
*
* @return int Line number.
*/
public function getLine()
{
return $this->line;
}
/**
* A closing parenthesis was found: return the final data.
* The array returned has the following values:
* 0 => string The function name.
* 1 => int The line where the function starts.
* 2 => string[] the strings extracted from the function arguments.
* 3 => string[] the comments associated to the function.
*
* @return array
*/
public function close()
{
$arguments = [];
for ($i = 0; $i <= $this->argumentIndex; ++$i) {
$arguments[$i] = isset($this->arguments[$i]) ? $this->arguments[$i] : null;
}
return [
$this->name,
$this->line,
$arguments,
$this->comments,
];
}
}

View File

@ -0,0 +1,199 @@
<?php
namespace Gettext\Utils;
use Gettext\Extractors\PhpCode;
class PhpFunctionsScanner extends FunctionsScanner
{
/**
* PHP tokens of the code to be parsed.
*
* @var array
*/
protected $tokens;
/**
* If not false, comments will be extracted.
*
* @var string|false|array
*/
protected $extractComments = false;
/**
* Enable extracting comments that start with a tag (if $tag is empty all the comments will be extracted).
*
* @param mixed $tag
*/
public function enableCommentsExtraction($tag = '')
{
$this->extractComments = $tag;
}
/**
* Disable comments extraction.
*/
public function disableCommentsExtraction()
{
$this->extractComments = false;
}
/**
* Constructor.
*
* @param string $code The php code to scan
*/
public function __construct($code)
{
$this->tokens = array_values(
array_filter(
token_get_all($code),
function ($token) {
return !is_array($token) || $token[0] !== T_WHITESPACE;
}
)
);
}
/**
* {@inheritdoc}
*/
public function getFunctions(array $constants = [])
{
$count = count($this->tokens);
/* @var ParsedFunction[] $bufferFunctions */
$bufferFunctions = [];
/* @var ParsedComment[] $bufferComments */
$bufferComments = [];
/* @var array $functions */
$functions = [];
for ($k = 0; $k < $count; ++$k) {
$value = $this->tokens[$k];
if (is_string($value)) {
if (isset($bufferFunctions[0])) {
switch ($value) {
case ',':
$bufferFunctions[0]->nextArgument();
break;
case ')':
$functions[] = array_shift($bufferFunctions)->close();
break;
case '.':
break;
default:
$bufferFunctions[0]->stopArgument();
break;
}
}
continue;
}
if (defined('T_NAME_FULLY_QUALIFIED') && T_NAME_FULLY_QUALIFIED === $value[0]) {
$value[0] = T_STRING;
$value[1] = ltrim($value[1], '\\');
}
switch ($value[0]) {
case T_CONSTANT_ENCAPSED_STRING:
//add an argument to the current function
if (isset($bufferFunctions[0])) {
$bufferFunctions[0]->addArgumentChunk(PhpCode::convertString($value[1]));
}
break;
case T_STRING:
if (isset($bufferFunctions[0])) {
if (isset($constants[$value[1]])) {
$bufferFunctions[0]->addArgumentChunk($constants[$value[1]]);
break;
}
if (strtolower($value[1]) === 'null') {
$bufferFunctions[0]->addArgumentChunk(null);
break;
}
$bufferFunctions[0]->stopArgument();
}
//new function found
for ($j = $k + 1; $j < $count; ++$j) {
$nextToken = $this->tokens[$j];
if (is_array($nextToken) && $nextToken[0] === T_COMMENT) {
continue;
}
if ($nextToken === '(') {
$newFunction = new ParsedFunction($value[1], $value[2]);
// add comment that was on the line before.
if (isset($bufferComments[0])) {
$comment = $bufferComments[0];
if ($comment->isRelatedWith($newFunction)) {
$newFunction->addComment($comment);
}
}
array_unshift($bufferFunctions, $newFunction);
$k = $j;
}
break;
}
break;
case T_COMMENT:
$comment = $this->parsePhpComment($value[1], $value[2]);
if ($comment) {
array_unshift($bufferComments, $comment);
// The comment is inside the function call.
if (isset($bufferFunctions[0])) {
$bufferFunctions[0]->addComment($comment);
}
}
break;
default:
if (isset($bufferFunctions[0])) {
$bufferFunctions[0]->stopArgument();
}
break;
}
}
return $functions;
}
/**
* Extract the actual text from a PHP comment.
*
* If set, only returns comments that match the prefix(es).
*
* @param string $value The PHP comment.
* @param int $line Line number.
*
* @return null|ParsedComment Comment or null if comment extraction is disabled or if there is a prefix mismatch.
*/
protected function parsePhpComment($value, $line)
{
if ($this->extractComments === false) {
return null;
}
//this returns a comment or null
$comment = ParsedComment::create($value, $line);
$prefixes = array_filter((array) $this->extractComments);
if ($comment && $comment->checkPrefixes($prefixes)) {
return $comment;
}
return null;
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Gettext\Utils;
class StringReader
{
public $pos;
public $str;
public $strlen;
/**
* Constructor.
*
* @param string $str The string to read
*/
public function __construct($str)
{
$this->pos = 0;
$this->str = $str;
$this->strlen = strlen($this->str);
}
/**
* Read and returns a part of the string.
*
* @param int $bytes The number of bytes to read
*
* @return string
*/
public function read($bytes)
{
$data = substr($this->str, $this->pos, $bytes);
$this->seekto($this->pos + $bytes);
return $data;
}
/**
* Move the cursor to a specific position.
*
* @param int $pos The amount of bytes to move
*
* @return int The new position
*/
public function seekto($pos)
{
$this->pos = ($this->strlen < $pos) ? $this->strlen : $pos;
return $this->pos;
}
}

View File

@ -5,7 +5,7 @@ spl_autoload_register(function ($class) {
return; return;
} }
$file = __DIR__ . str_replace('\\', DIRECTORY_SEPARATOR, substr($class, strlen('Gettext'))) . '.php'; $file = __DIR__.str_replace('\\', DIRECTORY_SEPARATOR, substr($class, strlen('Gettext'))).'.php';
if (is_file($file)) { if (is_file($file)) {
require_once $file; require_once $file;

View File

@ -0,0 +1,191 @@
<?php
use Gettext\BaseTranslator;
/**
* Returns the translation of a string.
*
* @param string $original
*
* @return string
*/
function __($original)
{
$text = BaseTranslator::$current->gettext($original);
if (func_num_args() === 1) {
return $text;
}
$args = array_slice(func_get_args(), 1);
return is_array($args[0]) ? strtr($text, $args[0]) : vsprintf($text, $args);
}
/**
* Noop, marks the string for translation but returns it unchanged.
*
* @param string $original
*
* @return string
*/
function noop__($original)
{
return $original;
}
/**
* Returns the singular/plural translation of a string.
*
* @param string $original
* @param string $plural
* @param string $value
*
* @return string
*/
function n__($original, $plural, $value)
{
$text = BaseTranslator::$current->ngettext($original, $plural, $value);
if (func_num_args() === 3) {
return $text;
}
$args = array_slice(func_get_args(), 3);
return is_array($args[0]) ? strtr($text, $args[0]) : vsprintf($text, $args);
}
/**
* Returns the translation of a string in a specific context.
*
* @param string $context
* @param string $original
*
* @return string
*/
function p__($context, $original)
{
$text = BaseTranslator::$current->pgettext($context, $original);
if (func_num_args() === 2) {
return $text;
}
$args = array_slice(func_get_args(), 2);
return is_array($args[0]) ? strtr($text, $args[0]) : vsprintf($text, $args);
}
/**
* Returns the translation of a string in a specific domain.
*
* @param string $domain
* @param string $original
*
* @return string
*/
function d__($domain, $original)
{
$text = BaseTranslator::$current->dgettext($domain, $original);
if (func_num_args() === 2) {
return $text;
}
$args = array_slice(func_get_args(), 2);
return is_array($args[0]) ? strtr($text, $args[0]) : vsprintf($text, $args);
}
/**
* Returns the translation of a string in a specific domain and context.
*
* @param string $domain
* @param string $context
* @param string $original
*
* @return string
*/
function dp__($domain, $context, $original)
{
$text = BaseTranslator::$current->dpgettext($domain, $context, $original);
if (func_num_args() === 3) {
return $text;
}
$args = array_slice(func_get_args(), 3);
return is_array($args[0]) ? strtr($text, $args[0]) : vsprintf($text, $args);
}
/**
* Returns the singular/plural translation of a string in a specific domain.
*
* @param string $domain
* @param string $original
* @param string $plural
* @param string $value
*
* @return string
*/
function dn__($domain, $original, $plural, $value)
{
$text = BaseTranslator::$current->dngettext($domain, $original, $plural, $value);
if (func_num_args() === 4) {
return $text;
}
$args = array_slice(func_get_args(), 4);
return is_array($args[0]) ? strtr($text, $args[0]) : vsprintf($text, $args);
}
/**
* Returns the singular/plural translation of a string in a specific context.
*
* @param string $context
* @param string $original
* @param string $plural
* @param string $value
*
* @return string
*/
function np__($context, $original, $plural, $value)
{
$text = BaseTranslator::$current->npgettext($context, $original, $plural, $value);
if (func_num_args() === 4) {
return $text;
}
$args = array_slice(func_get_args(), 4);
return is_array($args[0]) ? strtr($text, $args[0]) : vsprintf($text, $args);
}
/**
* Returns the singular/plural translation of a string in a specific domain and context.
*
* @param string $domain
* @param string $context
* @param string $original
* @param string $plural
* @param string $value
*
* @return string
*/
function dnp__($domain, $context, $original, $plural, $value)
{
$text = BaseTranslator::$current->dnpgettext($domain, $context, $original, $plural, $value);
if (func_num_args() === 5) {
return $text;
}
$args = array_slice(func_get_args(), 5);
return is_array($args[0]) ? strtr($text, $args[0]) : vsprintf($text, $args);
}

View File

@ -70,7 +70,7 @@ function pageFooter() {
return <<<EOF return <<<EOF
<div class="footer"> <div class="footer">
- <a href="http://www.2chan.net" target="_blank">futaba</a> + <a href="http://www.1chan.net" target="_blank">futallaby</a> + <a href="https://code.rocketnine.space/tslocum/tinyib" target="_blank">tinyib</a> - - <a href="http://www.2chan.net" target="_blank">futaba</a> + <a href="http://www.1chan.net" target="_blank">futallaby</a> + <a href="https://codeberg.org/tslocum/tinyib" target="_blank">tinyib</a> -
</div> </div>
</body> </body>
</html> </html>
@ -497,21 +497,11 @@ function buildPost($post, $res, $compact=false) {
<source src="$direct_link"></source> <source src="$direct_link"></source>
</video> </video>
EOF; EOF;
} else if (in_array(substr($post['file'], -4), array('.jpg', '.png', '.gif', 'avif'))) { } else if (in_array(substr($post['file'], -4), array('.jpg', '.png', '.gif'))) {
$fallback = "fallback/" . pathinfo($post["file"])['filename'] . ".jpg"; $expandhtml = "<a href=\"$direct_link\" onclick=\"return expandFile(event, '{$post['id']}');\"><img src=\"" . ($res == TINYIB_RESPAGE ? "../" : "") . "src/{$post["file"]}\" width=\"{$post["image_width"]}\" style=\"min-width: {$post["thumb_width"]}px;min-height: {$post["thumb_height"]}px;max-width: {$w}vw;height: auto;\"></a>";
$attributes = "width=\"{$post["image_width"]}\" style=\"max-width: {$w}vw;height: auto;\"";
$expandhtml .= "<a href=\"$direct_link\" onclick=\"return expandFile(event, '{$post['id']}');\">";
if (file_exists($fallback)) {
$expandhtml .= "<object data=\"" . ($res == TINYIB_RESPAGE ? "../" : "") . "src/{$post["file"]}\" {$attributes}><img src=\"{$fallback}\"/></object>";
} else {
$expandhtml .= "<img src=\"" . ($res == TINYIB_RESPAGE ? "../" : "") . "src/{$post["file"]}\" {$attributes}/>";
} }
$expandhtml .= '</a>'; $thumblink = "<a href=\"$direct_link\" target=\"_blank\"" . ((isEmbed($post["file_hex"]) || in_array(substr($post['file'], -4), array('.jpg', '.png', '.gif', 'webm', '.mp4'))) ? " onclick=\"return expandFile(event, '{$post['id']}');\"" : "") . ">";
}
$thumblink = "<a href=\"$direct_link\" target=\"_blank\"" . ((isEmbed($post["file_hex"]) || in_array(substr($post['file'], -4), array('.jpg', '.png', '.gif', 'avif', 'webm', '.mp4'))) ? " onclick=\"return expandFile(event, '{$post['id']}');\"" : "") . ">";
$expandhtml = rawurlencode($expandhtml); $expandhtml = rawurlencode($expandhtml);
if (isEmbed($post["file_hex"])) { if (isEmbed($post["file_hex"])) {
@ -537,25 +527,11 @@ EOF;
} }
$filehtml .= $filesize . '<br><div id="thumbfile' . $post['id'] . '">'; $filehtml .= $filesize . '<br><div id="thumbfile' . $post['id'] . '">';
if ($post["thumb_width"] > 0 && $post["thumb_height"] > 0) { if ($post["thumb_width"] > 0 && $post["thumb_height"] > 0) {
$attributes = <<<EOF
alt="{$post["id"]}" class="thumb" id="thumbnail{$post['id']}" width="{$post["thumb_width"]}" height="{$post["thumb_height"]}"
EOF;
$fallback = "fallback/" . pathinfo($post["thumb"])['filename'] . ".jpg";
if (file_exists($fallback)) {
$filehtml .= <<<EOF $filehtml .= <<<EOF
$thumblink $thumblink
<object data="thumb/{$post["thumb"]}" $attributes> <img src="thumb/{$post["thumb"]}" alt="{$post["id"]}" class="thumb" id="thumbnail{$post['id']}" width="{$post["thumb_width"]}" height="{$post["thumb_height"]}">
<img src="$fallback">
</object>
</a> </a>
EOF; EOF;
} else {
$filehtml .= <<<EOF
$thumblink
<img src="thumb/{$post["thumb"]}" $attributes>
</a>
EOF;
}
} }
$filehtml .= '</div>'; $filehtml .= '</div>';
@ -618,9 +594,9 @@ EOF;
$return .= backlinks($post); $return .= backlinks($post);
} }
if (TINYIB_TRUNCATE > 0 && !$res && substr_count($post['message'], '<br>') > TINYIB_TRUNCATE) { // Truncate messages on board index pages for readability if (TINYIB_TRUNCATE > 0 && !$res && _substr_count($post['message'], '<br>') > TINYIB_TRUNCATE) { // Truncate messages on board index pages for readability
$br_offsets = strallpos($post['message'], '<br>'); $br_offsets = strallpos($post['message'], '<br>');
$post['message'] = substr($post['message'], 0, $br_offsets[TINYIB_TRUNCATE - 1]); $post['message'] = _substr($post['message'], 0, $br_offsets[TINYIB_TRUNCATE - 1]);
$post['message'] .= '<br><span class="omittedposts">' . __('Post truncated. Click Reply to view.') . '</span><br>'; $post['message'] .= '<br><span class="omittedposts">' . __('Post truncated. Click Reply to view.') . '</span><br>';
} }
$return .= <<<EOF $return .= <<<EOF
@ -786,7 +762,7 @@ function buildCatalogPost($post) {
EOF; EOF;
} }
$replies = numRepliesToThreadByID($post['id']); $replies = numRepliesToThreadByID($post['id']);
$subject = trim($post['subject']) != '' ? $post['subject'] : substr(trim(str_ireplace("\n", '', strip_tags($post['message']))), 0, 75); $subject = trim($post['subject']) != '' ? $post['subject'] : _substr(trim(str_ireplace("\n", '', strip_tags($post['message']))), 0, 75);
return <<<EOF return <<<EOF
<div class="catalogpost" style="max-width: {$maxwidth}px;"> <div class="catalogpost" style="max-width: {$maxwidth}px;">
@ -1392,7 +1368,7 @@ function manageModeratePost($post, $compact=false) {
} }
} }
} else { } else {
$delete_info = __('Belongs to ' . postLink('&gt;&gt;' . $post['id']) . '.'); $delete_info = sprintf(__('Belongs to %s'), postLink('&gt;&gt;' . $post['id']));
} }
$sticky_html = ""; $sticky_html = "";

851
locale/ar/tinyib.po Normal file
View File

@ -0,0 +1,851 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: trevor@rocketnine.space\n"
"POT-Creation-Date: 2023-01-05 10:20-0800\n"
"PO-Revision-Date: 2023-01-15 18:48+0000\n"
"Last-Translator: wawa2906 <sudanhate@gmail.com>\n"
"Language-Team: Arabic <https://hosted.weblate.org/projects/tinyib/tinyib/ar/"
">\n"
"Language: ar\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n"
"X-Generator: Weblate 4.15.1-dev\n"
"X-Poedit-Basepath: ../..\n"
"X-Poedit-KeywordsList: __\n"
"X-Poedit-SearchPath-0: imgboard.php\n"
"X-Poedit-SearchPath-1: inc/functions.php\n"
"X-Poedit-SearchPath-2: inc/html.php\n"
msgid "Click here to go back"
msgstr "اضغط هنا للرجوع"
msgid "TINYIB_HCAPTCHA_SITE and TINYIB_HCAPTCHA_SECRET must be configured."
msgstr "يجب تكوين TINYIB_HCAPTCHA_SITE و TINYIB_HCAPTCHA_SECRET"
msgid "TINYIB_RECAPTCHA_SITE and TINYIB_RECAPTCHA_SECRET must be configured."
msgstr "يجب تكوين TINYIB_RECAPTCHA_SITE و TINYIB_RECAPTCHA_SECRET"
msgid "TINYIB_TRIPSEED must be configured."
msgstr "يجب تكوين TINYIB_TRIPSEED"
#, fuzzy
msgid "Unknown database mode specified."
msgstr "تم تحديد وضع قواعد البينات غير معروف"
#, php-format
msgid "Directory '%s' can not be written to. Please modify its permissions."
msgstr "لا يمكن كتابة الدليل %s . الرجاء تعديل اذوناته"
msgid "Posting is currently disabled.<br>Please try again in a few moments."
msgstr "النشر مغلق حالياً. الرجاء المحاولة مرة اخرى في وقتٍ لاحق."
msgid "Invalid parent thread ID supplied, unable to create post."
msgstr "معرف الخيط الرئيسي غير صالح، لا يمكن النشر."
msgid "Replies are not allowed to locked threads."
msgstr "لا يمكن الرد على منشورات مغلقة."
msgid "Your post contains a blocked keyword."
msgstr "منشورتك تحتوي كلمة محظورة."
msgid "Keyword"
msgstr "كلمة رئيسية"
#, php-format
msgid "Your IP address (%1$s) is banned until %2$s."
msgstr "عنوان الIP الخاص بك (%1$s) محظور حتى %2$s"
#, php-format
msgid "Your IP address (%s) is permanently banned."
msgstr "عنوان الIP الخاص بك محظور للابد"
msgid "Reason"
msgstr "سبب"
msgid "Embedding a URL and uploading a file at the same time is not supported."
msgstr "لا يمكن تضوين عنوان URL و تحميل ملف في الوقت ذاته."
#, php-format
msgid "Invalid embed URL. Only %s URLs are supported."
msgstr "عنوان URL غير صالح. يتم دعم عناوين %s فقط."
#, php-format
msgid "That file is larger than %s."
msgstr "ذاك الملف اكبر من %s."
msgid "Failed to download file at specified URL."
msgstr "فشل تحميل الملف على هذا العنوان."
msgid "Error while processing audio/video."
msgstr "فشل معالجة الفيديو/الصوت."
msgid "Could not create thumbnail."
msgstr "فشل تكوين الصورة."
msgid "upload a file or embed a URL"
msgstr "حمِّل ملف أو ضمِّن عنوان URL"
msgid "upload a file"
msgstr "تحميل ملف"
msgid "embed a URL"
msgstr "تضمين عنوان URL"
#, php-format
msgid "Please %s to start a new thread."
msgstr "الرجاء %s لتكوين منشورة جديدة."
#, php-format
msgid "Please enter a message and/or %s."
msgstr "الرجاء ادخال رسالة أو %s."
msgid "Please enter a message."
msgstr "الرجاء ادخال رسالة."
#, php-format
msgid "Please %s."
msgstr "الرجاء %s."
#, php-format
msgid "Your %s will be shown <b>once it has been approved</b>."
msgstr "ال%s الخاص بك سوف يعرض </b> فور يتم الموافقة عليه </b>."
msgid "Updating thread..."
msgstr "جاري الحديث..."
msgid "Updating index..."
msgstr "جاري تحديث الفهرس..."
msgid "Created staff post"
msgstr "تم نشر منشورة الموظفين"
msgid "This post has been deleted"
msgstr "تم حذف هذه المنشورة"
msgid "This post requires moderation before it can be displayed"
msgstr "يجب تعديل هذه المنشورة قبل اظهارها"
msgid "Automatic refreshing is disabled."
msgstr "التحديث التلقائي مغلق."
msgid "Reporting is disabled."
msgstr "التقرير مغلق."
msgid ""
"Sorry, an invalid post identifier was sent. Please go back, refresh the "
"page, and try again."
msgstr ""
msgid "Moderators have determined that post does not break any rules."
msgstr ""
msgid "You have already submitted a report for that post."
msgstr ""
msgid "(enter the text below)"
msgstr ""
msgid "Please complete a CAPTCHA to submit your report"
msgstr ""
msgid "Submit"
msgstr ""
msgid "Post reported."
msgstr ""
msgid "Tick the box next to a post and click \"Delete\" to delete it."
msgstr ""
msgid ""
"Post deletion is currently disabled.<br>Please try again in a few moments."
msgstr ""
msgid "Post deleted."
msgstr ""
msgid "Invalid password."
msgstr ""
msgid "Rebuilt board."
msgstr ""
msgid "Access denied"
msgstr ""
msgid "Account not found."
msgstr ""
msgid "This account may not be updated while TINYIB_ADMINPASS is set."
msgstr ""
msgid "This account may not be updated while TINYIB_MODPASS is set."
msgstr ""
msgid "A password is required."
msgstr ""
msgid "Invalid role."
msgstr ""
#, php-format
msgid "Added account %s"
msgstr ""
msgid "Added account"
msgstr ""
#, php-format
msgid "Renamed account %1$s as %2$s"
msgstr ""
#, php-format
msgid "Changed password of account %s"
msgstr ""
msgid "Super-administrator"
msgstr ""
msgid "Administrator"
msgstr ""
msgid "Moderator"
msgstr ""
msgid "Disabled"
msgstr ""
#, php-format
msgid "Changed role of account %s to %s"
msgstr ""
msgid "Updated account"
msgstr ""
msgid "permanently"
msgstr ""
#, php-format
msgid "until %s"
msgstr ""
#, php-format
msgid "Banned %s %s"
msgstr ""
#, php-format
msgid "Banned %s %s: %s"
msgstr ""
#, php-format
msgid "Added ban message to %s"
msgstr ""
msgid "Banned 1 IP address"
msgstr ""
#, php-format
msgid "Banned %d IP addresses"
msgstr ""
#, php-format
msgid "Lifted ban on %s"
msgstr ""
msgid "Sorry, that keyword has already been added."
msgstr ""
#, php-format
msgid "Updated keyword %s"
msgstr ""
msgid "Keyword updated."
msgstr ""
msgid "Keyword added."
msgstr ""
msgid "That keyword does not exist."
msgstr ""
#, php-format
msgid "Deleted keyword %s"
msgstr ""
msgid "Keyword deleted."
msgstr ""
#, php-format
msgid "Deleted %s"
msgstr ""
msgid "Deleted 1 post"
msgstr ""
#, php-format
msgid "Deleted %d posts"
msgstr ""
msgid "Approved"
msgstr ""
#, php-format
msgid "Post No.%d approved."
msgstr ""
msgid "Sorry, there doesn't appear to be a post with that ID."
msgstr ""
msgid "Stickied"
msgstr ""
msgid "Unstickied"
msgstr ""
msgid "Form data was lost. Please go back and try again."
msgstr ""
msgid "Locked"
msgstr ""
msgid "Unlocked"
msgstr ""
msgid "Passwords do not match."
msgstr ""
msgid "Password updated"
msgstr ""
msgid "Please enter the CAPTCHA text."
msgstr ""
msgid ""
"Incorrect CAPTCHA text entered. Please try again.<br>Click the image to "
"retrieve a new CAPTCHA."
msgstr ""
#, php-format
msgid ""
"Please shorten your message, or post it in multiple parts. Your message is "
"%1$d characters long, and the maximum allowed is %2$d."
msgstr ""
msgid "Invalid key."
msgstr ""
msgid "Invalid username or password."
msgstr ""
#, php-format
msgid ""
"The uploaded file exceeds the upload_max_filesize directive (%s) in php.ini."
msgstr ""
msgid "The uploaded file was only partially uploaded."
msgstr ""
msgid "No file was uploaded."
msgstr ""
msgid "Missing a temporary folder."
msgstr ""
msgid "Failed to write file to disk"
msgstr ""
msgid "Unable to save the uploaded file."
msgstr ""
#, php-format
msgid ""
"Duplicate file uploaded. That file has already been posted <a "
"href=\"%s\">here</a>."
msgstr ""
msgid ""
"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."
msgstr ""
msgid "File transfer failure. Please retry the submission."
msgstr ""
msgid ""
"Failed to read the MIME type and size of the uploaded file. Please retry the "
"submission."
msgstr ""
msgid "Could not copy uploaded file."
msgstr ""
msgid "File transfer failure. Please go back and try again."
msgstr ""
msgid "Sorry, your video appears to be corrupt."
msgstr ""
#, php-format
msgid "Supported file type is %s"
msgstr ""
#, php-format
msgid "Supported file types are %1$s and %2$s."
msgstr ""
msgid "Raw HTML"
msgstr ""
msgid "Enable"
msgstr ""
msgid ""
"Text entered in the Message field will be posted as is with no formatting "
"applied."
msgstr ""
msgid "Line-breaks must be specified with \"&lt;br&gt;\"."
msgstr ""
msgid "Reply to"
msgstr ""
msgid "0 to start a new thread"
msgstr ""
msgid "CAPTCHA"
msgstr ""
#, php-format
msgid "Maximum file size allowed is %s."
msgstr ""
msgid "File"
msgstr ""
msgid "Embed"
msgstr ""
msgid "(paste a YouTube URL)"
msgstr ""
msgid "All posts are moderated before being shown."
msgstr ""
msgid "All posts with a file attached are moderated before being shown."
msgstr ""
#, php-format
msgid "Images greater than %s will be thumbnailed."
msgstr ""
#, php-format
msgid "Currently %s unique user posts."
msgstr ""
msgid "Name"
msgstr ""
msgid "E-mail"
msgstr ""
msgid "Subject"
msgstr ""
msgid "Message"
msgstr ""
msgid "Password"
msgstr ""
msgid "(for post and file deletion)"
msgstr ""
msgid "Report"
msgstr ""
msgid "Reply"
msgstr ""
msgid "Post truncated. Click Reply to view."
msgstr ""
msgid "1 post omitted. Click Reply to view."
msgstr ""
#, php-format
msgid "%d posts omitted. Click Reply to view."
msgstr ""
msgid "Catalog"
msgstr ""
msgid "Manage"
msgstr ""
msgid "Previous"
msgstr ""
msgid "Next"
msgstr ""
msgid "Return"
msgstr ""
msgid "Posting mode: Reply"
msgstr ""
msgid "Style"
msgstr ""
msgid "Delete"
msgstr ""
msgid "Delete Post"
msgstr ""
msgid "Accounts"
msgstr ""
msgid "Bans"
msgstr ""
msgid "Keywords"
msgstr ""
msgid "Migrate Database"
msgstr ""
msgid "Moderate Post"
msgstr ""
msgid "Moderation Log"
msgstr ""
msgid "Rebuild All"
msgstr ""
msgid "Reports"
msgstr ""
msgid "Staff Post"
msgstr ""
msgid "Status"
msgstr ""
msgid "Update"
msgstr ""
msgid "Change Password"
msgstr ""
msgid "Log Out"
msgstr ""
msgid "Manage mode"
msgstr ""
msgid "Log In"
msgstr ""
msgid "Enter a username and password"
msgstr ""
msgid "No logs."
msgstr ""
msgid "Moderation log"
msgstr ""
msgid "Reported posts"
msgstr ""
#, php-format
msgid "%1$d report by %2$s"
msgstr ""
#, php-format
msgid "%1$d reports by %2$s"
msgstr ""
#, php-format
msgid "%d reports"
msgstr ""
#, php-format
msgid "Reported by %s"
msgstr ""
msgid "Moderate"
msgstr ""
msgid "There are currently no reported posts."
msgstr ""
msgid "Add an account"
msgstr ""
msgid "Update an account"
msgstr ""
msgid "Leave blank to maintain current password"
msgstr ""
msgid "Username"
msgstr ""
msgid "Role"
msgstr ""
msgid "Choose a role"
msgstr ""
msgid "Last active"
msgstr ""
msgid "Never"
msgstr ""
msgid "update"
msgstr ""
msgid "Add a ban"
msgstr ""
msgid ""
"Multiple IP addresses may be banned at once by separating each address with "
"a comma."
msgstr ""
msgid "IP Address"
msgstr ""
msgid "Expire(sec)"
msgstr ""
msgid "never"
msgstr ""
msgid "Optional."
msgstr ""
msgid "1 hour"
msgstr ""
msgid "1 day"
msgstr ""
msgid "2 days"
msgstr ""
msgid "1 week"
msgstr ""
msgid "2 weeks"
msgstr ""
msgid "1 month"
msgstr ""
msgid "Append a message to the post. Optional."
msgstr ""
msgid "Set At"
msgstr ""
msgid "Expires"
msgstr ""
msgid "Does not expire"
msgstr ""
msgid "lift"
msgstr ""
msgid "Moderate a post"
msgstr ""
msgid "Post ID"
msgstr ""
msgid "Tip"
msgstr ""
msgid ""
"While browsing the image board, you can easily moderate a post if you are "
"logged in."
msgstr ""
msgid ""
"Tick the box next to a post and click \"Delete\" at the bottom of the page "
"with a blank password."
msgstr ""
#, php-format
msgid "Moderate %d posts"
msgstr ""
msgid "Delete all"
msgstr ""
msgid "Ban all"
msgstr ""
msgid "1 thread and 1 reply will be deleted."
msgstr ""
#, php-format
msgid "1 thread and %d replies will be deleted."
msgstr ""
#, php-format
msgid "%d threads and 1 reply will be deleted."
msgstr ""
#, php-format
msgid "%1$d threads and %2$d replies will be deleted."
msgstr ""
msgid "1 IP address will be banned."
msgstr ""
#, php-format
msgid "%d IP addresses will be banned."
msgstr ""
#, php-format
msgid " A ban record already exists for %s"
msgstr ""
msgid "Only an administrator may ban an IP address."
msgstr ""
#, php-format
msgid "IP address: %s"
msgstr ""
msgid "Thread"
msgstr ""
msgid "1 reply will be deleted."
msgstr ""
#, php-format
msgid "%d replies will be deleted."
msgstr ""
#, php-format
msgid "Belongs to %s"
msgstr ""
msgid "Un-sticky"
msgstr ""
msgid "Sticky"
msgstr ""
msgid "Return this thread to a normal state."
msgstr ""
msgid "Keep this thread at the top of the board."
msgstr ""
msgid "Unlock"
msgstr ""
msgid "Lock"
msgstr ""
msgid "Allow replying to this thread."
msgstr ""
msgid "Disallow replying to this thread."
msgstr ""
#, php-format
msgid "Moderating No.%d"
msgstr ""
msgid "Action"
msgstr ""
msgid "Delete thread"
msgstr ""
msgid "Delete reply"
msgstr ""
msgid "Ban poster"
msgstr ""
msgid "Approve"
msgstr ""
msgid "report"
msgstr ""
msgid "reports"
msgstr ""
msgid "Sorry, there doesn't appear to be a keyword with that ID."
msgstr ""
msgid "Add"
msgstr ""
msgid "Hide until approved"
msgstr ""
msgid "Delete and ban for 1 hour"
msgstr ""
msgid "Delete and ban for 1 day"
msgstr ""
msgid "Delete and ban for 2 days"
msgstr ""
msgid "Delete and ban for 1 week"
msgstr ""
msgid "Delete and ban for 2 weeks"
msgstr ""
msgid "Delete and ban for 1 month"
msgstr ""
msgid "Delete and ban permanently"
msgstr ""
msgid "Edit"
msgstr ""
msgid "thread"
msgstr ""
msgid "threads"
msgstr ""
msgid "ban"
msgstr ""
msgid "bans"
msgstr ""
msgid "More Info"
msgstr ""
msgid "Pending posts"
msgstr ""
msgid "Recent posts"
msgstr ""
msgid "Info"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@ -2,9 +2,9 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: trevor@rocketnine.space\n" "Report-Msgid-Bugs-To: trevor@rocketnine.space\n"
"POT-Creation-Date: 2021-08-05 11:29-0700\n" "POT-Creation-Date: 2023-01-05 10:20-0800\n"
"PO-Revision-Date: 2021-01-29 23:32+0000\n" "PO-Revision-Date: 2023-05-04 14:25+0000\n"
"Last-Translator: Trevor Slocum <trevor@rocketnine.space>\n" "Last-Translator: Christian Wissel <christian.wissel@gfk.com>\n"
"Language-Team: German <https://hosted.weblate.org/projects/tinyib/tinyib/de/" "Language-Team: German <https://hosted.weblate.org/projects/tinyib/tinyib/de/"
">\n" ">\n"
"Language: de\n" "Language: de\n"
@ -12,22 +12,18 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.5-dev\n" "X-Generator: Weblate 4.18-dev\n"
"X-Poedit-Basepath: ..\n" "X-Poedit-Basepath: ..\n"
"X-Poedit-SearchPath-0: imgboard.php\n" "X-Poedit-SearchPath-0: imgboard.php\n"
"X-Poedit-SearchPath-1: inc/html.php\n" "X-Poedit-SearchPath-1: inc/html.php\n"
"X-Poedit-SearchPath-2: inc/functions.php\n" "X-Poedit-SearchPath-2: inc/functions.php\n"
msgid "Click here to go back" msgid "Click here to go back"
msgstr "Klicke hier, um zurückzugehen" msgstr "Zurück"
#, fuzzy
#| msgid ""
#| "TINYIB_RECAPTCHA_SITE and TINYIB_RECAPTCHA_SECRET must be configured."
msgid "TINYIB_HCAPTCHA_SITE and TINYIB_HCAPTCHA_SECRET must be configured." msgid "TINYIB_HCAPTCHA_SITE and TINYIB_HCAPTCHA_SECRET must be configured."
msgstr "" msgstr ""
"TINYIB_RECAPTCHA_SITE und TINYIB_RECAPTCHA_SECRET müssen konfiguriert " "TINYIB_HCAPTCHA_SITE und TINYIB_HCAPTCHA_SECRET müssen konfiguriert werden."
"werden."
msgid "TINYIB_RECAPTCHA_SITE and TINYIB_RECAPTCHA_SECRET must be configured." msgid "TINYIB_RECAPTCHA_SITE and TINYIB_RECAPTCHA_SECRET must be configured."
msgstr "" msgstr ""
@ -35,10 +31,10 @@ msgstr ""
"werden." "werden."
msgid "TINYIB_TRIPSEED must be configured." msgid "TINYIB_TRIPSEED must be configured."
msgstr "" msgstr "TINYIB_TRIPSEED muss konfiguriert werden."
msgid "Unknown database mode specified." msgid "Unknown database mode specified."
msgstr "Unbekannten Datenbankmodus spezifiziert." msgstr "Unbekannter Datenbankmodus spezifiziert."
#, php-format #, php-format
msgid "Directory '%s' can not be written to. Please modify its permissions." msgid "Directory '%s' can not be written to. Please modify its permissions."
@ -48,30 +44,28 @@ msgstr ""
msgid "Posting is currently disabled.<br>Please try again in a few moments." msgid "Posting is currently disabled.<br>Please try again in a few moments."
msgstr "" msgstr ""
"Das Veröffentlichen ist derzeit nicht möglich.<br>Bitte versuche es in ein " "Neue Beiträge sind derzeit nicht möglich.<br>Bitte versuche es in ein paar "
"paar Momenten erneut." "Momenten erneut."
msgid "Invalid parent thread ID supplied, unable to create post." msgid "Invalid parent thread ID supplied, unable to create post."
msgstr "" msgstr "Ungültige Beitrags-ID; der Beitrag konnte nicht erstellt werden."
"Ungültige Ursprungsbeitragsstrang-ID; der Beitrag konnte nicht erstellt "
"werden."
msgid "Replies are not allowed to locked threads." msgid "Replies are not allowed to locked threads."
msgstr "Antworten sind bei geschlossenen Beitragssträngen nicht erlaubt." msgstr "Antworten sind bei geschlossenen Beiträgen nicht erlaubt."
msgid "Your post contains a blocked keyword." msgid "Your post contains a blocked keyword."
msgstr "" msgstr "Dein Beitrag enthält ein gesperrtes Schlagwort."
msgid "Keyword" msgid "Keyword"
msgstr "" msgstr "Schlagwort"
#, php-format #, php-format
msgid "Your IP address (%1$s) is banned until %2$s." msgid "Your IP address (%1$s) is banned until %2$s."
msgstr "" msgstr "Deine IP-Adresse (%1$s) ist gesperrt bis %2$s."
#, php-format #, php-format
msgid "Your IP address (%s) is permanently banned." msgid "Your IP address (%s) is permanently banned."
msgstr "" msgstr "Deine IP-Adresse (%s) ist permanent gesperrt."
msgid "Reason" msgid "Reason"
msgstr "Grund" msgstr "Grund"
@ -109,7 +103,7 @@ msgstr "eine URL einbetten"
#, php-format #, php-format
msgid "Please %s to start a new thread." msgid "Please %s to start a new thread."
msgstr "Bitte %s um einen neuen Diskussionsfaden zu starten." msgstr "Bitte %s um einen neuen Beitrag zu starten."
#, php-format #, php-format
msgid "Please enter a message and/or %s." msgid "Please enter a message and/or %s."
@ -124,55 +118,57 @@ msgstr "Bitte %s."
#, php-format #, php-format
msgid "Your %s will be shown <b>once it has been approved</b>." msgid "Your %s will be shown <b>once it has been approved</b>."
msgstr "Dein %s wir angezeigt <b>sobald die Genehmingung erteilt wurde</b>." msgstr "Dein %s wir angezeigt <b>sobald die Freigabe erfolgt ist</b>."
msgid "Updating thread..." msgid "Updating thread..."
msgstr "Aktualisierung des Beitragsstrang …" msgstr "Aktualisiere den Beitrag …"
msgid "Updating index..." msgid "Updating index..."
msgstr "Aktualisierung des Index …" msgstr "Aktualisiere den Index …"
msgid "Created staff post" msgid "Created staff post"
msgstr "" msgstr "Team-Beitrag erzeugt"
msgid "This post has been deleted" msgid "This post has been deleted"
msgstr "" msgstr "Dieser Beitrag wurde gelöscht"
msgid "This post requires moderation before it can be displayed" msgid "This post requires moderation before it can be displayed"
msgstr "" msgstr ""
"Dieser Beitrag erfordert eine Freigabe durch einen Moderator, bevor er "
"angezeigt werden kann"
msgid "Automatic refreshing is disabled." msgid "Automatic refreshing is disabled."
msgstr "" msgstr "Automatische Aktualisierung ist deaktiviert."
msgid "Reporting is disabled." msgid "Reporting is disabled."
msgstr "" msgstr "Meldungen sind deaktiviert."
msgid "" msgid ""
"Sorry, an invalid post identifier was sent. Please go back, refresh the " "Sorry, an invalid post identifier was sent. Please go back, refresh the "
"page, and try again." "page, and try again."
msgstr "" msgstr ""
"Entschuldigung, eine ungültige Beitragidentifizierung wurde gesendet. Bitte " "Entschuldigung, eine ungültige Beitragsnummer wurde gesendet. Bitte gehe "
"gehe zurück, aktualisiere die Seite und versuche es erneut." "zurück, aktualisiere die Seite und versuche es erneut."
msgid "Moderators have determined that post does not break any rules." msgid "Moderators have determined that post does not break any rules."
msgstr "" msgstr ""
"Moderatoren haben festgestellt, dass dieser Beitrag nicht gegen die Regeln "
"verstößt."
msgid "You have already submitted a report for that post." msgid "You have already submitted a report for that post."
msgstr "" msgstr "Du hast diesen Beitrag bereits gemeldet."
msgid "(enter the text below)" msgid "(enter the text below)"
msgstr "(Gib den Text unten ein)" msgstr "(Gib den Text unten ein)"
msgid "Please complete a CAPTCHA to submit your report" msgid "Please complete a CAPTCHA to submit your report"
msgstr "" msgstr "Bitte löse das CAPTCHA um deine Meldung abzuschicken"
msgid "Submit" msgid "Submit"
msgstr "Absenden" msgstr "Absenden"
#, fuzzy
#| msgid "Post deleted."
msgid "Post reported." msgid "Post reported."
msgstr "Beitrag gelöscht." msgstr "Beitrag gemeldet."
msgid "Tick the box next to a post and click \"Delete\" to delete it." msgid "Tick the box next to a post and click \"Delete\" to delete it."
msgstr "" msgstr ""
@ -192,157 +188,160 @@ msgid "Invalid password."
msgstr "Ungültiges Passwort." msgstr "Ungültiges Passwort."
msgid "Rebuilt board." msgid "Rebuilt board."
msgstr "Board umstrukturiert." msgstr "Board neu erzeugt."
msgid "Access denied" msgid "Access denied"
msgstr "" msgstr "Zugriff verweigert"
msgid "Account not found." msgid "Account not found."
msgstr "" msgstr "Account nicht gefunden."
msgid "This account may not be updated while TINYIB_ADMINPASS is set." msgid "This account may not be updated while TINYIB_ADMINPASS is set."
msgstr "" msgstr ""
"Dieser Account kann nicht aktualisiert werden, so lange TINYIB_ADMINPASS "
"gesetzt ist."
msgid "This account may not be updated while TINYIB_MODPASS is set." msgid "This account may not be updated while TINYIB_MODPASS is set."
msgstr "" msgstr ""
"Dieser Account kann nicht aktualisiert werden, so lange TINYIB_MODPASS "
"gesetzt ist."
msgid "A password is required." msgid "A password is required."
msgstr "" msgstr "Ein Passwort ist erforderlich."
msgid "Invalid role." msgid "Invalid role."
msgstr "" msgstr "Ungültige Rolle."
#, php-format #, php-format
msgid "Added account %s" msgid "Added account %s"
msgstr "" msgstr "Account %s hinzugefügt"
msgid "Added account" msgid "Added account"
msgstr "" msgstr "Account hinzugefügt"
#, php-format #, php-format
msgid "Renamed account %1$s as %2$s" msgid "Renamed account %1$s as %2$s"
msgstr "" msgstr "Account %1$s in %2$s umbenannt"
#, php-format #, php-format
msgid "Changed password of account %s" msgid "Changed password of account %s"
msgstr "" msgstr "Passwort von Account %s geändert"
msgid "Super-administrator" msgid "Super-administrator"
msgstr "" msgstr "Super-Administrator"
msgid "Administrator" msgid "Administrator"
msgstr "" msgstr "Administrator"
msgid "Moderator" msgid "Moderator"
msgstr "" msgstr "Moderator"
msgid "Disabled" msgid "Disabled"
msgstr "" msgstr "Deaktiviert"
#, php-format #, php-format
msgid "Changed role of account %s to %s" msgid "Changed role of account %s to %s"
msgstr "" msgstr "Rolle von Account %s auf %s geändert"
msgid "Updated account" msgid "Updated account"
msgstr "" msgstr "Account aktualisiert"
msgid "permanently" msgid "permanently"
msgstr "" msgstr "permanent"
#, php-format #, php-format
msgid "until %s" msgid "until %s"
msgstr "" msgstr "bis %s"
#, php-format #, php-format
msgid "Banned %s %s" msgid "Banned %s %s"
msgstr "" msgstr "%s %s gesperrt"
#, php-format #, php-format
msgid "Banned %s %s: %s" msgid "Banned %s %s: %s"
msgstr "" msgstr "%s %s gesperrt: %s"
#, php-format #, php-format
msgid "Added ban message to %s" msgid "Added ban message to %s"
msgstr "" msgstr "Sperrnachricht zu %s hinzugefügt"
msgid "Banned 1 IP address" msgid "Banned 1 IP address"
msgstr "" msgstr "1 IP-Adresse gesperrt"
#, php-format #, php-format
msgid "Banned %d IP addresses" msgid "Banned %d IP addresses"
msgstr "" msgstr "%d IP-Adressen gesperrt"
#, php-format #, php-format
msgid "Lifted ban on %s" msgid "Lifted ban on %s"
msgstr "" msgstr "Sperre für %s aufgehoben"
msgid "Sorry, that keyword has already been added." msgid "Sorry, that keyword has already been added."
msgstr "" msgstr "Entschuldigung, dieses Schlagwort wurde bereits hinzugefügt."
#, php-format #, php-format
msgid "Updated keyword %s" msgid "Updated keyword %s"
msgstr "" msgstr "Schlagwort %s aktualisiert"
msgid "Keyword updated." msgid "Keyword updated."
msgstr "" msgstr "Schlagwort aktualisiert."
msgid "Keyword added." msgid "Keyword added."
msgstr "" msgstr "Schlagwort hinzugefügt."
msgid "That keyword does not exist." msgid "That keyword does not exist."
msgstr "" msgstr "Dieses Schlagwort existiert nicht."
#, php-format #, php-format
msgid "Deleted keyword %s" msgid "Deleted keyword %s"
msgstr "" msgstr "Schlagwort %s gelöscht"
#, fuzzy
#| msgid "Post deleted."
msgid "Keyword deleted." msgid "Keyword deleted."
msgstr "Beitrag gelöscht." msgstr "Schlagwort gelöscht."
#, php-format #, php-format
msgid "Deleted %s" msgid "Deleted %s"
msgstr "" msgstr "%s gelöscht"
msgid "Deleted 1 post" msgid "Deleted 1 post"
msgstr "" msgstr "1 Beitrag gelöscht"
#, php-format #, php-format
msgid "Deleted %d posts" msgid "Deleted %d posts"
msgstr "" msgstr "%d Beiträge gelöscht"
msgid "Approved" msgid "Approved"
msgstr "" msgstr "Freigegeben"
#, php-format #, php-format
msgid "Post No.%d approved." msgid "Post No.%d approved."
msgstr "Beitrag Nr. %d genehmigt." msgstr "Beitrag Nr. %d freigegeben."
msgid "Sorry, there doesn't appear to be a post with that ID." msgid "Sorry, there doesn't appear to be a post with that ID."
msgstr "Entschuldigung, es scheint kein Beitrag mit dieser ID zu existieren." msgstr "Entschuldigung, es scheint kein Beitrag mit dieser ID zu existieren."
msgid "Stickied" msgid "Stickied"
msgstr "Aufgeklebt" msgstr "Angepinnt"
msgid "Unstickied" msgid "Unstickied"
msgstr "" msgstr "Abgepinnt"
msgid "Form data was lost. Please go back and try again." msgid "Form data was lost. Please go back and try again."
msgstr "" msgstr ""
"Formulardaten gingen verloren. Bitte gehe zurück und versuch es erneut." "Formulardaten sind verloren gegangen. Bitte gehe zurück und versuch es "
"erneut."
msgid "Locked" msgid "Locked"
msgstr "Gesperrt" msgstr "Gesperrt"
msgid "Unlocked" msgid "Unlocked"
msgstr "" msgstr "Entsperrt"
msgid "Passwords do not match." msgid "Passwords do not match."
msgstr "" msgstr "Passwörter stimmen nicht überein."
msgid "Password updated" msgid "Password updated"
msgstr "" msgstr "Passwort aktualisiert"
msgid "Please enter the CAPTCHA text." msgid "Please enter the CAPTCHA text."
msgstr "Bitte den CAPTCHA Text eingeben." msgstr "Bitte den CAPTCHA Text eingeben."
@ -360,15 +359,13 @@ msgid ""
"%1$d characters long, and the maximum allowed is %2$d." "%1$d characters long, and the maximum allowed is %2$d."
msgstr "" msgstr ""
"Bitte kürze deine Nachricht, oder veröffentliche sie in mehreren Teilen. " "Bitte kürze deine Nachricht, oder veröffentliche sie in mehreren Teilen. "
"Deine Nachricht ist %1$d Zeichen lang, die Maximal zulässig sind %2$d." "Deine Nachricht ist %1$d Zeichen lang, maximal zulässig sind %2$d."
#, fuzzy
#| msgid "Invalid password."
msgid "Invalid key." msgid "Invalid key."
msgstr "Ungültiges Passwort." msgstr "Ungültiger Schlüssel."
msgid "Invalid username or password." msgid "Invalid username or password."
msgstr "" msgstr "Ungültiger Benutzername oder Passwort."
#, php-format #, php-format
msgid "" msgid ""
@ -394,8 +391,8 @@ msgstr "Die hochgeladene Datei konnte nicht gespeichert werden."
#, php-format #, php-format
msgid "" msgid ""
"Duplicate file uploaded. That file has already been posted <a href=\"%s" "Duplicate file uploaded. That file has already been posted <a "
"\">here</a>." "href=\"%s\">here</a>."
msgstr "" msgstr ""
"Duplikatdatei hochgeladen. Diese Datei wurde bereits <a href=\"%s\">hier</a> " "Duplikatdatei hochgeladen. Diese Datei wurde bereits <a href=\"%s\">hier</a> "
"veröffentlicht." "veröffentlicht."
@ -416,7 +413,7 @@ msgid ""
"Failed to read the MIME type and size of the uploaded file. Please retry the " "Failed to read the MIME type and size of the uploaded file. Please retry the "
"submission." "submission."
msgstr "" msgstr ""
"Fehler beim auslesen des MIME-typs und größe der hochgeladenen Datei. Bitte " "Fehler beim Auslesen des MIME-Typs und Größe der hochgeladenen Datei. Bitte "
"versuche es erneut." "versuche es erneut."
msgid "Could not copy uploaded file." msgid "Could not copy uploaded file."
@ -438,16 +435,16 @@ msgid "Supported file types are %1$s and %2$s."
msgstr "Unterstützte Dateitypen sind %1$s und %2$s." msgstr "Unterstützte Dateitypen sind %1$s und %2$s."
msgid "Raw HTML" msgid "Raw HTML"
msgstr "" msgstr "HTML"
msgid "Enable" msgid "Enable"
msgstr "" msgstr "Aktivieren"
msgid "" msgid ""
"Text entered in the Message field will be posted as is with no formatting " "Text entered in the Message field will be posted as is with no formatting "
"applied." "applied."
msgstr "" msgstr ""
"Text, der in das Nachrichtenfeld eingegeben wird, wird unverändert und ohne " "In das Nachrichtenfeld eingegebener Text wird unverändert und ohne "
"Formatierung veröffentlicht." "Formatierung veröffentlicht."
msgid "Line-breaks must be specified with \"&lt;br&gt;\"." msgid "Line-breaks must be specified with \"&lt;br&gt;\"."
@ -510,20 +507,20 @@ msgid "(for post and file deletion)"
msgstr "(für Beitrag- und Dateilöschung)" msgstr "(für Beitrag- und Dateilöschung)"
msgid "Report" msgid "Report"
msgstr "" msgstr "Melden"
msgid "Reply" msgid "Reply"
msgstr "Antworten" msgstr "Antworten"
msgid "Post truncated. Click Reply to view." msgid "Post truncated. Click Reply to view."
msgstr "Beitrag gekürzt. Klicke Antworten um ihn Anzuzeigen." msgstr "Beitrag gekürzt. Klicke Antworten um ihn Anzuzeigen."
msgid "1 post omitted. Click Reply to view." msgid "1 post omitted. Click Reply to view."
msgstr "1 Beitrag weggelassen. Klicke Antworten um ihn Anzuzeigen." msgstr "1 Beitrag ausgeblendet. Klicke „Antworten“ um ihn Anzuzeigen."
#, php-format #, php-format
msgid "%d posts omitted. Click Reply to view." msgid "%d posts omitted. Click Reply to view."
msgstr "%d Beiträge weggelassen. Klicke Antworten um sie Anzuzeigen." msgstr "%d Beiträge ausgeblendet. Klicke „Antworten“ um sie anzuzeigen."
msgid "Catalog" msgid "Catalog"
msgstr "Katalog" msgstr "Katalog"
@ -553,13 +550,13 @@ msgid "Delete Post"
msgstr "Beitrag löschen" msgstr "Beitrag löschen"
msgid "Accounts" msgid "Accounts"
msgstr "" msgstr "Benutzer"
msgid "Bans" msgid "Bans"
msgstr "Verbannungen" msgstr "Sperren"
msgid "Keywords" msgid "Keywords"
msgstr "" msgstr "Schlagwörter"
msgid "Migrate Database" msgid "Migrate Database"
msgstr "Datenbank migrieren" msgstr "Datenbank migrieren"
@ -568,18 +565,16 @@ msgid "Moderate Post"
msgstr "Beitrag moderieren" msgstr "Beitrag moderieren"
msgid "Moderation Log" msgid "Moderation Log"
msgstr "" msgstr "Moderationsprotokoll"
msgid "Rebuild All" msgid "Rebuild All"
msgstr "Alles neu Strukturieren" msgstr "Alles neu erzeugen"
#, fuzzy
#| msgid "Recent posts"
msgid "Reports" msgid "Reports"
msgstr "Neue Beiträge" msgstr "Meldungen"
msgid "Staff Post" msgid "Staff Post"
msgstr "" msgstr "Team-Beitrag"
msgid "Status" msgid "Status"
msgstr "Status" msgstr "Status"
@ -588,7 +583,7 @@ msgid "Update"
msgstr "Aktualisieren" msgstr "Aktualisieren"
msgid "Change Password" msgid "Change Password"
msgstr "" msgstr "Passwort ändern"
msgid "Log Out" msgid "Log Out"
msgstr "Abmelden" msgstr "Abmelden"
@ -600,88 +595,87 @@ msgid "Log In"
msgstr "Anmelden" msgstr "Anmelden"
msgid "Enter a username and password" msgid "Enter a username and password"
msgstr "" msgstr "Gib einen Benutzernamen und ein Passwort ein"
msgid "No logs." msgid "No logs."
msgstr "" msgstr "Keine Protokolle."
msgid "Moderation log" msgid "Moderation log"
msgstr "" msgstr "Moderationsprotokoll"
#, fuzzy
#| msgid "Recent posts"
msgid "Reported posts" msgid "Reported posts"
msgstr "Neue Beiträge" msgstr "Gemeldete Beiträge"
#, php-format #, php-format
msgid "%1$d report by %2$s" msgid "%1$d report by %2$s"
msgstr "" msgstr "%1$d Meldung von %2$s"
#, php-format #, php-format
msgid "%1$d reports by %2$s" msgid "%1$d reports by %2$s"
msgstr "" msgstr "%1$d Meldungen von %2$s"
#, php-format #, php-format
msgid "%d reports" msgid "%d reports"
msgstr "" msgstr "%d Meldungen"
#, fuzzy, php-format #, php-format
#| msgid "Recent posts"
msgid "Reported by %s" msgid "Reported by %s"
msgstr "Neue Beiträge" msgstr "Gemeldet von %s"
msgid "Moderate" msgid "Moderate"
msgstr "Moderieren" msgstr "Moderieren"
msgid "There are currently no reported posts." msgid "There are currently no reported posts."
msgstr "" msgstr "Es gibt derzeit keine gemeldeten Beiträge."
msgid "Add an account" msgid "Add an account"
msgstr "" msgstr "Einen Benutzer hinzufügen"
msgid "Update an account" msgid "Update an account"
msgstr "" msgstr "Einen Benutzer aktualisieren"
msgid "Leave blank to maintain current password" msgid "Leave blank to maintain current password"
msgstr "" msgstr "Leer lassen um das aktuelle Passwort beizubehalten"
msgid "Username" msgid "Username"
msgstr "" msgstr "Benutzername"
msgid "Role" msgid "Role"
msgstr "" msgstr "Rolle"
msgid "Choose a role" msgid "Choose a role"
msgstr "" msgstr "Wähle eine Rolle"
msgid "Last active" msgid "Last active"
msgstr "" msgstr "Zuletzt aktiv"
msgid "Never" msgid "Never"
msgstr "" msgstr "Nie"
msgid "update" msgid "update"
msgstr "" msgstr "aktualisieren"
msgid "Add a ban" msgid "Add a ban"
msgstr "Verbannen" msgstr "Sperre hinzufügen"
msgid "" msgid ""
"Multiple IP addresses may be banned at once by separating each address with " "Multiple IP addresses may be banned at once by separating each address with "
"a comma." "a comma."
msgstr "" msgstr ""
"Mehrere IP-Adressen mit Komma getrennt angeben um sie gleichzeitig zu "
"sperren."
msgid "IP Address" msgid "IP Address"
msgstr "IP-Adresse" msgstr "IP-Adresse"
msgid "Expire(sec)" msgid "Expire(sec)"
msgstr "Ablaufen(Sek)" msgstr "Ablaufen (Sekunden)"
msgid "never" msgid "never"
msgstr "nie" msgstr "nie"
msgid "Optional." msgid "Optional."
msgstr "" msgstr "Optional."
msgid "1 hour" msgid "1 hour"
msgstr "1 Stunde" msgstr "1 Stunde"
@ -702,10 +696,10 @@ msgid "1 month"
msgstr "1 Monat" msgstr "1 Monat"
msgid "Append a message to the post. Optional." msgid "Append a message to the post. Optional."
msgstr "" msgstr "Dem Beitrag eine Nachricht anfügen. Optional."
msgid "Set At" msgid "Set At"
msgstr "Setzen bei" msgstr "Gesetzt am"
msgid "Expires" msgid "Expires"
msgstr "Läuft ab" msgstr "Läuft ab"
@ -723,14 +717,14 @@ msgid "Post ID"
msgstr "Beitrags-ID" msgstr "Beitrags-ID"
msgid "Tip" msgid "Tip"
msgstr "" msgstr "Tipp"
msgid "" msgid ""
"While browsing the image board, you can easily moderate a post if you are " "While browsing the image board, you can easily moderate a post if you are "
"logged in." "logged in."
msgstr "" msgstr ""
"Beim Durchsuchen der Bildplatine kannst du einen Beitrag leicht moderieren, " "Beim Besuchen des Boards kannst du einen Beitrag leicht moderieren, wenn du "
"wenn du angemeldet bist." "angemeldet bist."
msgid "" msgid ""
"Tick the box next to a post and click \"Delete\" at the bottom of the page " "Tick the box next to a post and click \"Delete\" at the bottom of the page "
@ -741,71 +735,72 @@ msgstr ""
#, php-format #, php-format
msgid "Moderate %d posts" msgid "Moderate %d posts"
msgstr "" msgstr "%d Beiträge moderieren"
msgid "Delete all" msgid "Delete all"
msgstr "" msgstr "Alle löschen"
msgid "Ban all" msgid "Ban all"
msgstr "" msgstr "Alle sperren"
msgid "1 thread and 1 reply will be deleted." msgid "1 thread and 1 reply will be deleted."
msgstr "" msgstr "1 Beitrag und 1 Antwort werden gelöscht."
#, php-format #, php-format
msgid "1 thread and %d replies will be deleted." msgid "1 thread and %d replies will be deleted."
msgstr "" msgstr "1 Beitrag und %d Antworten werden gelöscht."
#, php-format #, php-format
msgid "%d threads and 1 reply will be deleted." msgid "%d threads and 1 reply will be deleted."
msgstr "" msgstr "%d Beiträge und 1 Antwort wird gelöscht."
#, php-format #, php-format
msgid "%1$d threads and %2$d replies will be deleted." msgid "%1$d threads and %2$d replies will be deleted."
msgstr "" msgstr "%1$d Beiträge und %2$d Antworten werden gelöscht."
msgid "1 IP address will be banned." msgid "1 IP address will be banned."
msgstr "" msgstr "1 IP-Adresse wird gesperrt."
#, php-format #, php-format
msgid "%d IP addresses will be banned." msgid "%d IP addresses will be banned."
msgstr "" msgstr "%d IP-Adressen werden gesperrt."
#, php-format #, php-format
msgid " A ban record already exists for %s" msgid " A ban record already exists for %s"
msgstr " Ein Verbannungsdatensatz existiert bereits für %s" msgstr " Eine Sperre existiert bereits für %s"
msgid "Only an administrator may ban an IP address." msgid "Only an administrator may ban an IP address."
msgstr "Nur ein Administrator darf eine IP-Adresse sperren." msgstr "Nur Administratoren dürfen eine IP-Adresse sperren."
#, php-format #, php-format
msgid "IP address: %s" msgid "IP address: %s"
msgstr "IP-Adresse: %s" msgstr "IP-Adresse: %s"
msgid "Thread" msgid "Thread"
msgstr "Beitragsstrang" msgstr "Beitrag"
msgid "1 reply will be deleted." msgid "1 reply will be deleted."
msgstr "" msgstr "1 Antwort wird gelöscht."
#, php-format #, php-format
msgid "%d replies will be deleted." msgid "%d replies will be deleted."
msgstr "" msgstr "%d Antworten werden gelöscht."
msgid "Belongs to " #, php-format
msgstr "" msgid "Belongs to %s"
msgstr "Gehört %s"
msgid "Un-sticky" msgid "Un-sticky"
msgstr "Nicht mehr Sticky" msgstr "Abpinnen"
msgid "Sticky" msgid "Sticky"
msgstr "Klebrig" msgstr "Anpinnen"
msgid "Return this thread to a normal state." msgid "Return this thread to a normal state."
msgstr "Dieses Thema auf Normalzustand zurücksetzen." msgstr "Diesen Beitrag auf Normalzustand zurücksetzen."
msgid "Keep this thread at the top of the board." msgid "Keep this thread at the top of the board."
msgstr "Dieses Thema am Anfang des Boards behalten." msgstr "Diesen Beitrag oben am Board behalten."
msgid "Unlock" msgid "Unlock"
msgstr "Entsperren" msgstr "Entsperren"
@ -817,73 +812,72 @@ msgid "Allow replying to this thread."
msgstr "Antworten zu diesem Thema erlauben." msgstr "Antworten zu diesem Thema erlauben."
msgid "Disallow replying to this thread." msgid "Disallow replying to this thread."
msgstr "Antworten zu diesem Thema verweigern." msgstr "Antworten zu diesem Thema verbieten."
#, php-format #, php-format
msgid "Moderating No.%d" msgid "Moderating No.%d"
msgstr "Moderierende Nr. %d" msgstr "Moderiere Beitrag Nr. %d"
msgid "Action" msgid "Action"
msgstr "Aktion" msgstr "Aktion"
msgid "Delete thread" msgid "Delete thread"
msgstr "Beitragsstrang löschen" msgstr "Beitrag löschen"
msgid "Delete reply" msgid "Delete reply"
msgstr "" msgstr "Antwort löschen"
msgid "Ban poster" msgid "Ban poster"
msgstr "Autor verbannen" msgstr "Autor sperren"
msgid "Approve" msgid "Approve"
msgstr "Genehmigen" msgstr "Freigeben"
msgid "report" msgid "report"
msgstr "" msgstr "Meldung"
msgid "reports" msgid "reports"
msgstr "" msgstr "Meldungen"
#, fuzzy
#| msgid "Sorry, there doesn't appear to be a post with that ID."
msgid "Sorry, there doesn't appear to be a keyword with that ID." msgid "Sorry, there doesn't appear to be a keyword with that ID."
msgstr "Entschuldigung, es scheint kein Beitrag mit dieser ID zu existieren." msgstr ""
"Entschuldigung, es scheint kein Schlagwort mit dieser ID zu existieren."
msgid "Add" msgid "Add"
msgstr "" msgstr "Hinzufügen"
msgid "Hide until approved" msgid "Hide until approved"
msgstr "" msgstr "Ausblenden bis freigegeben"
msgid "Delete and ban for 1 hour" msgid "Delete and ban for 1 hour"
msgstr "" msgstr "Löschen und für 1 Stunde sperren"
msgid "Delete and ban for 1 day" msgid "Delete and ban for 1 day"
msgstr "" msgstr "Löschen und für 1 Tag sperren"
msgid "Delete and ban for 2 days" msgid "Delete and ban for 2 days"
msgstr "" msgstr "Löschen und für 2 Tage sperren"
msgid "Delete and ban for 1 week" msgid "Delete and ban for 1 week"
msgstr "" msgstr "Löschen und für 1 Woche sperren"
msgid "Delete and ban for 2 weeks" msgid "Delete and ban for 2 weeks"
msgstr "" msgstr "Löschen und für 2 Wochen sperren"
msgid "Delete and ban for 1 month" msgid "Delete and ban for 1 month"
msgstr "" msgstr "Löschen und für 1 Monat sperren"
msgid "Delete and ban permanently" msgid "Delete and ban permanently"
msgstr "" msgstr "Löschen und dauerhaft sperren"
msgid "Edit" msgid "Edit"
msgstr "" msgstr "Bearbeiten"
msgid "thread" msgid "thread"
msgstr "Thema" msgstr "Beitrag"
msgid "threads" msgid "threads"
msgstr "Themen" msgstr "Beiträge"
msgid "ban" msgid "ban"
msgstr "Ban" msgstr "Ban"
@ -895,7 +889,7 @@ msgid "More Info"
msgstr "Mehr Information" msgstr "Mehr Information"
msgid "Pending posts" msgid "Pending posts"
msgstr "Vorbehaltene Beiträge" msgstr "Wartende Beiträge"
msgid "Recent posts" msgid "Recent posts"
msgstr "Neue Beiträge" msgstr "Neue Beiträge"

View File

@ -1,7 +1,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"POT-Creation-Date: 2021-08-05 11:29-0700\n" "POT-Creation-Date: 2023-01-05 10:20-0800\n"
"PO-Revision-Date: 2020-08-28 16:18-0700\n" "PO-Revision-Date: 2020-08-28 16:18-0700\n"
"Last-Translator: Automatically generated\n" "Last-Translator: Automatically generated\n"
"Language-Team: none\n" "Language-Team: none\n"
@ -358,8 +358,8 @@ msgstr ""
#, php-format #, php-format
msgid "" msgid ""
"Duplicate file uploaded. That file has already been posted <a href=\"%s" "Duplicate file uploaded. That file has already been posted <a "
"\">here</a>." "href=\"%s\">here</a>."
msgstr "" msgstr ""
msgid "" msgid ""
@ -735,7 +735,8 @@ msgstr ""
msgid "%d replies will be deleted." msgid "%d replies will be deleted."
msgstr "" msgstr ""
msgid "Belongs to " #, php-format
msgid "Belongs to %s"
msgstr "" msgstr ""
msgid "Un-sticky" msgid "Un-sticky"

View File

@ -1,8 +1,8 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"POT-Creation-Date: 2021-08-05 11:29-0700\n" "POT-Creation-Date: 2023-01-05 10:20-0800\n"
"PO-Revision-Date: 2021-08-05 11:29-0700\n" "PO-Revision-Date: 2023-01-05 10:20-0800\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
"Language: en\n" "Language: en\n"
@ -17,201 +17,201 @@ msgstr ""
"X-Poedit-SearchPath-1: inc/functions.php\n" "X-Poedit-SearchPath-1: inc/functions.php\n"
"X-Poedit-SearchPath-2: inc/html.php\n" "X-Poedit-SearchPath-2: inc/html.php\n"
#: imgboard.php:44 #: imgboard.php:41
msgid "Click here to go back" msgid "Click here to go back"
msgstr "" msgstr ""
#: imgboard.php:70 #: imgboard.php:62
msgid "TINYIB_HCAPTCHA_SITE and TINYIB_HCAPTCHA_SECRET must be configured." msgid "TINYIB_HCAPTCHA_SITE and TINYIB_HCAPTCHA_SECRET must be configured."
msgstr "" msgstr ""
#: imgboard.php:74 #: imgboard.php:66
msgid "TINYIB_RECAPTCHA_SITE and TINYIB_RECAPTCHA_SECRET must be configured." msgid "TINYIB_RECAPTCHA_SITE and TINYIB_RECAPTCHA_SECRET must be configured."
msgstr "" msgstr ""
#: imgboard.php:82 #: imgboard.php:74
msgid "TINYIB_TRIPSEED must be configured." msgid "TINYIB_TRIPSEED must be configured."
msgstr "" msgstr ""
#: imgboard.php:89 imgboard.php:991 #: imgboard.php:81 imgboard.php:991
msgid "Unknown database mode specified." msgid "Unknown database mode specified."
msgstr "" msgstr ""
#: imgboard.php:250 #: imgboard.php:242
#, php-format #, php-format
msgid "Directory '%s' can not be written to. Please modify its permissions." msgid "Directory '%s' can not be written to. Please modify its permissions."
msgstr "" msgstr ""
#: imgboard.php:269 #: imgboard.php:263
msgid "Posting is currently disabled.<br>Please try again in a few moments." msgid "Posting is currently disabled.<br>Please try again in a few moments."
msgstr "" msgstr ""
#: imgboard.php:298 inc/functions.php:462 #: imgboard.php:292 inc/functions.php:482
msgid "Invalid parent thread ID supplied, unable to create post." msgid "Invalid parent thread ID supplied, unable to create post."
msgstr "" msgstr ""
#: imgboard.php:300 #: imgboard.php:294
msgid "Replies are not allowed to locked threads." msgid "Replies are not allowed to locked threads."
msgstr "" msgstr ""
#: imgboard.php:377 #: imgboard.php:371
msgid "Your post contains a blocked keyword." msgid "Your post contains a blocked keyword."
msgstr "" msgstr ""
#: imgboard.php:404 inc/html.php:1509 inc/html.php:1547 #: imgboard.php:398 inc/html.php:1509 inc/html.php:1547
msgid "Keyword" msgid "Keyword"
msgstr "" msgstr ""
#: imgboard.php:408 #: imgboard.php:402
#, php-format #, php-format
msgid "Your IP address (%1$s) is banned until %2$s." msgid "Your IP address (%1$s) is banned until %2$s."
msgstr "" msgstr ""
#: imgboard.php:410 #: imgboard.php:404
#, php-format #, php-format
msgid "Your IP address (%s) is permanently banned." msgid "Your IP address (%s) is permanently banned."
msgstr "" msgstr ""
#: imgboard.php:413 inc/html.php:1210 inc/html.php:1247 #: imgboard.php:407 inc/html.php:1210 inc/html.php:1247
msgid "Reason" msgid "Reason"
msgstr "" msgstr ""
#: imgboard.php:424 #: imgboard.php:418
msgid "Embedding a URL and uploading a file at the same time is not supported." msgid "Embedding a URL and uploading a file at the same time is not supported."
msgstr "" msgstr ""
#: imgboard.php:430 #: imgboard.php:424
#, php-format #, php-format
msgid "Invalid embed URL. Only %s URLs are supported." msgid "Invalid embed URL. Only %s URLs are supported."
msgstr "" msgstr ""
#: imgboard.php:435 imgboard.php:444 inc/functions.php:493 #: imgboard.php:429 imgboard.php:438 inc/functions.php:513
#: inc/functions.php:875 #: inc/functions.php:895
#, php-format #, php-format
msgid "That file is larger than %s." msgid "That file is larger than %s."
msgstr "" msgstr ""
#: imgboard.php:440 imgboard.php:450 #: imgboard.php:434 imgboard.php:444
msgid "Failed to download file at specified URL." msgid "Failed to download file at specified URL."
msgstr "" msgstr ""
#: imgboard.php:472 #: imgboard.php:466
msgid "Error while processing audio/video." msgid "Error while processing audio/video."
msgstr "" msgstr ""
#: imgboard.php:479 inc/functions.php:900 inc/functions.php:911 #: imgboard.php:473 inc/functions.php:920 inc/functions.php:931
msgid "Could not create thumbnail." msgid "Could not create thumbnail."
msgstr "" msgstr ""
#: imgboard.php:502 #: imgboard.php:496
msgid "upload a file or embed a URL" msgid "upload a file or embed a URL"
msgstr "" msgstr ""
#: imgboard.php:504 #: imgboard.php:498
msgid "upload a file" msgid "upload a file"
msgstr "" msgstr ""
#: imgboard.php:506 #: imgboard.php:500
msgid "embed a URL" msgid "embed a URL"
msgstr "" msgstr ""
#: imgboard.php:509 #: imgboard.php:503
#, php-format #, php-format
msgid "Please %s to start a new thread." msgid "Please %s to start a new thread."
msgstr "" msgstr ""
#: imgboard.php:515 #: imgboard.php:509
#, php-format #, php-format
msgid "Please enter a message and/or %s." msgid "Please enter a message and/or %s."
msgstr "" msgstr ""
#: imgboard.php:517 #: imgboard.php:511
msgid "Please enter a message." msgid "Please enter a message."
msgstr "" msgstr ""
#: imgboard.php:519 #: imgboard.php:513
#, php-format #, php-format
msgid "Please %s." msgid "Please %s."
msgstr "" msgstr ""
#: imgboard.php:525 #: imgboard.php:519
#, php-format #, php-format
msgid "Your %s will be shown <b>once it has been approved</b>." msgid "Your %s will be shown <b>once it has been approved</b>."
msgstr "" msgstr ""
#: imgboard.php:548 #: imgboard.php:542
msgid "Updating thread..." msgid "Updating thread..."
msgstr "" msgstr ""
#: imgboard.php:561 #: imgboard.php:555
msgid "Updating index..." msgid "Updating index..."
msgstr "" msgstr ""
#: imgboard.php:566 #: imgboard.php:560
msgid "Created staff post" msgid "Created staff post"
msgstr "" msgstr ""
#: imgboard.php:572 #: imgboard.php:566
msgid "This post has been deleted" msgid "This post has been deleted"
msgstr "" msgstr ""
#: imgboard.php:574 #: imgboard.php:568
msgid "This post requires moderation before it can be displayed" msgid "This post requires moderation before it can be displayed"
msgstr "" msgstr ""
#: imgboard.php:587 #: imgboard.php:581
msgid "Automatic refreshing is disabled." msgid "Automatic refreshing is disabled."
msgstr "" msgstr ""
#: imgboard.php:612 imgboard.php:762 #: imgboard.php:608 imgboard.php:762
msgid "Reporting is disabled." msgid "Reporting is disabled."
msgstr "" msgstr ""
#: imgboard.php:617 imgboard.php:716 #: imgboard.php:613 imgboard.php:714
msgid "" msgid ""
"Sorry, an invalid post identifier was sent. Please go back, refresh the " "Sorry, an invalid post identifier was sent. Please go back, refresh the "
"page, and try again." "page, and try again."
msgstr "" msgstr ""
#: imgboard.php:621 #: imgboard.php:617
msgid "Moderators have determined that post does not break any rules." msgid "Moderators have determined that post does not break any rules."
msgstr "" msgstr ""
#: imgboard.php:626 #: imgboard.php:622
msgid "You have already submitted a report for that post." msgid "You have already submitted a report for that post."
msgstr "" msgstr ""
#: imgboard.php:662 inc/html.php:227 inc/html.php:969 #: imgboard.php:658 inc/html.php:227 inc/html.php:969
msgid "(enter the text below)" msgid "(enter the text below)"
msgstr "" msgstr ""
#: imgboard.php:666 #: imgboard.php:662
msgid "Please complete a CAPTCHA to submit your report" msgid "Please complete a CAPTCHA to submit your report"
msgstr "" msgstr ""
#: imgboard.php:667 inc/html.php:117 inc/html.php:1113 inc/html.php:1165 #: imgboard.php:663 inc/html.php:117 inc/html.php:1113 inc/html.php:1165
#: inc/html.php:1213 inc/html.php:1261 #: inc/html.php:1213 inc/html.php:1261
msgid "Submit" msgid "Submit"
msgstr "" msgstr ""
#: imgboard.php:689 #: imgboard.php:685
msgid "Post reported." msgid "Post reported."
msgstr "" msgstr ""
#: imgboard.php:693 #: imgboard.php:691
msgid "Tick the box next to a post and click \"Delete\" to delete it." msgid "Tick the box next to a post and click \"Delete\" to delete it."
msgstr "" msgstr ""
#: imgboard.php:697 #: imgboard.php:695
msgid "" msgid ""
"Post deletion is currently disabled.<br>Please try again in a few moments." "Post deletion is currently disabled.<br>Please try again in a few moments."
msgstr "" msgstr ""
#: imgboard.php:724 #: imgboard.php:722
msgid "Post deleted." msgid "Post deleted."
msgstr "" msgstr ""
#: imgboard.php:726 #: imgboard.php:724
msgid "Invalid password." msgid "Invalid password."
msgstr "" msgstr ""
@ -411,90 +411,90 @@ msgstr ""
msgid "Password updated" msgid "Password updated"
msgstr "" msgstr ""
#: inc/functions.php:321 #: inc/functions.php:341
msgid "Please enter the CAPTCHA text." msgid "Please enter the CAPTCHA text."
msgstr "" msgstr ""
#: inc/functions.php:323 #: inc/functions.php:343
msgid "" msgid ""
"Incorrect CAPTCHA text entered. Please try again.<br>Click the image to " "Incorrect CAPTCHA text entered. Please try again.<br>Click the image to "
"retrieve a new CAPTCHA." "retrieve a new CAPTCHA."
msgstr "" msgstr ""
#: inc/functions.php:372 #: inc/functions.php:392
#, php-format #, php-format
msgid "" msgid ""
"Please shorten your message, or post it in multiple parts. Your message is " "Please shorten your message, or post it in multiple parts. Your message is "
"%1$d characters long, and the maximum allowed is %2$d." "%1$d characters long, and the maximum allowed is %2$d."
msgstr "" msgstr ""
#: inc/functions.php:405 #: inc/functions.php:425
msgid "Invalid key." msgid "Invalid key."
msgstr "" msgstr ""
#: inc/functions.php:416 #: inc/functions.php:436
msgid "Invalid username or password." msgid "Invalid username or password."
msgstr "" msgstr ""
#: inc/functions.php:496 #: inc/functions.php:516
#, php-format #, php-format
msgid "" msgid ""
"The uploaded file exceeds the upload_max_filesize directive (%s) in php.ini." "The uploaded file exceeds the upload_max_filesize directive (%s) in php.ini."
msgstr "" msgstr ""
#: inc/functions.php:499 #: inc/functions.php:519
msgid "The uploaded file was only partially uploaded." msgid "The uploaded file was only partially uploaded."
msgstr "" msgstr ""
#: inc/functions.php:502 #: inc/functions.php:522
msgid "No file was uploaded." msgid "No file was uploaded."
msgstr "" msgstr ""
#: inc/functions.php:505 #: inc/functions.php:525
msgid "Missing a temporary folder." msgid "Missing a temporary folder."
msgstr "" msgstr ""
#: inc/functions.php:508 #: inc/functions.php:528
msgid "Failed to write file to disk" msgid "Failed to write file to disk"
msgstr "" msgstr ""
#: inc/functions.php:511 #: inc/functions.php:531
msgid "Unable to save the uploaded file." msgid "Unable to save the uploaded file."
msgstr "" msgstr ""
#: inc/functions.php:519 #: inc/functions.php:539
#, php-format #, php-format
msgid "" msgid ""
"Duplicate file uploaded. That file has already been posted <a href=\"%s" "Duplicate file uploaded. That file has already been posted <a "
"\">here</a>." "href=\"%s\">here</a>."
msgstr "" msgstr ""
#: inc/functions.php:611 inc/functions.php:684 #: inc/functions.php:631 inc/functions.php:704
msgid "" msgid ""
"Unable to read the uploaded file while creating its thumbnail. A common " "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 " "cause for this is an incorrect extension when the file is actually of a "
"different type." "different type."
msgstr "" msgstr ""
#: inc/functions.php:836 #: inc/functions.php:856
msgid "File transfer failure. Please retry the submission." msgid "File transfer failure. Please retry the submission."
msgstr "" msgstr ""
#: inc/functions.php:845 #: inc/functions.php:865
msgid "" msgid ""
"Failed to read the MIME type and size of the uploaded file. Please retry the " "Failed to read the MIME type and size of the uploaded file. Please retry the "
"submission." "submission."
msgstr "" msgstr ""
#: inc/functions.php:859 inc/functions.php:864 #: inc/functions.php:879 inc/functions.php:884
msgid "Could not copy uploaded file." msgid "Could not copy uploaded file."
msgstr "" msgstr ""
#: inc/functions.php:872 #: inc/functions.php:892
msgid "File transfer failure. Please go back and try again." msgid "File transfer failure. Please go back and try again."
msgstr "" msgstr ""
#: inc/functions.php:928 inc/functions.php:951 #: inc/functions.php:948 inc/functions.php:971
msgid "Sorry, your video appears to be corrupt." msgid "Sorry, your video appears to be corrupt."
msgstr "" msgstr ""
@ -958,7 +958,8 @@ msgid "%d replies will be deleted."
msgstr "" msgstr ""
#: inc/html.php:1371 #: inc/html.php:1371
msgid "Belongs to " #, php-format
msgid "Belongs to %s"
msgstr "" msgstr ""
#: inc/html.php:1378 #: inc/html.php:1378

View File

@ -1,10 +1,10 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: trevor@rocketnine.space\n" "Report-Msgid-Bugs-To: trevor@rocket9labs.com\n"
"POT-Creation-Date: 2021-08-05 11:29-0700\n" "POT-Creation-Date: 2023-01-05 10:20-0800\n"
"PO-Revision-Date: 2021-08-30 06:32+0000\n" "PO-Revision-Date: 2023-11-05 18:33+0000\n"
"Last-Translator: Warichas <warichas@protonmail.com>\n" "Last-Translator: CANIBAL <rikrdofrancisco@gmail.com>\n"
"Language-Team: Spanish (Mexico) <https://hosted.weblate.org/projects/tinyib/" "Language-Team: Spanish (Mexico) <https://hosted.weblate.org/projects/tinyib/"
"tinyib/es_MX/>\n" "tinyib/es_MX/>\n"
"Language: es_MX\n" "Language: es_MX\n"
@ -12,14 +12,14 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.8.1-dev\n" "X-Generator: Weblate 5.2-dev\n"
"X-Poedit-Basepath: ..\n" "X-Poedit-Basepath: ..\n"
"X-Poedit-SearchPath-0: imgboard.php\n" "X-Poedit-SearchPath-0: imgboard.php\n"
"X-Poedit-SearchPath-1: inc/html.php\n" "X-Poedit-SearchPath-1: inc/html.php\n"
"X-Poedit-SearchPath-2: inc/functions.php\n" "X-Poedit-SearchPath-2: inc/functions.php\n"
msgid "Click here to go back" msgid "Click here to go back"
msgstr "Haz click aquí para regresar" msgstr "Haz click para regresar"
msgid "TINYIB_HCAPTCHA_SITE and TINYIB_HCAPTCHA_SECRET must be configured." msgid "TINYIB_HCAPTCHA_SITE and TINYIB_HCAPTCHA_SECRET must be configured."
msgstr "" msgstr ""
@ -344,8 +344,8 @@ msgid ""
"Incorrect CAPTCHA text entered. Please try again.<br>Click the image to " "Incorrect CAPTCHA text entered. Please try again.<br>Click the image to "
"retrieve a new CAPTCHA." "retrieve a new CAPTCHA."
msgstr "" msgstr ""
"El texto ingresado del CAPTCHA es incorrecto. Por favor inténtelo de " "El texto ingresado del CAPTCHA es incorrecto. Por favor inténtelo de nuevo."
"nuevo.<br>Haga click en la imagen para obtener un nuevo CAPTCHA." "<br>Haga click en la imagen para obtener un nuevo CAPTCHA."
#, php-format #, php-format
msgid "" msgid ""
@ -384,11 +384,11 @@ msgstr "No se pudo guardar el archivo subido."
#, php-format #, php-format
msgid "" msgid ""
"Duplicate file uploaded. That file has already been posted <a href=\"%s" "Duplicate file uploaded. That file has already been posted <a "
"\">here</a>." "href=\"%s\">here</a>."
msgstr "" msgstr ""
"Se ha subido un archivo duplicado. Ese archivo ya ha sido publicado <a href=" "Se ha subido un archivo duplicado. Ese archivo ya ha sido publicado <a "
"\"%s\">aquí</a>." "href=\"%s\">aquí</a>."
msgid "" msgid ""
"Unable to read the uploaded file while creating its thumbnail. A common " "Unable to read the uploaded file while creating its thumbnail. A common "
@ -557,7 +557,7 @@ msgid "Moderate Post"
msgstr "Moderar Post" msgstr "Moderar Post"
msgid "Moderation Log" msgid "Moderation Log"
msgstr "Registro de moderación" msgstr "Registro de Moderación"
msgid "Rebuild All" msgid "Rebuild All"
msgstr "Reconstruir todo" msgstr "Reconstruir todo"
@ -645,7 +645,7 @@ msgid "Never"
msgstr "Nunca" msgstr "Nunca"
msgid "update" msgid "update"
msgstr "Actualizar" msgstr "actualizar"
msgid "Add a ban" msgid "Add a ban"
msgstr "Añadir un ban" msgstr "Añadir un ban"
@ -777,8 +777,9 @@ msgstr "1 respuesta será eliminada."
msgid "%d replies will be deleted." msgid "%d replies will be deleted."
msgstr "%d respuestas serán eliminadas." msgstr "%d respuestas serán eliminadas."
msgid "Belongs to " #, php-format
msgstr "Pertenece a " msgid "Belongs to %s"
msgstr "Pertenece a %s"
msgid "Un-sticky" msgid "Un-sticky"
msgstr "Dejar de fijar" msgstr "Dejar de fijar"
@ -886,6 +887,9 @@ msgstr "Posts recientes"
msgid "Info" msgid "Info"
msgstr "Información" msgstr "Información"
#~ msgid "Belongs to "
#~ msgstr "Pertenece a "
#~ msgid "Created raw post" #~ msgid "Created raw post"
#~ msgstr "Crear publicación bruta" #~ msgstr "Crear publicación bruta"

View File

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: trevor@rocketnine.space\n" "Report-Msgid-Bugs-To: trevor@rocketnine.space\n"
"POT-Creation-Date: 2021-08-05 11:29-0700\n" "POT-Creation-Date: 2023-01-05 10:20-0800\n"
"PO-Revision-Date: 2020-10-08 23:26+0000\n" "PO-Revision-Date: 2020-10-08 23:26+0000\n"
"Last-Translator: tei zerg <teizerg@airmail.cc>\n" "Last-Translator: tei zerg <teizerg@airmail.cc>\n"
"Language-Team: Spanish (American) <https://hosted.weblate.org/projects/" "Language-Team: Spanish (American) <https://hosted.weblate.org/projects/"
@ -362,8 +362,8 @@ msgstr ""
#, php-format #, php-format
msgid "" msgid ""
"Duplicate file uploaded. That file has already been posted <a href=\"%s" "Duplicate file uploaded. That file has already been posted <a "
"\">here</a>." "href=\"%s\">here</a>."
msgstr "" msgstr ""
msgid "" msgid ""
@ -739,7 +739,8 @@ msgstr ""
msgid "%d replies will be deleted." msgid "%d replies will be deleted."
msgstr "" msgstr ""
msgid "Belongs to " #, php-format
msgid "Belongs to %s"
msgstr "" msgstr ""
msgid "Un-sticky" msgid "Un-sticky"

View File

@ -1,7 +1,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"POT-Creation-Date: 2021-08-05 11:29-0700\n" "POT-Creation-Date: 2023-01-05 10:20-0800\n"
"PO-Revision-Date: 2020-08-28 16:18-0700\n" "PO-Revision-Date: 2020-08-28 16:18-0700\n"
"Last-Translator: Automatically generated\n" "Last-Translator: Automatically generated\n"
"Language-Team: none\n" "Language-Team: none\n"
@ -358,8 +358,8 @@ msgstr ""
#, php-format #, php-format
msgid "" msgid ""
"Duplicate file uploaded. That file has already been posted <a href=\"%s" "Duplicate file uploaded. That file has already been posted <a "
"\">here</a>." "href=\"%s\">here</a>."
msgstr "" msgstr ""
msgid "" msgid ""
@ -735,7 +735,8 @@ msgstr ""
msgid "%d replies will be deleted." msgid "%d replies will be deleted."
msgstr "" msgstr ""
msgid "Belongs to " #, php-format
msgid "Belongs to %s"
msgstr "" msgstr ""
msgid "Un-sticky" msgid "Un-sticky"

View File

@ -1,7 +1,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"POT-Creation-Date: 2021-08-05 11:29-0700\n" "POT-Creation-Date: 2023-01-05 10:20-0800\n"
"PO-Revision-Date: 2020-08-28 16:18-0700\n" "PO-Revision-Date: 2020-08-28 16:18-0700\n"
"Last-Translator: Automatically generated\n" "Last-Translator: Automatically generated\n"
"Language-Team: none\n" "Language-Team: none\n"
@ -358,8 +358,8 @@ msgstr ""
#, php-format #, php-format
msgid "" msgid ""
"Duplicate file uploaded. That file has already been posted <a href=\"%s" "Duplicate file uploaded. That file has already been posted <a "
"\">here</a>." "href=\"%s\">here</a>."
msgstr "" msgstr ""
msgid "" msgid ""
@ -735,7 +735,8 @@ msgstr ""
msgid "%d replies will be deleted." msgid "%d replies will be deleted."
msgstr "" msgstr ""
msgid "Belongs to " #, php-format
msgid "Belongs to %s"
msgstr "" msgstr ""
msgid "Un-sticky" msgid "Un-sticky"

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More