Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Locale override for getName() (with multiple fallbacks) #195

Merged
merged 5 commits into from
May 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/Yasumi/Exception/MissingTranslationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php declare(strict_types=1);
/**
* This file is part of the Yasumi package.
*
* Copyright (c) 2015 - 2019 AzuyaLabs
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Sacha Telgenhof <me@sachatelgenhof.com>
*/

namespace Yasumi\Exception;

use Exception as BaseException;

/**
* Class MissingTranslationException.
*/
class MissingTranslationException extends BaseException implements Exception
{
/**
* Initializes the Exception instance
*
* @param string $shortName The short name (internal name) of the holiday
* @param array $locales The locales that was searched
*/
public function __construct(string $shortName, array $locales)
{
parent::__construct(\sprintf("Translation for '%s' not found for any locale: '%s'", $shortName, \implode("', '", $locales)));
}
}
74 changes: 57 additions & 17 deletions src/Yasumi/Holiday.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use InvalidArgumentException;
use JsonSerializable;
use Yasumi\Exception\InvalidDateException;
use Yasumi\Exception\MissingTranslationException;
use Yasumi\Exception\UnknownLocaleException;

/**
Expand Down Expand Up @@ -53,6 +54,11 @@ class Holiday extends DateTime implements JsonSerializable
*/
public const DEFAULT_LOCALE = 'en_US';

/**
* Pseudo-locale representing the short name (internal name) of the holiday.
*/
public const LOCALE_SHORT_NAME = 'shortName';

/**
* @var array list of all defined locales
*/
Expand Down Expand Up @@ -152,41 +158,75 @@ public function jsonSerialize(): self
}

/**
* Returns the name of this holiday.
* Returns the localized name of this holiday
*
* The provided locales are searched for a translation. The first locale containing a translation will be used.
*
* The name of this holiday is returned translated in the given locale. If for the given locale no translation is
* defined, the name in the default locale ('en_US') is returned. In case there is no translation at all, the short
* internal name is returned.
* If no locale is provided, proceed as if an array containing the display locale, Holiday::DEFAULT_LOCALE ('en_US'), and
* Holiday::LOCALE_SHORT_NAME (the short name (internal name) of this holiday) was provided.
*
* @param array $locales The locales to search for translations
*
* @throws MissingTranslationException
*
* @see Holiday::DEFAULT_LOCALE
* @see Holiday::LOCALE_SHORT_NAME
*/
public function getName(): string
public function getName(array $locales = null): string
{
foreach ($this->getLocales() as $locale) {
$locales = $this->getLocales($locales);
foreach ($locales as $locale) {
if ($locale === self::LOCALE_SHORT_NAME) {
return $this->shortName;
}
if (isset($this->translations[$locale])) {
return $this->translations[$locale];
}
}

return $this->shortName;
throw new MissingTranslationException($this->shortName, $locales);
}

/**
* Returns the display locale and its fallback locales.
* Expands the provided locale into an array of locales to check for translations.
*
* For each provided locale, return all locales including their parent locales. E.g.
* ['ca_ES_VALENCIA', 'es_ES'] is expanded into ['ca_ES_VALENCIA', 'ca_ES', 'ca', 'es_ES', 'es'].
*
* If a string is provided, return as if this string, Holiday::DEFAULT_LOCALE, and Holiday::LOCALE_SHORT_NAM
* was provided. E.g. 'de_DE' is expanded into ['de_DE', 'de', 'en_US', 'en', Holiday::LOCALE_SHORT_NAME].
*
* If null is provided, return as if the display locale was provided as a string.
*
* @param array $locales Array of locales, or null if the display locale should be used
*
* @return array
*
* @see Holiday::DEFAULT_LOCALE
* @see Holiday::LOCALE_SHORT_NAME
*/
protected function getLocales(): array
protected function getLocales(?array $locales): array
{
$locales = [$this->displayLocale];
$parts = \explode('_', $this->displayLocale);
while (\array_pop($parts) && $parts) {
$locales[] = \implode('_', $parts);
if ($locales) {
$expanded = [];
$locales = $locales;
} else {
$locales = [$this->displayLocale];
// DEFAULT_LOCALE is 'en_US', and its parent is 'en'.
$expanded = [self::LOCALE_SHORT_NAME, 'en', 'en_US'];
}

// DEFAULT_LOCALE is en_US
$locales[] = 'en_US';
$locales[] = 'en';
// Expand e.g. ['de_DE', 'en_GB'] into ['de_DE', 'de', 'en_GB', 'en'].
foreach (\array_reverse($locales) as $locale) {
$parent = \strtok($locale, '_');
while ($child = \strtok('_')) {
$expanded[] = $parent;
$parent .= '_' . $child;
}
$expanded[] = $locale;
}

return $locales;
return \array_reverse($expanded);
}

/**
Expand Down
23 changes: 16 additions & 7 deletions src/Yasumi/SubstituteHoliday.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
namespace Yasumi;

use Yasumi\Exception\InvalidDateException;
use Yasumi\Exception\MissingTranslationException;
use Yasumi\Exception\UnknownLocaleException;

/**
Expand Down Expand Up @@ -76,19 +77,27 @@ public function __construct(
}

/**
* Returns the name of this holiday.
* Returns the localized name of this holiday
*
* The name of this holiday is returned translated in the given locale. If for the given locale no translation is
* defined, the name in the default locale ('en_US') is returned. In case there is no translation at all, the short
* internal name is returned.
* The provided locales are searched for a translation. The first locale containing a translation will be used.
*
* If no locale is provided, proceed as if an array containing the display locale, Holiday::DEFAULT_LOCALE ('en_US'), and
* Holiday::LOCALE_SHORT_NAME (the short name (internal name) of this holiday) was provided.
*
* @param array $locales The locales to search for translations
*
* @throws MissingTranslationException
*
* @see Holiday::DEFAULT_LOCALE
* @see Holiday::LOCALE_SHORT_NAME
*/
public function getName(): string
public function getName($locales = null): string
{
$name = parent::getName();

if ($name === $this->shortName) {
foreach ($this->getLocales() as $locale) {
$pattern = $this->substituteHolidayTranslations[$locale] ?? null;
foreach ($this->getLocales($locales) as $locales) {
$pattern = $this->substituteHolidayTranslations[$locales] ?? null;
if ($pattern) {
return \str_replace('{0}', $this->substitutedHoliday->getName(), $pattern);
}
Expand Down
123 changes: 76 additions & 47 deletions tests/Base/HolidayTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Exception;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use Yasumi\Exception\MissingTranslationException;
use Yasumi\Exception\UnknownLocaleException;
use Yasumi\Holiday;
use Yasumi\tests\YasumiBase;
Expand Down Expand Up @@ -89,73 +90,101 @@ public function testHolidayWithDateTimeInterface(): void
}

/**
* Tests the getName function of the Holiday object with no translations for the name given.
* @throws Exception
* Tests the getLocales function of the Holiday object.
*/
public function testHolidayGetNameWithNoTranslations(): void
public function testHolidayGetLocales(): void
{
$name = 'testHoliday';
$holiday = new Holiday($name, [], new DateTime(), 'en_US');
$holiday = new Holiday('testHoliday', [], new DateTime(), 'ca_ES_VALENCIA');
$method = new \ReflectionMethod(Holiday::class, 'getLocales');
$method->setAccessible(true);

$this->assertIsString($holiday->getName());
$this->assertEquals($name, $holiday->getName());
$this->assertEquals(['ca_ES_VALENCIA', 'ca_ES', 'ca', 'en_US', 'en', Holiday::LOCALE_SHORT_NAME], $method->invoke($holiday, null));
$this->assertEquals(['de_DE', 'de', 'es_ES', 'es'], $method->invoke($holiday, ['de_DE', 'es_ES']));
$this->assertEquals(['de_DE', 'de', Holiday::LOCALE_SHORT_NAME], $method->invoke($holiday, ['de_DE', Holiday::LOCALE_SHORT_NAME]));
}

/**
* Tests the getName function of the Holiday object with only a parent translation for the name given.
* @throws Exception
* Tests the getName function of the Holiday object without any arguments provided.
*/
public function testHolidayGetNameWithParentLocaleTranslation(): void
public function testHolidayGetNameWithoutArgument(): void
{
$name = 'testHoliday';
$translation = 'My Holiday';
$holiday = new Holiday($name, ['de' => $translation], new DateTime(), 'de_DE');
// 'en_US' fallback
$translations = [
'de' => 'Holiday DE',
'de_AT' => 'Holiday DE-AT',
'en' => 'Holiday EN',
'en_US' => 'Holiday EN-US',
];

$this->assertIsString($holiday->getName());
$this->assertEquals($translation, $holiday->getName());
}
$holiday = new Holiday('testHoliday', $translations, new DateTime(), 'de_AT');
$this->assertEquals('Holiday DE-AT', $holiday->getName());

/**
* Tests the getName function of the Holiday object with only a default translation for the name given.
* @throws Exception
*/
public function testHolidayGetNameWithOnlyDefaultTranslation(): void
{
$name = 'testHoliday';
$holiday = new Holiday($name, ['en' => 'Holiday EN', 'en_US' => 'Holiday EN-US'], new DateTime(), 'nl_NL');
$holiday = new Holiday('testHoliday', $translations, new DateTime(), 'de');
$this->assertEquals('Holiday DE', $holiday->getName());

$this->assertIsString($holiday->getName());
$holiday = new Holiday('testHoliday', $translations, new DateTime(), 'de_DE');
$this->assertEquals('Holiday DE', $holiday->getName());

$holiday = new Holiday('testHoliday', $translations, new DateTime(), 'ja');
$this->assertEquals('Holiday EN-US', $holiday->getName());
}

/**
* Tests the getName function of the Holiday object with only a default translation for the name given.
* @throws Exception
*/
public function testHolidayGetNameWithOnlyDefaultTranslationAndFallback(): void
{
$name = 'testHoliday';
$translation = 'My Holiday';
$holiday = new Holiday($name, ['en' => $translation], new DateTime(), 'nl_NL');
// 'en' fallback
$translations = [
'de' => 'Holiday DE',
'en' => 'Holiday EN',
];

$this->assertIsString($holiday->getName());
$this->assertEquals($translation, $holiday->getName());
$holiday = new Holiday('testHoliday', $translations, new DateTime(), 'de_DE');
$this->assertEquals('Holiday DE', $holiday->getName());

$holiday = new Holiday('testHoliday', $translations, new DateTime(), 'ja');
$this->assertEquals('Holiday EN', $holiday->getName());


// No 'en' or 'en_US' fallback
$translations = [
'de' => 'Holiday DE',
];

$holiday = new Holiday('testHoliday', $translations, new DateTime(), 'de_DE');
$this->assertEquals('Holiday DE', $holiday->getName());

$holiday = new Holiday('testHoliday', $translations, new DateTime(), 'ja');
$this->assertEquals('testHoliday', $holiday->getName());
}

/**
* Tests the getName function of the Holiday object with only a default translation for the name given.
*
* @throws Exception
* Tests the getName function of the Holiday object with an explicit list of locales.
*/
public function testHolidayGetNameWithOneNonDefaultTranslation(): void
public function testHolidayGetNameWithArgument(): void
{
$name = 'testHoliday';
$translation = 'My Holiday';
$holiday = new Holiday($name, ['en_US' => $translation], new DateTime(), 'nl_NL');
$translations = [
'de' => 'Holiday DE',
'de_AT' => 'Holiday DE-AT',
'nl' => 'Holiday NL',
'it_IT' => 'Holiday IT-IT',
'en_US' => 'Holiday EN-US',
];
$holiday = new Holiday('testHoliday', $translations, new DateTime(), 'de_DE');

$this->assertEquals('Holiday DE', $holiday->getName(['de']));
$this->assertEquals('Holiday DE', $holiday->getName(['ja', 'de', 'nl', 'it_IT']));
$this->assertEquals('Holiday DE', $holiday->getName(['de_DE']));
$this->assertEquals('Holiday DE', $holiday->getName(['de_DE_berlin']));
$this->assertEquals('Holiday DE', $holiday->getName(['de_DE_berlin', 'nl', 'it_IT']));
$this->assertEquals('Holiday DE-AT', $holiday->getName(['de_AT']));
$this->assertEquals('Holiday DE-AT', $holiday->getName(['de_AT_vienna']));
$this->assertEquals('Holiday NL', $holiday->getName(['nl']));
$this->assertEquals('Holiday NL', $holiday->getName(['nl_NL']));
$this->assertEquals('Holiday IT-IT', $holiday->getName(['it_IT']));
$this->assertEquals('Holiday IT-IT', $holiday->getName(['it_IT', Holiday::LOCALE_SHORT_NAME]));
$this->assertEquals('testHoliday', $holiday->getName([Holiday::LOCALE_SHORT_NAME]));

$holiday = new Holiday('testHoliday', $translations, new DateTime(), 'ja');
$this->assertEquals('Holiday EN-US', $holiday->getName());

$this->assertNotNull($holiday->getName());
$this->assertIsString($holiday->getName());
$this->assertEquals($translation, $holiday->getName());
$this->expectException(MissingTranslationException::class);
$holiday->getName(['it']);
}

/**
Expand Down