Commit c1426b7e authored by Andrea Vos's avatar Andrea Vos
Browse files

init

parents
Pipeline #15995904 passed with stage
in 5 seconds
/.idea/*
**/.DS_Store
/vendor/*
/tests/_output/*
ci_tests:
script:
- composer install --dev
- vendor/bin/phpunit
- vendor/bin/phpcs --standard=PSR2 src/
# Avris Polonisator
An extension to [Avris Localisator](https://gitlab.com/Avris/Localisator)
and [Avris Stringer](https://gitlab.com/Avris/Stringer)
introducing support for the Polish language.
## Installation
composer require avris/polonisator
## Usage
$builder = (new LocalisatorBuilder())
->registerExtension(new LocalisatorExtension('pl'))
->registerExtension(new StringerExtension())
->registerExtension(new PolonisatorExtension());
$stringer = $builder->build(Stringer::class);
$polonisator = $builder->build(Polonisator::class);
`StringerTwig` and `PolonisatorTwig` can be registered as [Twig](https://twig.symfony.com/) extensions.
### decline
Pluralisation is not so straight-forward in Polish -- one can say
"zdobyłeś 1 punkt", "2 punkty" and "5 punktów".
**decline** takes care of choosing the right form based on a provided number:
Zdobyłeś {{ points }} punkt{{ points|decline('','y','ów') }}
It also provides a translation selector:
points: <polishDeclination> punkt|%count% punkty|%count% punktów
used like this:
{{ 'points'|l({count: points}) }}
### vocative
{{ user.firstName|vocative }}
This declines Polish first names into a vocative, without a need for a database with all the names (tested on over a 1000 names).
### getGender
**getGender** tries to guess a person's gender based on their name (unless explicitly provided in `$person->getGender()`).
$polonisator->getGender($person); // returns either Stringer::MASCULINE or Stringer::FEMININE
It also provides a translation selector:
said: <gender> powiedział|powiedziała
### genderify
**genderify** automatically adjusts some 2nd person expressions to match the recipients gender (useful for mailings).
{% filter genderify(user.gender) %}
<p>
Drog{_I_A} {{ user.firstName|vocative|capitalize }}!<br/>
Chcielibyśmy Cię poinformować, że zdobył{_ES_AS} {{ user.points }}
{{ user.points|decline('punkt','punkty','punktów') }}!<br/>
</p>
<p>
Pozdrawiamy,<br/>
xyz
</p>
{% endfilter %}
Might return for some user:
> Drogi Tomku!
>
> Chcielibyśmy Cię poinformować, że zdobyłeś 932 punkty!
>
> Pozdrawiamy,
>
> xyz
Available tags:
| Tag | Male | Female |
| ------------- | --------- | --------- |
| {_A} | | a |
| {_I_A} | i | a |
| {_ES_AS} | eś | aś |
| {_Y_A} | y | a |
| {_AL_ELA} | ął | ęła |
| {_CHCIALBY} | chciałby | chciałaby |
| {_POWINIEN} | powinien | powinna |
| {_MOGL} | mógł | mogła |
| {_WSZEDL} | wszedł | weszła |
| {_PAN_PANI_M} | Pan | Pani |
| {_PAN_PANI_C} | Panu | Pani |
| {_PAN_PANI_D} | Pana | Pani |
| {_PAN_PANI_B} | Pana | Panią |
| {_PAN_PANI_N} | Panem | Panią |
| {_PAN_PANI_W} | Panie | Pani |
This list can be expanded / changed by passing a different one into `Genderify`'s constructor.
### numberInWords
See in [Avris Stringer](https://gitlab.com/Avris/Stringer)'s documentation.
Provides Polish translations, like:
375 → trzysta siedemdziesiąt pięć
-279 → minus dwieście siedemdziesiąt dziewięć
146 → sto czterdzieści sześć
10000000 → dziesięć milionów
5371.18 → pięć tysięcy trzysta siedemdziesiąt jeden i osiemnaście setnych
5678 → pięć i sześćset siedemdziesiąt osiem tysięcznych
0.1 → jedna dziesiąta
0.000908 → zero komma zero zero zero dziewięć zero osiem
### timeInWords
See in [Avris Stringer](https://gitlab.com/Avris/Stringer)'s documentation.
Provides Polish translations, like:
`$stringer->timeInWords($time, TimeInWords::MODE_NICE)` (default):
14:15:16 → kwadrans po czternastej.
05:13:32 → trzynaście po piątej.
19:49:20 → za jedenaście dwudziesta.
11:53:11 → za siedem południe.
`$stringer->timeInWords($time, TimeInWords::MODE_SHORT)`:
14:15:16 → czternasta piętnaście.
05:13:32 → piąta trzynaście.
19:49:20 → dziewiętnasta czterdzieści dziewięć.
11:53:11 → jedenasta pięćdziesiąt trzy.
`$stringer->timeInWords($time, TimeInWords::MODE_LONG)`:
14:15:16 → czternasta piętnaście i szesnaście sekund.
05:13:32 → piąta trzynaście i trzydzieści dwie sekundy.
19:49:20 → dziewiętnasta czterdzieści dziewięć i dwadzieścia sekund.
11:53:11 → jedenasta pięćdziesiąt trzy i jedenaście sekund.
### timeDiff
See in [Avris Stringer](https://gitlab.com/Avris/Stringer)'s documentation.
Provides Polish translations, like: "pięć lat temu", "dwadzieścia pięć dni temu", "półtorej godziny temu",
"minutę temu", "teraz", "za trzy dni".
### phone
See in [Avris Stringer](https://gitlab.com/Avris/Stringer)'s documentation.
Provides formatting of `+48` numbers.
## Framework integration
### Micrus
In your `App\App:registerModules` register the Localisator, Stringer and Polonisator module:
yield new \Avris\Localisator\LocalisatorModule;
yield new \Avris\Stringer\StringerModule;
yield new \Avris\Polonisator\PolonisatorModule;
And it should work out of the box: `Avris\Stringer\Stringer` and `Avris\Polonisator\Polonisator` available in the container
and the Twig extensions registered.
### Symfony
Example `servies.yaml` config:
Avris\Localisator\LocalisatorBuilder:
calls:
- [registerExtension, ['@Avris\Localisator\LocalisatorExtension']]
- [registerExtension, ['@Avris\Polonisator\PolonisatorExtension']]
- [registerExtension, ['@Avris\Stringer\StringerExtension']]
Avris\Localisator\LocalisatorExtension:
arguments:
$locale: '%locale%'
Avris\Polonisator\PolonisatorExtension: ~
Avris\Stringer\StringerExtension: ~
Avris\Localisator\LocalisatorInterface:
factory: ['@Avris\Localisator\LocalisatorBuilder', build]
arguments: ['Avris\Localisator\LocalisatorInterface']
Avris\Localisator\LocalisatorTwig: ~
Avris\Stringer\Stringer:
factory: ['@Avris\Localisator\LocalisatorBuilder', build]
arguments: ['Avris\Stringer\Stringer']
Avris\Stringer\StringerTwig: ~
Avris\Polonisator\Polonisator:
factory: ['@Avris\Localisator\LocalisatorBuilder', build]
arguments: ['Avris\Polonisator\Polonisator']
Avris\Polonisator\PolonisatorTwig: ~
### Copyright ###
* **Author:** Andre Prusinowski [(Avris.it)](https://avris.it)
* **Licence:** [MIT](https://mit.avris.it)
{
"name": "avris/polonisator",
"type": "library",
"description": "An extension to Avris Localisator and Stringer introducing support for the Polish language",
"license": "MIT",
"authors": [{
"name": "Avris",
"email": "andre@avris.it",
"homepage": "https://avris.it"
}],
"require": {
"avris/localisator": "^4.0",
"avris/stringer": "v4.0.x-dev"
},
"require-dev": {
"phpunit/phpunit": "^6.5",
"squizlabs/php_codesniffer": "^3.2",
"symfony/var-dumper": "^4.0"
},
"autoload": {
"psr-4": { "Avris\\Polonisator\\": "src" }
},
"autoload-dev": {
"psr-4": { "Avris\\Polonisator\\": "tests" }
}
}
This diff is collapsed.
Avris\Polonisator\:
dir: '%MODULE_DIR%/src/'
Avris\Polonisator\Polonisator:
public: true
Avris\Polonisator\Service\Genderify:
arguments:
$versions:
'{_A}': ['', 'a']
'{_I_A}': ['i', 'a']
'{_ES_AS}': ['eś', 'aś']
'{_Y_A}': ['y', 'a']
'{_AL_ELA}': ['ął', 'ęła']
'{_CHCIALBY}': ['chciałby', 'chciałaby']
'{_POWINIEN}': ['powinien', 'powinna']
'{_MOGL}': ['mógł', 'mogła']
'{_WSZEDL}': ['wszedł', 'weszła']
'{_PAN_PANI_M}': ['Pan', 'Pani']
'{_PAN_PANI_C}': ['Panu', 'Pani']
'{_PAN_PANI_D}': ['Pana', 'Pani']
'{_PAN_PANI_B}': ['Pana', 'Panią']
'{_PAN_PANI_N}': ['Panem', 'Panią']
'{_PAN_PANI_W}': ['Panie', 'Pani']
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.8/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="tests/_bootstrap.php">
<testsuites>
<testsuite name="main">
<directory>tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist addUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
</whitelist>
</filter>
<logging>
<log type="coverage-html" target="tests/_output/coverage"/>
<log type="coverage-clover" target="tests/_output/coverage.xml"/>
<log type="coverage-php" target="tests/_output/coverage.serialized"/>
<log type="coverage-text" target="php://stdout" showOnlySummary="true"/>
</logging>
</phpunit>
<?php
namespace Avris\Polonisator\Entity;
final class MaleVocativeRule
{
private static $cache = [];
/** @var string|string[] */
private $ending;
/** @var int */
private $length;
/** @var string */
private $vocativeEnding;
/** @var int|callable */
private $cutLength;
/**
* @codeCoverageIgnore
*/
public function __construct($ending, $vocativeEnding, $cutLength = 0)
{
$this->ending = $ending;
$this->length = is_array($ending) ? mb_strlen($ending[0]) : mb_strlen($ending);
$this->vocativeEnding = $vocativeEnding;
$this->cutLength = $cutLength;
}
public function matches($name)
{
return is_array($this->ending)
? in_array($this->getLast($name), $this->ending)
: $this->getLast($name) == $this->ending;
}
public function apply($name)
{
return $this->cut($name) . $this->vocativeEnding;
}
private function getLast(string $name): string
{
if (!isset(self::$cache[$name])) {
self::$cache[$name] = [
1 => mb_substr($name, -1),
2 => mb_substr($name, -2),
];
}
return self::$cache[$name][$this->length];
}
private function cut($name)
{
if (!$this->cutLength) {
return $name;
}
$cutLength = is_callable($this->cutLength)
? call_user_func($this->cutLength, $name)
: $this->cutLength;
return mb_substr($name, 0, -$cutLength);
}
}
<?php
namespace Avris\Polonisator\Entity;
use Avris\Stringer\Entity\Time;
use Avris\Stringer\Service\NumberInWords;
use Avris\Stringer\Stringer;
final class NiceTimeRule
{
/** @var int */
private $min;
/** @var int */
private $max;
/** @var string */
private $template;
/**
* @codeCoverageIgnore
*/
public function __construct(int $min, int $max, string $template)
{
$this->min = $min;
$this->max = $max;
$this->template = $template;
}
public function matches(Time $time): bool
{
return $time->getMinutes() >= $this->min && $time->getMinutes() <= $this->max;
}
public function apply(Time $time, NumberInWords $numberInWords, $hours)
{
return preg_replace_callback('#\{([^{]*)\}#', function ($matches) use ($time, $numberInWords, $hours) {
$params = explode(':', $matches[1]);
switch ($params[0]) {
case 'hours':
return $this->formatHours($time, $params, $hours);
case 'minutes':
return $this->formatMinutes($time, $params, $numberInWords);
default:
return ''; // @codeCoverageIgnore
}
}, $this->template);
}
private function formatHours(Time $time, $params, $hoursNames)
{
if (isset($params[2]) && $params[2] == 'noon' && $time->getHours() === 12) {
return 'południe';
}
$hours = isset($params[2]) && $params[2] == 'next' ? $time->getHoursNext() : $time->getHours();
return $hoursNames[$params[1]][$hours];
}
private function formatMinutes(Time $time, $params, NumberInWords $numberInWords)
{
$minutes = isset($params[2]) ? abs($params[2] - $time->getMinutes()) : $time->getMinutes();
return $minutes == 1
? 'minut' . $params[1]
: $numberInWords->numberInWords($minutes, Stringer::FEMININE);
}
}
<?php
namespace Avris\Polonisator\LocaleFormatter;
use Avris\Stringer\LocaleFormatter\LocaleFormatter;
abstract class PolishFormatter implements LocaleFormatter
{
public function getLocales(): array
{
return ['pl', 'pl_PL'];
}
}
<?php
namespace Avris\Polonisator\LocaleFormatter;
use Avris\Localisator\Locale\Locale;
use Avris\Polonisator\Service\Declinator;
use Avris\Stringer\LocaleFormatter\NumberInWordsLocaleFormatter;
use Avris\Stringer\Stringer;
final class PolishNumberInWords extends PolishFormatter implements NumberInWordsLocaleFormatter
{
const ONES = [
0 => 'zero',
1 => 'jeden',
2 => 'dwa',
3 => 'trzy',
4 => 'cztery',
5 => 'pięć',
6 => 'sześć',
7 => 'siedem',
8 => 'osiem',
9 => 'dziewięć',
];
const ONES_FEMININE = [
1 => 'jedna',
2 => 'dwie',
];
const TEENS = [
1 => 'jedenaście',
2 => 'dwanaście',
3 => 'trzynaście',
4 => 'czternaście',
5 => 'piętnaście',
6 => 'szesnaście',
7 => 'siedemnaście',
8 => 'osiemnaście',
9 => 'dziewiętnaście',
];
const TENS = [
1 => 'dziesięć',
2 => 'dwadzieścia',
3 => 'trzydzieści',
4 => 'czterdzieści',
5 => 'pięćdziesiąt',
6 => 'sześćdzisiąt',
7 => 'siedemdziesiąt',
8 => 'osiemdziesiąt',
9 => 'dziewięćdziesiąt',
];
const HUNDREDS = [
1 => 'sto',
2 => 'dwieście',
3 => 'trzysta',
4 => 'czterysta',
5 => 'pięćset',
6 => 'sześćset',
7 => 'siedemset',
8 => 'osiemset',
9 => 'dziewięćset',
];
const LEVELS = [
0 => '',
1 => 'tysiąc',
2 => 'milion',
3 => 'miliard',
4 => 'bilion',
5 => 'biliard',
6 => 'trylion',
7 => 'tryliard',
8 => 'kwadrylion',
9 => 'kwadryliard',
];
const DECIMAL_LEVELS = [
1 => 'dziesiętn',
2 => 'setn',
3 => 'tysięczn',
];
/** @var Declinator */
private $declinator;
/**
* @codeCoverageIgnore
*/
public function __construct(Declinator $declinator)
{
$this->declinator = $declinator;
}
public function wordifyWhole($whole, int $gender)
{
if ($whole == 0) {
return 'zero';
}
$out = [];
$threes = $this->splitNumberInThrees($whole);
for ($level = count($threes)-1; $level >= 0; $level--) {
$part = $threes[$level];
if ($part == 0) {
continue;
}
if ($part > 1 || $level == 0) {
$out[] = $this->numberPartInWords($part, $gender);
}
if ($level > 0) {
$out[] = $this->getLevelName($level, $part);
}
}
return join(' ', $out);
}
private function splitNumberInThrees($whole)
{
$whole = number_format($whole, 0, '', '');
$result = [];
do {
$end = substr($whole, -3);
$whole = substr($whole, 0, -3);
$result[] = $end;
} while (strlen($whole) > 0);
return $result;
}
private function numberPartInWords($part, int $gender = Stringer::MASCULINE)
{
$out = [];
$ones = $part % 10;
$tens = floor($part / 10) % 10;
$hundreds = floor($part / 100) % 10;
if ($hundreds) {
$out[] = self::HUNDREDS[$hundreds];
}
if ($tens == 1 && $ones > 0) {
$out[] = self::TEENS[$ones];
} elseif ($tens) {
$out[] = self::TENS[$tens];
}
$out[] = $this->onesInWords($ones, $tens, $part, $gender);
return trim(join(' ', $out));
}
private function onesInWords($ones, $tens, $part, $gender)
{
if (!$ones || $tens == 1) {
return '';
}
if ($gender == Stringer::FEMININE) {
if ($part == 1) {
return 'jedna';
}
if ($ones == 2) {