mirror of
https://github.com/FreshRSS/FreshRSS.git
synced 2026-05-20 09:30:36 +00:00
Show time since when a feed has problems + new timeago() method and i18n plurals (#8670)
Closes https://github.com/FreshRSS/FreshRSS/issues/8508 Changes proposed in this pull request: - Use an integer for `Feed::error` everywhere (follow up to #8646) - Extract `Entry::machineReadableDate()` into function for use in HTML templates - Add `timeago()` function that converts a unix timestamp into a "4 weeks ago" string - Show the last successful feed update, and the last erroneous update How to test the feature manually: 1. Update a feed 2. Modify the feed URL in the database and set it to a non-existing URL 3. Update the feed again 4. Open the "Manage feed" and see the expanded error message: > Blast! This feed has encountered a problem. If this situation persists, please verify that it is still reachable. > Last successful update 3 hours ago, last erroneous update 1 hour ago. You can hover the relative dates to see the timestamp. * Make Feed::error an int everywhere Related: https://github.com/FreshRSS/FreshRSS/pull/8646 * Extract timestamptomachinedate() .. for later usage in the feed error time display. * Show time since when a feed has problems We add our own "timeago" function that converts a unix timestamp into a "4 weeks ago" string. Resolves: https://github.com/FreshRSS/FreshRSS/issues/8508 * Add new translation keys * i18n fr, en-US * Minor XHTML preference * Slightly shorter rewrite, also hopefully easier to read * Rewrite to allow (simple) plural I also moved some functions around for hopefully a more generic and better structure. I made some changes for the sake of speed (e.g. second-based logic instead of datetime intervals). Note: I used automatic translation as I was worried it would be too complicated to explain to translators... I proofread the few languages I have some familiarity with. * Add reference to CLDR * Slightly more compact syntax * Always show last update, fix case of unknown error date * Remove forgotten span * No need for multi-lines anymore * Fix error date thresshold * plurals forms * Extract gettext formula conversion script to cli * Simplify a bit * Escort excess parentheses to the door * Simplify * Avoid being too clever in localization * Fix German * Fix plural TODO parsing * Ignore en-US translation * make fix-all * git update-index --chmod=+x cli/compile.plurals.php * Heredoc indent PHP 7.3+ * compileAll: Continue on error * PHP strict comparisons * Light logical simplification * Cache plural_message_families * Avoid case of empty value * A bit of documentation --------- Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr> Co-authored-by: Frans de Jonge <frans@clevercast.com> Co-authored-by: Frans de Jonge <fransdejonge@gmail.com>
This commit is contained in:
@@ -168,9 +168,15 @@ endif
|
||||
$(PHP) ./cli/manipulate.translation.php --action add --language $(lang) --origin-language $(ref)
|
||||
@echo Language added.
|
||||
|
||||
.PHONY: i18n-compile-plurals
|
||||
i18n-compile-plurals: ## Compile plural formulas from app/i18n/*/plurals.php
|
||||
@$(PHP) ./cli/compile.plurals.php --all
|
||||
@echo Plural files compiled.
|
||||
|
||||
.PHONY: i18n-format
|
||||
i18n-format: ## Format I18N files
|
||||
@$(PHP) ./cli/manipulate.translation.php --action format
|
||||
@$(PHP) ./cli/compile.plurals.php --all
|
||||
@echo Files formatted.
|
||||
|
||||
.PHONY: i18n-ignore-key
|
||||
|
||||
+2
-2
@@ -229,14 +229,14 @@ Voir le [dépôt dédié à ces extensions](https://github.com/FreshRSS/Extensio
|
||||
| - | - | - |
|
||||
| Čeština (cs) | ■■■■■■■■・・ 82% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fcs+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| Deutsch (de) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fde+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| Ελληνικά (el) | ■■■・・・・・・・ 37% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fel+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| Ελληνικά (el) | ■■■・・・・・・・ 38% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fel+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| English (en) | ■■■■■■■■■■ 100% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fen+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| English (United States) (en-US) | ■■■■■■■■■■ 100% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fen-US+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| Español (es) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fes+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| فارسی (fa) | ■■■■■■■■■・ 91% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffa+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| Suomi (fi) | ■■■■■■■■■・ 93% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffi+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| Français (fr) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffr+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| עברית (he) | ■■■■・・・・・・ 41% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fhe+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| עברית (he) | ■■■■・・・・・・ 42% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fhe+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| Magyar (hu) | ■■■■■■■■■・ 97% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fhu+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| Bahasa Indonesia (id) | ■■■■■■■■■・ 90% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fid+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| Italiano (it) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fit+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
|
||||
@@ -125,14 +125,14 @@ See the [repository dedicated to those extensions](https://github.com/FreshRSS/E
|
||||
| - | - | - |
|
||||
| Čeština (cs) | ■■■■■■■■・・ 82% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fcs+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| Deutsch (de) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fde+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| Ελληνικά (el) | ■■■・・・・・・・ 37% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fel+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| Ελληνικά (el) | ■■■・・・・・・・ 38% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fel+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| English (en) | ■■■■■■■■■■ 100% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fen+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| English (United States) (en-US) | ■■■■■■■■■■ 100% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fen-US+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| Español (es) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fes+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| فارسی (fa) | ■■■■■■■■■・ 91% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffa+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| Suomi (fi) | ■■■■■■■■■・ 93% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffi+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| Français (fr) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffr+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| עברית (he) | ■■■■・・・・・・ 41% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fhe+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| עברית (he) | ■■■■・・・・・・ 42% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fhe+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| Magyar (hu) | ■■■■■■■■■・ 97% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fhu+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| Bahasa Indonesia (id) | ■■■■■■■■■・ 90% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fid+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
| Italiano (it) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fit+%2F%28TODO%7CDIRTY%29%24%2F) |
|
||||
|
||||
@@ -482,7 +482,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
|
||||
/**
|
||||
* @param array<array{c_name:string,c_id:int,c_kind:int,c_last_update:int,c_error:int|bool,c_attributes?:string,
|
||||
* id?:int,name?:string,url?:string,kind?:int,website?:string,priority?:int,
|
||||
* error?:int|bool,attributes?:string,cache_nbEntries?:int,cache_nbUnreads?:int,ttl?:int}> $listDAO
|
||||
* error?:int,attributes?:string,cache_nbEntries?:int,cache_nbUnreads?:int,ttl?:int}> $listDAO
|
||||
* @return array<int,FreshRSS_Category> where the key is the category ID
|
||||
*/
|
||||
private static function daoToCategoriesPrepopulated(array $listDAO): array {
|
||||
|
||||
+16
-5
@@ -62,7 +62,7 @@ class FreshRSS_Feed extends Minz_Model {
|
||||
private int $priority = self::PRIORITY_MAIN_STREAM;
|
||||
private string $pathEntries = '';
|
||||
private string $httpAuth = '';
|
||||
private bool $error = false;
|
||||
private int $error = 0;
|
||||
private int $ttl = self::TTL_DEFAULT;
|
||||
private bool $mute = false;
|
||||
private string $hash = '';
|
||||
@@ -354,10 +354,21 @@ class FreshRSS_Feed extends Minz_Model {
|
||||
return $curl_options;
|
||||
}
|
||||
|
||||
public function inError(): bool {
|
||||
/**
|
||||
* Timestamp of last update error.
|
||||
* Legacy: may return 1 if the feed has an error but the timestamp is not available.
|
||||
*/
|
||||
public function lastError(): int {
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the feed has an error
|
||||
*/
|
||||
public function inError(): bool {
|
||||
return $this->error > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $raw true for database version combined with mute information, false otherwise
|
||||
*/
|
||||
@@ -525,8 +536,8 @@ class FreshRSS_Feed extends Minz_Model {
|
||||
$this->httpAuth = $value;
|
||||
}
|
||||
|
||||
public function _error(bool|int $value): void {
|
||||
$this->error = (bool)$value;
|
||||
public function _error(int $value): void {
|
||||
$this->error = $value;
|
||||
}
|
||||
public function _mute(bool $value): void {
|
||||
$this->mute = $value;
|
||||
@@ -754,7 +765,7 @@ class FreshRSS_Feed extends Minz_Model {
|
||||
return $this->loadGuids($simplePie, $invalidGuidsTolerance);
|
||||
}
|
||||
}
|
||||
$this->_error(true);
|
||||
$this->_error(time());
|
||||
}
|
||||
|
||||
return $guids;
|
||||
|
||||
@@ -704,7 +704,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
|
||||
|
||||
/**
|
||||
* @param array<array{id?:int,url?:string,kind?:int,category?:int,name?:string,website?:string,description?:string,lastUpdate?:int,priority?:int,
|
||||
* pathEntries?:string,httpAuth?:string,error?:int|bool,ttl?:int,attributes?:string,cache_nbUnreads?:int,cache_nbEntries?:int}> $listDAO
|
||||
* pathEntries?:string,httpAuth?:string,error?:int,ttl?:int,attributes?:string,cache_nbUnreads?:int,cache_nbEntries?:int}> $listDAO
|
||||
* @return array<int,FreshRSS_Feed> where the key is the feed ID
|
||||
*/
|
||||
public static function daoToFeeds(array $listDAO, ?int $catID = null): array {
|
||||
|
||||
@@ -140,6 +140,39 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => 'O FreshRSS',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => 'před %d den',
|
||||
1 => 'před %d dny',
|
||||
2 => 'před %d dní',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => 'před %d hodina',
|
||||
1 => 'před %d hodiny',
|
||||
2 => 'před %d hodin',
|
||||
),
|
||||
'justnow' => 'právě teď',
|
||||
'minute' => array(
|
||||
0 => 'před %d minuta',
|
||||
1 => 'před %d minuty',
|
||||
2 => 'před %d minut',
|
||||
),
|
||||
'month' => array(
|
||||
0 => 'před %d měsíc',
|
||||
1 => 'před %d měsíce',
|
||||
2 => 'před %d měsíců',
|
||||
),
|
||||
'second' => array(
|
||||
0 => 'před %d sekunda',
|
||||
1 => 'před %d sekundy',
|
||||
2 => 'před %d sekund',
|
||||
),
|
||||
'year' => array(
|
||||
0 => 'před %d rok',
|
||||
1 => 'před %d roky',
|
||||
2 => 'před %d let',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Prázdná kategorie',
|
||||
'confirm_action' => 'Opravdu chcete provést tuto akci? Toto nelze zrušit!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 3,
|
||||
'plural' => static fn (int $n): int => ($n === 1 ? 0 : ($n >= 2 && $n <= 4 ? 1 : 2)),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (výchozí)',
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Vymazat mezipaměť',
|
||||
'clear_cache_help' => 'Vymazat mezipaměť pro tento kanál.',
|
||||
|
||||
@@ -140,6 +140,33 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => 'Über FreshRSS',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => 'vor %d Tag',
|
||||
1 => 'vor %d Tage',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => 'vor %d Stunde',
|
||||
1 => 'vor %d Stunden',
|
||||
),
|
||||
'justnow' => 'gerade eben',
|
||||
'minute' => array(
|
||||
0 => 'vor %d Minute',
|
||||
1 => 'vor %d Minuten',
|
||||
),
|
||||
'month' => array(
|
||||
0 => 'vor %d Monat',
|
||||
1 => 'vor %d Monaten',
|
||||
),
|
||||
'second' => array(
|
||||
0 => 'vor %d Sekunde',
|
||||
1 => 'vor %d Sekunden',
|
||||
),
|
||||
'year' => array(
|
||||
0 => 'vor %d Jahr',
|
||||
1 => 'vor %d Jahre',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Kategorie leeren',
|
||||
'confirm_action' => 'Sind Sie sicher, dass Sie diese Aktion durchführen wollen? Diese Aktion kann nicht abgebrochen werden!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=2; plural=(n != 1);
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 2,
|
||||
'plural' => static fn (int $n): int => (($n !== 1) ? 1 : 0),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (Standard)',
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'last-error-date' => 'Letzte fehlerhafte Aktualisierung <time datetime="%1$s" title="%1$s">%2$s</time>.',
|
||||
'last-update' => 'Letzte erfolgreiche Aktualisierung <time datetime="%1$s" title="%1$s">%2$s</time>.',
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Zwischenspeicher leeren',
|
||||
'clear_cache_help' => 'Zwischenspeicher für diesen Feed leeren.',
|
||||
|
||||
@@ -140,6 +140,33 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => 'Σχετικά με το FreshRSS',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => 'πριν από %d ημέρα',
|
||||
1 => 'πριν από %d ημέρες',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => 'πριν από %d ώρα',
|
||||
1 => 'πριν από %d ώρες',
|
||||
),
|
||||
'justnow' => 'μόλις τώρα',
|
||||
'minute' => array(
|
||||
0 => 'πριν από %d λεπτό',
|
||||
1 => 'πριν από %d λεπτά',
|
||||
),
|
||||
'month' => array(
|
||||
0 => 'πριν από %d μήνας',
|
||||
1 => 'πριν από %d μήνες',
|
||||
),
|
||||
'second' => array(
|
||||
0 => 'πριν από %d δευτερόλεπτο',
|
||||
1 => 'πριν από %d δευτερόλεπτα',
|
||||
),
|
||||
'year' => array(
|
||||
0 => 'πριν από %d έτος',
|
||||
1 => 'πριν από %d έτη',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Άδειασμα κατηγορίας',
|
||||
'confirm_action' => 'Είστε σίγουροι για την ενέργεια; Είναι μη αναστρέψιμη!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=2; plural=(n != 1);
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 2,
|
||||
'plural' => static fn (int $n): int => (($n !== 1) ? 1 : 0),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (default)', // TODO
|
||||
'xml_xpath' => 'XML + XPath', // TODO
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Clear cache', // TODO
|
||||
'clear_cache_help' => 'Clear the cache for this feed.', // TODO
|
||||
|
||||
@@ -140,6 +140,33 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => 'About FreshRSS', // IGNORE
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => '%d day ago', // IGNORE
|
||||
1 => '%d days ago', // IGNORE
|
||||
),
|
||||
'hour' => array(
|
||||
0 => '%d hour ago', // IGNORE
|
||||
1 => '%d hours ago', // IGNORE
|
||||
),
|
||||
'justnow' => 'just now', // IGNORE
|
||||
'minute' => array(
|
||||
0 => '%d minute ago', // IGNORE
|
||||
1 => '%d minutes ago', // IGNORE
|
||||
),
|
||||
'month' => array(
|
||||
0 => '%d month ago', // IGNORE
|
||||
1 => '%d months ago', // IGNORE
|
||||
),
|
||||
'second' => array(
|
||||
0 => '%d second ago', // IGNORE
|
||||
1 => '%d seconds ago', // IGNORE
|
||||
),
|
||||
'year' => array(
|
||||
0 => '%d year ago', // IGNORE
|
||||
1 => '%d years ago', // IGNORE
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Empty category', // IGNORE
|
||||
'confirm_action' => 'Are you sure you want to perform this action? It cannot be canceled!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=2; plural=(n != 1);
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 2,
|
||||
'plural' => static fn (int $n): int => (($n !== 1) ? 1 : 0),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (default)', // IGNORE
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // IGNORE
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // IGNORE
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Clear cache', // IGNORE
|
||||
'clear_cache_help' => 'Clear the cache for this feed.', // IGNORE
|
||||
|
||||
+28
-1
@@ -140,6 +140,33 @@ return array(
|
||||
'_' => 'FreshRSS',
|
||||
'about' => 'About FreshRSS',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => '%d day ago',
|
||||
1 => '%d days ago',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => '%d hour ago',
|
||||
1 => '%d hours ago',
|
||||
),
|
||||
'justnow' => 'just now',
|
||||
'minute' => array(
|
||||
0 => '%d minute ago',
|
||||
1 => '%d minutes ago',
|
||||
),
|
||||
'month' => array(
|
||||
0 => '%d month ago',
|
||||
1 => '%d months ago',
|
||||
),
|
||||
'second' => array(
|
||||
0 => '%d second ago',
|
||||
1 => '%d seconds ago',
|
||||
),
|
||||
'year' => array(
|
||||
0 => '%d year ago',
|
||||
1 => '%d years ago',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Empty category',
|
||||
'confirm_action' => 'Are you sure you want to perform this action? It cannot be cancelled!',
|
||||
@@ -276,7 +303,7 @@ return array(
|
||||
'raindrop' => 'Raindrop.io',
|
||||
'reddit' => 'Reddit',
|
||||
'shaarli' => 'Shaarli',
|
||||
'telegram' => 'Telegram', // IGNORE
|
||||
'telegram' => 'Telegram',
|
||||
'twitter' => 'Twitter',
|
||||
'wallabag' => 'wallabag v1',
|
||||
'wallabagv2' => 'wallabag v2',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=2; plural=(n != 1);
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 2,
|
||||
'plural' => static fn (int $n): int => (($n !== 1) ? 1 : 0),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (default)',
|
||||
'xml_xpath' => 'XML + XPath',
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.',
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.',
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Clear cache',
|
||||
'clear_cache_help' => 'Clear the cache for this feed.',
|
||||
|
||||
@@ -140,6 +140,33 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => 'Acerca de FreshRSS',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => 'hace %d día',
|
||||
1 => 'hace %d días',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => 'hace %d hora',
|
||||
1 => 'hace %d horas',
|
||||
),
|
||||
'justnow' => 'justo ahora',
|
||||
'minute' => array(
|
||||
0 => 'hace %d minuto',
|
||||
1 => 'hace %d minutos',
|
||||
),
|
||||
'month' => array(
|
||||
0 => 'hace %d mes',
|
||||
1 => 'hace %d meses',
|
||||
),
|
||||
'second' => array(
|
||||
0 => 'hace %d segundo',
|
||||
1 => 'hace %d segundos',
|
||||
),
|
||||
'year' => array(
|
||||
0 => 'hace %d año',
|
||||
1 => 'hace %d años',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Vaciar categoría',
|
||||
'confirm_action' => '¿Seguro que quieres hacerlo? No hay marcha atrás…',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=2; plural=(n != 1);
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 2,
|
||||
'plural' => static fn (int $n): int => (($n !== 1) ? 1 : 0),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (por defecto)',
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Borrar caché',
|
||||
'clear_cache_help' => 'Borrar la memoria caché de esta fuente.',
|
||||
|
||||
@@ -140,6 +140,33 @@ return array(
|
||||
'_' => ' FreshRSS',
|
||||
'about' => 'درباره FreshRSS',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => '%d روز پیش',
|
||||
1 => '%d روز پیش',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => '%d ساعت پیش',
|
||||
1 => '%d ساعت پیش',
|
||||
),
|
||||
'justnow' => 'همین الان',
|
||||
'minute' => array(
|
||||
0 => '%d دقیقه پیش',
|
||||
1 => '%d دقیقه پیش',
|
||||
),
|
||||
'month' => array(
|
||||
0 => '%d ماه پیش',
|
||||
1 => '%d ماه پیش',
|
||||
),
|
||||
'second' => array(
|
||||
0 => '%d ثانیه پیش',
|
||||
1 => '%d ثانیه پیش',
|
||||
),
|
||||
'year' => array(
|
||||
0 => '%d سال پیش',
|
||||
1 => '%d سال پیش',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => ' دسته خالی',
|
||||
'confirm_action' => ' آیا مطمئن هستید که می خواهید این عمل را انجام دهید؟ نمی توان آن را لغو کرد!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=2; plural=(n > 1);
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 2,
|
||||
'plural' => static fn (int $n): int => (($n > 1) ? 1 : 0),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => ' RSS / Atom (پیشفرض)',
|
||||
'xml_xpath' => ' XML + XPath',
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => ' کش را پاک کنید',
|
||||
'clear_cache_help' => ' کش این فید را پاک کنید.',
|
||||
|
||||
@@ -140,6 +140,33 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => 'Tietoja FreshRSS-sovelluksesta',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => '%d päivä sitten',
|
||||
1 => '%d päivää sitten',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => '%d tunti sitten',
|
||||
1 => '%d tuntia sitten',
|
||||
),
|
||||
'justnow' => 'juuri nyt',
|
||||
'minute' => array(
|
||||
0 => '%d minuutti sitten',
|
||||
1 => '%d minuuttia sitten',
|
||||
),
|
||||
'month' => array(
|
||||
0 => '%d kuukausi sitten',
|
||||
1 => '%d kuukautta sitten',
|
||||
),
|
||||
'second' => array(
|
||||
0 => '%d sekunti sitten',
|
||||
1 => '%d sekuntia sitten',
|
||||
),
|
||||
'year' => array(
|
||||
0 => '%d vuosi sitten',
|
||||
1 => '%d vuotta sitten',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Tyhjennä luokka',
|
||||
'confirm_action' => 'Haluatko varmasti toteuttaa toiminnon? Sitä ei voi peruuttaa!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=2; plural=(n != 1);
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 2,
|
||||
'plural' => static fn (int $n): int => (($n !== 1) ? 1 : 0),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS/Atom (oletus)',
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Tyhjennä välimuisti',
|
||||
'clear_cache_help' => 'Tyhjennä syötteen välimuisti.',
|
||||
|
||||
@@ -140,6 +140,33 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => 'À propos de FreshRSS',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => 'il y a %d jour',
|
||||
1 => 'il y a %d jours',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => 'il y a %d heure',
|
||||
1 => 'il y a %d heures',
|
||||
),
|
||||
'justnow' => 'à l’instant',
|
||||
'minute' => array(
|
||||
0 => 'il y a %d minute',
|
||||
1 => 'il y a %d minutes',
|
||||
),
|
||||
'month' => array(
|
||||
0 => 'il y a %d mois',
|
||||
1 => 'il y a %d mois',
|
||||
),
|
||||
'second' => array(
|
||||
0 => 'il y a %d seconde',
|
||||
1 => 'il y a %d secondes',
|
||||
),
|
||||
'year' => array(
|
||||
0 => 'il y a %d an',
|
||||
1 => 'il y a %d ans',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Catégorie vide',
|
||||
'confirm_action' => 'Êtes-vous sûr(e) de vouloir continuer ? Cette action ne peut être annulée !',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=2; plural=(n > 1);
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 2,
|
||||
'plural' => static fn (int $n): int => (($n > 1) ? 1 : 0),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (par défaut)',
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'last-error-date' => 'Dernière mise à jour erronée <time datetime="%1$s" title="%1$s">%2$s</time>.',
|
||||
'last-update' => 'Dernière mise à jour réussie <time datetime="%1$s" title="%1$s">%2$s</time>.',
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Vider le cache',
|
||||
'clear_cache_help' => 'Supprime le cache de ce flux.',
|
||||
|
||||
@@ -140,6 +140,33 @@ return array(
|
||||
'_' => 'FreshRSS', // TODO
|
||||
'about' => 'אודות FreshRSS',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => 'לפני %d יום',
|
||||
1 => 'לפני %d ימים',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => 'לפני %d שעה',
|
||||
1 => 'לפני %d שעות',
|
||||
),
|
||||
'justnow' => 'הרגע',
|
||||
'minute' => array(
|
||||
0 => 'לפני %d דקה',
|
||||
1 => 'לפני %d דקות',
|
||||
),
|
||||
'month' => array(
|
||||
0 => 'לפני %d חודש',
|
||||
1 => 'לפני %d חודשים',
|
||||
),
|
||||
'second' => array(
|
||||
0 => 'לפני %d שנייה',
|
||||
1 => 'לפני %d שניות',
|
||||
),
|
||||
'year' => array(
|
||||
0 => 'לפני %d שנה',
|
||||
1 => 'לפני %d שנים',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Empty category', // TODO
|
||||
'confirm_action' => 'האם אתם בטוחים שברצונכם לבצע פעולה זו? אין אפשרות לבטל אותה!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=2; plural=(n != 1);
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 2,
|
||||
'plural' => static fn (int $n): int => (($n !== 1) ? 1 : 0),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (default)', // TODO
|
||||
'xml_xpath' => 'XML + XPath', // TODO
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Clear cache', // TODO
|
||||
'clear_cache_help' => 'Clear the cache for this feed.', // TODO
|
||||
|
||||
@@ -140,6 +140,33 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => 'FreshRSS névjegy',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => '%d nap ezelőtt',
|
||||
1 => '%d nap ezelőtt',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => '%d óra ezelőtt',
|
||||
1 => '%d óra ezelőtt',
|
||||
),
|
||||
'justnow' => 'épp most',
|
||||
'minute' => array(
|
||||
0 => '%d perc ezelőtt',
|
||||
1 => '%d perc ezelőtt',
|
||||
),
|
||||
'month' => array(
|
||||
0 => '%d hónap ezelőtt',
|
||||
1 => '%d hónap ezelőtt',
|
||||
),
|
||||
'second' => array(
|
||||
0 => '%d másodperc ezelőtt',
|
||||
1 => '%d másodperc ezelőtt',
|
||||
),
|
||||
'year' => array(
|
||||
0 => '%d év ezelőtt',
|
||||
1 => '%d év ezelőtt',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Üres kategória',
|
||||
'confirm_action' => 'Biztos vagy benne hogy végrehajtod ezt a műveletet? A művelet nem megszakítható!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=2; plural=(n != 1);
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 2,
|
||||
'plural' => static fn (int $n): int => (($n !== 1) ? 1 : 0),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (alapértelmezett)',
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Gyorsítótár törlése',
|
||||
'clear_cache_help' => 'Gyorsítótár törlése ehhez a hírforráshoz.',
|
||||
|
||||
@@ -140,6 +140,27 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => 'Tentang FreshRSS',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => '%d hari yang lalu',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => '%d jam yang lalu',
|
||||
),
|
||||
'justnow' => 'baru saja',
|
||||
'minute' => array(
|
||||
0 => '%d menit yang lalu',
|
||||
),
|
||||
'month' => array(
|
||||
0 => '%d bulan yang lalu',
|
||||
),
|
||||
'second' => array(
|
||||
0 => '%d detik yang lalu',
|
||||
),
|
||||
'year' => array(
|
||||
0 => '%d tahun yang lalu',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Kategori kosong',
|
||||
'confirm_action' => 'Apakah Anda yakin ingin melakukan ini? Ini tidak dapat dibatalkan!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=1; plural=0;
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 1,
|
||||
'plural' => static fn (int $n): int => 0,
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (baku)',
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Bersihkan tembolok',
|
||||
'clear_cache_help' => 'Bersihkan tembolok untuk umpan ini.',
|
||||
|
||||
@@ -140,6 +140,33 @@ return array(
|
||||
'_' => 'Feed RSS Reader',
|
||||
'about' => 'Informazioni',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => '%d giorno fa',
|
||||
1 => '%d giorni fa',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => '%d ora fa',
|
||||
1 => '%d ore fa',
|
||||
),
|
||||
'justnow' => 'proprio adesso',
|
||||
'minute' => array(
|
||||
0 => '%d minuto fa',
|
||||
1 => '%d minuti fa',
|
||||
),
|
||||
'month' => array(
|
||||
0 => '%d mese fa',
|
||||
1 => '%d mesi fa',
|
||||
),
|
||||
'second' => array(
|
||||
0 => '%d secondo fa',
|
||||
1 => '%d secondi fa',
|
||||
),
|
||||
'year' => array(
|
||||
0 => '%d anno fa',
|
||||
1 => '%d anni fa',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Categoria vuota',
|
||||
'confirm_action' => 'Sei sicuro di voler continuare?',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=2; plural=(n != 1);
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 2,
|
||||
'plural' => static fn (int $n): int => (($n !== 1) ? 1 : 0),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (predefinito)',
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Svuota cache',
|
||||
'clear_cache_help' => 'Svuota la cache per questo feed.',
|
||||
|
||||
@@ -140,6 +140,27 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => 'FreshRSSについて',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => '%d日前',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => '%d時間前',
|
||||
),
|
||||
'justnow' => 'たった今',
|
||||
'minute' => array(
|
||||
0 => '%d分前',
|
||||
),
|
||||
'month' => array(
|
||||
0 => '%dか月前',
|
||||
),
|
||||
'second' => array(
|
||||
0 => '%d秒前',
|
||||
),
|
||||
'year' => array(
|
||||
0 => '%d年前',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => '空白のカテゴリ',
|
||||
'confirm_action' => '本当に実行してもいいですか?キャンセルはできません!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=1; plural=0;
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 1,
|
||||
'plural' => static fn (int $n): int => 0,
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (標準)',
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'キャッシュのクリア',
|
||||
'clear_cache_help' => 'このフィードのキャッシュをクリアします。',
|
||||
|
||||
@@ -140,6 +140,27 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => '정보',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => '%d일 전',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => '%d시간 전',
|
||||
),
|
||||
'justnow' => '방금 전',
|
||||
'minute' => array(
|
||||
0 => '%d분 전',
|
||||
),
|
||||
'month' => array(
|
||||
0 => '%d개월 전',
|
||||
),
|
||||
'second' => array(
|
||||
0 => '%d초 전',
|
||||
),
|
||||
'year' => array(
|
||||
0 => '%d년 전',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => '빈 카테고리',
|
||||
'confirm_action' => '정말 이 작업을 수행하시겠습니까? 이 작업은 되돌릴 수 없습니다!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=1; plural=0;
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 1,
|
||||
'plural' => static fn (int $n): int => 0,
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (기본값)',
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => '캐쉬 지우기',
|
||||
'clear_cache_help' => '이 피드의 캐쉬 지우기.',
|
||||
|
||||
@@ -140,6 +140,39 @@ return array(
|
||||
'_' => 'FreshRSS', // TODO
|
||||
'about' => 'Par FreshRSS',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => 'pirms %d diena',
|
||||
1 => 'pirms %d dienas',
|
||||
2 => 'pirms %d dienu',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => 'pirms %d stunda',
|
||||
1 => 'pirms %d stundas',
|
||||
2 => 'pirms %d stundu',
|
||||
),
|
||||
'justnow' => 'tikko',
|
||||
'minute' => array(
|
||||
0 => 'pirms %d minūte',
|
||||
1 => 'pirms %d minūtes',
|
||||
2 => 'pirms %d minūšu',
|
||||
),
|
||||
'month' => array(
|
||||
0 => 'pirms %d mēnesis',
|
||||
1 => 'pirms %d mēneši',
|
||||
2 => 'pirms %d mēnešu',
|
||||
),
|
||||
'second' => array(
|
||||
0 => 'pirms %d sekunde',
|
||||
1 => 'pirms %d sekundes',
|
||||
2 => 'pirms %d sekunžu',
|
||||
),
|
||||
'year' => array(
|
||||
0 => 'pirms %d gads',
|
||||
1 => 'pirms %d gadi',
|
||||
2 => 'pirms %d gadu',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Tukša kategorija',
|
||||
'confirm_action' => 'Vai esat pārliecināts, ka vēlaties veikt šo darbību? To nevar atcelt!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 3,
|
||||
'plural' => static fn (int $n): int => ($n % 10 === 1 && $n % 100 !== 11 ? 0 : ($n !== 0 ? 1 : 2)),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (noklusējums)',
|
||||
'xml_xpath' => 'XML + XPath', // TODO
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Iztīrīt kešatmiņu',
|
||||
'clear_cache_help' => 'Iztīrīt kešatmiņu priekš šīs barotnes.',
|
||||
|
||||
@@ -140,6 +140,33 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => 'Over FreshRSS',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => '%d dag geleden',
|
||||
1 => '%d dagen geleden',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => '%d uur geleden',
|
||||
1 => '%d uur geleden',
|
||||
),
|
||||
'justnow' => 'zojuist',
|
||||
'minute' => array(
|
||||
0 => '%d minuut geleden',
|
||||
1 => '%d minuten geleden',
|
||||
),
|
||||
'month' => array(
|
||||
0 => '%d maand geleden',
|
||||
1 => '%d maanden geleden',
|
||||
),
|
||||
'second' => array(
|
||||
0 => '%d seconde geleden',
|
||||
1 => '%d seconden geleden',
|
||||
),
|
||||
'year' => array(
|
||||
0 => '%d jaar geleden',
|
||||
1 => '%d jaar geleden',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Lege categorie',
|
||||
'confirm_action' => 'Weet u zeker dat u dit wilt doen? Het kan niet ongedaan worden gemaakt!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=2; plural=(n != 1);
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 2,
|
||||
'plural' => static fn (int $n): int => (($n !== 1) ? 1 : 0),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (standaard)',
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Cache leegmaken',
|
||||
'clear_cache_help' => 'Cache voor deze feed leegmaken.',
|
||||
|
||||
@@ -140,6 +140,33 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => 'A prepaus de FreshRSS',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => 'fa %d jorn',
|
||||
1 => 'fa %d jorns',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => 'fa %d ora',
|
||||
1 => 'fa %d oras',
|
||||
),
|
||||
'justnow' => 'ara meteis',
|
||||
'minute' => array(
|
||||
0 => 'fa %d minuta',
|
||||
1 => 'fa %d minutas',
|
||||
),
|
||||
'month' => array(
|
||||
0 => 'fa %d mes',
|
||||
1 => 'fa %d meses',
|
||||
),
|
||||
'second' => array(
|
||||
0 => 'fa %d segonda',
|
||||
1 => 'fa %d segondas',
|
||||
),
|
||||
'year' => array(
|
||||
0 => 'fa %d an',
|
||||
1 => 'fa %d ans',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Categoria voida',
|
||||
'confirm_action' => 'Volètz vertadièrament contunhar ? Aquesta accion se pòt pas anullar !',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=2; plural=(n > 1);
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 2,
|
||||
'plural' => static fn (int $n): int => (($n > 1) ? 1 : 0),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (defaut)',
|
||||
'xml_xpath' => 'XML + XPath', // TODO
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Escafar lo cache',
|
||||
'clear_cache_help' => 'Escafar lo cache d’aqueste flux sul disc',
|
||||
|
||||
@@ -140,6 +140,39 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => 'O oprogramowaniu FreshRSS',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => '%d dzień temu',
|
||||
1 => '%d dni temu',
|
||||
2 => '%d dni temu',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => '%d godzina temu',
|
||||
1 => '%d godziny temu',
|
||||
2 => '%d godzin temu',
|
||||
),
|
||||
'justnow' => 'przed chwilą',
|
||||
'minute' => array(
|
||||
0 => '%d minuta temu',
|
||||
1 => '%d minuty temu',
|
||||
2 => '%d minut temu',
|
||||
),
|
||||
'month' => array(
|
||||
0 => '%d miesiąc temu',
|
||||
1 => '%d miesiące temu',
|
||||
2 => '%d miesięcy temu',
|
||||
),
|
||||
'second' => array(
|
||||
0 => '%d sekunda temu',
|
||||
1 => '%d sekundy temu',
|
||||
2 => '%d sekund temu',
|
||||
),
|
||||
'year' => array(
|
||||
0 => '%d rok temu',
|
||||
1 => '%d lata temu',
|
||||
2 => '%d lat temu',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Pusta kategoria',
|
||||
'confirm_action' => 'Czy jesteś pewien, że chcesz przeprowadzić daną operację? Nie można cofnąć jej rezultatów!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 3,
|
||||
'plural' => static fn (int $n): int => ($n === 1 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2)),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (domyślne)',
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Wyczyść pamięć podręczną',
|
||||
'clear_cache_help' => 'Czyści pamięć podręczną tego kanału.',
|
||||
|
||||
@@ -140,6 +140,33 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => 'Sobre FreshRSS',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => 'há %d dia',
|
||||
1 => 'há %d dias',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => 'há %d hora',
|
||||
1 => 'há %d horas',
|
||||
),
|
||||
'justnow' => 'agora mesmo',
|
||||
'minute' => array(
|
||||
0 => 'há %d minuto',
|
||||
1 => 'há %d minutos',
|
||||
),
|
||||
'month' => array(
|
||||
0 => 'há %d mês',
|
||||
1 => 'há %d meses',
|
||||
),
|
||||
'second' => array(
|
||||
0 => 'há %d segundo',
|
||||
1 => 'há %d segundos',
|
||||
),
|
||||
'year' => array(
|
||||
0 => 'há %d ano',
|
||||
1 => 'há %d anos',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Categoria vazia',
|
||||
'confirm_action' => 'Você tem certeza que deseja efetuar esta ação? Ela não poderá ser cancelada!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=2; plural=(n > 1);
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 2,
|
||||
'plural' => static fn (int $n): int => (($n > 1) ? 1 : 0),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (padrão)',
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Limpar o cache',
|
||||
'clear_cache_help' => 'Limpar o cache em disco deste feed',
|
||||
|
||||
@@ -140,6 +140,33 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => 'Sobre FreshRSS',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => 'há %d dia',
|
||||
1 => 'há %d dias',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => 'há %d hora',
|
||||
1 => 'há %d horas',
|
||||
),
|
||||
'justnow' => 'agora mesmo',
|
||||
'minute' => array(
|
||||
0 => 'há %d minuto',
|
||||
1 => 'há %d minutos',
|
||||
),
|
||||
'month' => array(
|
||||
0 => 'há %d mês',
|
||||
1 => 'há %d meses',
|
||||
),
|
||||
'second' => array(
|
||||
0 => 'há %d segundo',
|
||||
1 => 'há %d segundos',
|
||||
),
|
||||
'year' => array(
|
||||
0 => 'há %d ano',
|
||||
1 => 'há %d anos',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Categoria vazia',
|
||||
'confirm_action' => 'Tem certeza que deseja efetuar esta ação? Ela não poderá ser revertida!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=2; plural=(n != 1);
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 2,
|
||||
'plural' => static fn (int $n): int => (($n !== 1) ? 1 : 0),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (padrão)',
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Limpar o cache',
|
||||
'clear_cache_help' => 'Limpar o cache em disco deste feed',
|
||||
|
||||
@@ -140,6 +140,39 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => 'О FreshRSS',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => '%d день назад',
|
||||
1 => '%d дня назад',
|
||||
2 => '%d дней назад',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => '%d час назад',
|
||||
1 => '%d часа назад',
|
||||
2 => '%d часов назад',
|
||||
),
|
||||
'justnow' => 'только что',
|
||||
'minute' => array(
|
||||
0 => '%d минута назад',
|
||||
1 => '%d минуты назад',
|
||||
2 => '%d минут назад',
|
||||
),
|
||||
'month' => array(
|
||||
0 => '%d месяц назад',
|
||||
1 => '%d месяца назад',
|
||||
2 => '%d месяцев назад',
|
||||
),
|
||||
'second' => array(
|
||||
0 => '%d секунда назад',
|
||||
1 => '%d секунды назад',
|
||||
2 => '%d секунд назад',
|
||||
),
|
||||
'year' => array(
|
||||
0 => '%d год назад',
|
||||
1 => '%d года назад',
|
||||
2 => '%d лет назад',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Пустая категория',
|
||||
'confirm_action' => 'Вы уверены, что хотите выполнить это действие? Это нельзя отменить!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 3,
|
||||
'plural' => static fn (int $n): int => ($n % 10 === 1 && $n % 100 !== 11 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2)),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (по умолчанию)',
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Очистить кэш',
|
||||
'clear_cache_help' => 'Очистить кэш для этой ленты.',
|
||||
|
||||
@@ -140,6 +140,39 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => 'O FreshRSS',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => 'pred %d deň',
|
||||
1 => 'pred %d dni',
|
||||
2 => 'pred %d dní',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => 'pred %d hodina',
|
||||
1 => 'pred %d hodiny',
|
||||
2 => 'pred %d hodín',
|
||||
),
|
||||
'justnow' => 'práve teraz',
|
||||
'minute' => array(
|
||||
0 => 'pred %d minúta',
|
||||
1 => 'pred %d minúty',
|
||||
2 => 'pred %d minút',
|
||||
),
|
||||
'month' => array(
|
||||
0 => 'pred %d mesiac',
|
||||
1 => 'pred %d mesiace',
|
||||
2 => 'pred %d mesiacov',
|
||||
),
|
||||
'second' => array(
|
||||
0 => 'pred %d sekunda',
|
||||
1 => 'pred %d sekundy',
|
||||
2 => 'pred %d sekúnd',
|
||||
),
|
||||
'year' => array(
|
||||
0 => 'pred %d rok',
|
||||
1 => 'pred %d roky',
|
||||
2 => 'pred %d rokov',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Prázdna kategória',
|
||||
'confirm_action' => 'Určite chcete vykonať túto akciu? Zmeny budú nezvratné!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 3,
|
||||
'plural' => static fn (int $n): int => ($n === 1 ? 0 : ($n >= 2 && $n <= 4 ? 1 : 2)),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (prednastavené)',
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Vymazať vyrovnáciu pamäť',
|
||||
'clear_cache_help' => 'Vymazať vyrovnáciu pamäť pre tento kanál.',
|
||||
|
||||
@@ -140,6 +140,33 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => 'FreshRSS Hakkında',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => '%d gün once',
|
||||
1 => '%d gün once',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => '%d saat once',
|
||||
1 => '%d saat once',
|
||||
),
|
||||
'justnow' => 'az once',
|
||||
'minute' => array(
|
||||
0 => '%d dakika once',
|
||||
1 => '%d dakika once',
|
||||
),
|
||||
'month' => array(
|
||||
0 => '%d ay once',
|
||||
1 => '%d ay once',
|
||||
),
|
||||
'second' => array(
|
||||
0 => '%d saniye once',
|
||||
1 => '%d saniye once',
|
||||
),
|
||||
'year' => array(
|
||||
0 => '%d yıl once',
|
||||
1 => '%d yıl once',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Boş kategori',
|
||||
'confirm_action' => 'Bu eylemi gerçekleştirmek istediğinizden emin misiniz? Bu işlem geri alınamaz!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=2; plural=(n > 1);
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 2,
|
||||
'plural' => static fn (int $n): int => (($n > 1) ? 1 : 0),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (varsayılan)',
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Önbelleği temizle',
|
||||
'clear_cache_help' => 'Bu besleme için önbelleği temizle.',
|
||||
|
||||
@@ -140,6 +140,39 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => 'Про FreshRSS',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => '%d день тому',
|
||||
1 => '%d дні тому',
|
||||
2 => '%d днів тому',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => '%d година тому',
|
||||
1 => '%d години тому',
|
||||
2 => '%d годин тому',
|
||||
),
|
||||
'justnow' => 'щойно',
|
||||
'minute' => array(
|
||||
0 => '%d хвилина тому',
|
||||
1 => '%d хвилини тому',
|
||||
2 => '%d хвилин тому',
|
||||
),
|
||||
'month' => array(
|
||||
0 => '%d місяць тому',
|
||||
1 => '%d місяці тому',
|
||||
2 => '%d місяців тому',
|
||||
),
|
||||
'second' => array(
|
||||
0 => '%d секунда тому',
|
||||
1 => '%d секунди тому',
|
||||
2 => '%d секунд тому',
|
||||
),
|
||||
'year' => array(
|
||||
0 => '%d рік тому',
|
||||
1 => '%d роки тому',
|
||||
2 => '%d років тому',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => 'Порожня категорія',
|
||||
'confirm_action' => 'Точно виконати цю дію? Її неможливо скасувати!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 3,
|
||||
'plural' => static fn (int $n): int => ($n % 10 === 1 && $n % 100 !== 11 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2)),
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS/Atom (типово)',
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Очистити кеш',
|
||||
'clear_cache_help' => 'Спорожнити кеш стрічки.',
|
||||
|
||||
@@ -140,6 +140,27 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => '关于 FreshRSS',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => '%d天前',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => '%d小时前',
|
||||
),
|
||||
'justnow' => '刚刚',
|
||||
'minute' => array(
|
||||
0 => '%d分钟前',
|
||||
),
|
||||
'month' => array(
|
||||
0 => '%d个月前',
|
||||
),
|
||||
'second' => array(
|
||||
0 => '%d秒前',
|
||||
),
|
||||
'year' => array(
|
||||
0 => '%d年前',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => '清空分类',
|
||||
'confirm_action' => '你确定要执行此操作吗?这将不可撤销!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=1; plural=0;
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 1,
|
||||
'plural' => static fn (int $n): int => 0,
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (默认)',
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => '清理缓存',
|
||||
'clear_cache_help' => '清除该feed的缓存',
|
||||
|
||||
@@ -140,6 +140,27 @@ return array(
|
||||
'_' => 'FreshRSS', // IGNORE
|
||||
'about' => '關於 FreshRSS',
|
||||
),
|
||||
'interval' => array(
|
||||
'day' => array(
|
||||
0 => '%d天前',
|
||||
),
|
||||
'hour' => array(
|
||||
0 => '%d小時前',
|
||||
),
|
||||
'justnow' => '剛剛',
|
||||
'minute' => array(
|
||||
0 => '%d分鐘前',
|
||||
),
|
||||
'month' => array(
|
||||
0 => '%d個月前',
|
||||
),
|
||||
'second' => array(
|
||||
0 => '%d秒前',
|
||||
),
|
||||
'year' => array(
|
||||
0 => '%d年前',
|
||||
),
|
||||
),
|
||||
'js' => array(
|
||||
'category_empty' => '清空分類',
|
||||
'confirm_action' => '你確定要執行此操作嗎?這將不可撤銷!',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// Plural-Forms: nplurals=1; plural=0;
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => 1,
|
||||
'plural' => static fn (int $n): int => 0,
|
||||
);
|
||||
@@ -185,6 +185,8 @@ return array(
|
||||
'rss' => 'RSS / Atom (預設)',
|
||||
'xml_xpath' => 'XML + XPath', // TODO
|
||||
),
|
||||
'last-error-date' => 'Last erroneous update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'last-update' => 'Last successful update <time datetime="%1$s" title="%1$s">%2$s</time>.', // TODO
|
||||
'maintenance' => array(
|
||||
'clear_cache' => '清理暫存',
|
||||
'clear_cache_help' => '清除該feed的暫存',
|
||||
|
||||
@@ -14,13 +14,25 @@
|
||||
<a href="<?= _url('stats', 'repartition', 'id', $this->feed->id()) ?>"><?= _i('stats') ?> <?= _t('sub.feed.stats') ?></a>
|
||||
</div>
|
||||
|
||||
<?php $nbEntries = $this->feed->nbEntries(); ?>
|
||||
<?php if ($this->feed->inError()): ?>
|
||||
<p class="alert alert-error">
|
||||
<span class="alert-head"><?= _t('gen.short.damn') ?></span>
|
||||
<?= _t('sub.feed.error') ?><br />
|
||||
<?= _t('sub.feed.last-update', timestampToMachineDate($this->feed->lastUpdate()), timeago($this->feed->lastUpdate())) ?>
|
||||
<?php if ($this->feed->lastError() > 1) { ?><br />
|
||||
<?= _t('sub.feed.last-error-date', timestampToMachineDate($this->feed->lastError()), timeago($this->feed->lastError())) ?>
|
||||
<?php } ?>
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<p class="alert alert-success">
|
||||
<?= _t('sub.feed.last-update', timestampToMachineDate($this->feed->lastUpdate()), timeago($this->feed->lastUpdate())) ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($this->feed->inError()) { ?>
|
||||
<p class="alert alert-error"><span class="alert-head"><?= _t('gen.short.damn') ?></span> <?= _t('sub.feed.error') ?></p>
|
||||
<?php } elseif ($nbEntries === 0) { ?>
|
||||
<p class="alert alert-warn"><?= _t('sub.feed.empty') ?></p>
|
||||
<?php } ?>
|
||||
<?php $nbEntries = $this->feed->nbEntries(); ?>
|
||||
<?php if ($nbEntries === 0): ?>
|
||||
<p class="alert alert-warn"><?= _t('sub.feed.empty') ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$from = Minz_Request::paramString('from');
|
||||
|
||||
@@ -151,6 +151,13 @@ cd /usr/share/FreshRSS
|
||||
# -r, --revert revert the action (only used with ignore action).
|
||||
# -o, --origin-language selects the origin language (only used with add language action).
|
||||
|
||||
./cli/compile.plurals.php [ --all --file app/i18n/en/plurals.php --formula 'nplurals=2; plural=(n != 1);' ]
|
||||
# Compile gettext plural formulas into PHP callables for runtime use.
|
||||
# Plural source files are driven by a leading comment such as:
|
||||
# // Plural-Forms: nplurals=2; plural=(n != 1);
|
||||
# Run this command, or `make fix-all`, after editing those comments.
|
||||
# See examples: https://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html
|
||||
|
||||
./cli/check.translation.php [ ---display-result --help --language fr --display-report --generate-readme ]
|
||||
# Check if translation files have missing keys or missing translations.
|
||||
# -d, --display-result display results of check.
|
||||
|
||||
@@ -49,7 +49,13 @@ $percentage = [];
|
||||
|
||||
foreach ($languages as $language) {
|
||||
if ($language === $i18nData::REFERENCE_LANGUAGE) {
|
||||
$i18nValidator = new I18nUsageValidator($i18nData->getReferenceLanguage(), findUsedTranslations());
|
||||
$usedTranslations = findUsedTranslations();
|
||||
$referenceLanguage = $i18nData->getReferenceLanguage();
|
||||
$pluralFamilies = loadPluralReferenceFamilies($referenceLanguage);
|
||||
if ($pluralFamilies !== []) {
|
||||
$referenceLanguage['plurals.php'] = $pluralFamilies;
|
||||
}
|
||||
$i18nValidator = new I18nUsageValidator($referenceLanguage, $usedTranslations['keys'], $usedTranslations['prefixes']);
|
||||
} else {
|
||||
$i18nValidator = new I18nCompletionValidator($i18nData->getReferenceLanguage(), $i18nData->getLanguage($language));
|
||||
}
|
||||
@@ -150,13 +156,14 @@ if (!$isValidated) {
|
||||
* Iterates through all php and phtml files in the whole project and extracts all
|
||||
* translation keys used.
|
||||
*
|
||||
* @return list<string>
|
||||
* @return array{keys:list<string>,prefixes:list<string>}
|
||||
*/
|
||||
function findUsedTranslations(): array {
|
||||
$directory = new RecursiveDirectoryIterator(__DIR__ . '/..');
|
||||
$iterator = new RecursiveIteratorIterator($directory);
|
||||
$directory = new RecursiveDirectoryIterator(__DIR__ . '/..', FilesystemIterator::SKIP_DOTS);
|
||||
$iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::LEAVES_ONLY, RecursiveIteratorIterator::CATCH_GET_CHILD);
|
||||
$regex = new RegexIterator($iterator, '/^.+\.(php|phtml)$/i', RecursiveRegexIterator::GET_MATCH);
|
||||
$usedI18n = [];
|
||||
$usedPrefixes = [];
|
||||
foreach ($regex as $file => $value) {
|
||||
if (!is_string($file) || $file === '') {
|
||||
continue;
|
||||
@@ -167,8 +174,53 @@ function findUsedTranslations(): array {
|
||||
}
|
||||
preg_match_all('/_t\([\'"](?P<strings>[^\'"]+)[\'"]/', $fileContent, $matches);
|
||||
$usedI18n = array_merge($usedI18n, $matches['strings']);
|
||||
preg_match_all('/Minz_Translate::plural\(\s*[\'"](?P<string>[^\'"]+)[\'"](?P<dynamic>\s*\.)?/', $fileContent, $pluralMatches, PREG_SET_ORDER);
|
||||
foreach ($pluralMatches as $match) {
|
||||
$string = $match['string'];
|
||||
if (($match['dynamic'] ?? '') !== '') {
|
||||
$usedPrefixes[] = $string;
|
||||
} else {
|
||||
$usedI18n[] = $string;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $usedI18n;
|
||||
return [
|
||||
'keys' => array_values(array_unique($usedI18n)),
|
||||
'prefixes' => array_values(array_unique($usedPrefixes)),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,array<string,I18nValue>> $referenceLanguage
|
||||
* @return array<string,I18nValue>
|
||||
*/
|
||||
function loadPluralReferenceFamilies(array $referenceLanguage): array {
|
||||
$pluralFamilies = [];
|
||||
foreach ($referenceLanguage as $values) {
|
||||
foreach ($values as $key => $value) {
|
||||
if (preg_match('/^(?P<base>.+)\.(?P<index>\d+)$/', $key, $matches) !== 1) {
|
||||
continue;
|
||||
}
|
||||
$baseKey = $matches['base'];
|
||||
$index = $matches['index'];
|
||||
$pluralFamilies[$baseKey][(int)$index] = $value->__toString();
|
||||
}
|
||||
}
|
||||
|
||||
$normalisedFamilies = [];
|
||||
foreach ($pluralFamilies as $baseKey => $messageFamily) {
|
||||
$messages = [];
|
||||
ksort($messageFamily);
|
||||
foreach ($messageFamily as $message) {
|
||||
if ($message !== '') {
|
||||
$messages[] = $message;
|
||||
}
|
||||
}
|
||||
|
||||
$normalisedFamilies[$baseKey] = new I18nValue(implode(' | ', $messages));
|
||||
}
|
||||
|
||||
return $normalisedFamilies;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/_cli.php';
|
||||
require_once __DIR__ . '/i18n/PluralFormsCompiler.php';
|
||||
|
||||
$cliOptions = new class extends CliOptionsParser {
|
||||
public bool $all;
|
||||
public string $file;
|
||||
public string $formula;
|
||||
public bool $help;
|
||||
|
||||
public function __construct() {
|
||||
$this->addOption('all', (new CliOption('all', 'a'))->withValueNone());
|
||||
$this->addOption('file', new CliOption('file', 'f'));
|
||||
$this->addOption('formula', new CliOption('formula', 'p'));
|
||||
$this->addOption('help', (new CliOption('help', 'h'))->withValueNone());
|
||||
parent::__construct();
|
||||
}
|
||||
};
|
||||
|
||||
if (!empty($cliOptions->errors)) {
|
||||
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
|
||||
}
|
||||
if ($cliOptions->help || (!isset($cliOptions->formula) && !isset($cliOptions->file) && !$cliOptions->all)) {
|
||||
compilePluralsHelp();
|
||||
}
|
||||
|
||||
$compiler = new PluralFormsCompiler();
|
||||
|
||||
if (isset($cliOptions->formula)) {
|
||||
echo $compiler->compileFormulaToLambda($cliOptions->formula) . "\n";
|
||||
done();
|
||||
}
|
||||
|
||||
if (isset($cliOptions->file)) {
|
||||
$compiler->compileFile($cliOptions->file);
|
||||
echo 'Compiled ' . $cliOptions->file . "\n";
|
||||
done();
|
||||
}
|
||||
|
||||
$changed = $compiler->compileAll();
|
||||
echo 'Compiled ' . $changed . " plural file(s).\n";
|
||||
done();
|
||||
|
||||
function compilePluralsHelp(): never {
|
||||
$file = str_replace(__DIR__ . '/', '', __FILE__);
|
||||
|
||||
echo <<<HELP
|
||||
NAME
|
||||
$file
|
||||
|
||||
SYNOPSIS
|
||||
php $file [ --all | --file=<path> | --formula='<plural-forms>' ]
|
||||
|
||||
DESCRIPTION
|
||||
Compile gettext plural formulas into PHP callables for runtime consumption.
|
||||
|
||||
-a, --all compile all app/i18n/*/plurals.php files in place.
|
||||
-f, --file=FILE compile a single plural file in place.
|
||||
-p, --formula=FORMULA output the compiled PHP lambda for a gettext plural formula.
|
||||
-h, --help display this help and exit.
|
||||
|
||||
EXAMPLES
|
||||
php $file --formula 'nplurals=2; plural=(n != 1);'
|
||||
php $file --file app/i18n/en/plurals.php
|
||||
php $file --all
|
||||
|
||||
REFERENCES
|
||||
https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html
|
||||
https://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html
|
||||
HELP, PHP_EOL;
|
||||
exit();
|
||||
}
|
||||
@@ -19,6 +19,24 @@ class I18nCompletionValidator implements I18nValidatorInterface {
|
||||
) {
|
||||
}
|
||||
|
||||
private static function isPluralVariantKey(string $key): bool {
|
||||
return preg_match('/\.\d+$/', $key) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{base:string,index:int}|null
|
||||
*/
|
||||
private static function parsePluralVariantKey(string $key): ?array {
|
||||
if (preg_match('/^(?P<base>.+)\.(?P<index>\d+)$/', $key, $matches) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'base' => $matches['base'],
|
||||
'index' => (int)$matches['index'],
|
||||
];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function displayReport(bool $percentage_only = false): string {
|
||||
if ($this->passEntries > $this->totalEntries) {
|
||||
@@ -43,26 +61,97 @@ class I18nCompletionValidator implements I18nValidatorInterface {
|
||||
public function validate(): bool {
|
||||
foreach ($this->reference as $file => $data) {
|
||||
foreach ($data as $refKey => $refValue) {
|
||||
if (!$this->pluralVariantAppliesToLanguage($file, $refKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->totalEntries++;
|
||||
if (!array_key_exists($file, $this->language) || !array_key_exists($refKey, $this->language[$file])) {
|
||||
$this->result .= "Missing key $refKey" . PHP_EOL;
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $this->language[$file][$refKey];
|
||||
if ($value->isIgnore()) {
|
||||
$this->passEntries++;
|
||||
$this->validateValue($refKey, $refValue, $this->language[$file][$refKey]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->language as $file => $data) {
|
||||
$referenceValues = $this->reference[$file] ?? [];
|
||||
foreach ($data as $key => $value) {
|
||||
if (!self::isPluralVariantKey($key) || array_key_exists($key, $referenceValues)) {
|
||||
continue;
|
||||
}
|
||||
if ($refValue->equal($value)) {
|
||||
$this->result .= "Untranslated key $refKey - $refValue" . PHP_EOL;
|
||||
|
||||
$referenceValue = $this->referenceValueForKey($referenceValues, $key);
|
||||
if ($referenceValue === null) {
|
||||
continue;
|
||||
}
|
||||
$this->passEntries++;
|
||||
|
||||
$this->totalEntries++;
|
||||
$this->validateValue($key, $referenceValue, $value);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->totalEntries === $this->passEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,I18nValue> $referenceValues
|
||||
*/
|
||||
private function referenceValueForKey(array $referenceValues, string $key): ?I18nValue {
|
||||
if (array_key_exists($key, $referenceValues)) {
|
||||
return $referenceValues[$key];
|
||||
}
|
||||
|
||||
$parsedKey = self::parsePluralVariantKey($key);
|
||||
if ($parsedKey === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$pluralKey = $parsedKey['base'] . '.1';
|
||||
if (array_key_exists($pluralKey, $referenceValues)) {
|
||||
return $referenceValues[$pluralKey];
|
||||
}
|
||||
|
||||
$singularKey = $parsedKey['base'] . '.0';
|
||||
return $referenceValues[$singularKey] ?? null;
|
||||
}
|
||||
|
||||
private function validateValue(string $key, I18nValue $referenceValue, I18nValue $value): void {
|
||||
if ($value->isIgnore()) {
|
||||
$this->passEntries++;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($referenceValue->equal($value)) {
|
||||
$this->result .= "Untranslated key $key - $referenceValue" . PHP_EOL;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->passEntries++;
|
||||
}
|
||||
|
||||
private function pluralVariantAppliesToLanguage(string $file, string $key): bool {
|
||||
$parsedKey = self::parsePluralVariantKey($key);
|
||||
if ($parsedKey === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$indexes = [];
|
||||
foreach ($this->language[$file] ?? [] as $languageKey => $value) {
|
||||
$parsedLanguageKey = self::parsePluralVariantKey($languageKey);
|
||||
if ($parsedLanguageKey === null || $parsedLanguageKey['base'] !== $parsedKey['base']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$indexes[$parsedLanguageKey['index']] = true;
|
||||
}
|
||||
|
||||
if ($indexes === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return array_key_exists($parsedKey['index'], $indexes);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+171
-8
@@ -9,10 +9,29 @@ class I18nData {
|
||||
/** @param array<string,array<string,array<string,I18nValue>>> $data */
|
||||
public function __construct(private array $data) {
|
||||
$this->addMissingKeysFromReference();
|
||||
$this->addMissingPluralVariantsFromReference();
|
||||
$this->removeExtraKeysFromOtherLanguages();
|
||||
$this->processValueStates();
|
||||
}
|
||||
|
||||
private static function isPluralVariantKey(string $key): bool {
|
||||
return self::parsePluralVariantKey($key) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{base:string,index:int}|null
|
||||
*/
|
||||
private static function parsePluralVariantKey(string $key): ?array {
|
||||
if (preg_match('/^(?P<base>.+)\.(?P<index>\d+)$/', $key, $matches) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'base' => $matches['base'],
|
||||
'index' => (int)$matches['index'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,array<string,array<string,I18nValue>>>
|
||||
*/
|
||||
@@ -26,6 +45,9 @@ class I18nData {
|
||||
|
||||
foreach ($reference as $file => $refValues) {
|
||||
foreach ($refValues as $key => $refValue) {
|
||||
if (self::isPluralVariantKey($key)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($languages as $language) {
|
||||
if (!array_key_exists($file, $this->data[$language]) || !array_key_exists($key, $this->data[$language][$file])) {
|
||||
$this->data[$language][$file][$key] = clone $refValue;
|
||||
@@ -39,11 +61,52 @@ class I18nData {
|
||||
}
|
||||
}
|
||||
|
||||
private function addMissingPluralVariantsFromReference(): void {
|
||||
$reference = $this->getReferenceLanguage();
|
||||
|
||||
foreach ($this->getNonReferenceLanguages() as $language) {
|
||||
$expectedIndexes = $this->pluralVariantIndexesForLanguage($language);
|
||||
foreach ($reference as $file => $refValues) {
|
||||
$pluralBases = [];
|
||||
foreach ($refValues as $key => $refValue) {
|
||||
$parsedKey = self::parsePluralVariantKey($key);
|
||||
if ($parsedKey === null) {
|
||||
continue;
|
||||
}
|
||||
$pluralBases[$parsedKey['base']] = true;
|
||||
}
|
||||
|
||||
if (!array_key_exists($file, $this->data[$language])) {
|
||||
$this->data[$language][$file] = [];
|
||||
}
|
||||
|
||||
foreach (array_keys($pluralBases) as $pluralBase) {
|
||||
foreach ($expectedIndexes as $index) {
|
||||
$pluralKey = $pluralBase . '.' . $index;
|
||||
if (array_key_exists($pluralKey, $this->data[$language][$file])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$referenceValue = $this->referenceValueForKey($refValues, $pluralKey);
|
||||
if ($referenceValue === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->data[$language][$file][$pluralKey] = clone $referenceValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function removeExtraKeysFromOtherLanguages(): void {
|
||||
$reference = $this->getReferenceLanguage();
|
||||
foreach ($this->getNonReferenceLanguages() as $language) {
|
||||
foreach ($this->getLanguage($language) as $file => $values) {
|
||||
foreach ($values as $key => $value) {
|
||||
if (self::isPluralVariantKey($key)) {
|
||||
continue;
|
||||
}
|
||||
if (!array_key_exists($key, $reference[$file])) {
|
||||
unset($this->data[$language][$file][$key]);
|
||||
}
|
||||
@@ -59,18 +122,118 @@ class I18nData {
|
||||
foreach ($reference as $file => $refValues) {
|
||||
foreach ($refValues as $key => $refValue) {
|
||||
foreach ($languages as $language) {
|
||||
if (!$this->pluralVariantAppliesToLanguage($language, $key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $this->data[$language][$file][$key];
|
||||
if ($refValue->equal($value) && !$value->isIgnore()) {
|
||||
$value->markAsTodo();
|
||||
continue;
|
||||
}
|
||||
if (!$refValue->equal($value) && $value->isTodo()) {
|
||||
$value->markAsDirty();
|
||||
continue;
|
||||
}
|
||||
$this->syncValueState($refValue, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($languages as $language) {
|
||||
foreach ($this->getLanguage($language) as $file => $values) {
|
||||
$referenceValues = $reference[$file] ?? [];
|
||||
foreach ($values as $key => $value) {
|
||||
if (!self::isPluralVariantKey($key) || array_key_exists($key, $referenceValues)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$referenceValue = $this->referenceValueForKey($referenceValues, $key);
|
||||
if ($referenceValue === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->syncValueState($referenceValue, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function syncValueState(I18nValue $referenceValue, I18nValue $value): void {
|
||||
if ($referenceValue->equal($value) && !$value->isIgnore()) {
|
||||
$value->markAsTodo();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$referenceValue->equal($value) && $value->isTodo()) {
|
||||
$value->markAsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
private function pluralVariantAppliesToLanguage(string $language, string $key): bool {
|
||||
$parsedKey = self::parsePluralVariantKey($key);
|
||||
if ($parsedKey === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return in_array($parsedKey['index'], $this->pluralVariantIndexesForLanguage($language), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,I18nValue> $referenceValues
|
||||
*/
|
||||
private function referenceValueForKey(array $referenceValues, string $key): ?I18nValue {
|
||||
if (array_key_exists($key, $referenceValues)) {
|
||||
return $referenceValues[$key];
|
||||
}
|
||||
|
||||
$parsedKey = self::parsePluralVariantKey($key);
|
||||
if ($parsedKey === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$pluralKey = $parsedKey['base'] . '.1';
|
||||
if (array_key_exists($pluralKey, $referenceValues)) {
|
||||
return $referenceValues[$pluralKey];
|
||||
}
|
||||
|
||||
$singularKey = $parsedKey['base'] . '.0';
|
||||
return $referenceValues[$singularKey] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<int>
|
||||
*/
|
||||
private function pluralVariantIndexesForLanguage(string $language): array {
|
||||
$pluralCount = $this->pluralCountForLanguage($language);
|
||||
if ($pluralCount !== null) {
|
||||
return range(0, $pluralCount - 1);
|
||||
}
|
||||
|
||||
$indexes = [];
|
||||
foreach ($this->data[$language] as $values) {
|
||||
foreach (array_keys($values) as $key) {
|
||||
$parsedKey = self::parsePluralVariantKey($key);
|
||||
if ($parsedKey === null) {
|
||||
continue;
|
||||
}
|
||||
$indexes[$parsedKey['index']] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($indexes === []) {
|
||||
return [0];
|
||||
}
|
||||
|
||||
ksort($indexes, SORT_NUMERIC);
|
||||
return array_map('intval', array_keys($indexes));
|
||||
}
|
||||
|
||||
private function pluralCountForLanguage(string $language): ?int {
|
||||
if (!defined('I18N_PATH')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$pluralFile = I18N_PATH . '/' . $language . '/plurals.php';
|
||||
if (!is_file($pluralFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$pluralData = include $pluralFile;
|
||||
$pluralCount = is_array($pluralData) ? ($pluralData['nplurals'] ?? null) : null;
|
||||
return is_int($pluralCount) && $pluralCount > 0 ? $pluralCount : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+24
-10
@@ -7,13 +7,10 @@ class I18nFile {
|
||||
|
||||
/**
|
||||
* @param array<mixed,mixed> $array
|
||||
* @phpstan-assert-if-true array<string,string|array<string,mixed>> $array
|
||||
* @phpstan-assert-if-true array<int|string,string|array<mixed>> $array
|
||||
*/
|
||||
public static function is_array_recursive_string(array $array): bool {
|
||||
foreach ($array as $key => $value) {
|
||||
if (!is_string($key)) {
|
||||
return false;
|
||||
}
|
||||
foreach ($array as $value) {
|
||||
if (!is_string($value) && !(is_array($value) && self::is_array_recursive_string($value))) {
|
||||
return false;
|
||||
}
|
||||
@@ -36,6 +33,9 @@ class I18nFile {
|
||||
if (!$file->isFile()) {
|
||||
continue;
|
||||
}
|
||||
if ($file->getFilename() === 'plurals.php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$i18n[$dir->getFilename()][$file->getFilename()] = $this->flatten($this->process($file->getPathname()), $file->getBasename('.php'));
|
||||
}
|
||||
@@ -62,7 +62,7 @@ class I18nFile {
|
||||
|
||||
/**
|
||||
* Process the content of an i18n file
|
||||
* @return array<string,string|array<string,mixed>>
|
||||
* @return array<int|string,string|array<mixed>>
|
||||
*/
|
||||
private function process(string $filename): array {
|
||||
$fileContent = file_get_contents($filename);
|
||||
@@ -101,7 +101,7 @@ class I18nFile {
|
||||
/**
|
||||
* Flatten an array of translation
|
||||
*
|
||||
* @param array<string,I18nValue|string|array<string,I18nValue>|mixed> $translation
|
||||
* @param array<int|string,I18nValue|string|array<mixed>|mixed> $translation
|
||||
* @return array<string,I18nValue>
|
||||
*/
|
||||
private function flatten(array $translation, string $prefix = ''): array {
|
||||
@@ -112,7 +112,8 @@ class I18nFile {
|
||||
}
|
||||
|
||||
foreach ($translation as $key => $value) {
|
||||
if (is_array($value) && is_array_keys_string($value)) {
|
||||
$key = (string)$key;
|
||||
if (is_array($value) && self::is_array_recursive_string($value)) {
|
||||
$a += $this->flatten($value, $prefix . $key);
|
||||
} elseif (is_string($value) || $value instanceof I18nValue) {
|
||||
$a[$prefix . $key] = new I18nValue($value);
|
||||
@@ -129,7 +130,7 @@ class I18nFile {
|
||||
* no use of it.
|
||||
*
|
||||
* @param array<string,I18nValue> $translation
|
||||
* @return array<string,array<string,I18nValue>>
|
||||
* @return array<int|string,mixed>
|
||||
*/
|
||||
private function unflatten(array $translation): array {
|
||||
$a = [];
|
||||
@@ -138,7 +139,20 @@ class I18nFile {
|
||||
foreach ($translation as $compoundKey => $value) {
|
||||
$keys = explode('.', $compoundKey);
|
||||
array_shift($keys);
|
||||
eval("\$a['" . implode("']['", $keys) . "'] = '" . addcslashes($value->__toString(), "'") . "';");
|
||||
$current =& $a;
|
||||
$lastIndex = count($keys) - 1;
|
||||
foreach ($keys as $index => $key) {
|
||||
$normalisedKey = ctype_digit($key) ? (int)$key : $key;
|
||||
if ($index === $lastIndex) {
|
||||
$current[$normalisedKey] = $value->__toString();
|
||||
continue;
|
||||
}
|
||||
if (!isset($current[$normalisedKey]) || !is_array($current[$normalisedKey])) {
|
||||
$current[$normalisedKey] = [];
|
||||
}
|
||||
$current =& $current[$normalisedKey];
|
||||
}
|
||||
unset($current);
|
||||
}
|
||||
|
||||
return $a;
|
||||
|
||||
@@ -12,13 +12,33 @@ class I18nUsageValidator implements I18nValidatorInterface {
|
||||
/**
|
||||
* @param array<string,array<string,I18nValue>> $reference
|
||||
* @param array<string> $code
|
||||
* @param array<string> $codePrefixes
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly array $reference,
|
||||
private readonly array $code,
|
||||
private readonly array $codePrefixes = [],
|
||||
) {
|
||||
}
|
||||
|
||||
private function isUsed(string $key): bool {
|
||||
if (preg_match('/\._$/', $key) === 1 && in_array(preg_replace('/\._$/', '', $key), $this->code, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (in_array($key, $this->code, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($this->codePrefixes as $prefix) {
|
||||
if (str_starts_with($key, $prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function displayReport(bool $percentage_only = false): string {
|
||||
if ($this->failedEntries > $this->totalEntries) {
|
||||
@@ -43,10 +63,7 @@ class I18nUsageValidator implements I18nValidatorInterface {
|
||||
foreach ($this->reference as $file => $data) {
|
||||
foreach ($data as $key => $value) {
|
||||
$this->totalEntries++;
|
||||
if (preg_match('/\._$/', $key) === 1 && in_array(preg_replace('/\._$/', '', $key), $this->code, true)) {
|
||||
continue;
|
||||
}
|
||||
if (!in_array($key, $this->code, true)) {
|
||||
if (!$this->isUsed($key)) {
|
||||
$this->result .= sprintf('Unused key %s - %s', $key, $value) . PHP_EOL;
|
||||
$this->failedEntries++;
|
||||
continue;
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Plural form inspired by GNU gettext plural forms, converted into PHP lambdas.
|
||||
* https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html
|
||||
*/
|
||||
final class PluralFormsCompiler {
|
||||
private const FORMULA_PATTERN = '/^\s*nplurals\s*=\s*(\d+)\s*;\s*plural\s*=\s*(.+?)\s*;\s*$/';
|
||||
|
||||
private const COMMENT_PATTERN = '/^\s*\/\/\s*Plural-Forms:\s*(?P<formula>.+?)\s*$/mi';
|
||||
|
||||
private const COMMENT_PREFIX_PATTERN = '/^\s*\/\/\s*/';
|
||||
|
||||
private const ALLOWED_EXPRESSION_PATTERN = '/^[0-9n\s!<>=&|?:()%+*\/-]+$/';
|
||||
|
||||
/**
|
||||
* @return array{formula:string,nplurals:int,lambda:string}
|
||||
*/
|
||||
public function compileFormula(string $pluralForms): array {
|
||||
['formula' => $formula, 'nplurals' => $pluralCount, 'expression' => $expression] =
|
||||
$this->parsePluralHeader($pluralForms);
|
||||
$this->validatePluralExpression($expression, $formula);
|
||||
|
||||
$lambdaExpression = $pluralCount === 2 && !str_contains($expression, '?')
|
||||
? '((' . $this->transpileLeafExpression($expression) . ') ? 1 : 0)'
|
||||
: $this->transpileExpression($expression);
|
||||
|
||||
return [
|
||||
'formula' => $formula,
|
||||
'nplurals' => $pluralCount,
|
||||
'lambda' => 'static fn (int $n): int => ' . $lambdaExpression,
|
||||
];
|
||||
}
|
||||
|
||||
public function compileFormulaToLambda(string $pluralForms): string {
|
||||
return $this->compileFormula($pluralForms)['lambda'];
|
||||
}
|
||||
|
||||
public function compileFile(string $filePath, bool $throwOnError = true): bool {
|
||||
try {
|
||||
if (!is_file($filePath)) {
|
||||
throw new InvalidArgumentException('Plural file not found: ' . $filePath);
|
||||
}
|
||||
|
||||
$compiled = $this->compileFormula($this->extractPluralFormsFromFile($filePath));
|
||||
$newContent = $this->renderCompiledFile($compiled);
|
||||
$currentContent = file_get_contents($filePath);
|
||||
if (!is_string($currentContent)) {
|
||||
throw new RuntimeException('Unable to read plural file: ' . $filePath);
|
||||
}
|
||||
|
||||
if ($currentContent === $newContent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file_put_contents($filePath, $newContent) === false) {
|
||||
throw new RuntimeException('Unable to write plural file: ' . $filePath);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
if ($throwOnError) {
|
||||
throw $e;
|
||||
}
|
||||
$message = 'Error compiling plural file `' . $filePath . '`: ' . $e->getMessage() . "\n";
|
||||
if (defined('STDERR')) {
|
||||
fwrite(STDERR, $message);
|
||||
} else {
|
||||
echo $message;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function compileAll(string $globPattern = I18N_PATH . '/*/plurals.php'): int {
|
||||
$files = glob($globPattern) ?: [];
|
||||
sort($files, SORT_NATURAL);
|
||||
|
||||
$changed = 0;
|
||||
foreach ($files as $filePath) {
|
||||
if ($this->compileFile($filePath, throwOnError: false)) {
|
||||
$changed++;
|
||||
}
|
||||
}
|
||||
|
||||
return $changed;
|
||||
}
|
||||
|
||||
private function extractPluralFormsFromFile(string $filePath): string {
|
||||
$fileContent = file_get_contents($filePath);
|
||||
if (!is_string($fileContent)) {
|
||||
throw new RuntimeException('Unable to read plural file: ' . $filePath);
|
||||
}
|
||||
|
||||
if (preg_match(self::COMMENT_PATTERN, $fileContent, $matches) === 1) {
|
||||
return $this->normalisePluralForms($matches['formula']);
|
||||
}
|
||||
|
||||
return $this->extractGetTextPluralFormsFromFile($filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{formula:string,nplurals:int,lambda:string} $compiled
|
||||
*/
|
||||
private function renderCompiledFile(array $compiled): string {
|
||||
return <<<PHP
|
||||
<?php
|
||||
|
||||
// Plural-Forms: {$compiled['formula']}
|
||||
// This file is generated by cli/compile.plurals.php.
|
||||
// Edit the formula comment and run `make fix-all`.
|
||||
|
||||
return array(
|
||||
'nplurals' => {$compiled['nplurals']},
|
||||
'plural' => {$compiled['lambda']},
|
||||
);
|
||||
|
||||
PHP;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{formula:string,nplurals:int,expression:string}
|
||||
*/
|
||||
private function parsePluralHeader(string $pluralForms): array {
|
||||
$formula = $this->normalisePluralForms($pluralForms);
|
||||
if (!preg_match(self::FORMULA_PATTERN, $formula, $matches)) {
|
||||
throw new InvalidArgumentException('Invalid plural formula: ' . $formula);
|
||||
}
|
||||
|
||||
return [
|
||||
'formula' => $formula,
|
||||
'nplurals' => max(1, (int)$matches[1]),
|
||||
'expression' => $matches[2],
|
||||
];
|
||||
}
|
||||
|
||||
private function normalisePluralForms(string $pluralForms): string {
|
||||
$pluralForms = trim($pluralForms);
|
||||
$pluralForms = preg_replace(self::COMMENT_PREFIX_PATTERN, '', $pluralForms) ?? $pluralForms;
|
||||
if (preg_match('/^\s*Plural-Forms:\s*(?P<formula>.+?)\s*$/i', $pluralForms, $matches) === 1) {
|
||||
$pluralForms = $matches['formula'];
|
||||
}
|
||||
|
||||
return trim($pluralForms);
|
||||
}
|
||||
|
||||
private function extractGetTextPluralFormsFromFile(string $filePath): string {
|
||||
$pluralData = include $filePath;
|
||||
$pluralForms = is_array($pluralData) ? ($pluralData['plural-forms'] ?? null) : null;
|
||||
if (!is_string($pluralForms) || $pluralForms === '') {
|
||||
throw new RuntimeException('No plural formula found in `' . $filePath . '`');
|
||||
}
|
||||
|
||||
return $this->normalisePluralForms($pluralForms);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight validation only. The compiler transpiles real shipped formulas heuristically.
|
||||
*/
|
||||
private function validatePluralExpression(string $expression, string $pluralForms): void {
|
||||
if (!preg_match(self::ALLOWED_EXPRESSION_PATTERN, $expression)) {
|
||||
throw new RuntimeException('Unsupported token in plural expression `' . $pluralForms . '`');
|
||||
}
|
||||
|
||||
$depth = 0;
|
||||
$length = strlen($expression);
|
||||
for ($index = 0; $index < $length; $index++) {
|
||||
$character = $expression[$index];
|
||||
if ($character === '(') {
|
||||
$depth++;
|
||||
} elseif ($character === ')') {
|
||||
$depth--;
|
||||
if ($depth < 0) {
|
||||
throw new RuntimeException('Unbalanced parentheses in plural expression `' . $pluralForms . '`');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($depth !== 0) {
|
||||
throw new RuntimeException('Unbalanced parentheses in plural expression `' . $pluralForms . '`');
|
||||
}
|
||||
|
||||
if (substr_count($expression, '?') !== substr_count($expression, ':')) {
|
||||
throw new RuntimeException('Unbalanced ternary operators in plural expression `' . $pluralForms . '`');
|
||||
}
|
||||
|
||||
if (str_contains($expression, '/')) {
|
||||
throw new RuntimeException('Operator `/` is not supported in plural expression `' . $pluralForms . '`');
|
||||
}
|
||||
}
|
||||
|
||||
private function transpileExpression(string $expression): string {
|
||||
$expression = $this->stripOuterParentheses(trim($expression));
|
||||
[$condition, $ifTrue, $ifFalse] = $this->splitTopLevelTernary($expression);
|
||||
if ($condition === null) {
|
||||
return $this->transpileLeafExpression($expression);
|
||||
}
|
||||
|
||||
return '(' . $this->transpileLeafExpression($condition) . ' ? ' . $this->transpileExpression($ifTrue) . ' : '
|
||||
. $this->transpileExpression($ifFalse) . ')';
|
||||
}
|
||||
|
||||
private function transpileLeafExpression(string $expression): string {
|
||||
$expression = $this->stripOuterParentheses(trim($expression));
|
||||
// Convert gettext variable name to PHP variable syntax
|
||||
$expression = preg_replace('/\bn\b/', '$n', $expression) ?? $expression;
|
||||
// Enforce strict equality
|
||||
$expression = preg_replace('/(?<![=!<>])==(?!=)/', '===', $expression) ?? $expression;
|
||||
// Enforce strict inequality
|
||||
$expression = preg_replace('/!=(?!=)/', '!==', $expression) ?? $expression;
|
||||
// Normalise operator spacing
|
||||
$expression = preg_replace('/\s*(===|!==|==|!=|<=|>=|\|\||&&|[%*+\-<>])\s*/', ' $1 ', $expression) ?? $expression;
|
||||
// Collapse repeated whitespace
|
||||
$expression = preg_replace('/\s+/', ' ', trim($expression)) ?? trim($expression);
|
||||
return $expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:?string,1:string,2:string}
|
||||
*/
|
||||
private function splitTopLevelTernary(string $expression): array {
|
||||
$questionPosition = null;
|
||||
$depth = 0;
|
||||
$ternaryDepth = 0;
|
||||
$length = strlen($expression);
|
||||
|
||||
for ($index = 0; $index < $length; $index++) {
|
||||
$character = $expression[$index];
|
||||
if ($character === '(') {
|
||||
$depth++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($character === ')') {
|
||||
$depth--;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($depth !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($character === '?') {
|
||||
$questionPosition ??= $index;
|
||||
$ternaryDepth++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($character === ':' && $questionPosition !== null) {
|
||||
$ternaryDepth--;
|
||||
if ($ternaryDepth === 0) {
|
||||
return [
|
||||
trim(substr($expression, 0, $questionPosition)),
|
||||
trim(substr($expression, $questionPosition + 1, $index - $questionPosition - 1)),
|
||||
trim(substr($expression, $index + 1)),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [null, '', ''];
|
||||
}
|
||||
|
||||
private function stripOuterParentheses(string $expression): string {
|
||||
$expression = trim($expression);
|
||||
while (str_starts_with($expression, '(') && str_ends_with($expression, ')')) {
|
||||
$depth = 0;
|
||||
$isWrapped = true;
|
||||
$length = strlen($expression);
|
||||
for ($index = 0; $index < $length; $index++) {
|
||||
$character = $expression[$index];
|
||||
if ($character === '(') {
|
||||
$depth++;
|
||||
} elseif ($character === ')') {
|
||||
$depth--;
|
||||
}
|
||||
|
||||
if ($depth === 0 && $index < $length - 1) {
|
||||
$isWrapped = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isWrapped) {
|
||||
break;
|
||||
}
|
||||
|
||||
$expression = trim(substr($expression, 1, -1));
|
||||
}
|
||||
|
||||
return $expression;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -72,7 +72,8 @@
|
||||
"phpstan": "phpstan analyse --memory-limit 512M .",
|
||||
"phpstan-next": "phpstan analyse --memory-limit 512M -c phpstan-next.neon .",
|
||||
"phpunit": "phpunit --bootstrap ./tests/bootstrap.php --display-notices --display-deprecations --display-phpunit-deprecations ./tests",
|
||||
"translations": "cli/manipulate.translation.php --action format && cli/check.translation.php --generate-readme",
|
||||
"compile-plurals": "php cli/compile.plurals.php --all",
|
||||
"translations": "cli/manipulate.translation.php --action format && php cli/compile.plurals.php --all && cli/check.translation.php --generate-readme",
|
||||
"test": [
|
||||
"@php-lint",
|
||||
"@phtml-lint",
|
||||
|
||||
+207
-43
@@ -30,12 +30,30 @@ class Minz_Translate {
|
||||
*/
|
||||
private static array $lang_files = [];
|
||||
|
||||
/**
|
||||
* Dedicated plural catalogue files registered for the current language.
|
||||
* @var array<int,array{path:string,use_formula:bool}>
|
||||
*/
|
||||
private static array $plural_files = [];
|
||||
|
||||
/**
|
||||
* $translates is a cache for i18n translation.
|
||||
* @var array<string,mixed>
|
||||
*/
|
||||
private static array $translates = [];
|
||||
|
||||
/**
|
||||
* Cache of normalised plural message families by i18n key.
|
||||
* @var array<string,array<int,string>>
|
||||
*/
|
||||
private static array $plural_message_families = [];
|
||||
|
||||
private static bool $plural_catalogue_loaded = false;
|
||||
|
||||
private static ?int $plural_count = null;
|
||||
|
||||
private static ?\Closure $plural_function = null;
|
||||
|
||||
/**
|
||||
* Init the translation object.
|
||||
* @param string $lang_name the lang to show.
|
||||
@@ -43,7 +61,10 @@ class Minz_Translate {
|
||||
public static function init(string $lang_name = ''): void {
|
||||
self::$lang_name = $lang_name;
|
||||
self::$lang_files = [];
|
||||
self::$plural_files = [];
|
||||
self::$translates = [];
|
||||
self::$plural_message_families = [];
|
||||
self::resetPluralCache();
|
||||
self::registerPath(APP_PATH . '/i18n');
|
||||
foreach (self::$path_list as $path) {
|
||||
self::loadLang($path);
|
||||
@@ -57,7 +78,10 @@ class Minz_Translate {
|
||||
public static function reset(string $lang_name): void {
|
||||
self::$lang_name = $lang_name;
|
||||
self::$lang_files = [];
|
||||
self::$plural_files = [];
|
||||
self::$translates = [];
|
||||
self::$plural_message_families = [];
|
||||
self::resetPluralCache();
|
||||
foreach (self::$path_list as $path) {
|
||||
self::loadLang($path);
|
||||
}
|
||||
@@ -132,8 +156,10 @@ class Minz_Translate {
|
||||
* @param string $path the path containing i18n directories.
|
||||
*/
|
||||
private static function loadLang(string $path): void {
|
||||
$selected_lang_path = $path . '/' . self::$lang_name;
|
||||
$lang_path = $path . '/' . self::$lang_name;
|
||||
if (self::$lang_name === '' || !is_dir($lang_path)) {
|
||||
$uses_selected_language = self::$lang_name !== '' && is_dir($selected_lang_path);
|
||||
if (!$uses_selected_language) {
|
||||
// The lang path does not exist, fallback to English ('en')
|
||||
$lang_path = $path . '/en';
|
||||
if (!is_dir($lang_path)) {
|
||||
@@ -146,11 +172,20 @@ class Minz_Translate {
|
||||
scandir($lang_path) ?: [],
|
||||
['..', '.']
|
||||
));
|
||||
self::$plural_message_families = [];
|
||||
|
||||
// Each file basename correspond to a top-level i18n key. For each of
|
||||
// these keys we store the file pathname and mark translations must be
|
||||
// reloaded (by setting $translates[$i18n_key] to null).
|
||||
foreach ($list_i18n_files as $i18n_filename) {
|
||||
if ($i18n_filename === 'plurals.php') {
|
||||
self::$plural_files[] = [
|
||||
'path' => $lang_path . '/' . $i18n_filename,
|
||||
'use_formula' => $uses_selected_language || self::$lang_name === '',
|
||||
];
|
||||
self::resetPluralCache();
|
||||
continue;
|
||||
}
|
||||
$i18n_key = basename($i18n_filename, '.php');
|
||||
if (!isset(self::$lang_files[$i18n_key])) {
|
||||
self::$lang_files[$i18n_key] = [];
|
||||
@@ -198,51 +233,13 @@ class Minz_Translate {
|
||||
* If no value is found, return the key itself.
|
||||
*/
|
||||
public static function t(string $key, ...$args): string {
|
||||
$group = explode('.', $key);
|
||||
|
||||
if (count($group) < 2) {
|
||||
Minz_Log::debug($key . ' is not in a valid format');
|
||||
$top_level = 'gen';
|
||||
} else {
|
||||
$top_level = array_shift($group) ?? '';
|
||||
}
|
||||
|
||||
// If $translates[$top_level] is null it means we have to load the
|
||||
// corresponding files.
|
||||
if (empty(self::$translates[$top_level])) {
|
||||
$res = self::loadKey($top_level);
|
||||
if (!$res) {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
|
||||
// Go through the i18n keys to get the correct translation value.
|
||||
$translates = self::$translates[$top_level];
|
||||
if (!is_array($translates)) {
|
||||
$translates = [];
|
||||
}
|
||||
$size_group = count($group);
|
||||
$level_processed = 0;
|
||||
$translation_value = $key;
|
||||
foreach ($group as $i18n_level) {
|
||||
if (!is_array($translates)) {
|
||||
continue; // Not needed. To help PHPStan
|
||||
}
|
||||
$level_processed++;
|
||||
if (!isset($translates[$i18n_level])) {
|
||||
Minz_Log::debug($key . ' is not a valid key');
|
||||
return $key;
|
||||
}
|
||||
|
||||
if ($level_processed < $size_group) {
|
||||
$translates = $translates[$i18n_level];
|
||||
} else {
|
||||
$translation_value = $translates[$i18n_level];
|
||||
}
|
||||
$translation_value = self::resolveKey($key);
|
||||
if ($translation_value === null) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
if (!is_string($translation_value)) {
|
||||
$translation_value = is_array($translation_value) ? ($translation_value['_'] ?? null) : null;
|
||||
$translation_value = $translation_value['_'] ?? null;
|
||||
if (!is_string($translation_value)) {
|
||||
Minz_Log::debug($key . ' is not a valid key');
|
||||
return $key;
|
||||
@@ -253,12 +250,179 @@ class Minz_Translate {
|
||||
return empty($args) ? $translation_value : vsprintf($translation_value, $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a translation key to its raw string or array value.
|
||||
* @return array<mixed>|string|null
|
||||
*/
|
||||
private static function resolveKey(string $key): array|string|null {
|
||||
$group = explode('.', $key);
|
||||
|
||||
if (count($group) < 2) {
|
||||
Minz_Log::debug($key . ' is not in a valid format');
|
||||
$top_level = 'gen';
|
||||
} else {
|
||||
$top_level = array_shift($group) ?? '';
|
||||
}
|
||||
|
||||
if ((self::$translates[$top_level] ?? null) === null) {
|
||||
$res = self::loadKey($top_level);
|
||||
if (!$res) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$translationValue = self::$translates[$top_level] ?? null;
|
||||
if (!is_array($translationValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($group as $i18n_level) {
|
||||
if (!is_array($translationValue) || !array_key_exists($i18n_level, $translationValue)) {
|
||||
Minz_Log::debug($key . ' is not a valid key');
|
||||
return null;
|
||||
}
|
||||
$translationValue = $translationValue[$i18n_level];
|
||||
}
|
||||
|
||||
if (!is_array($translationValue) && !is_string($translationValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $translationValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current language.
|
||||
*/
|
||||
public static function language(): string {
|
||||
return self::$lang_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all cached plural data.
|
||||
*/
|
||||
private static function resetPluralCache(): void {
|
||||
self::$plural_catalogue_loaded = false;
|
||||
self::$plural_count = null;
|
||||
self::$plural_function = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the plural catalogue for the current language.
|
||||
*/
|
||||
private static function loadPluralCatalogue(): void {
|
||||
if (self::$plural_catalogue_loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::$plural_catalogue_loaded = true;
|
||||
$fallbackPluralCount = null;
|
||||
$fallbackPluralFunction = null;
|
||||
|
||||
foreach (self::$plural_files as $pluralFile) {
|
||||
$pluralData = include $pluralFile['path'];
|
||||
if (!is_array($pluralData)) {
|
||||
Minz_Log::warning('`' . $pluralFile['path'] . '` does not contain a PHP array');
|
||||
continue;
|
||||
}
|
||||
|
||||
$pluralCount = $pluralData['nplurals'] ?? null;
|
||||
$pluralFunction = $pluralData['plural'] ?? null;
|
||||
if (!is_int($pluralCount) || $pluralCount < 1 || !($pluralFunction instanceof \Closure)) {
|
||||
Minz_Log::warning('Invalid compiled plural data in `' . $pluralFile['path'] . '`. Run `make fix-all`.');
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($pluralFile['use_formula']) {
|
||||
if (self::$plural_function === null) {
|
||||
self::$plural_count = $pluralCount;
|
||||
self::$plural_function = $pluralFunction;
|
||||
} elseif (self::$plural_count !== $pluralCount) {
|
||||
Minz_Log::warning('Conflicting compiled plural count in `' . $pluralFile['path'] . '`');
|
||||
}
|
||||
} elseif ($fallbackPluralFunction === null) {
|
||||
$fallbackPluralCount = $pluralCount;
|
||||
$fallbackPluralFunction = $pluralFunction;
|
||||
}
|
||||
}
|
||||
|
||||
if (self::$plural_function === null) {
|
||||
self::$plural_count = $fallbackPluralCount;
|
||||
self::$plural_function = $fallbackPluralFunction;
|
||||
}
|
||||
}
|
||||
|
||||
private static function pluralIndex(int $value): ?int {
|
||||
if (self::$plural_count === null || self::$plural_function === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$index = (self::$plural_function)($value);
|
||||
if (!is_int($index)) {
|
||||
return null;
|
||||
}
|
||||
$index = max(0, $index);
|
||||
return min($index, self::$plural_count - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a count-based key using gettext plural indexes.
|
||||
* @param string $baseKey Base i18n key without plural suffix (e.g. `gen.interval.second`).
|
||||
* @param int $value Count used for plural category and `%d` substitution.
|
||||
* @return string|null Translated string or null if no translation is found.
|
||||
*/
|
||||
public static function plural(string $baseKey, int $value): ?string {
|
||||
self::loadPluralCatalogue();
|
||||
|
||||
if (!isset(self::$plural_message_families[$baseKey])) {
|
||||
$rawMessageFamily = self::resolveKey($baseKey);
|
||||
if (!is_array($rawMessageFamily) || $rawMessageFamily === []) {
|
||||
Minz_Log::debug($baseKey . ' is not a valid plural key');
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var array<int,string> $messageFamily */
|
||||
$messageFamily = [];
|
||||
foreach ($rawMessageFamily as $index => $message) {
|
||||
if (is_int($index)) {
|
||||
$integerIndex = $index;
|
||||
} elseif (ctype_digit($index)) {
|
||||
$integerIndex = (int)$index;
|
||||
} else {
|
||||
$integerIndex = null;
|
||||
}
|
||||
if ($integerIndex === null) {
|
||||
continue;
|
||||
}
|
||||
if (!is_string($message)) {
|
||||
continue;
|
||||
}
|
||||
$messageFamily[$integerIndex] = $message;
|
||||
}
|
||||
|
||||
if ($messageFamily === []) {
|
||||
Minz_Log::debug($baseKey . ' is not a valid plural key');
|
||||
return null;
|
||||
}
|
||||
|
||||
ksort($messageFamily);
|
||||
self::$plural_message_families[$baseKey] = $messageFamily;
|
||||
}
|
||||
|
||||
$messageFamily = self::$plural_message_families[$baseKey];
|
||||
|
||||
$index = self::pluralIndex($value);
|
||||
if ($index !== null && isset($messageFamily[$index]) && $messageFamily[$index] !== '') {
|
||||
return vsprintf($messageFamily[$index], [$value]);
|
||||
}
|
||||
|
||||
$lastMessage = end($messageFamily);
|
||||
if ($lastMessage === false || $lastMessage === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return vsprintf($lastMessage, [$value]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -215,6 +215,40 @@ function timestamptodate(int $t, bool $hour = true): string {
|
||||
return @date($date, $t) ?: '';
|
||||
}
|
||||
|
||||
function timestampToMachineDate(int $t): string {
|
||||
return @date(DATE_ATOM, $t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Human readable string how long this timestamp is ago ("5 years ago").
|
||||
*/
|
||||
function timeago(int $timestamp, ?int $baseTimestamp = null): string {
|
||||
$baseTimestamp ??= time();
|
||||
$delta = abs($baseTimestamp - $timestamp);
|
||||
|
||||
$units = [
|
||||
[31536000, 'year'],
|
||||
[2592000, 'month'],
|
||||
[86400, 'day'],
|
||||
[3600, 'hour'],
|
||||
[60, 'minute'],
|
||||
];
|
||||
|
||||
$diff = '';
|
||||
foreach ($units as [$unitSeconds, $unit]) {
|
||||
if ($delta >= $unitSeconds) {
|
||||
$unitValue = intdiv($delta, $unitSeconds);
|
||||
$diff = Minz_Translate::plural('gen.interval.' . $unit, $unitValue) ?? ($unitValue . ' ' . $unit . ' ago');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($diff === '') {
|
||||
return Minz_Translate::t('gen.interval.justnow');
|
||||
}
|
||||
return $diff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode HTML entities but preserve XML entities.
|
||||
*/
|
||||
|
||||
@@ -140,4 +140,40 @@ final class I18nCompletionValidatorTest extends \PHPUnit\Framework\TestCase {
|
||||
self::assertTrue($validator->validate());
|
||||
self::assertSame('', $validator->displayResult());
|
||||
}
|
||||
|
||||
public function testValidateFlagsHigherPluralVariantWhenEqualToEnglishPlural(): void {
|
||||
$validator = new I18nCompletionValidator([
|
||||
'gen.php' => [
|
||||
'gen.interval.day.0' => new I18nValue('%d day ago'),
|
||||
'gen.interval.day.1' => new I18nValue('%d days ago'),
|
||||
],
|
||||
], [
|
||||
'gen.php' => [
|
||||
'gen.interval.day.0' => new I18nValue('%d dzień temu'),
|
||||
'gen.interval.day.1' => new I18nValue('%d dni temu'),
|
||||
'gen.interval.day.2' => new I18nValue('%d days ago'),
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertFalse($validator->validate());
|
||||
self::assertSame("Untranslated key gen.interval.day.2 - %d days ago\n", $validator->displayResult());
|
||||
self::assertSame("Translation is 66.7% complete.\n", $validator->displayReport());
|
||||
}
|
||||
|
||||
public function testValidateSkipsEnglishPluralVariantsMissingFromOneFormLanguage(): void {
|
||||
$validator = new I18nCompletionValidator([
|
||||
'gen.php' => [
|
||||
'gen.interval.day.0' => new I18nValue('%d day ago'),
|
||||
'gen.interval.day.1' => new I18nValue('%d days ago'),
|
||||
],
|
||||
], [
|
||||
'gen.php' => [
|
||||
'gen.interval.day.0' => new I18nValue('%d hari yang lalu'),
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertTrue($validator->validate());
|
||||
self::assertSame('', $validator->displayResult());
|
||||
self::assertSame("Translation is 100.0% complete.\n", $validator->displayReport());
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user