Skip to content

Commit

Permalink
Locale override for getName() (with multiple fallbacks) (#195)
Browse files Browse the repository at this point in the history
  • Loading branch information
c960657 authored May 1, 2020
1 parent 46ff738 commit 91eec58
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 71 deletions.
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 @@ -17,6 +17,7 @@
use InvalidArgumentException;
use JsonSerializable;
use Yasumi\Exception\InvalidDateException;
use Yasumi\Exception\MissingTranslationException;
use Yasumi\Exception\UnknownLocaleException;

/**
Expand Down Expand Up @@ -54,6 +55,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 @@ -153,41 +159,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

0 comments on commit 91eec58

Please sign in to comment.