diff --git a/src/Yasumi/Exception/MissingTranslationException.php b/src/Yasumi/Exception/MissingTranslationException.php new file mode 100644 index 000000000..eac6f75ab --- /dev/null +++ b/src/Yasumi/Exception/MissingTranslationException.php @@ -0,0 +1,32 @@ + + */ + +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))); + } +} diff --git a/src/Yasumi/Holiday.php b/src/Yasumi/Holiday.php index 55841b65a..1696e1284 100755 --- a/src/Yasumi/Holiday.php +++ b/src/Yasumi/Holiday.php @@ -17,6 +17,7 @@ use InvalidArgumentException; use JsonSerializable; use Yasumi\Exception\InvalidDateException; +use Yasumi\Exception\MissingTranslationException; use Yasumi\Exception\UnknownLocaleException; /** @@ -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 */ @@ -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); } /** diff --git a/src/Yasumi/SubstituteHoliday.php b/src/Yasumi/SubstituteHoliday.php index 5e1a4c942..9321c8493 100755 --- a/src/Yasumi/SubstituteHoliday.php +++ b/src/Yasumi/SubstituteHoliday.php @@ -13,6 +13,7 @@ namespace Yasumi; use Yasumi\Exception\InvalidDateException; +use Yasumi\Exception\MissingTranslationException; use Yasumi\Exception\UnknownLocaleException; /** @@ -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); } diff --git a/tests/Base/HolidayTest.php b/tests/Base/HolidayTest.php index 50ef9e192..c4839c9a6 100644 --- a/tests/Base/HolidayTest.php +++ b/tests/Base/HolidayTest.php @@ -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; @@ -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']); } /**