summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/Census/CensusColumnRelationToHead.php3
-rw-r--r--app/Functions/Functions.php2262
-rw-r--r--app/Functions/FunctionsPrintFacts.php3
-rw-r--r--app/I18N.php45
-rw-r--r--app/Module/LanguageEnglishAustralia.php4
-rw-r--r--app/Module/LanguageEnglishGreatBritain.php36
-rw-r--r--app/Module/LanguageEnglishUnitedStates.php196
-rw-r--r--app/Module/LanguageSlovakian.php149
-rw-r--r--app/Module/ModuleLanguageInterface.php6
-rw-r--r--app/Module/ModuleLanguageTrait.php9
-rw-r--r--app/Module/PedigreeMapModule.php3
-rw-r--r--app/Module/RelationshipsChartModule.php47
-rw-r--r--app/Relationship.php554
-rw-r--r--app/Services/RelationshipService.php2368
-rw-r--r--resources/views/modules/family_nav/sidebar-family.phtml13
-rw-r--r--resources/views/modules/relatives/family.phtml7
-rw-r--r--tests/feature/RelationshipNamesTest.php185
17 files changed, 3583 insertions, 2307 deletions
diff --git a/app/Census/CensusColumnRelationToHead.php b/app/Census/CensusColumnRelationToHead.php
index 51bd4c1278..7a3d0f957d 100644
--- a/app/Census/CensusColumnRelationToHead.php
+++ b/app/Census/CensusColumnRelationToHead.php
@@ -21,6 +21,7 @@ namespace Fisharebest\Webtrees\Census;
use Fisharebest\Webtrees\Functions\Functions;
use Fisharebest\Webtrees\Individual;
+use Fisharebest\Webtrees\Services\RelationshipService;
/**
* Relationship to head of household.
@@ -44,6 +45,6 @@ class CensusColumnRelationToHead extends AbstractCensusColumn implements CensusC
return $this->head_of_household;
}
- return Functions::getCloseRelationshipName($head, $individual);
+ return app(RelationshipService::class)->getCloseRelationshipName($head, $individual);
}
}
diff --git a/app/Functions/Functions.php b/app/Functions/Functions.php
index 746f247ccb..a4aaea2565 100644
--- a/app/Functions/Functions.php
+++ b/app/Functions/Functions.php
@@ -21,8 +21,6 @@ namespace Fisharebest\Webtrees\Functions;
use Exception;
use Fisharebest\Webtrees\Auth;
-use Fisharebest\Webtrees\Fact;
-use Fisharebest\Webtrees\Family;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
@@ -148,2264 +146,4 @@ class Functions
return $text;
}
-
- /**
- * For close family relationships, such as the families tab and the family navigator
- * Display a tick if both individuals are the same.
- *
- * @param Individual $individual1
- * @param Individual $individual2
- *
- * @return string
- */
- public static function getCloseRelationshipName(Individual $individual1, Individual $individual2): string
- {
- if ($individual1 === $individual2) {
- return self::reflexivePronoun($individual1);
- }
-
- try {
- $relationship = self::getRelationship($individual1, $individual2);
-
- return self::getRelationshipName($relationship);
- } catch (Exception $ex) {
- return '';
- }
- }
-
- /**
- * Generate a reflexive pronoun for an individual
- *
- * @param Individual $individual
- *
- * @return string
- */
- private static function reflexivePronoun(Individual $individual): string
- {
- switch ($individual->sex()) {
- case 'M':
- /* I18N: reflexive pronoun */
- return I18N::translate('himself');
- case 'F':
- /* I18N: reflexive pronoun */
- return I18N::translate('herself');
- default:
- /* I18N: reflexive pronoun - gender neutral version of himself/herself */
- return I18N::translate('themself');
- }
- }
-
- /**
- * Get relationship between two individuals in the gedcom. This function
- * takes account of pending changes, so we can display names of newly added
- * relations.
- *
- * @param Individual $individual1 The person to compute the relationship from
- * @param Individual $individual2 The person to compute the relatiohip to
- * @param int $maxlength The maximum length of path
- *
- * @return array{'path':array<Individual|Family>,'relations':array<string>} An array of nodes on the relationship path
- * @throws Exception If no relationship exists
- */
- public static function getRelationship(Individual $individual1, Individual $individual2, int $maxlength = 4): array
- {
- $spouse_codes = [
- 'M' => 'hus',
- 'F' => 'wif',
- 'U' => 'spo',
- ];
- $parent_codes = [
- 'M' => 'fat',
- 'F' => 'mot',
- 'U' => 'par',
- ];
- $child_codes = [
- 'M' => 'son',
- 'F' => 'dau',
- 'U' => 'chi',
- ];
- $sibling_codes = [
- 'M' => 'bro',
- 'F' => 'sis',
- 'U' => 'sib',
- ];
-
- // Only examine each individual once
- $visited = [
- $individual1->xref() => true,
- ];
-
- // Build paths out from the first individual
- $paths = [
- [
- 'path' => [$individual1],
- 'relations' => ['self'],
- ],
- ];
-
- // Loop over paths of length 1, 2, 3, ...
- while ($paths !== [] && $maxlength >= 0) {
- $maxlength--;
-
- foreach ($paths as $i => $path) {
- // Try each new relation from the end of the path
- $indi = $path['path'][count($path['path']) - 1];
-
- // Parents and siblings
- foreach ($indi->childFamilies(Auth::PRIV_HIDE) as $family) {
- $visited[$family->xref()] = true;
- foreach ($family->spouses(Auth::PRIV_HIDE) as $spouse) {
- if (!isset($visited[$spouse->xref()])) {
- $new_path = $path;
- $new_path['path'][] = $family;
- $new_path['path'][] = $spouse;
- $new_path['relations'][] = $parent_codes[$spouse->sex()];
- if ($spouse === $individual2) {
- return $new_path;
- }
-
- $paths[] = $new_path;
- $visited[$spouse->xref()] = true;
- }
- }
- foreach ($family->children(Auth::PRIV_HIDE) as $child) {
- if (!isset($visited[$child->xref()])) {
- $new_path = $path;
- $new_path['path'][] = $family;
- $new_path['path'][] = $child;
- $new_path['relations'][] = $sibling_codes[$child->sex()];
- if ($child === $individual2) {
- return $new_path;
- }
-
- $paths[] = $new_path;
- $visited[$child->xref()] = true;
- }
- }
- }
-
- // Spouses and children
- foreach ($indi->spouseFamilies(Auth::PRIV_HIDE) as $family) {
- $visited[$family->xref()] = true;
- foreach ($family->spouses(Auth::PRIV_HIDE) as $spouse) {
- if (!isset($visited[$spouse->xref()])) {
- $new_path = $path;
- $new_path['path'][] = $family;
- $new_path['path'][] = $spouse;
- $new_path['relations'][] = $spouse_codes[$spouse->sex()];
- if ($spouse === $individual2) {
- return $new_path;
- }
-
- $paths[] = $new_path;
- $visited[$spouse->xref()] = true;
- }
- }
- foreach ($family->children(Auth::PRIV_HIDE) as $child) {
- if (!isset($visited[$child->xref()])) {
- $new_path = $path;
- $new_path['path'][] = $family;
- $new_path['path'][] = $child;
- $new_path['relations'][] = $child_codes[$child->sex()];
- if ($child === $individual2) {
- return $new_path;
- }
-
- $paths[] = $new_path;
- $visited[$child->xref()] = true;
- }
- }
- }
- unset($paths[$i]);
- }
- }
-
- throw new Exception('getCloseRelationshipName() called on unrelated individuals');
- }
-
- /**
- * Convert the result of get_relationship() into a relationship name.
- *
- * @param array{'path':array<Individual|Family>,'relations':array<string>} $nodes
- *
- * @return string
- */
- public static function getRelationshipName(array $nodes): string
- {
- $person1 = $nodes['path'][0];
- $person2 = $nodes['path'][count($nodes['path']) - 1];
- $path = array_slice($nodes['relations'], 1);
- // Look for paths with *specific* names first.
- // Note that every combination must be listed separately, as the same English
- // name can be used for many different relationships. e.g.
- // brother’s wife & husband’s sister = sister-in-law.
- //
- // $path is an array of the 12 possible gedcom family relationships:
- // mother/father/parent
- // brother/sister/sibling
- // husband/wife/spouse
- // son/daughter/child
- //
- // This is always the shortest path, so “father, daughter” is “half-sister”, not “sister”.
- //
- // This is very repetitive in English, but necessary in order to handle the
- // complexities of other languages.
-
- // Assertions help static analysis tools to know which array element we selected.
- assert(!$person1 instanceof Family);
- assert(!$person2 instanceof Family);
-
- return self::getRelationshipNameFromPath(
- implode('', $path),
- $person1,
- $person2
- );
- }
-
- /**
- * Calculate the name of a cousin.
- *
- * @param int $n
- * @param string $sex
- *
- * @return string
- */
- public static function cousinName(int $n, string $sex): string
- {
- if ($sex === 'M') {
- switch ($n) {
- case 1:
- /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
- return I18N::translateContext('MALE', 'first cousin');
- case 2:
- /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
- return I18N::translateContext('MALE', 'second cousin');
- case 3:
- /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
- return I18N::translateContext('MALE', 'third cousin');
- case 4:
- /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
- return I18N::translateContext('MALE', 'fourth cousin');
- case 5:
- /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
- return I18N::translateContext('MALE', 'fifth cousin');
- case 6:
- /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
- return I18N::translateContext('MALE', 'sixth cousin');
- case 7:
- /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
- return I18N::translateContext('MALE', 'seventh cousin');
- case 8:
- /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
- return I18N::translateContext('MALE', 'eighth cousin');
- case 9:
- /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
- return I18N::translateContext('MALE', 'ninth cousin');
- case 10:
- /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
- return I18N::translateContext('MALE', 'tenth cousin');
- case 11:
- /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
- return I18N::translateContext('MALE', 'eleventh cousin');
- case 12:
- /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
- return I18N::translateContext('MALE', 'twelfth cousin');
- case 13:
- /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
- return I18N::translateContext('MALE', 'thirteenth cousin');
- case 14:
- /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
- return I18N::translateContext('MALE', 'fourteenth cousin');
- case 15:
- /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
- return I18N::translateContext('MALE', 'fifteenth cousin');
- default:
- /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
- return I18N::translateContext('MALE', '%s × cousin', I18N::number($n));
- }
- }
-
- if ($sex === 'F') {
- switch ($n) {
- case 1:
- return I18N::translateContext('FEMALE', 'first cousin');
- case 2:
- return I18N::translateContext('FEMALE', 'second cousin');
- case 3:
- return I18N::translateContext('FEMALE', 'third cousin');
- case 4:
- return I18N::translateContext('FEMALE', 'fourth cousin');
- case 5:
- return I18N::translateContext('FEMALE', 'fifth cousin');
- case 6:
- return I18N::translateContext('FEMALE', 'sixth cousin');
- case 7:
- return I18N::translateContext('FEMALE', 'seventh cousin');
- case 8:
- return I18N::translateContext('FEMALE', 'eighth cousin');
- case 9:
- return I18N::translateContext('FEMALE', 'ninth cousin');
- case 10:
- return I18N::translateContext('FEMALE', 'tenth cousin');
- case 11:
- return I18N::translateContext('FEMALE', 'eleventh cousin');
- case 12:
- return I18N::translateContext('FEMALE', 'twelfth cousin');
- case 13:
- return I18N::translateContext('FEMALE', 'thirteenth cousin');
- case 14:
- return I18N::translateContext('FEMALE', 'fourteenth cousin');
- case 15:
- return I18N::translateContext('FEMALE', 'fifteenth cousin');
- default:
- return I18N::translateContext('FEMALE', '%s × cousin', I18N::number($n));
- }
- }
-
- switch ($n) {
- case 1:
- return I18N::translate('first cousin');
- case 2:
- return I18N::translate('second cousin');
- case 3:
- return I18N::translate('third cousin');
- case 4:
- return I18N::translate('fourth cousin');
- case 5:
- return I18N::translate('fifth cousin');
- case 6:
- return I18N::translate('sixth cousin');
- case 7:
- return I18N::translate('seventh cousin');
- case 8:
- return I18N::translate('eighth cousin');
- case 9:
- return I18N::translate('ninth cousin');
- case 10:
- return I18N::translate('tenth cousin');
- case 11:
- return I18N::translate('eleventh cousin');
- case 12:
- return I18N::translate('twelfth cousin');
- case 13:
- return I18N::translate('thirteenth cousin');
- case 14:
- return I18N::translate('fourteenth cousin');
- case 15:
- return I18N::translate('fifteenth cousin');
- default:
- return I18N::translate('%s × cousin', I18N::number($n));
- }
- }
-
- /**
- * A variation on cousin_name(), for constructs such as “sixth great-nephew”
- * Currently used only by Spanish relationship names.
- *
- * @param int $n
- * @param string $sex
- * @param string $relation
- *
- * @return string
- */
- public static function cousinName2(int $n, string $sex, string $relation): string
- {
- if ($sex === 'M') {
- switch ($n) {
- case 1:
- /* I18N: A Spanish relationship name, such as third great-nephew */
- return I18N::translateContext('MALE', 'first %s', $relation);
- case 2:
- /* I18N: A Spanish relationship name, such as third great-nephew */
- return I18N::translateContext('MALE', 'second %s', $relation);
- case 3:
- /* I18N: A Spanish relationship name, such as third great-nephew */
- return I18N::translateContext('MALE', 'third %s', $relation);
- case 4:
- /* I18N: A Spanish relationship name, such as third great-nephew */
- return I18N::translateContext('MALE', 'fourth %s', $relation);
- case 5:
- /* I18N: A Spanish relationship name, such as third great-nephew */
- return I18N::translateContext('MALE', 'fifth %s', $relation);
- default:
- /* I18N: A Spanish relationship name, such as third great-nephew */
- return I18N::translateContext('MALE', '%1$s × %2$s', I18N::number($n), $relation);
- }
- }
-
- if ($sex === 'F') {
- switch ($n) {
- case 1:
- /* I18N: A Spanish relationship name, such as third great-nephew */
- return I18N::translateContext('FEMALE', 'first %s', $relation);
- case 2:
- /* I18N: A Spanish relationship name, such as third great-nephew */
- return I18N::translateContext('FEMALE', 'second %s', $relation);
- case 3:
- /* I18N: A Spanish relationship name, such as third great-nephew */
- return I18N::translateContext('FEMALE', 'third %s', $relation);
- case 4:
- /* I18N: A Spanish relationship name, such as third great-nephew */
- return I18N::translateContext('FEMALE', 'fourth %s', $relation);
- case 5:
- /* I18N: A Spanish relationship name, such as third great-nephew */
- return I18N::translateContext('FEMALE', 'fifth %s', $relation);
- default:
- /* I18N: A Spanish relationship name, such as third great-nephew */
- return I18N::translateContext('FEMALE', '%1$s × %2$s', I18N::number($n), $relation);
- }
- }
-
- switch ($n) {
- case 1:
- /* I18N: A Spanish relationship name, such as third great-nephew */
- return I18N::translate('first %s', $relation);
- case 2:
- /* I18N: A Spanish relationship name, such as third great-nephew */
- return I18N::translate('second %s', $relation);
- case 3:
- /* I18N: A Spanish relationship name, such as third great-nephew */
- return I18N::translate('third %s', $relation);
- case 4:
- /* I18N: A Spanish relationship name, such as third great-nephew */
- return I18N::translate('fourth %s', $relation);
- case 5:
- /* I18N: A Spanish relationship name, such as third great-nephew */
- return I18N::translate('fifth %s', $relation);
- default:
- /* I18N: A Spanish relationship name, such as third great-nephew */
- return I18N::translate('%1$s × %2$s', I18N::number($n), $relation);
- }
- }
-
- /**
- * @var string[] Cache for generic relationships (key stores the path, and value represents the relationship name)
- */
- protected static $relationshipsCache = [];
-
- /**
- * Convert a relationship path into a relationship name.
- *
- * @param string $path
- * @param Individual|null $person1
- * @param Individual|null $person2
- *
- * @return string
- */
- public static function getRelationshipNameFromPath(string $path, Individual $person1 = null, Individual $person2 = null): string
- {
- if (!preg_match('/^(mot|fat|par|hus|wif|spo|son|dau|chi|bro|sis|sib)*$/', $path)) {
- return '<span class="error">' . $path . '</span>';
- }
- // The path does not include the starting person. In some languages, the
- // translation for a man’s (relative) is different from a woman’s (relative),
- // due to inflection.
- $sex1 = $person1 ? $person1->sex() : 'U';
-
- // The sex of the last person in the relationship determines the name in
- // many cases. e.g. great-aunt / great-uncle
- if (preg_match('/(fat|hus|son|bro)$/', $path)) {
- $sex2 = 'M';
- } elseif (preg_match('/(mot|wif|dau|sis)$/', $path)) {
- $sex2 = 'F';
- } else {
- $sex2 = 'U';
- }
-
- switch ($path) {
- case '':
- return I18N::translate('self');
- // Level One relationships
- case 'mot':
- return I18N::translate('mother');
- case 'fat':
- return I18N::translate('father');
- case 'par':
- return I18N::translate('parent');
- case 'hus':
- if ($person1 instanceof Individual && $person2 instanceof Individual) {
- // We had the linking family earlier, but lost it. Find it again.
- foreach ($person1->spouseFamilies() as $family) {
- if ($person2 === $family->spouse($person1)) {
- $event = $family->facts(['ANUL', 'DIV', 'ENGA', 'MARR'], true, Auth::PRIV_HIDE, true)->last();
-
- if ($event instanceof Fact) {
- switch ($event->tag()) {
- case 'FAM:ANUL':
- case 'FAM:DIV':
- return I18N::translate('ex-husband');
- case 'FAM:MARR':
- return I18N::translate('husband');
- case 'FAM:ENGA':
- return I18N::translate('fiancé');
- }
- }
- }
- }
- }
-
- return I18N::translateContext('MALE', 'partner');
-
- case 'wif':
- if ($person1 instanceof Individual && $person2 instanceof Individual) {
- // We had the linking family earlier, but lost it. Find it again.
- foreach ($person1->spouseFamilies() as $family) {
- if ($person2 === $family->spouse($person1)) {
- $event = $family->facts(['ANUL', 'DIV', 'ENGA', 'MARR'], true, Auth::PRIV_HIDE, true)->last();
-
- if ($event instanceof Fact) {
- switch ($event->tag()) {
- case 'FAM:ANUL':
- case 'FAM:DIV':
- return I18N::translate('ex-wife');
- case 'FAM:MARR':
- return I18N::translate('wife');
- case 'FAM:ENGA':
- return I18N::translate('fiancée');
- }
- }
- }
- }
- }
-
- return I18N::translateContext('FEMALE', 'partner');
- case 'spo':
- if ($person1 instanceof Individual && $person2 instanceof Individual) {
- // We had the linking family earlier, but lost it. Find it again.
- foreach ($person1->spouseFamilies() as $family) {
- if ($person2 === $family->spouse($person1)) {
- $event = $family->facts(['ANUL', 'DIV', 'ENGA', 'MARR'], true, Auth::PRIV_HIDE, true)->last();
-
- if ($event instanceof Fact) {
- switch ($event->tag()) {
- case 'FAM:ANUL':
- case 'FAM:DIV':
- return I18N::translate('ex-spouse');
- case 'FAM:MARR':
- return I18N::translate('spouse');
- case 'FAM:ENGA':
- return I18N::translate('fiancé(e)');
- }
- }
- }
- }
- }
-
- return I18N::translate('partner');
-
- case 'son':
- return I18N::translate('son');
- case 'dau':
- return I18N::translate('daughter');
- case 'chi':
- return I18N::translate('child');
- case 'bro':
- if ($person1 && $person2) {
- $dob1 = $person1->getBirthDate();
- $dob2 = $person2->getBirthDate();
- if ($dob1->isOK() && $dob2->isOK()) {
- if (abs($dob1->julianDay() - $dob2->julianDay()) < 2 && $dob1->minimumDate()->day > 0 && $dob2->minimumDate()->day > 0) {
- // Exclude BEF, AFT, etc.
- return I18N::translate('twin brother');
- }
-
- if ($dob1->maximumJulianDay() < $dob2->minimumJulianDay()) {
- return I18N::translate('younger brother');
- }
-
- if ($dob1->minimumJulianDay() > $dob2->maximumJulianDay()) {
- return I18N::translate('elder brother');
- }
- }
- }
-
- return I18N::translate('brother');
- case 'sis':
- if ($person1 && $person2) {
- $dob1 = $person1->getBirthDate();
- $dob2 = $person2->getBirthDate();
- if ($dob1->isOK() && $dob2->isOK()) {
- if (abs($dob1->julianDay() - $dob2->julianDay()) < 2 && $dob1->minimumDate()->day > 0 && $dob2->minimumDate()->day > 0) {
- // Exclude BEF, AFT, etc.
- return I18N::translate('twin sister');
- }
-
- if ($dob1->maximumJulianDay() < $dob2->minimumJulianDay()) {
- return I18N::translate('younger sister');
- }
-
- if ($dob1->minimumJulianDay() > $dob2->maximumJulianDay()) {
- return I18N::translate('elder sister');
- }
- }
- }
-
- return I18N::translate('sister');
- case 'sib':
- if ($person1 && $person2) {
- $dob1 = $person1->getBirthDate();
- $dob2 = $person2->getBirthDate();
- if ($dob1->isOK() && $dob2->isOK()) {
- if (abs($dob1->julianDay() - $dob2->julianDay()) < 2 && $dob1->minimumDate()->day > 0 && $dob2->minimumDate()->day > 0) {
- // Exclude BEF, AFT, etc.
- return I18N::translate('twin sibling');
- }
-
- if ($dob1->maximumJulianDay() < $dob2->minimumJulianDay()) {
- return I18N::translate('younger sibling');
- }
-
- if ($dob1->minimumJulianDay() > $dob2->maximumJulianDay()) {
- return I18N::translate('elder sibling');
- }
- }
- }
-
- return I18N::translate('sibling');
-
- // Level Two relationships
- case 'brochi':
- return I18N::translateContext('brother’s child', 'nephew/niece');
- case 'brodau':
- return I18N::translateContext('brother’s daughter', 'niece');
- case 'broson':
- return I18N::translateContext('brother’s son', 'nephew');
- case 'browif':
- return I18N::translateContext('brother’s wife', 'sister-in-law');
- case 'chichi':
- return I18N::translateContext('child’s child', 'grandchild');
- case 'chidau':
- return I18N::translateContext('child’s daughter', 'granddaughter');
- case 'chihus':
- return I18N::translateContext('child’s husband', 'son-in-law');
- case 'chison':
- return I18N::translateContext('child’s son', 'grandson');
- case 'chispo':
- return I18N::translateContext('child’s spouse', 'son/daughter-in-law');
- case 'chiwif':
- return I18N::translateContext('child’s wife', 'daughter-in-law');
- case 'dauchi':
- return I18N::translateContext('daughter’s child', 'grandchild');
- case 'daudau':
- return I18N::translateContext('daughter’s daughter', 'granddaughter');
- case 'dauhus':
- return I18N::translateContext('daughter’s husband', 'son-in-law');
- case 'dauson':
- return I18N::translateContext('daughter’s son', 'grandson');
- case 'fatbro':
- return I18N::translateContext('father’s brother', 'uncle');
- case 'fatchi':
- return I18N::translateContext('father’s child', 'half-sibling');
- case 'fatdau':
- return I18N::translateContext('father’s daughter', 'half-sister');
- case 'fatfat':
- return I18N::translateContext('father’s father', 'paternal grandfather');
- case 'fatmot':
- return I18N::translateContext('father’s mother', 'paternal grandmother');
- case 'fatpar':
- return I18N::translateContext('father’s parent', 'paternal grandparent');
- case 'fatsib':
- return I18N::translateContext('father’s sibling', 'aunt/uncle');
- case 'fatsis':
- return I18N::translateContext('father’s sister', 'aunt');
- case 'fatson':
- return I18N::translateContext('father’s son', 'half-brother');
- case 'fatwif':
- return I18N::translateContext('father’s wife', 'step-mother');
- case 'husbro':
- return I18N::translateContext('husband’s brother', 'brother-in-law');
- case 'huschi':
- return I18N::translateContext('husband’s child', 'step-child');
- case 'husdau':
- return I18N::translateContext('husband’s daughter', 'step-daughter');
- case 'husfat':
- return I18N::translateContext('husband’s father', 'father-in-law');
- case 'husmot':
- return I18N::translateContext('husband’s mother', 'mother-in-law');
- case 'hussib':
- return I18N::translateContext('husband’s sibling', 'brother/sister-in-law');
- case 'hussis':
- return I18N::translateContext('husband’s sister', 'sister-in-law');
- case 'husson':
- return I18N::translateContext('husband’s son', 'step-son');
- case 'motbro':
- return I18N::translateContext('mother’s brother', 'uncle');
- case 'motchi':
- return I18N::translateContext('mother’s child', 'half-sibling');
- case 'motdau':
- return I18N::translateContext('mother’s daughter', 'half-sister');
- case 'motfat':
- return I18N::translateContext('mother’s father', 'maternal grandfather');
- case 'mothus':
- return I18N::translateContext('mother’s husband', 'step-father');
- case 'motmot':
- return I18N::translateContext('mother’s mother', 'maternal grandmother');
- case 'motpar':
- return I18N::translateContext('mother’s parent', 'maternal grandparent');
- case 'motsib':
- return I18N::translateContext('mother’s sibling', 'aunt/uncle');
- case 'motsis':
- return I18N::translateContext('mother’s sister', 'aunt');
- case 'motson':
- return I18N::translateContext('mother’s son', 'half-brother');
- case 'parbro':
- return I18N::translateContext('parent’s brother', 'uncle');
- case 'parchi':
- return I18N::translateContext('parent’s child', 'half-sibling');
- case 'pardau':
- return I18N::translateContext('parent’s daughter', 'half-sister');
- case 'parfat':
- return I18N::translateContext('parent’s father', 'grandfather');
- case 'parmot':
- return I18N::translateContext('parent’s mother', 'grandmother');
- case 'parpar':
- return I18N::translateContext('parent’s parent', 'grandparent');
- case 'parsib':
- return I18N::translateContext('parent’s sibling', 'aunt/uncle');
- case 'parsis':
- return I18N::translateContext('parent’s sister', 'aunt');
- case 'parson':
- return I18N::translateContext('parent’s son', 'half-brother');
- case 'parspo':
- return I18N::translateContext('parent’s spouse', 'step-parent');
- case 'sibchi':
- return I18N::translateContext('sibling’s child', 'nephew/niece');
- case 'sibdau':
- return I18N::translateContext('sibling’s daughter', 'niece');
- case 'sibson':
- return I18N::translateContext('sibling’s son', 'nephew');
- case 'sibspo':
- return I18N::translateContext('sibling’s spouse', 'brother/sister-in-law');
- case 'sischi':
- return I18N::translateContext('sister’s child', 'nephew/niece');
- case 'sisdau':
- return I18N::translateContext('sister’s daughter', 'niece');
- case 'sishus':
- return I18N::translateContext('sister’s husband', 'brother-in-law');
- case 'sisson':
- return I18N::translateContext('sister’s son', 'nephew');
- case 'sonchi':
- return I18N::translateContext('son’s child', 'grandchild');
- case 'sondau':
- return I18N::translateContext('son’s daughter', 'granddaughter');
- case 'sonson':
- return I18N::translateContext('son’s son', 'grandson');
- case 'sonwif':
- return I18N::translateContext('son’s wife', 'daughter-in-law');
- case 'spobro':
- return I18N::translateContext('spouse’s brother', 'brother-in-law');
- case 'spochi':
- return I18N::translateContext('spouse’s child', 'step-child');
- case 'spodau':
- return I18N::translateContext('spouse’s daughter', 'step-daughter');
- case 'spofat':
- return I18N::translateContext('spouse’s father', 'father-in-law');
- case 'spomot':
- return I18N::translateContext('spouse’s mother', 'mother-in-law');
- case 'sposis':
- return I18N::translateContext('spouse’s sister', 'sister-in-law');
- case 'sposon':
- return I18N::translateContext('spouse’s son', 'step-son');
- case 'spopar':
- return I18N::translateContext('spouse’s parent', 'mother/father-in-law');
- case 'sposib':
- return I18N::translateContext('spouse’s sibling', 'brother/sister-in-law');
- case 'wifbro':
- return I18N::translateContext('wife’s brother', 'brother-in-law');
- case 'wifchi':
- return I18N::translateContext('wife’s child', 'step-child');
- case 'wifdau':
- return I18N::translateContext('wife’s daughter', 'step-daughter');
- case 'wiffat':
- return I18N::translateContext('wife’s father', 'father-in-law');
- case 'wifmot':
- return I18N::translateContext('wife’s mother', 'mother-in-law');
- case 'wifsib':
- return I18N::translateContext('wife’s sibling', 'brother/sister-in-law');
- case 'wifsis':
- return I18N::translateContext('wife’s sister', 'sister-in-law');
- case 'wifson':
- return I18N::translateContext('wife’s son', 'step-son');
-
- // Level Three relationships
- case 'brochichi':
- if ($sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s child’s child', 'great-nephew/niece');
- }
-
- return I18N::translateContext('(a woman’s) brother’s child’s child', 'great-nephew/niece');
- case 'brochidau':
- if ($sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s child’s daughter', 'great-niece');
- }
-
- return I18N::translateContext('(a woman’s) brother’s child’s daughter', 'great-niece');
- case 'brochison':
- if ($sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s child’s son', 'great-nephew');
- }
-
- return I18N::translateContext('(a woman’s) brother’s child’s son', 'great-nephew');
- case 'brodauchi':
- if ($sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s daughter’s child', 'great-nephew/niece');
- }
-
- return I18N::translateContext('(a woman’s) brother’s daughter’s child', 'great-nephew/niece');
- case 'brodaudau':
- if ($sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s daughter’s daughter', 'great-niece');
- }
-
- return I18N::translateContext('(a woman’s) brother’s daughter’s daughter', 'great-niece');
- case 'brodauhus':
- return I18N::translateContext('brother’s daughter’s husband', 'nephew-in-law');
- case 'brodauson':
- if ($sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s daughter’s son', 'great-nephew');
- }
-
- return I18N::translateContext('(a woman’s) brother’s daughter’s son', 'great-nephew');
- case 'brosonchi':
- if ($sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s son’s child', 'great-nephew/niece');
- }
-
- return I18N::translateContext('(a woman’s) brother’s son’s child', 'great-nephew/niece');
- case 'brosondau':
- if ($sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s son’s daughter', 'great-niece');
- }
-
- return I18N::translateContext('(a woman’s) brother’s son’s daughter', 'great-niece');
- case 'brosonson':
- if ($sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s son’s son', 'great-nephew');
- }
-
- return I18N::translateContext('(a woman’s) brother’s son’s son', 'great-nephew');
- case 'brosonwif':
- return I18N::translateContext('brother’s son’s wife', 'niece-in-law');
- case 'browifbro':
- return I18N::translateContext('brother’s wife’s brother', 'brother-in-law');
- case 'browifsib':
- return I18N::translateContext('brother’s wife’s sibling', 'brother/sister-in-law');
- case 'browifsis':
- return I18N::translateContext('brother’s wife’s sister', 'sister-in-law');
- case 'chichichi':
- return I18N::translateContext('child’s child’s child', 'great-grandchild');
- case 'chichidau':
- return I18N::translateContext('child’s child’s daughter', 'great-granddaughter');
- case 'chichison':
- return I18N::translateContext('child’s child’s son', 'great-grandson');
- case 'chidauchi':
- return I18N::translateContext('child’s daughter’s child', 'great-grandchild');
- case 'chidaudau':
- return I18N::translateContext('child’s daughter’s daughter', 'great-granddaughter');
- case 'chidauhus':
- return I18N::translateContext('child’s daughter’s husband', 'granddaughter’s husband');
- case 'chidauson':
- return I18N::translateContext('child’s daughter’s son', 'great-grandson');
- case 'chisonchi':
- return I18N::translateContext('child’s son’s child', 'great-grandchild');
- case 'chisondau':
- return I18N::translateContext('child’s son’s daughter', 'great-granddaughter');
- case 'chisonson':
- return I18N::translateContext('child’s son’s son', 'great-grandson');
- case 'chisonwif':
- return I18N::translateContext('child’s son’s wife', 'grandson’s wife');
- case 'dauchichi':
- return I18N::translateContext('daughter’s child’s child', 'great-grandchild');
- case 'dauchidau':
- return I18N::translateContext('daughter’s child’s daughter', 'great-granddaughter');
- case 'dauchison':
- return I18N::translateContext('daughter’s child’s son', 'great-grandson');
- case 'daudauchi':
- return I18N::translateContext('daughter’s daughter’s child', 'great-grandchild');
- case 'daudaudau':
- return I18N::translateContext('daughter’s daughter’s daughter', 'great-granddaughter');
- case 'daudauhus':
- return I18N::translateContext('daughter’s daughter’s husband', 'granddaughter’s husband');
- case 'daudauson':
- return I18N::translateContext('daughter’s daughter’s son', 'great-grandson');
- case 'dauhusfat':
- return I18N::translateContext('daughter’s husband’s father', 'son-in-law’s father');
- case 'dauhusmot':
- return I18N::translateContext('daughter’s husband’s mother', 'son-in-law’s mother');
- case 'dauhuspar':
- return I18N::translateContext('daughter’s husband’s parent', 'son-in-law’s parent');
- case 'dausonchi':
- return I18N::translateContext('daughter’s son’s child', 'great-grandchild');
- case 'dausondau':
- return I18N::translateContext('daughter’s son’s daughter', 'great-granddaughter');
- case 'dausonson':
- return I18N::translateContext('daughter’s son’s son', 'great-grandson');
- case 'dausonwif':
- return I18N::translateContext('daughter’s son’s wife', 'grandson’s wife');
- case 'fatbrochi':
- return I18N::translateContext('father’s brother’s child', 'first cousin');
- case 'fatbrodau':
- return I18N::translateContext('father’s brother’s daughter', 'first cousin');
- case 'fatbroson':
- return I18N::translateContext('father’s brother’s son', 'first cousin');
- case 'fatbrowif':
- return I18N::translateContext('father’s brother’s wife', 'aunt');
- case 'fatfatbro':
- return I18N::translateContext('father’s father’s brother', 'great-uncle');
- case 'fatfatfat':
- return I18N::translateContext('father’s father’s father', 'great-grandfather');
- case 'fatfatmot':
- return I18N::translateContext('father’s father’s mother', 'great-grandmother');
- case 'fatfatpar':
- return I18N::translateContext('father’s father’s parent', 'great-grandparent');
- case 'fatfatsib':
- return I18N::translateContext('father’s father’s sibling', 'great-aunt/uncle');
- case 'fatfatsis':
- return I18N::translateContext('father’s father’s sister', 'great-aunt');
- case 'fatmotbro':
- return I18N::translateContext('father’s mother’s brother', 'great-uncle');
- case 'fatmotfat':
- return I18N::translateContext('father’s mother’s father', 'great-grandfather');
- case 'fatmotmot':
- return I18N::translateContext('father’s mother’s mother', 'great-grandmother');
- case 'fatmotpar':
- return I18N::translateContext('father’s mother’s parent', 'great-grandparent');
- case 'fatmotsib':
- return I18N::translateContext('father’s mother’s sibling', 'great-aunt/uncle');
- case 'fatmotsis':
- return I18N::translateContext('father’s mother’s sister', 'great-aunt');
- case 'fatparbro':
- return I18N::translateContext('father’s parent’s brother', 'great-uncle');
- case 'fatparfat':
- return I18N::translateContext('father’s parent’s father', 'great-grandfather');
- case 'fatparmot':
- return I18N::translateContext('father’s parent’s mother', 'great-grandmother');
- case 'fatparpar':
- return I18N::translateContext('father’s parent’s parent', 'great-grandparent');
- case 'fatparsib':
- return I18N::translateContext('father’s parent’s sibling', 'great-aunt/uncle');
- case 'fatparsis':
- return I18N::translateContext('father’s parent’s sister', 'great-aunt');
- case 'fatsischi':
- return I18N::translateContext('father’s sister’s child', 'first cousin');
- case 'fatsisdau':
- return I18N::translateContext('father’s sister’s daughter', 'first cousin');
- case 'fatsishus':
- return I18N::translateContext('father’s sister’s husband', 'uncle');
- case 'fatsisson':
- return I18N::translateContext('father’s sister’s son', 'first cousin');
- case 'fatwifchi':
- return I18N::translateContext('father’s wife’s child', 'step-sibling');
- case 'fatwifdau':
- return I18N::translateContext('father’s wife’s daughter', 'step-sister');
- case 'fatwifson':
- return I18N::translateContext('father’s wife’s son', 'step-brother');
- case 'husbrowif':
- return I18N::translateContext('husband’s brother’s wife', 'sister-in-law');
- case 'hussishus':
- return I18N::translateContext('husband’s sister’s husband', 'brother-in-law');
- case 'hussibchi':
- return I18N::translateContext('husband’s sibling’s child', 'nephew/niece');
- case 'hussischi':
- return I18N::translateContext('husband’s sister’s child', 'nephew/niece');
- case 'husbrochi':
- return I18N::translateContext('husband’s brother’s child', 'nephew/niece');
- case 'hussibdau':
- return I18N::translateContext('husband’s sibling’s daughter', 'niece');
- case 'hussisdau':
- return I18N::translateContext('husband’s sister’s daughter', 'niece');
- case 'husbrodau':
- return I18N::translateContext('husband’s brother’s daughter', 'niece');
- case 'hussibson':
- return I18N::translateContext('husband’s sibling’s son', 'nephew');
- case 'hussisson':
- return I18N::translateContext('husband’s sister’s son', 'nephew');
- case 'husbroson':
- return I18N::translateContext('husband’s brother’s son', 'nephew');
- case 'motbrochi':
- return I18N::translateContext('mother’s brother’s child', 'first cousin');
- case 'motbrodau':
- return I18N::translateContext('mother’s brother’s daughter', 'first cousin');
- case 'motbroson':
- return I18N::translateContext('mother’s brother’s son', 'first cousin');
- case 'motbrowif':
- return I18N::translateContext('mother’s brother’s wife', 'aunt');
- case 'motfatbro':
- return I18N::translateContext('mother’s father’s brother', 'great-uncle');
- case 'motfatfat':
- return I18N::translateContext('mother’s father’s father', 'great-grandfather');
- case 'motfatmot':
- return I18N::translateContext('mother’s father’s mother', 'great-grandmother');
- case 'motfatpar':
- return I18N::translateContext('mother’s father’s parent', 'great-grandparent');
- case 'motfatsib':
- return I18N::translateContext('mother’s father’s sibling', 'great-aunt/uncle');
- case 'motfatsis':
- return I18N::translateContext('mother’s father’s sister', 'great-aunt');
- case 'mothuschi':
- return I18N::translateContext('mother’s husband’s child', 'step-sibling');
- case 'mothusdau':
- return I18N::translateContext('mother’s husband’s daughter', 'step-sister');
- case 'mothusson':
- return I18N::translateContext('mother’s husband’s son', 'step-brother');
- case 'motmotbro':
- return I18N::translateContext('mother’s mother’s brother', 'great-uncle');
- case 'motmotfat':
- return I18N::translateContext('mother’s mother’s father', 'great-grandfather');
- case 'motmotmot':
- return I18N::translateContext('mother’s mother’s mother', 'great-grandmother');
- case 'motmotpar':
- return I18N::translateContext('mother’s mother’s parent', 'great-grandparent');
- case 'motmotsib':
- return I18N::translateContext('mother’s mother’s sibling', 'great-aunt/uncle');
- case 'motmotsis':
- return I18N::translateContext('mother’s mother’s sister', 'great-aunt');
- case 'motparbro':
- return I18N::translateContext('mother’s parent’s brother', 'great-uncle');
- case 'motparfat':
- return I18N::translateContext('mother’s parent’s father', 'great-grandfather');
- case 'motparmot':
- return I18N::translateContext('mother’s parent’s mother', 'great-grandmother');
- case 'motparpar':
- return I18N::translateContext('mother’s parent’s parent', 'great-grandparent');
- case 'motparsib':
- return I18N::translateContext('mother’s parent’s sibling', 'great-aunt/uncle');
- case 'motparsis':
- return I18N::translateContext('mother’s parent’s sister', 'great-aunt');
- case 'motsischi':
- return I18N::translateContext('mother’s sister’s child', 'first cousin');
- case 'motsisdau':
- return I18N::translateContext('mother’s sister’s daughter', 'first cousin');
- case 'motsishus':
- return I18N::translateContext('mother’s sister’s husband', 'uncle');
- case 'motsisson':
- return I18N::translateContext('mother’s sister’s son', 'first cousin');
- case 'parbrowif':
- return I18N::translateContext('parent’s brother’s wife', 'aunt');
- case 'parfatbro':
- return I18N::translateContext('parent’s father’s brother', 'great-uncle');
- case 'parfatfat':
- return I18N::translateContext('parent’s father’s father', 'great-grandfather');
- case 'parfatmot':
- return I18N::translateContext('parent’s father’s mother', 'great-grandmother');
- case 'parfatpar':
- return I18N::translateContext('parent’s father’s parent', 'great-grandparent');
- case 'parfatsib':
- return I18N::translateContext('parent’s father’s sibling', 'great-aunt/uncle');
- case 'parfatsis':
- return I18N::translateContext('parent’s father’s sister', 'great-aunt');
- case 'parmotbro':
- return I18N::translateContext('parent’s mother’s brother', 'great-uncle');
- case 'parmotfat':
- return I18N::translateContext('parent’s mother’s father', 'great-grandfather');
- case 'parmotmot':
- return I18N::translateContext('parent’s mother’s mother', 'great-grandmother');
- case 'parmotpar':
- return I18N::translateContext('parent’s mother’s parent', 'great-grandparent');
- case 'parmotsib':
- return I18N::translateContext('parent’s mother’s sibling', 'great-aunt/uncle');
- case 'parmotsis':
- return I18N::translateContext('parent’s mother’s sister', 'great-aunt');
- case 'parparbro':
- return I18N::translateContext('parent’s parent’s brother', 'great-uncle');
- case 'parparfat':
- return I18N::translateContext('parent’s parent’s father', 'great-grandfather');
- case 'parparmot':
- return I18N::translateContext('parent’s parent’s mother', 'great-grandmother');
- case 'parparpar':
- return I18N::translateContext('parent’s parent’s parent', 'great-grandparent');
- case 'parparsib':
- return I18N::translateContext('parent’s parent’s sibling', 'great-aunt/uncle');
- case 'parparsis':
- return I18N::translateContext('parent’s parent’s sister', 'great-aunt');
- case 'parsishus':
- return I18N::translateContext('parent’s sister’s husband', 'uncle');
- case 'parspochi':
- return I18N::translateContext('parent’s spouse’s child', 'step-sibling');
- case 'parspodau':
- return I18N::translateContext('parent’s spouse’s daughter', 'step-sister');
- case 'parsposon':
- return I18N::translateContext('parent’s spouse’s son', 'step-brother');
- case 'sibchichi':
- return I18N::translateContext('sibling’s child’s child', 'great-nephew/niece');
- case 'sibchidau':
- return I18N::translateContext('sibling’s child’s daughter', 'great-niece');
- case 'sibchison':
- return I18N::translateContext('sibling’s child’s son', 'great-nephew');
- case 'sibdauchi':
- return I18N::translateContext('sibling’s daughter’s child', 'great-nephew/niece');
- case 'sibdaudau':
- return I18N::translateContext('sibling’s daughter’s daughter', 'great-niece');
- case 'sibdauhus':
- return I18N::translateContext('sibling’s daughter’s husband', 'nephew-in-law');
- case 'sibdauson':
- return I18N::translateContext('sibling’s daughter’s son', 'great-nephew');
- case 'sibsonchi':
- return I18N::translateContext('sibling’s son’s child', 'great-nephew/niece');
- case 'sibsondau':
- return I18N::translateContext('sibling’s son’s daughter', 'great-niece');
- case 'sibsonson':
- return I18N::translateContext('sibling’s son’s son', 'great-nephew');
- case 'sibsonwif':
- return I18N::translateContext('sibling’s son’s wife', 'niece-in-law');
- case 'sischichi':
- if ($sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s child’s child', 'great-nephew/niece');
- }
-
- return I18N::translateContext('(a woman’s) sister’s child’s child', 'great-nephew/niece');
- case 'sischidau':
- if ($sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s child’s daughter', 'great-niece');
- }
-
- return I18N::translateContext('(a woman’s) sister’s child’s daughter', 'great-niece');
- case 'sischison':
- if ($sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s child’s son', 'great-nephew');
- }
-
- return I18N::translateContext('(a woman’s) sister’s child’s son', 'great-nephew');
- case 'sisdauchi':
- if ($sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s daughter’s child', 'great-nephew/niece');
- }
-
- return I18N::translateContext('(a woman’s) sister’s daughter’s child', 'great-nephew/niece');
- case 'sisdaudau':
- if ($sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s daughter’s daughter', 'great-niece');
- }
-
- return I18N::translateContext('(a woman’s) sister’s daughter’s daughter', 'great-niece');
- case 'sisdauhus':
- return I18N::translateContext('sisters’s daughter’s husband', 'nephew-in-law');
- case 'sisdauson':
- if ($sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s daughter’s son', 'great-nephew');
- }
-
- return I18N::translateContext('(a woman’s) sister’s daughter’s son', 'great-nephew');
- case 'sishusbro':
- return I18N::translateContext('sister’s husband’s brother', 'brother-in-law');
- case 'sishussib':
- return I18N::translateContext('sister’s husband’s sibling', 'brother/sister-in-law');
- case 'sishussis':
- return I18N::translateContext('sister’s husband’s sister', 'sister-in-law');
- case 'sissonchi':
- if ($sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s son’s child', 'great-nephew/niece');
- }
-
- return I18N::translateContext('(a woman’s) sister’s son’s child', 'great-nephew/niece');
- case 'sissondau':
- if ($sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s son’s daughter', 'great-niece');
- }
-
- return I18N::translateContext('(a woman’s) sister’s son’s daughter', 'great-niece');
- case 'sissonson':
- if ($sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s son’s son', 'great-nephew');
- }
-
- return I18N::translateContext('(a woman’s) sister’s son’s son', 'great-nephew');
- case 'sissonwif':
- return I18N::translateContext('sisters’s son’s wife', 'niece-in-law');
- case 'sonchichi':
- return I18N::translateContext('son’s child’s child', 'great-grandchild');
- case 'sonchidau':
- return I18N::translateContext('son’s child’s daughter', 'great-granddaughter');
- case 'sonchison':
- return I18N::translateContext('son’s child’s son', 'great-grandson');
- case 'sondauchi':
- return I18N::translateContext('son’s daughter’s child', 'great-grandchild');
- case 'sondaudau':
- return I18N::translateContext('son’s daughter’s daughter', 'great-granddaughter');
- case 'sondauhus':
- return I18N::translateContext('son’s daughter’s husband', 'granddaughter’s husband');
- case 'sondauson':
- return I18N::translateContext('son’s daughter’s son', 'great-grandson');
- case 'sonsonchi':
- return I18N::translateContext('son’s son’s child', 'great-grandchild');
- case 'sonsondau':
- return I18N::translateContext('son’s son’s daughter', 'great-granddaughter');
- case 'sonsonson':
- return I18N::translateContext('son’s son’s son', 'great-grandson');
- case 'sonsonwif':
- return I18N::translateContext('son’s son’s wife', 'grandson’s wife');
- case 'sonwiffat':
- return I18N::translateContext('son’s wife’s father', 'daughter-in-law’s father');
- case 'sonwifmot':
- return I18N::translateContext('son’s wife’s mother', 'daughter-in-law’s mother');
- case 'sonwifpar':
- return I18N::translateContext('son’s wife’s parent', 'daughter-in-law’s parent');
- case 'wifbrowif':
- return I18N::translateContext('wife’s brother’s wife', 'sister-in-law');
- case 'wifsishus':
- return I18N::translateContext('wife’s sister’s husband', 'brother-in-law');
- case 'wifsibchi':
- return I18N::translateContext('wife’s sibling’s child', 'nephew/niece');
- case 'wifsischi':
- return I18N::translateContext('wife’s sister’s child', 'nephew/niece');
- case 'wifbrochi':
- return I18N::translateContext('wife’s brother’s child', 'nephew/niece');
- case 'wifsibdau':
- return I18N::translateContext('wife’s sibling’s daughter', 'niece');
- case 'wifsisdau':
- return I18N::translateContext('wife’s sister’s daughter', 'niece');
- case 'wifbrodau':
- return I18N::translateContext('wife’s brother’s daughter', 'niece');
- case 'wifsibson':
- return I18N::translateContext('wife’s sibling’s son', 'nephew');
- case 'wifsisson':
- return I18N::translateContext('wife’s sister’s son', 'nephew');
- case 'wifbroson':
- return I18N::translateContext('wife’s brother’s son', 'nephew');
-
- // Some “special case” level four relationships that have specific names in certain languages
- case 'fatfatbrowif':
- return I18N::translateContext('father’s father’s brother’s wife', 'great-aunt');
- case 'fatfatsibspo':
- return I18N::translateContext('father’s father’s sibling’s spouse', 'great-aunt/uncle');
- case 'fatfatsishus':
- return I18N::translateContext('father’s father’s sister’s husband', 'great-uncle');
- case 'fatmotbrowif':
- return I18N::translateContext('father’s mother’s brother’s wife', 'great-aunt');
- case 'fatmotsibspo':
- return I18N::translateContext('father’s mother’s sibling’s spouse', 'great-aunt/uncle');
- case 'fatmotsishus':
- return I18N::translateContext('father’s mother’s sister’s husband', 'great-uncle');
- case 'fatparbrowif':
- return I18N::translateContext('father’s parent’s brother’s wife', 'great-aunt');
- case 'fatparsibspo':
- return I18N::translateContext('father’s parent’s sibling’s spouse', 'great-aunt/uncle');
- case 'fatparsishus':
- return I18N::translateContext('father’s parent’s sister’s husband', 'great-uncle');
- case 'motfatbrowif':
- return I18N::translateContext('mother’s father’s brother’s wife', 'great-aunt');
- case 'motfatsibspo':
- return I18N::translateContext('mother’s father’s sibling’s spouse', 'great-aunt/uncle');
- case 'motfatsishus':
- return I18N::translateContext('mother’s father’s sister’s husband', 'great-uncle');
- case 'motmotbrowif':
- return I18N::translateContext('mother’s mother’s brother’s wife', 'great-aunt');
- case 'motmotsibspo':
- return I18N::translateContext('mother’s mother’s sibling’s spouse', 'great-aunt/uncle');
- case 'motmotsishus':
- return I18N::translateContext('mother’s mother’s sister’s husband', 'great-uncle');
- case 'motparbrowif':
- return I18N::translateContext('mother’s parent’s brother’s wife', 'great-aunt');
- case 'motparsibspo':
- return I18N::translateContext('mother’s parent’s sibling’s spouse', 'great-aunt/uncle');
- case 'motparsishus':
- return I18N::translateContext('mother’s parent’s sister’s husband', 'great-uncle');
- case 'parfatbrowif':
- return I18N::translateContext('parent’s father’s brother’s wife', 'great-aunt');
- case 'parfatsibspo':
- return I18N::translateContext('parent’s father’s sibling’s spouse', 'great-aunt/uncle');
- case 'parfatsishus':
- return I18N::translateContext('parent’s father’s sister’s husband', 'great-uncle');
- case 'parmotbrowif':
- return I18N::translateContext('parent’s mother’s brother’s wife', 'great-aunt');
- case 'parmotsibspo':
- return I18N::translateContext('parent’s mother’s sibling’s spouse', 'great-aunt/uncle');
- case 'parmotsishus':
- return I18N::translateContext('parent’s mother’s sister’s husband', 'great-uncle');
- case 'parparbrowif':
- return I18N::translateContext('parent’s parent’s brother’s wife', 'great-aunt');
- case 'parparsibspo':
- return I18N::translateContext('parent’s parent’s sibling’s spouse', 'great-aunt/uncle');
- case 'parparsishus':
- return I18N::translateContext('parent’s parent’s sister’s husband', 'great-uncle');
- case 'fatfatbrodau':
- return I18N::translateContext('father’s father’s brother’s daughter', 'first cousin once removed ascending');
- case 'fatfatbroson':
- return I18N::translateContext('father’s father’s brother’s son', 'first cousin once removed ascending');
- case 'fatfatbrochi':
- return I18N::translateContext('father’s father’s brother’s child', 'first cousin once removed ascending');
- case 'fatfatsisdau':
- return I18N::translateContext('father’s father’s sister’s daughter', 'first cousin once removed ascending');
- case 'fatfatsisson':
- return I18N::translateContext('father’s father’s sister’s son', 'first cousin once removed ascending');
- case 'fatfatsischi':
- return I18N::translateContext('father’s father’s sister’s child', 'first cousin once removed ascending');
- case 'fatmotbrodau':
- return I18N::translateContext('father’s mother’s brother’s daughter', 'first cousin once removed ascending');
- case 'fatmotbroson':
- return I18N::translateContext('father’s mother’s brother’s son', 'first cousin once removed ascending');
- case 'fatmotbrochi':
- return I18N::translateContext('father’s mother’s brother’s child', 'first cousin once removed ascending');
- case 'fatmotsisdau':
- return I18N::translateContext('father’s mother’s sister’s daughter', 'first cousin once removed ascending');
- case 'fatmotsisson':
- return I18N::translateContext('father’s mother’s sister’s son', 'first cousin once removed ascending');
- case 'fatmotsischi':
- return I18N::translateContext('father’s mother’s sister’s child', 'first cousin once removed ascending');
- case 'motfatbrodau':
- return I18N::translateContext('mother’s father’s brother’s daughter', 'first cousin once removed ascending');
- case 'motfatbroson':
- return I18N::translateContext('mother’s father’s brother’s son', 'first cousin once removed ascending');
- case 'motfatbrochi':
- return I18N::translateContext('mother’s father’s brother’s child', 'first cousin once removed ascending');
- case 'motfatsisdau':
- return I18N::translateContext('mother’s father’s sister’s daughter', 'first cousin once removed ascending');
- case 'motfatsisson':
- return I18N::translateContext('mother’s father’s sister’s son', 'first cousin once removed ascending');
- case 'motfatsischi':
- return I18N::translateContext('mother’s father’s sister’s child', 'first cousin once removed ascending');
- case 'motmotbrodau':
- return I18N::translateContext('mother’s mother’s brother’s daughter', 'first cousin once removed ascending');
- case 'motmotbroson':
- return I18N::translateContext('mother’s mother’s brother’s son', 'first cousin once removed ascending');
- case 'motmotbrochi':
- return I18N::translateContext('mother’s mother’s brother’s child', 'first cousin once removed ascending');
- case 'motmotsisdau':
- return I18N::translateContext('mother’s mother’s sister’s daughter', 'first cousin once removed ascending');
- case 'motmotsisson':
- return I18N::translateContext('mother’s mother’s sister’s son', 'first cousin once removed ascending');
- case 'motmotsischi':
- return I18N::translateContext('mother’s mother’s sister’s child', 'first cousin once removed ascending');
- }
-
- // Some “special case” level five relationships that have specific names in certain languages
- if (preg_match('/^(mot|fat|par)fatbro(son|dau|chi)dau$/', $path)) {
- return I18N::translateContext('grandfather’s brother’s granddaughter', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)fatbro(son|dau|chi)son$/', $path)) {
- return I18N::translateContext('grandfather’s brother’s grandson', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)fatbro(son|dau|chi)chi$/', $path)) {
- return I18N::translateContext('grandfather’s brother’s grandchild', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)fatsis(son|dau|chi)dau$/', $path)) {
- return I18N::translateContext('grandfather’s sister’s granddaughter', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)fatsis(son|dau|chi)son$/', $path)) {
- return I18N::translateContext('grandfather’s sister’s grandson', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)fatsis(son|dau|chi)chi$/', $path)) {
- return I18N::translateContext('grandfather’s sister’s grandchild', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)fatsib(son|dau|chi)dau$/', $path)) {
- return I18N::translateContext('grandfather’s sibling’s granddaughter', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)fatsib(son|dau|chi)son$/', $path)) {
- return I18N::translateContext('grandfather’s sibling’s grandson', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)fatsib(son|dau|chi)chi$/', $path)) {
- return I18N::translateContext('grandfather’s sibling’s grandchild', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)motbro(son|dau|chi)dau$/', $path)) {
- return I18N::translateContext('grandmother’s brother’s granddaughter', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)motbro(son|dau|chi)son$/', $path)) {
- return I18N::translateContext('grandmother’s brother’s grandson', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)motbro(son|dau|chi)chi$/', $path)) {
- return I18N::translateContext('grandmother’s brother’s grandchild', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)motsis(son|dau|chi)dau$/', $path)) {
- return I18N::translateContext('grandmother’s sister’s granddaughter', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)motsis(son|dau|chi)son$/', $path)) {
- return I18N::translateContext('grandmother’s sister’s grandson', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)motsis(son|dau|chi)chi$/', $path)) {
- return I18N::translateContext('grandmother’s sister’s grandchild', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)motsib(son|dau|chi)dau$/', $path)) {
- return I18N::translateContext('grandmother’s sibling’s granddaughter', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)motsib(son|dau|chi)son$/', $path)) {
- return I18N::translateContext('grandmother’s sibling’s grandson', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)motsib(son|dau|chi)chi$/', $path)) {
- return I18N::translateContext('grandmother’s sibling’s grandchild', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)parbro(son|dau|chi)dau$/', $path)) {
- return I18N::translateContext('grandparent’s brother’s granddaughter', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)parbro(son|dau|chi)son$/', $path)) {
- return I18N::translateContext('grandparent’s brother’s grandson', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)parbro(son|dau|chi)chi$/', $path)) {
- return I18N::translateContext('grandparent’s brother’s grandchild', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)parsis(son|dau|chi)dau$/', $path)) {
- return I18N::translateContext('grandparent’s sister’s granddaughter', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)parsis(son|dau|chi)son$/', $path)) {
- return I18N::translateContext('grandparent’s sister’s grandson', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)parsis(son|dau|chi)chi$/', $path)) {
- return I18N::translateContext('grandparent’s sister’s grandchild', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)parsib(son|dau|chi)dau$/', $path)) {
- return I18N::translateContext('grandparent’s sibling’s granddaughter', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)parsib(son|dau|chi)son$/', $path)) {
- return I18N::translateContext('grandparent’s sibling’s grandson', 'second cousin');
- }
-
- if (preg_match('/^(mot|fat|par)parsib(son|dau|chi)chi$/', $path)) {
- return I18N::translateContext('grandparent’s sibling’s grandchild', 'second cousin');
- }
-
- // Look for generic/pattern relationships.
- if (preg_match('/^((?:mot|fat|par)+)(bro|sis|sib)$/', $path, $match)) {
- // siblings of direct ancestors
- $up = intdiv(strlen($match[1]), 3);
- $bef_last = substr($path, -6, 3);
- switch ($up) {
- case 3:
- if ($sex2 === 'M') {
- if ($bef_last === 'fat') {
- return I18N::translateContext('great-grandfather’s brother', 'great-great-uncle');
- }
-
- if ($bef_last === 'mot') {
- return I18N::translateContext('great-grandmother’s brother', 'great-great-uncle');
- }
-
- return I18N::translateContext('great-grandparent’s brother', 'great-great-uncle');
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great-great-aunt');
- }
-
- return I18N::translate('great-great-aunt/uncle');
-
- case 4:
- if ($sex2 === 'M') {
- if ($bef_last === 'fat') {
- return I18N::translateContext('great-great-grandfather’s brother', 'great-great-great-uncle');
- }
-
- if ($bef_last === 'mot') {
- return I18N::translateContext('great-great-grandmother’s brother', 'great-great-great-uncle');
- }
-
- return I18N::translateContext('great-great-grandparent’s brother', 'great-great-great-uncle');
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great-great-great-aunt');
- }
-
- return I18N::translate('great-great-great-aunt/uncle');
-
- case 5:
- if ($sex2 === 'M') {
- if ($bef_last === 'fat') {
- return I18N::translateContext('great-great-great-grandfather’s brother', 'great ×4 uncle');
- }
-
- if ($bef_last === 'mot') {
- return I18N::translateContext('great-great-great-grandmother’s brother', 'great ×4 uncle');
- }
-
- return I18N::translateContext('great-great-great-grandparent’s brother', 'great ×4 uncle');
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×4 aunt');
- }
-
- return I18N::translate('great ×4 aunt/uncle');
-
- case 6:
- if ($sex2 === 'M') {
- if ($bef_last === 'fat') {
- return I18N::translateContext('great ×4 grandfather’s brother', 'great ×5 uncle');
- }
-
- if ($bef_last === 'mot') {
- return I18N::translateContext('great ×4 grandmother’s brother', 'great ×5 uncle');
- }
-
- return I18N::translateContext('great ×4 grandparent’s brother', 'great ×5 uncle');
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×5 aunt');
- }
-
- return I18N::translate('great ×5 aunt/uncle');
-
- case 7:
- if ($sex2 === 'M') {
- if ($bef_last === 'fat') {
- return I18N::translateContext('great ×5 grandfather’s brother', 'great ×6 uncle');
- }
-
- if ($bef_last === 'mot') {
- return I18N::translateContext('great ×5 grandmother’s brother', 'great ×6 uncle');
- }
-
- return I18N::translateContext('great ×5 grandparent’s brother', 'great ×6 uncle');
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×6 aunt');
- }
-
- return I18N::translate('great ×6 aunt/uncle');
-
- case 8:
- if ($sex2 === 'M') {
- if ($bef_last === 'fat') {
- return I18N::translateContext('great ×6 grandfather’s brother', 'great ×7 uncle');
- }
-
- if ($bef_last === 'mot') {
- return I18N::translateContext('great ×6 grandmother’s brother', 'great ×7 uncle');
- }
-
- return I18N::translateContext('great ×6 grandparent’s brother', 'great ×7 uncle');
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×7 aunt');
- }
-
- return I18N::translate('great ×7 aunt/uncle');
-
- default:
- // Different languages have different rules for naming generations.
- // An English great ×12 uncle is a Danish great ×10 uncle.
- //
- // Need to find out which languages use which rules.
- switch (I18N::languageTag()) {
- case 'da':
- if ($sex2 === 'M') {
- return I18N::translate('great ×%s uncle', I18N::number($up - 4));
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×%s aunt', I18N::number($up - 4));
- }
-
- return I18N::translate('great ×%s aunt/uncle', I18N::number($up - 4));
-
- case 'pl':
- if ($sex2 === 'M') {
- if ($bef_last === 'fat') {
- return I18N::translateContext('great ×(%s-1) grandfather’s brother', 'great ×%s uncle', I18N::number($up - 2));
- }
-
- if ($bef_last === 'mot') {
- return I18N::translateContext('great ×(%s-1) grandmother’s brother', 'great ×%s uncle', I18N::number($up - 2));
- }
-
- return I18N::translateContext('great ×(%s-1) grandparent’s brother', 'great ×%s uncle', I18N::number($up - 2));
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×%s aunt', I18N::number($up - 2));
- }
-
- return I18N::translate('great ×%s aunt/uncle', I18N::number($up - 2));
-
- case 'hi': // Source: MrQD
- if ($sex2 === 'M') {
- // I18N: if you need a different number for %s, contact the developers, as a code-change is required
- return I18N::translate('great ×%s uncle', I18N::number($up - 2));
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×%s aunt', I18N::number($up - 2));
- }
-
- return I18N::translate('great ×%s aunt/uncle', I18N::number($up - 2));
-
- case 'zh-Hans': // Source: xmlf
- case 'zh-Hant':
- if ($sex2 === 'M') {
- return I18N::translate('great ×%s uncle', I18N::number($up));
- }
- if ($sex2 === 'F') {
- return I18N::translate('great ×%s aunt', I18N::number($up));
- }
-
- return I18N::translate('great ×%s aunt/uncle', I18N::number($up));
-
- case 'it': // Source: Michele Locati
- case 'en_AU':
- case 'en_GB':
- case 'en_US':
- default:
- if ($sex2 === 'M') {
- // I18N: if you need a different number for %s, contact the developers, as a code-change is required
- return I18N::translate('great ×%s uncle', I18N::number($up - 1));
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×%s aunt', I18N::number($up - 1));
- }
-
- return I18N::translate('great ×%s aunt/uncle', I18N::number($up - 1));
- }
- }
- }
- if (preg_match('/^(?:bro|sis|sib)((?:son|dau|chi)+)$/', $path, $match)) {
- // direct descendants of siblings
- $down = intdiv(strlen($match[1]), 3) + 1; // Add one, as we count generations from the common ancestor
- $first = substr($path, 0, 3);
- switch ($down) {
- case 4:
- if ($sex2 === 'M') {
- if ($first === 'bro' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s great-grandson', 'great-great-nephew');
- }
-
- if ($first === 'sis' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s great-grandson', 'great-great-nephew');
- }
-
- return I18N::translateContext('(a woman’s) great-great-nephew', 'great-great-nephew');
- }
-
- if ($sex2 === 'F') {
- if ($first === 'bro' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s great-granddaughter', 'great-great-niece');
- }
-
- if ($first === 'sis' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s great-granddaughter', 'great-great-niece');
- }
-
- return I18N::translateContext('(a woman’s) great-great-niece', 'great-great-niece');
- }
-
- if ($first === 'bro' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s great-grandchild', 'great-great-nephew/niece');
- }
-
- if ($first === 'sis' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s great-grandchild', 'great-great-nephew/niece');
- }
-
- return I18N::translateContext('(a woman’s) great-great-nephew/niece', 'great-great-nephew/niece');
-
- case 5:
- if ($sex2 === 'M') {
- if ($first === 'bro' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s great-great-grandson', 'great-great-great-nephew');
- }
-
- if ($first === 'sis' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s great-great-grandson', 'great-great-great-nephew');
- }
-
- return I18N::translateContext('(a woman’s) great-great-great-nephew', 'great-great-great-nephew');
- }
-
- if ($sex2 === 'F') {
- if ($first === 'bro' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s great-great-granddaughter', 'great-great-great-niece');
- }
-
- if ($first === 'sis' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s great-great-granddaughter', 'great-great-great-niece');
- }
-
- return I18N::translateContext('(a woman’s) great-great-great-niece', 'great-great-great-niece');
- }
-
- if ($first === 'bro' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s great-great-grandchild', 'great-great-great-nephew/niece');
- }
-
- if ($first === 'sis' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s great-great-grandchild', 'great-great-great-nephew/niece');
- }
-
- return I18N::translateContext('(a woman’s) great-great-great-nephew/niece', 'great-great-great-nephew/niece');
-
- case 6:
- if ($sex2 === 'M') {
- if ($first === 'bro' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s great-great-great-grandson', 'great ×4 nephew');
- }
-
- if ($first === 'sis' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s great-great-great-grandson', 'great ×4 nephew');
- }
-
- return I18N::translateContext('(a woman’s) great ×4 nephew', 'great ×4 nephew');
- }
-
- if ($sex2 === 'F') {
- if ($first === 'bro' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s great-great-great-granddaughter', 'great ×4 niece');
- }
-
- if ($first === 'sis' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s great-great-great-granddaughter', 'great ×4 niece');
- }
-
- return I18N::translateContext('(a woman’s) great ×4 niece', 'great ×4 niece');
- }
-
- if ($first === 'bro' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s great-great-great-grandchild', 'great ×4 nephew/niece');
- }
-
- if ($first === 'sis' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s great-great-great-grandchild', 'great ×4 nephew/niece');
- }
-
- return I18N::translateContext('(a woman’s) great ×4 nephew/niece', 'great ×4 nephew/niece');
-
- case 7:
- if ($sex2 === 'M') {
- if ($first === 'bro' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s great ×4 grandson', 'great ×5 nephew');
- }
-
- if ($first === 'sis' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s great ×4 grandson', 'great ×5 nephew');
- }
-
- return I18N::translateContext('(a woman’s) great ×5 nephew', 'great ×5 nephew');
- }
-
- if ($sex2 === 'F') {
- if ($first === 'bro' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s great ×4 granddaughter', 'great ×5 niece');
- }
-
- if ($first === 'sis' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s great ×4 granddaughter', 'great ×5 niece');
- }
-
- return I18N::translateContext('(a woman’s) great ×5 niece', 'great ×5 niece');
- }
-
- if ($first === 'bro' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s great ×4 grandchild', 'great ×5 nephew/niece');
- }
-
- if ($first === 'sis' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s great ×4 grandchild', 'great ×5 nephew/niece');
- }
-
- return I18N::translateContext('(a woman’s) great ×5 nephew/niece', 'great ×5 nephew/niece');
-
- default:
- // Different languages have different rules for naming generations.
- // An English great ×12 nephew is a Polish great ×11 nephew.
- //
- // Need to find out which languages use which rules.
- switch (I18N::languageTag()) {
- case 'pl': // Source: Lukasz Wilenski
- if ($sex2 === 'M') {
- if ($first === 'bro' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s great ×(%s-1) grandson', 'great ×%s nephew', I18N::number($down - 3));
- }
-
- if ($first === 'sis' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s great ×(%s-1) grandson', 'great ×%s nephew', I18N::number($down - 3));
- }
-
- return I18N::translateContext('(a woman’s) great ×%s nephew', 'great ×%s nephew', I18N::number($down - 3));
- }
-
- if ($sex2 === 'F') {
- if ($first === 'bro' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s great ×(%s-1) granddaughter', 'great ×%s niece', I18N::number($down - 3));
- }
-
- if ($first === 'sis' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s great ×(%s-1) granddaughter', 'great ×%s niece', I18N::number($down - 3));
- }
-
- return I18N::translateContext('(a woman’s) great ×%s niece', 'great ×%s niece', I18N::number($down - 3));
- }
-
- if ($first === 'bro' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s great ×(%s-1) grandchild', 'great ×%s nephew/niece', I18N::number($down - 3));
- }
-
- if ($first === 'sis' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s great ×(%s-1) grandchild', 'great ×%s nephew/niece', I18N::number($down - 3));
- }
-
- return I18N::translateContext('(a woman’s) great ×%s nephew/niece', 'great ×%s nephew/niece', I18N::number($down - 3));
-
- case 'zh-Hans': // Source: xmlf
- case 'zh-Hant':
- if ($sex2 === 'M') {
- if ($first === 'bro' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s great ×(%s-1) grandson', 'great ×%s nephew', I18N::number($down - 1));
- }
- if ($first === 'sis' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s great ×(%s-1) grandson', 'great ×%s nephew', I18N::number($down - 1));
- }
- return I18N::translateContext('(a woman’s) great ×%s nephew', 'great ×%s nephew', I18N::number($down - 1));
- }
- if ($sex2 === 'F') {
- if ($first === 'bro' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s great ×(%s-1) granddaughter', 'great ×%s niece', I18N::number($down - 1));
- }
- if ($first === 'sis' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s great ×(%s-1) granddaughter', 'great ×%s niece', I18N::number($down - 1));
- }
- return I18N::translateContext('(a woman’s) great ×%s niece', 'great ×%s niece', I18N::number($down - 1));
- }
- if ($first === 'bro' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) brother’s great ×(%s-1) grandchild', 'great ×%s nephew/niece', I18N::number($down - 1));
- }
- if ($first === 'sis' && $sex1 === 'M') {
- return I18N::translateContext('(a man’s) sister’s great ×(%s-1) grandchild', 'great ×%s nephew/niece', I18N::number($down - 1));
- }
- return I18N::translateContext('(a woman’s) great ×%s nephew/niece', 'great ×%s nephew/niece', I18N::number($down - 1));
-
- case 'he': // Source: Meliza Amity
- if ($sex2 === 'M') {
- return I18N::translate('great ×%s nephew', I18N::number($down - 1));
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×%s niece', I18N::number($down - 1));
- }
-
- return I18N::translate('great ×%s nephew/niece', I18N::number($down - 1));
-
- case 'hi': // Source: MrQD.
- if ($sex2 === 'M') {
- // I18N: if you need a different number for %s, contact the developers, as a code-change is required
- return I18N::translate('great ×%s nephew', I18N::number($down - 3));
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×%s niece', I18N::number($down - 3));
- }
-
- return I18N::translate('great ×%s nephew/niece', I18N::number($down - 3));
-
- case 'it': // Source: Michele Locati.
- case 'en_AU':
- case 'en_GB':
- case 'en_US':
- default:
- if ($sex2 === 'M') {
- // I18N: if you need a different number for %s, contact the developers, as a code-change is required
- return I18N::translate('great ×%s nephew', I18N::number($down - 2));
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×%s niece', I18N::number($down - 2));
- }
-
- return I18N::translate('great ×%s nephew/niece', I18N::number($down - 2));
- }
- }
- }
- if (preg_match('/^((?:mot|fat|par)*)$/', $path, $match)) {
- // direct ancestors
- $up = intdiv(strlen($match[1]), 3);
- switch ($up) {
- case 4:
- if ($sex2 === 'M') {
- return I18N::translate('great-great-grandfather');
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great-great-grandmother');
- }
-
- return I18N::translate('great-great-grandparent');
-
- case 5:
- if ($sex2 === 'M') {
- return I18N::translate('great-great-great-grandfather');
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great-great-great-grandmother');
- }
-
- return I18N::translate('great-great-great-grandparent');
-
- case 6:
- if ($sex2 === 'M') {
- return I18N::translate('great ×4 grandfather');
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×4 grandmother');
- }
-
- return I18N::translate('great ×4 grandparent');
-
- case 7:
- if ($sex2 === 'M') {
- return I18N::translate('great ×5 grandfather');
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×5 grandmother');
- }
-
- return I18N::translate('great ×5 grandparent');
-
- case 8:
- if ($sex2 === 'M') {
- return I18N::translate('great ×6 grandfather');
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×6 grandmother');
- }
-
- return I18N::translate('great ×6 grandparent');
-
- case 9:
- if ($sex2 === 'M') {
- return I18N::translate('great ×7 grandfather');
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×7 grandmother');
- }
-
- return I18N::translate('great ×7 grandparent');
-
- default:
- // Different languages have different rules for naming generations.
- // An English great ×12 grandfather is a Danish great ×11 grandfather.
- //
- // Need to find out which languages use which rules.
- switch (I18N::languageTag()) {
- case 'da': // Source: Patrick Sorensen
- if ($sex2 === 'M') {
- return I18N::translate('great ×%s grandfather', I18N::number($up - 3));
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×%s grandmother', I18N::number($up - 3));
- }
-
- return I18N::translate('great ×%s grandparent', I18N::number($up - 3));
-
- case 'it': // Source: Michele Locati
- case 'zh-Hans': // Source: xmlf
- case 'zh-Hant':
- case 'es': // Source: Wes Groleau
- if ($sex2 === 'M') {
- return I18N::translate('great ×%s grandfather', I18N::number($up));
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×%s grandmother', I18N::number($up));
- }
-
- return I18N::translate('great ×%s grandparent', I18N::number($up));
-
- case 'fr': // Source: Jacqueline Tetreault
- case 'fr_CA':
- if ($sex2 === 'M') {
- return I18N::translate('great ×%s grandfather', I18N::number($up - 1));
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×%s grandmother', I18N::number($up - 1));
- }
-
- return I18N::translate('great ×%s grandparent', I18N::number($up - 1));
-
- case 'nn': // Source: Hogne Røed Nilsen (https://bugs.launchpad.net/webtrees/+bug/1168553)
- case 'nb':
- if ($sex2 === 'M') {
- // I18N: if you need a different number for %s, contact the developers, as a code-change is required
- return I18N::translate('great ×%s grandfather', I18N::number($up - 3));
- }
-
- if ($sex2 === 'F') {
- // I18N: if you need a different number for %s, contact the developers, as a code-change is required
- return I18N::translate('great ×%s grandmother', I18N::number($up - 3));
- }
-
- // I18N: if you need a different number for %s, contact the developers, as a code-change is required
- return I18N::translate('great ×%s grandparent', I18N::number($up - 3));
- case 'en_AU':
- case 'en_GB':
- case 'en_US':
- default:
- if ($sex2 === 'M') {
- // I18N: if you need a different number for %s, contact the developers, as a code-change is required
- return I18N::translate('great ×%s grandfather', I18N::number($up - 2));
- }
-
- if ($sex2 === 'F') {
- // I18N: if you need a different number for %s, contact the developers, as a code-change is required
- return I18N::translate('great ×%s grandmother', I18N::number($up - 2));
- }
-
- // I18N: if you need a different number for %s, contact the developers, as a code-change is required
- return I18N::translate('great ×%s grandparent', I18N::number($up - 2));
- }
- }
- }
- if (preg_match('/^((?:son|dau|chi)*)$/', $path, $match)) {
- // direct descendants
- $up = intdiv(strlen($match[1]), 3);
- switch ($up) {
- case 4:
- if ($sex2 === 'M') {
- return I18N::translate('great-great-grandson');
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great-great-granddaughter');
- }
-
- return I18N::translate('great-great-grandchild');
-
- case 5:
- if ($sex2 === 'M') {
- return I18N::translate('great-great-great-grandson');
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great-great-great-granddaughter');
- }
-
- return I18N::translate('great-great-great-grandchild');
-
- case 6:
- if ($sex2 === 'M') {
- return I18N::translate('great ×4 grandson');
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×4 granddaughter');
- }
-
- return I18N::translate('great ×4 grandchild');
-
- case 7:
- if ($sex2 === 'M') {
- return I18N::translate('great ×5 grandson');
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×5 granddaughter');
- }
-
- return I18N::translate('great ×5 grandchild');
-
- case 8:
- if ($sex2 === 'M') {
- return I18N::translate('great ×6 grandson');
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×6 granddaughter');
- }
-
- return I18N::translate('great ×6 grandchild');
-
- case 9:
- if ($sex2 === 'M') {
- return I18N::translate('great ×7 grandson');
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×7 granddaughter');
- }
-
- return I18N::translate('great ×7 grandchild');
-
- default:
- // Different languages have different rules for naming generations.
- // An English great ×12 grandson is a Danish great ×11 grandson.
- //
- // Need to find out which languages use which rules.
- switch (I18N::languageTag()) {
- case 'nn': // Source: Hogne Røed Nilsen
- case 'nb':
- case 'da': // Source: Patrick Sorensen
- if ($sex2 === 'M') {
- return I18N::translate('great ×%s grandson', I18N::number($up - 3));
- }
-
- if ($sex2 === 'F') {
- return I18N::translate('great ×%s granddaughter', I18N::number($up - 3));
- }
-
- return I18N::translate('great ×%s grandchild', I18N::number($up - 3));
-
- case 'zh-Hans': // Source: xmlf
- case 'zh-Hant':
- if ($sex2 === 'M') {
- return I18N::translate('great ×%s grandson', I18N::number($up));
- }
- if ($sex2 === 'F') {
- return I18N::translate('great ×%s granddaughter', I18N::number($up));
- }
- return I18N::translate('great ×%s grandchild', I18N::number($up));
-
- case 'it':
- // Source: Michele Locati
- case 'es':
- // Source: Wes Groleau (adding doesn’t change behavior, but needs to be better researched)
- case 'en_AU':
- case 'en_GB':
- case 'en_US':
- default:
- if ($sex2 === 'M') {
- // I18N: if you need a different number for %s, contact the developers, as a code-change is required
- return I18N::translate('great ×%s grandson', I18N::number($up - 2));
- }
-
- if ($sex2 === 'F') {
- // I18N: if you need a different number for %s, contact the developers, as a code-change is required
- return I18N::translate('great ×%s granddaughter', I18N::number($up - 2));
- }
-
- // I18N: if you need a different number for %s, contact the developers, as a code-change is required
- return I18N::translate('great ×%s grandchild', I18N::number($up - 2));
- }
- }
- }
- if (preg_match('/^((?:mot|fat|par)+)(?:bro|sis|sib)((?:son|dau|chi)+)$/', $path, $match)) {
- // cousins in English
- $ascent = $match[1];
- $descent = $match[2];
- $up = intdiv(strlen($ascent), 3);
- $down = intdiv(strlen($descent), 3);
- $cousin = min($up, $down); // Moved out of switch (en/default case) so that
- $removed = abs($down - $up); // Spanish (and other languages) can use it, too.
-
- // Different languages have different rules for naming cousins. For example,
- // an English “second cousin once removed” is a Polish “cousin of 7th degree”.
- //
- // Need to find out which languages use which rules.
- switch (I18N::languageTag()) {
- case 'pl': // Source: Lukasz Wilenski
- return self::cousinName($up + $down + 2, $sex2);
- case 'it':
- // Source: Michele Locati. See italian_cousins_names.zip
- // https://webtrees.net/forums/8-translation/1200-great-xn-grandparent?limit=6&start=6
- return self::cousinName($up + $down - 3, $sex2);
- case 'es':
- // Source: Wes Groleau. See http://UniGen.us/Parentesco.html & http://UniGen.us/Parentesco-D.html
- if ($down === $up) {
- return self::cousinName($cousin, $sex2);
- }
-
- if ($down < $up) {
- return self::cousinName2($cousin + 1, $sex2, self::getRelationshipNameFromPath('sib' . $descent));
- }
-
- if ($sex2 === 'M') {
- return self::cousinName2($cousin + 1, $sex2, self::getRelationshipNameFromPath('bro' . $descent));
- }
-
- if ($sex2 === 'F') {
- return self::cousinName2($cousin + 1, $sex2, self::getRelationshipNameFromPath('sis' . $descent));
- }
-
- return self::cousinName2($cousin + 1, $sex2, self::getRelationshipNameFromPath('sib' . $descent));
-
- case 'en_AU': // See: http://en.wikipedia.org/wiki/File:CousinTree.svg
- case 'en_GB':
- case 'en_US':
- default:
- switch ($removed) {
- case 0:
- return self::cousinName($cousin, $sex2);
- case 1:
- if ($up > $down) {
- /* I18N: %s=“fifth cousin”, etc. http://www.ancestry.com/learn/library/article.aspx?article=2856 */
- return I18N::translate('%s once removed ascending', self::cousinName($cousin, $sex2));
- }
-
- /* I18N: %s=“fifth cousin”, etc. http://www.ancestry.com/learn/library/article.aspx?article=2856 */
- return I18N::translate('%s once removed descending', self::cousinName($cousin, $sex2));
- case 2:
- if ($up > $down) {
- /* I18N: %s=“fifth cousin”, etc. */
- return I18N::translate('%s twice removed ascending', self::cousinName($cousin, $sex2));
- }
-
- /* I18N: %s=“fifth cousin”, etc. */
- return I18N::translate('%s twice removed descending', self::cousinName($cousin, $sex2));
- case 3:
- if ($up > $down) {
- /* I18N: %s=“fifth cousin”, etc. */
- return I18N::translate('%s three times removed ascending', self::cousinName($cousin, $sex2));
- }
-
- /* I18N: %s=“fifth cousin”, etc. */
- return I18N::translate('%s three times removed descending', self::cousinName($cousin, $sex2));
- default:
- if ($up > $down) {
- /* I18N: %1$s=“fifth cousin”, etc., %2$s>=4 */
- return I18N::translate('%1$s %2$s times removed ascending', self::cousinName($cousin, $sex2), I18N::number($removed));
- }
-
- /* I18N: %1$s=“fifth cousin”, etc., %2$s>=4 */
- return I18N::translate('%1$s %2$s times removed descending', self::cousinName($cousin, $sex2), I18N::number($removed));
- }
- }
- }
-
- // Split the relationship into sub-relationships, e.g., third-cousin’s great-uncle.
- // Try splitting at every point, and choose the path with the shorted translated name.
- // But before starting to recursively go through all combinations, do a cache look-up
- if (array_key_exists($path, self::$relationshipsCache)) {
- return self::$relationshipsCache[$path];
- }
-
- $relationship = null;
- $path1 = substr($path, 0, 3);
- $path2 = substr($path, 3);
- while ($path2) {
- // I18N: A complex relationship, such as “third-cousin’s great-uncle”
- $tmp = I18N::translate(
- '%1$s’s %2$s',
- self::getRelationshipNameFromPath($path1),
- self::getRelationshipNameFromPath($path2)
- );
- if (!$relationship || strlen($tmp) < strlen($relationship)) {
- $relationship = $tmp;
- }
- $path1 .= substr($path2, 0, 3);
- $path2 = substr($path2, 3);
- }
- // and store the result in the cache
- self::$relationshipsCache[$path] = $relationship;
-
- return $relationship;
- }
}
diff --git a/app/Functions/FunctionsPrintFacts.php b/app/Functions/FunctionsPrintFacts.php
index c9036e9981..064233c42c 100644
--- a/app/Functions/FunctionsPrintFacts.php
+++ b/app/Functions/FunctionsPrintFacts.php
@@ -38,6 +38,7 @@ use Fisharebest\Webtrees\Module\RelationshipsChartModule;
use Fisharebest\Webtrees\Note;
use Fisharebest\Webtrees\Registry;
use Fisharebest\Webtrees\Services\ModuleService;
+use Fisharebest\Webtrees\Services\RelationshipService;
use Fisharebest\Webtrees\Tree;
use Ramsey\Uuid\Uuid;
@@ -386,7 +387,7 @@ class FunctionsPrintFacts
if ($module instanceof RelationshipsChartModule) {
foreach ($associates as $associate) {
- $relationship_name = Functions::getCloseRelationshipName($associate, $person);
+ $relationship_name = app(RelationshipService::class)->getCloseRelationshipName($associate, $person);
if ($relationship_name === '') {
$relationship_name = GedcomTag::getLabel('RELA');
}
diff --git a/app/I18N.php b/app/I18N.php
index 669ca333ae..a547f91736 100644
--- a/app/I18N.php
+++ b/app/I18N.php
@@ -57,23 +57,27 @@ class I18N
// MO files use special characters for plurals and context.
public const PLURAL = "\x00";
public const CONTEXT = "\x04";
+
+ // Digits are always rendered LTR, even in RTL text.
private const DIGITS = '0123456789٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹';
+
+ // These locales need special handling for the dotless letter I.
private const DOTLESS_I_LOCALES = [
'az',
'tr',
];
+
private const DOTLESS_I_TOLOWER = [
'I' => 'ı',
'İ' => 'i',
];
- // Digits are always rendered LTR, even in RTL text.
private const DOTLESS_I_TOUPPER = [
'ı' => 'I',
'i' => 'İ',
];
- // These locales need special handling for the dotless letter I.
+ // The ranges of characters used by each script.
private const SCRIPT_CHARACTER_RANGES = [
[
'Latn',
@@ -174,6 +178,8 @@ class I18N
],
// Mixed CJK, not just Hans
];
+
+ // Characters that are displayed in mirror form in RTL text.
private const MIRROR_CHARACTERS = [
'(' => ')',
')' => '(',
@@ -194,18 +200,17 @@ class I18N
'‘ ' => '’',
'’ ' => '‘',
];
- /** @var string Punctuation used to separate list items, typically a comma */
- public static $list_separator;
- // The ranges of characters used by each script.
- /** @var LocaleInterface The current locale (e.g. LocaleEnGb) */
- private static $locale;
+ // Punctuation used to separate list items, typically a comma
+ public static string $list_separator;
- // Characters that are displayed in mirror form in RTL text.
- /** @var Translator An object that performs translation */
- private static $translator;
- /** @var Collator|null From the php-intl library */
- private static $collator;
+ private static ?ModuleLanguageInterface $language;
+
+ private static LocaleInterface $locale;
+
+ private static Translator $translator;
+
+ private static ?Collator $collator;
/**
* The preferred locales for this site, or a default list if no preference.
@@ -311,11 +316,17 @@ class I18N
// Add translations from custom modules (but not during setup, as we have no database/modules)
if (!$setup) {
- $translations = app(ModuleService::class)
+ $module_service = app(ModuleService::class);
+
+ $translations = $module_service
->findByInterface(ModuleCustomInterface::class)
->reduce(static function (array $carry, ModuleCustomInterface $item): array {
return array_merge($carry, $item->customTranslations(self::$locale->languageTag()));
}, $translations);
+
+ self::$language = $module_service
+ ->findByInterface(ModuleLanguageInterface::Class)
+ ->first(fn (ModuleLanguageInterface $module): bool => $module->locale()->languageTag() === $code);
}
// Create a translator
@@ -372,6 +383,14 @@ class I18N
}
/**
+ * @return ModuleLanguageInterface
+ */
+ public static function language(): ModuleLanguageInterface
+ {
+ return self::$language;
+ }
+
+ /**
* Translate a number into the local representation.
* e.g. 12345.67 becomes
* en: 12,345.67
diff --git a/app/Module/LanguageEnglishAustralia.php b/app/Module/LanguageEnglishAustralia.php
index e6b3533264..1eb95d3b0f 100644
--- a/app/Module/LanguageEnglishAustralia.php
+++ b/app/Module/LanguageEnglishAustralia.php
@@ -25,10 +25,8 @@ use Fisharebest\Localization\Locale\LocaleInterface;
/**
* Class LanguageEnglishAustralia.
*/
-class LanguageEnglishAustralia extends AbstractModule implements ModuleLanguageInterface
+class LanguageEnglishAustralia extends LanguageEnglishGreatBritain
{
- use ModuleLanguageTrait;
-
/**
* Should this module be enabled when it is first installed?
*
diff --git a/app/Module/LanguageEnglishGreatBritain.php b/app/Module/LanguageEnglishGreatBritain.php
index bc9136e68f..11f9b0cffa 100644
--- a/app/Module/LanguageEnglishGreatBritain.php
+++ b/app/Module/LanguageEnglishGreatBritain.php
@@ -25,9 +25,41 @@ use Fisharebest\Localization\Locale\LocaleInterface;
/**
* Class LanguageEnglishGreatBritain.
*/
-class LanguageEnglishGreatBritain extends AbstractModule implements ModuleLanguageInterface
+class LanguageEnglishGreatBritain extends LanguageEnglishUnitedStates
{
- use ModuleLanguageTrait;
+ // British English changes "three-times" to "thrice"
+ protected const REMOVED = [
+ '',
+ ' once removed',
+ ' twice removed',
+ ' thrice removed',
+ ' four times removed',
+ ' five times removed',
+ ' six times removed',
+ ' seven times removed',
+ ' eight times removed',
+ ' nine times removed',
+ ' ten times removed',
+ ' eleven removed',
+ ' twelve removed',
+ ' thirteen removed',
+ ' fourteen times removed',
+ ' fifteen times removed',
+ ' sixteen times removed',
+ ' seventeen times removed',
+ ' eighteen times removed',
+ ' nineteen times removed',
+ ' twenty times removed',
+ ' twenty-one times removed',
+ ' twenty-two times removed',
+ ' twenty-three times removed',
+ ' twenty-four times removed',
+ ' twenty-five times removed',
+ ' twenty-six times removed',
+ ' twenty-seven times removed',
+ ' twenty-eight times removed',
+ ' twenty-nine times removed',
+ ];
/**
* @return LocaleInterface
diff --git a/app/Module/LanguageEnglishUnitedStates.php b/app/Module/LanguageEnglishUnitedStates.php
index 58a9e5576d..f61f7e1eb9 100644
--- a/app/Module/LanguageEnglishUnitedStates.php
+++ b/app/Module/LanguageEnglishUnitedStates.php
@@ -21,6 +21,11 @@ namespace Fisharebest\Webtrees\Module;
use Fisharebest\Localization\Locale\LocaleEnUs;
use Fisharebest\Localization\Locale\LocaleInterface;
+use Fisharebest\Webtrees\Relationship;
+
+use function abs;
+use function min;
+use function str_repeat;
/**
* Class LanguageEnglishUnitedStates.
@@ -29,11 +34,196 @@ class LanguageEnglishUnitedStates extends AbstractModule implements ModuleLangua
{
use ModuleLanguageTrait;
- /**
- * @return LocaleInterface
- */
+ protected const COUSIN = [
+ 'sibling',
+ 'first cousin',
+ 'second cousin',
+ 'third cousin',
+ 'fourth cousin',
+ 'fifth cousin',
+ 'sixth cousin',
+ 'seventh cousin',
+ 'eighth cousin',
+ 'ninth cousin',
+ 'tenth cousin',
+ 'eleventh cousin',
+ 'twelfth cousin',
+ 'thirteenth cousin',
+ 'fourteenth cousin',
+ 'fifteenth cousin',
+ 'sixteenth cousin',
+ 'seventeenth cousin',
+ 'eighteenth cousin',
+ 'nineteenth cousin',
+ 'twentieth cousin',
+ 'twenty-first cousin',
+ 'twenty-second cousin',
+ 'twenty-third cousin',
+ 'twenty-fourth cousin',
+ 'twenty-fifth cousin',
+ 'twenty-sixth cousin',
+ 'twenty-seventh cousin',
+ 'twenty-eighth cousin',
+ 'twenty-ninth cousin',
+ 'thirtieth cousin',
+ ];
+
+ protected const REMOVED = [
+ '',
+ ' once removed',
+ ' twice removed',
+ ' three times removed',
+ ' four times removed',
+ ' five times removed',
+ ' six times removed',
+ ' seven times removed',
+ ' eight times removed',
+ ' nine times removed',
+ ' ten times removed',
+ ' eleven removed',
+ ' twelve removed',
+ ' thirteen removed',
+ ' fourteen times removed',
+ ' fifteen times removed',
+ ' sixteen times removed',
+ ' seventeen times removed',
+ ' eighteen times removed',
+ ' nineteen times removed',
+ ' twenty times removed',
+ ' twenty-one times removed',
+ ' twenty-two times removed',
+ ' twenty-three times removed',
+ ' twenty-four times removed',
+ ' twenty-five times removed',
+ ' twenty-six times removed',
+ ' twenty-seven times removed',
+ ' twenty-eight times removed',
+ ' twenty-nine times removed',
+ ];
+
+ protected const DIRECTION = [
+ -1 => ' descending',
+ 0 => '',
+ 1 => ' ascending',
+ ];
+
public function locale(): LocaleInterface
{
return new LocaleEnUs();
}
+
+ /**
+ * @return array<Relationship>
+ */
+ public function relationships(): array
+ {
+ // Genitive forms in English are simple/regular, as no relationship name ends in "s".
+ $genitive = fn (string $s): array => [$s, $s . '’s %s'];
+
+ $cousin = fn (int $up, int $down): array => $genitive(
+ (static::COUSIN[min($up, $down)] ?? 'distant cousin') .
+ (static::REMOVED[abs($up - $down)] ?? ' many times removed') .
+ static::DIRECTION[$up <=> $down]
+ );
+
+ $great = fn (int $n, string $prefix, string $suffix): array => $genitive(
+ $prefix . ($n > 3 ? 'great ×' . $n . ' ' : str_repeat('great-', $n)) . $suffix
+ );
+
+ return [
+ // Adopted
+ Relationship::fixed('adoptive-mother', 'adoptive-mother’s %s')->adoptive()->mother(),
+ Relationship::fixed('adoptive-father', 'adoptive-father’s %s')->adoptive()->father(),
+ Relationship::fixed('adoptive-parent', 'adoptive-parent’s %s')->adoptive()->parent(),
+ Relationship::fixed('adopted-daughter', 'adopted-daughter’s %s')->adopted()->daughter(),
+ Relationship::fixed('adopted-son', 'adopted-son’s %s')->adopted()->son(),
+ Relationship::fixed('adopted-child', 'adopted-child’s %s')->adopted()->child(),
+ // Fostered
+ Relationship::fixed('foster-mother', 'foster-mother’s %s')->fostering()->mother(),
+ Relationship::fixed('foster-father', 'foster-father’s %s')->fostering()->father(),
+ Relationship::fixed('foster-parent', 'foster-parent’s %s')->fostering()->parent(),
+ Relationship::fixed('foster-daughter', 'foster-daughter’s %s')->fostered()->daughter(),
+ Relationship::fixed('foster-son', 'foster-son’s %s')->fostered()->son(),
+ Relationship::fixed('foster-child', 'foster-child’s %s')->fostered()->child(),
+ // Parents
+ Relationship::fixed('mother', 'mother’s %s')->mother(),
+ Relationship::fixed('father', 'father’s %s')->father(),
+ Relationship::fixed('parent', 'parent’s %s')->parent(),
+ // Children
+ Relationship::fixed('daughter', 'daughter’s %s')->daughter(),
+ Relationship::fixed('son', 'son’s %s')->son(),
+ Relationship::fixed('child', 'child’s %s')->child(),
+ // Siblings
+ Relationship::fixed('twin sister', 'twin sister’s %s')->twin()->sister(),
+ Relationship::fixed('twin brother', 'twin brother’s %s')->twin()->brother(),
+ Relationship::fixed('twin sibling', 'twin sibling’s %s')->twin()->sibling(),
+ Relationship::fixed('elder sister', 'elder sister’s %s')->older()->sister(),
+ Relationship::fixed('elder brother', 'elder brother’s %s')->older()->brother(),
+ Relationship::fixed('elder sibling', 'elder sibling’s %s')->older()->sibling(),
+ Relationship::fixed('younger sister', 'younger sister’s %s')->younger()->sister(),
+ Relationship::fixed('younger brother', 'younger brother’s %s')->younger()->brother(),
+ Relationship::fixed('younger sibling', 'younger sibling’s %s')->younger()->sibling(),
+ Relationship::fixed('sister', 'sister’s %s')->sister(),
+ Relationship::fixed('brother', 'brother’s %s')->brother(),
+ Relationship::fixed('sibling', 'sibling’s %s')->sibling(),
+ // Partners
+ Relationship::fixed('ex-wife', 'ex-wife’s %s')->divorced()->partner()->female(),
+ Relationship::fixed('ex-husband', 'ex-husband’s %s')->divorced()->partner()->male(),
+ Relationship::fixed('ex-spouse', 'ex-spouse’s %s')->divorced()->partner(),
+ Relationship::fixed('fiancée', 'fiancée’s %s')->engaged()->partner()->female(),
+ Relationship::fixed('fiancé', 'fiancé’s %s')->engaged()->partner()->male(),
+ Relationship::fixed('wife', 'wife’s %s')->wife(),
+ Relationship::fixed('husband', 'husband’s %s')->husband(),
+ Relationship::fixed('spouse', 'spouse’s %s')->spouse(),
+ Relationship::fixed('partner', 'partner’s %s')->partner(),
+ // In-laws
+ Relationship::fixed('mother-in-law', 'mother-in-law’s %s')->married()->spouse()->mother(),
+ Relationship::fixed('father-in-law', 'father-in-law’s %s')->married()->spouse()->father(),
+ Relationship::fixed('parent-in-law', 'parent-in-law’s %s')->married()->spouse()->parent(),
+ Relationship::fixed('daughter-in-law', 'daughter-in-law’s %s')->child()->wife(),
+ Relationship::fixed('son-in-law', 'son-in-law’s %s')->child()->husband(),
+ Relationship::fixed('child-in-law', 'child-in-law’s %s')->child()->married()->spouse(),
+ Relationship::fixed('sister-in-law', 'sister-in-law’s %s')->sibling()->spouse()->sister(),
+ Relationship::fixed('brother-in-law', 'brother-in-law’s %s')->sibling()->spouse()->brother(),
+ Relationship::fixed('sibling-in-law', 'sibling-in-law’s %s')->sibling()->spouse()->sibling(),
+ Relationship::fixed('sister-in-law', 'sister-in-law’s %s')->spouse()->sister(),
+ Relationship::fixed('brother-in-law', 'brother-in-law’s %s')->spouse()->brother(),
+ Relationship::fixed('sibling-in-law', 'sibling-in-law’s %s')->spouse()->sibling(),
+ Relationship::fixed('sister-in-law', 'sister-in-law’s %s')->sibling()->wife(),
+ Relationship::fixed('brother-in-law', 'brother-in-law’s %s')->sibling()->husband(),
+ Relationship::fixed('sibling-in-law', 'sibling-in-law’s %s')->sibling()->spouse(),
+ // Grandparents
+ Relationship::fixed('maternal-grandmother', 'maternal-grandmother’s %s')->mother()->mother(),
+ Relationship::fixed('maternal-grandfather', 'maternal-grandfather’s %s')->mother()->father(),
+ Relationship::fixed('maternal-grandparent', 'maternal-grandfather’s %s')->mother()->parent(),
+ Relationship::fixed('paternal-grandmother', 'paternal-grandmother’s %s')->father()->mother(),
+ Relationship::fixed('paternal-grandfather', 'paternal-grandfather’s %s')->father()->father(),
+ Relationship::fixed('paternal-grandparent', 'paternal-grandfather’s %s')->father()->parent(),
+ Relationship::fixed('grandmother', 'grandmother’s %s')->parent()->mother(),
+ Relationship::fixed('grandfather', 'grandfather’s %s')->parent()->father(),
+ Relationship::fixed('grandparent', 'grandparent’s %s')->parent()->parent(),
+ // Grandchildren
+ Relationship::fixed('granddaughter', 'granddaughter’s %s')->child()->daughter(),
+ Relationship::fixed('grandson', 'grandson’s %s')->child()->son(),
+ Relationship::fixed('grandchild', 'grandchild’s %s')->child()->child(),
+ // Relationships with dynamically generated names
+ Relationship::dynamic(fn (int $n) => $great($n - 1, '', 'aunt'))->ancestor()->sister(),
+ Relationship::dynamic(fn (int $n) => $great($n - 1, '', 'aunt'))->ancestor()->sibling()->wife(),
+ Relationship::dynamic(fn (int $n) => $great($n - 1, '', 'uncle'))->ancestor()->brother(),
+ Relationship::dynamic(fn (int $n) => $great($n - 1, '', 'uncle'))->ancestor()->sibling()->husband(),
+ Relationship::dynamic(fn (int $n) => $great($n - 1, '', 'niece'))->descendant()->sister(),
+ Relationship::dynamic(fn (int $n) => $great($n - 1, '', 'niece'))->married()->spouse()->sibling()->descendant()->female(),
+ Relationship::dynamic(fn (int $n) => $great($n - 1, '', 'nephew'))->sibling()->descendant()->male(),
+ Relationship::dynamic(fn (int $n) => $great($n - 1, '', 'nephew'))->married()->spouse()->sibling()->descendant()->male(),
+ Relationship::dynamic(fn (int $n) => $great($n - 2, 'maternal ', 'grandmother'))->mother()->ancestor()->female(),
+ Relationship::dynamic(fn (int $n) => $great($n - 1, 'maternal ', 'grandfather'))->mother()->ancestor()->male(),
+ Relationship::dynamic(fn (int $n) => $great($n - 1, 'paternal ', 'grandmother'))->father()->ancestor()->female(),
+ Relationship::dynamic(fn (int $n) => $great($n - 1, 'paternal ', 'grandfather'))->father()->ancestor()->male(),
+ Relationship::dynamic(fn (int $n) => $great($n - 1, '', 'grandparent'))->ancestor(),
+ Relationship::dynamic(fn (int $n) => $great($n - 2, '', 'granddaughter'))->descendant()->female(),
+ Relationship::dynamic(fn (int $n) => $great($n - 2, '', 'grandson'))->descendant()->male(),
+ Relationship::dynamic(fn (int $n) => $great($n - 2, '', 'grandchild'))->descendant(),
+ Relationship::dynamic($cousin)->ancestor()->sibling()->descendant(),
+ ];
+ }
}
diff --git a/app/Module/LanguageSlovakian.php b/app/Module/LanguageSlovakian.php
index 933440067f..3605f9a452 100644
--- a/app/Module/LanguageSlovakian.php
+++ b/app/Module/LanguageSlovakian.php
@@ -21,6 +21,9 @@ namespace Fisharebest\Webtrees\Module;
use Fisharebest\Localization\Locale\LocaleInterface;
use Fisharebest\Localization\Locale\LocaleSk;
+use Fisharebest\Webtrees\Relationship;
+
+use function str_repeat;
/**
* Class LanguageSlovakian.
@@ -29,6 +32,48 @@ class LanguageSlovakian extends AbstractModule implements ModuleLanguageInterfac
{
use ModuleLanguageTrait;
+ protected const MALE_COUSINS = [
+ ['', ''],
+ ['bratranec', '%s bratranca'],
+ ['druhostupňový bratranca', '%s druhostupňový bratranca'],
+ ['bratranec z 3. kolena', '%s bratranca z 3. kolena'],
+ ['bratranec zo 4. kolena', '%s bratranca zo 4. kolena'],
+ ['bratranec z 5. kolena', '%s bratranca z 5. kolena'],
+ ['bratranec zo 6. kolena', '%s bratranca zo 6. kolena'],
+ ['bratranec zo 7. kolena', '%s bratranca zo 7. kolena'],
+ ['bratranec z 8. kolena', '%s bratranca z 8. kolena'],
+ ['bratranec z 9. kolena', '%s bratranca z 9. kolena'],
+ ['bratranec z 10. kolena', '%s bratranca z 10. kolena'],
+ ['bratranec z 11. kolena', '%s bratranca z 11. kolena'],
+ ['bratranec z 12. kolena', '%s bratranca z 12. kolena'],
+ ['bratranec z 13. kolena', '%s bratranca z 13. kolena'],
+ ['bratranec zo 14. kolena', '%s bratranca zo 14. kolena'],
+ ['bratranec z 15. kolena', '%s bratranca z 15. kolena'],
+ ['bratranec zo 16. kolena', '%s bratranca zo 16. kolena'],
+ ['bratranec zo 17. kolena', '%s bratranca zo 17. kolena'],
+ ];
+
+ protected const FEMALE_COUSINS = [
+ ['', ''],
+ ['sesternica', '%s sesternice'],
+ ['druhostupňový sesternica', '%s druhostupňový sesternice'],
+ ['sesternica z 3. kolena', '%s sesternice z 3. kolena'],
+ ['sesternica zo 4. kolena', '%s sesternice zo 4. kolena'],
+ ['sesternica z 5. kolena', '%s sesternice z 5. kolena'],
+ ['sesternica zo 6. kolena', '%s sesternice zo 6. kolena'],
+ ['sesternica zo 7. kolena', '%s sesternice zo 7. kolena'],
+ ['sesternica z 8. kolena', '%s sesternice z 8. kolena'],
+ ['sesternica z 9. kolena', '%s sesternice z 9. kolena'],
+ ['sesternica z 10. kolena', '%s sesternice z 10. kolena'],
+ ['sesternica z 11. kolena', '%s sesternice z 11. kolena'],
+ ['sesternica z 12. kolena', '%s sesternice z 12. kolena'],
+ ['sesternica z 13. kolena', '%s sesternice z 13. kolena'],
+ ['sesternica zo 14. kolena', '%s sesternice zo 14. kolena'],
+ ['sesternica z 15. kolena', '%s sesternice z 15. kolena'],
+ ['sesternica zo 16. kolena', '%s sesternice zo 16. kolena'],
+ ['sesternica zo 17. kolena', '%s sesternice zo 17. kolena'],
+ ];
+
/**
* @return LocaleInterface
*/
@@ -36,4 +81,108 @@ class LanguageSlovakian extends AbstractModule implements ModuleLanguageInterfac
{
return new LocaleSk();
}
+ /**
+ * @return array<Relationship>
+ */
+ public function relationships(): array
+ {
+ $pra = fn (int $n, string $nominative, string $genitive): array => [
+ ($n > 3 ? 'pra ×' . $n . ' ' : str_repeat('pra-', $n)) . $nominative,
+ ($n > 3 ? 'pra ×' . $n . ' ' : str_repeat('pra-', $n)) . $genitive,
+ ];
+
+ $cousin = fn (int $n, array $cousins, string $nominative, string $genitive): array => $cousins[$n] ?? [
+ $nominative . ' z ' . $n . '. kolena',
+ $genitive . '%s z ' . $n . '. kolena',
+ ];
+
+ return [
+ // Parents
+ Relationship::fixed('otec', '%s otca')->father(),
+ Relationship::fixed('matka', '%s matky')->mother(),
+ Relationship::fixed('rodič', '%s rodiča')->parent(),
+ // Children
+ Relationship::fixed('syn', '%s syna')->son(),
+ Relationship::fixed('dcéra', '%s dcéry')->daughter(),
+ Relationship::fixed('dcéra', '%s dcéry')->child(),
+ // Siblings
+ Relationship::fixed('brat', '%s brata')->brother(),
+ Relationship::fixed('sestra', '%s sestry')->sister(),
+ Relationship::fixed('súrodenec', '%s súrodenca')->sibling(),
+ // Divorced partners
+ Relationship::fixed('exmanželka', '%s exmanželka')->divorced()->partner()->female(),
+ Relationship::fixed('exmanžel', '%s exmanžela')->divorced()->partner()->male(),
+ Relationship::fixed('exmanžel/manželka', '%s exmanžel/manželka')->divorced()->partner(),
+ // Engaged partners
+ Relationship::fixed('snúbenec', '%s snúbenec')->engaged()->partner()->female(),
+ Relationship::fixed('snúbenica', '%s snúbenica')->engaged()->partner()->male(),
+ // Married parters
+ Relationship::fixed('manželka', '%s manželky')->wife(),
+ Relationship::fixed('manžel', '%s manžela')->husband(),
+ Relationship::fixed('manžel/manželka', '%s manžel/manželka')->spouse(),
+ Relationship::fixed('partnerka', '%s partnerky')->partner()->female(),
+ // Unmarried partners
+ Relationship::fixed('partner', '%s partnera')->partner(),
+ // In-laws
+ Relationship::fixed('tesť', '%s tesťa')->wife()->father(),
+ Relationship::fixed('testiná', '%s testinej')->wife()->mother(),
+ Relationship::fixed('svokor', '%s svokra')->spouse()->father(),
+ Relationship::fixed('svokora', '%s svokry')->spouse()->mother(),
+ Relationship::fixed('zať', '%s zaťa')->child()->husband(),
+ Relationship::fixed('nevesta', '%s nevesty')->child()->wife(),
+ Relationship::fixed('švagor', '%s švagra')->spouse()->brother(),
+ Relationship::fixed('švagor', '%s švagra')->sibling()->husband(),
+ Relationship::fixed('švagriná', '%s švagrinej')->spouse()->sister(),
+ Relationship::fixed('švagriná', '%s švagrinej')->sibling()->wife(),
+ // Half-siblings
+ Relationship::fixed('nevlastný brat', '%s nevlastného brata')->parent()->son(),
+ Relationship::fixed('nevlastná sestra', '%s nevlastnej sestry')->parent()->daughter(),
+ Relationship::fixed('nevlastná súrodenec', '%s nevlastnej súrodenca')->parent()->child(),
+ // Grandparents
+ Relationship::fixed('starý otec', '%s starého otca')->parent()->father(),
+ Relationship::fixed('stará matka', '%s starej matky')->parent()->mother(),
+ Relationship::fixed('starý rodič', '%s starého rodiča')->parent()->parent(),
+ // Great-grandparents
+ Relationship::fixed('prastarý otec', '%s prastarého otca')->parent()->parent()->father(),
+ Relationship::fixed('prastarý otec', '%s prastarého otca')->parent()->parent()->mother(),
+ Relationship::fixed('prastarý otec', '%s prastarého otca')->parent()->parent()->parent(),
+ // Ancestors
+ Relationship::dynamic(fn (int $n) => $pra($n - 1, 'prastarý otec', '%s prastarého otca'))->ancestor()->male(),
+ Relationship::dynamic(fn (int $n) => $pra($n - 1, 'prastará matka', '%s prastarej matky'))->ancestor()->female(),
+ Relationship::dynamic(fn (int $n) => $pra($n - 1, 'prastarý rodič', '%s prastarého rodiča'))->ancestor(),
+ // Grandchildren
+ Relationship::fixed('vnuk', '%s vnuka')->child()->son(),
+ Relationship::fixed('vnučka', '%s vnučky')->child()->daughter(),
+ Relationship::fixed('vnúča', '%s vnúčaťa')->child()->child(),
+ // Great-grandchildren
+ Relationship::fixed('pravnuk', '%s pravnuka')->child()->child()->son(),
+ Relationship::fixed('pravnučka', '%s pravnučky')->child()->child()->daughter(),
+ Relationship::fixed('pravnúča', '%s pravnúčaťa')->child()->child()->child(),
+ // Descendants
+ Relationship::dynamic(fn (int $n) => $pra($n - 1, 'pravnuk', '%s pravnuka'))->ancestor()->male(),
+ Relationship::dynamic(fn (int $n) => $pra($n - 1, 'pravnučka', '%s prastarej matky'))->ancestor()->female(),
+ Relationship::dynamic(fn (int $n) => $pra($n - 1, 'pravnúča', '%s pravnúčaťa'))->ancestor(),
+ // Aunts and uncles
+ Relationship::fixed('ujo', '%s uja')->mother()->brother(),
+ Relationship::fixed('ujčiná', '%s ujčinej')->mother()->brother()->wife(),
+ Relationship::fixed('stryná', '%s strynej')->father()->brother()->wife(),
+ Relationship::fixed('strýko', '%s strýka')->parent()->brother(),
+ Relationship::fixed('teta', '%s tety')->parent()->sister(),
+ // Great-aunts and great-uncles
+ Relationship::dynamic(fn (int $n) => $pra($n - 2, 'prastrýko', '%s prastrýka'))->ancestor()->brother(),
+ Relationship::dynamic(fn (int $n) => $pra($n - 2, 'prateta', '%s pratety'))->ancestor()->sister(),
+ // Nieces and nephews
+ Relationship::fixed('neter', '%s netere')->sibling()->son(),
+ Relationship::fixed('synovec', '%s synovca')->sibling()->daughter(),
+ // Great-nieces and great-nephews
+ Relationship::fixed('prasynovec', '%s prasynovca')->sibling()->child()->son(),
+ Relationship::fixed('praneter', '%s pranetere')->sibling()->child()->daughter(),
+ Relationship::dynamic(fn (int $n) => $pra($n - 2, 'prasynovec', '%s prasynovca'))->sibling()->descendant()->son(),
+ Relationship::dynamic(fn (int $n) => $pra($n - 2, 'praneter', '%s pranetere'))->sibling()->descendant()->daughter(),
+ // Cousins
+ Relationship::dynamic(fn (int $n): array => $cousin($n, static::FEMALE_COUSINS, '', ''))->symmetricCousin()->female(),
+ Relationship::dynamic(fn (int $n): array => $cousin($n, static::MALE_COUSINS, '', ''))->symmetricCousin()->male(),
+ Relationship::dynamic(fn (int $n): array => $cousin($n, static::MALE_COUSINS, '', ''))->symmetricCousin(),
+ ];
+ }
}
diff --git a/app/Module/ModuleLanguageInterface.php b/app/Module/ModuleLanguageInterface.php
index 5d1ba579f3..4d3f86c8e0 100644
--- a/app/Module/ModuleLanguageInterface.php
+++ b/app/Module/ModuleLanguageInterface.php
@@ -20,6 +20,7 @@ declare(strict_types=1);
namespace Fisharebest\Webtrees\Module;
use Fisharebest\Localization\Locale\LocaleInterface;
+use Fisharebest\Webtrees\Relationship;
/**
* Interface ModuleLanguageInterface - provide translation and localization.
@@ -30,4 +31,9 @@ interface ModuleLanguageInterface extends ModuleInterface
* @return LocaleInterface
*/
public function locale(): LocaleInterface;
+
+ /**
+ * @return array<Relationship>
+ */
+ public function relationships(): array;
}
diff --git a/app/Module/ModuleLanguageTrait.php b/app/Module/ModuleLanguageTrait.php
index 7f4bfdb4c9..2647f74bb3 100644
--- a/app/Module/ModuleLanguageTrait.php
+++ b/app/Module/ModuleLanguageTrait.php
@@ -22,6 +22,7 @@ namespace Fisharebest\Webtrees\Module;
use Fisharebest\Localization\Locale\LocaleEnUs;
use Fisharebest\Localization\Locale\LocaleInterface;
use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Relationship;
/**
* Trait ModuleLanguageEventsTrait - default implementation of ModuleLanguageInterface.
@@ -55,4 +56,12 @@ trait ModuleLanguageTrait
{
return new LocaleEnUs();
}
+
+ /**
+ * @return array<Relationship>
+ */
+ public function relationships(): array
+ {
+ return [];
+ }
}
diff --git a/app/Module/PedigreeMapModule.php b/app/Module/PedigreeMapModule.php
index c8c52e6aa4..bac433cf88 100644
--- a/app/Module/PedigreeMapModule.php
+++ b/app/Module/PedigreeMapModule.php
@@ -31,6 +31,7 @@ use Fisharebest\Webtrees\Individual;
use Fisharebest\Webtrees\PlaceLocation;
use Fisharebest\Webtrees\Menu;
use Fisharebest\Webtrees\Services\ChartService;
+use Fisharebest\Webtrees\Services\RelationshipService;
use Fisharebest\Webtrees\Tree;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
@@ -370,6 +371,6 @@ class PedigreeMapModule extends AbstractModule implements ModuleChartInterface,
$sosa = intdiv($sosa, 2);
}
- return Functions::getRelationshipNameFromPath($path);
+ return app(RelationshipService::class)->legacyNameAlgorithm($path);
}
}
diff --git a/app/Module/RelationshipsChartModule.php b/app/Module/RelationshipsChartModule.php
index 553c755c34..beddcb29c4 100644
--- a/app/Module/RelationshipsChartModule.php
+++ b/app/Module/RelationshipsChartModule.php
@@ -25,16 +25,20 @@ use Fig\Http\Message\RequestMethodInterface;
use Fisharebest\Algorithm\Dijkstra;
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Contracts\UserInterface;
-use Fisharebest\Webtrees\Registry;
use Fisharebest\Webtrees\FlashMessages;
use Fisharebest\Webtrees\Functions\Functions;
+use Fisharebest\Webtrees\GedcomRecord;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
use Fisharebest\Webtrees\Menu;
+use Fisharebest\Webtrees\Registry;
+use Fisharebest\Webtrees\Services\ModuleService;
+use Fisharebest\Webtrees\Services\RelationshipService;
use Fisharebest\Webtrees\Services\TreeService;
use Fisharebest\Webtrees\Tree;
use Illuminate\Database\Capsule\Manager as DB;
use Illuminate\Database\Query\JoinClause;
+use Illuminate\Support\Collection;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
@@ -69,15 +73,18 @@ class RelationshipsChartModule extends AbstractModule implements ModuleChartInte
'recursion' => self::DEFAULT_RECURSION,
];
- /** @var TreeService */
- private $tree_service;
+ private TreeService $tree_service;
+
+ private RelationshipService $relationship_service;
/**
- * @param TreeService $tree_service
+ * @param RelationshipService $relationship_service
+ * @param TreeService $tree_service
*/
- public function __construct(TreeService $tree_service)
+ public function __construct(RelationshipService $relationship_service, TreeService $tree_service)
{
- $this->tree_service = $tree_service;
+ $this->relationship_service = $relationship_service;
+ $this->tree_service = $tree_service;
}
/**
@@ -311,7 +318,23 @@ class RelationshipsChartModule extends AbstractModule implements ModuleChartInte
// Cannot see one of the families/individuals, due to privacy;
continue;
}
- echo '<h3>', I18N::translate('Relationship: %s', Functions::getRelationshipNameFromPath(implode('', $relationships), $individual1, $individual2)), '</h3>';
+
+ $nodes = Collection::make($path)
+ ->map(static function (string $xref, int $key) use ($tree): GedcomRecord {
+ if ($key % 2 === 0) {
+ return Registry::individualFactory()->make($xref, $tree);
+ }
+
+ return Registry::familyFactory()->make($xref, $tree);
+ });
+
+ $language = app(ModuleService::class)
+ ->findByInterface(ModuleLanguageInterface::class, true)
+ ->first(fn (ModuleLanguageInterface $language): bool => $language->locale()->languageTag() === I18N::languageTag());
+
+
+
+ echo '<h3>', I18N::translate('Relationship: %s', $this->relationship_service->nameFromPath($nodes->all(), $language)), '</h3>';
$num_paths++;
// Use a table/grid for layout.
@@ -333,17 +356,17 @@ class RelationshipsChartModule extends AbstractModule implements ModuleChartInte
case 'bro':
case 'sis':
case 'sib':
- $table[$x + 1][$y] = '<div style="background:url(' . e(asset('css/images/hline.png')) . ') repeat-x center; width: 94px; text-align: center"><div class="hline-text" style="height: 32px;">' . Functions::getRelationshipNameFromPath($relationships[$n], Registry::individualFactory()->make($path[$n - 1], $tree), Registry::individualFactory()->make($path[$n + 1], $tree)) . '</div><div style="height: 32px;">' . view('icons/arrow-right') . '</div></div>';
+ $table[$x + 1][$y] = '<div style="background:url(' . e(asset('css/images/hline.png')) . ') repeat-x center; width: 94px; text-align: center"><div class="hline-text" style="height: 32px;">' . app(RelationshipService::class)->legacyNameAlgorithm($relationships[$n], Registry::individualFactory()->make($path[$n - 1], $tree), Registry::individualFactory()->make($path[$n + 1], $tree)) . '</div><div style="height: 32px;">' . view('icons/arrow-right') . '</div></div>';
$x += 2;
break;
case 'son':
case 'dau':
case 'chi':
if ($n > 2 && preg_match('/fat|mot|par/', $relationships[$n - 2])) {
- $table[$x + 1][$y - 1] = '<div style="background:url(' . $diagonal2 . '); width: 64px; height: 64px; text-align: center;"><div style="height: 32px; text-align: end;">' . Functions::getRelationshipNameFromPath($relationships[$n], Registry::individualFactory()->make($path[$n - 1], $tree), Registry::individualFactory()->make($path[$n + 1], $tree)) . '</div><div style="height: 32px; text-align: start;">' . view('icons/arrow-down') . '</div></div>';
+ $table[$x + 1][$y - 1] = '<div style="background:url(' . $diagonal2 . '); width: 64px; height: 64px; text-align: center;"><div style="height: 32px; text-align: end;">' . app(RelationshipService::class)->legacyNameAlgorithm($relationships[$n], Registry::individualFactory()->make($path[$n - 1], $tree), Registry::individualFactory()->make($path[$n + 1], $tree)) . '</div><div style="height: 32px; text-align: start;">' . view('icons/arrow-down') . '</div></div>';
$x += 2;
} else {
- $table[$x][$y - 1] = '<div style="background:url(' . e('"' . asset('css/images/vline.png') . '"') . ') repeat-y center; height: 64px; text-align: center;"><div class="vline-text" style="display: inline-block; width:50%; line-height: 64px;">' . Functions::getRelationshipNameFromPath($relationships[$n], Registry::individualFactory()->make($path[$n - 1], $tree), Registry::individualFactory()->make($path[$n + 1], $tree)) . '</div><div style="display: inline-block; width:50%; line-height: 64px;">' . view('icons/arrow-down') . '</div></div>';
+ $table[$x][$y - 1] = '<div style="background:url(' . e('"' . asset('css/images/vline.png') . '"') . ') repeat-y center; height: 64px; text-align: center;"><div class="vline-text" style="display: inline-block; width:50%; line-height: 64px;">' . app(RelationshipService::class)->legacyNameAlgorithm($relationships[$n], Registry::individualFactory()->make($path[$n - 1], $tree), Registry::individualFactory()->make($path[$n + 1], $tree)) . '</div><div style="display: inline-block; width:50%; line-height: 64px;">' . view('icons/arrow-down') . '</div></div>';
}
$y -= 2;
break;
@@ -351,10 +374,10 @@ class RelationshipsChartModule extends AbstractModule implements ModuleChartInte
case 'mot':
case 'par':
if ($n > 2 && preg_match('/son|dau|chi/', $relationships[$n - 2])) {
- $table[$x + 1][$y + 1] = '<div style="background:url(' . $diagonal1 . '); background-position: top right; width: 64px; height: 64px; text-align: center;"><div style="height: 32px; text-align: start;">' . Functions::getRelationshipNameFromPath($relationships[$n], Registry::individualFactory()->make($path[$n - 1], $tree), Registry::individualFactory()->make($path[$n + 1], $tree)) . '</div><div style="height: 32px; text-align: end;">' . view('icons/arrow-down') . '</div></div>';
+ $table[$x + 1][$y + 1] = '<div style="background:url(' . $diagonal1 . '); background-position: top right; width: 64px; height: 64px; text-align: center;"><div style="height: 32px; text-align: start;">' . app(RelationshipService::class)->legacyNameAlgorithm($relationships[$n], Registry::individualFactory()->make($path[$n - 1], $tree), Registry::individualFactory()->make($path[$n + 1], $tree)) . '</div><div style="height: 32px; text-align: end;">' . view('icons/arrow-down') . '</div></div>';
$x += 2;
} else {
- $table[$x][$y + 1] = '<div style="background:url(' . e('"' . asset('css/images/vline.png') . '"') . ') repeat-y center; height: 64px; text-align:center; "><div class="vline-text" style="display: inline-block; width: 50%; line-height: 64px;">' . Functions::getRelationshipNameFromPath($relationships[$n], Registry::individualFactory()->make($path[$n - 1], $tree), Registry::individualFactory()->make($path[$n + 1], $tree)) . '</div><div style="display: inline-block; width: 50%; line-height: 32px">' . view('icons/arrow-up') . '</div></div>';
+ $table[$x][$y + 1] = '<div style="background:url(' . e('"' . asset('css/images/vline.png') . '"') . ') repeat-y center; height: 64px; text-align:center; "><div class="vline-text" style="display: inline-block; width: 50%; line-height: 64px;">' . app(RelationshipService::class)->legacyNameAlgorithm($relationships[$n], Registry::individualFactory()->make($path[$n - 1], $tree), Registry::individualFactory()->make($path[$n + 1], $tree)) . '</div><div style="display: inline-block; width: 50%; line-height: 32px">' . view('icons/arrow-up') . '</div></div>';
}
$y += 2;
break;
diff --git a/app/Relationship.php b/app/Relationship.php
new file mode 100644
index 0000000000..e3812a8a4f
--- /dev/null
+++ b/app/Relationship.php
@@ -0,0 +1,554 @@
+<?php
+
+/**
+ * webtrees: online genealogy
+ * Copyright (C) 2021 webtrees development team
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees;
+
+use Closure;
+
+use function abs;
+use function array_slice;
+use function count;
+use function in_array;
+use function intdiv;
+use function min;
+
+/**
+ * Class Relationship - define a relationship for a language.
+ */
+class Relationship
+{
+ // The basic components of a relationship.
+ // These strings are needed for compatibility with the legacy algorithm.
+ // Once that has been replaced, it may be more efficient to use integers here.
+ public const SISTER = 'sis';
+ public const BROTHER = 'bro';
+ public const SIBLING = 'sib';
+ public const MOTHER = 'mot';
+ public const FATHER = 'fat';
+ public const PARENT = 'par';
+ public const DAUGHTER = 'dau';
+ public const SON = 'son';
+ public const CHILD = 'chi';
+ public const WIFE = 'wif';
+ public const HUSBAND = 'hus';
+ public const SPOUSE = 'spo';
+
+ public const SIBLINGS = ['F' => self::SISTER, 'M' => self::BROTHER, 'U' => self::SIBLING];
+ public const PARENTS = ['F' => self::MOTHER, 'M' => self::FATHER, 'U' => self::PARENT];
+ public const CHILDREN = ['F' => self::DAUGHTER, 'M' => self::SON, 'U' => self::CHILD];
+ public const SPOUSES = ['F' => self::WIFE, 'M' => self::HUSBAND, 'U' => self::SPOUSE];
+
+ // Generates a name from the matched relationship.
+ private Closure $callback;
+
+ // List of rules that need to match.
+ private array $matchers;
+
+ /**
+ * Relationship constructor.
+ *
+ * @param Closure $callback
+ */
+ private function __construct(Closure $callback)
+ {
+ $this->callback = $callback;
+ $this->matchers = [];
+ }
+
+ /**
+ * Allow fluent constructor.
+ *
+ * @param string $nominative
+ * @param string $genitive
+ *
+ * @return Relationship
+ */
+ public static function fixed(string $nominative, string $genitive): Relationship
+ {
+ return new static(fn () => [$nominative, $genitive]);
+ }
+
+ /**
+ * Allow fluent constructor.
+ *
+ * @param Closure $callback
+ *
+ * @return Relationship
+ */
+ public static function dynamic(Closure $callback): Relationship
+ {
+ return new static($callback);
+ }
+
+ /**
+ * Does this relationship match the pattern?
+ *
+ * @param array<Individual|Family> $nodes
+ * @param array<string> $patterns
+ *
+ * @return array [nominative, genitive] or null
+ */
+ public function match(array $nodes, array $patterns): ?array
+ {
+ $captures = [];
+
+ foreach ($this->matchers as $matcher) {
+ if (!$matcher($nodes, $patterns, $captures)) {
+ return null;
+ }
+ }
+
+ if ($patterns === []) {
+ return ($this->callback)(...$captures);
+ }
+
+ return null;
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function adopted(): Relationship
+ {
+ $this->matchers[] = fn (array $nodes): bool => count($nodes) > 2 && $nodes[2]
+ ->facts(['FAMC'], false, Auth::PRIV_HIDE)
+ ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === 'adopted');
+
+ return $this;
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function adoptive(): Relationship
+ {
+ $this->matchers[] = fn (array $nodes): bool => $nodes[0]
+ ->facts(['FAMC'], false, Auth::PRIV_HIDE)
+ ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === 'adopted');
+
+ return $this;
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function brother(): Relationship
+ {
+ return $this->relation([self::BROTHER]);
+ }
+
+ /**
+ * Match the next relationship in the path.
+ *
+ * @param array<string> $relationships
+ *
+ * @return Relationship
+ */
+ protected function relation(array $relationships): Relationship
+ {
+ $this->matchers[] = static function (array &$nodes, array &$patterns) use ($relationships): bool {
+ if (in_array($patterns[0] ?? '', $relationships, true)) {
+ $nodes = array_slice($nodes, 2);
+ $patterns = array_slice($patterns, 1);
+
+ return true;
+ }
+
+ return false;
+ };
+
+ return $this;
+ }
+
+ /**
+ * The number of ancestors may be different to the number of descendants
+ *
+ * @return Relationship
+ */
+ public function asymmetricCousin(): Relationship
+ {
+ return $this->ancestor()->sibling()->descendant();
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function descendant(): Relationship
+ {
+ return $this->repeatedRelationship(self::CHILDREN);
+ }
+
+ /**
+ * Match a repeated number of the same type of component
+ *
+ * @param array<string> $relationships
+ *
+ * @return Relationship
+ */
+ protected function repeatedRelationship(array $relationships): Relationship
+ {
+ $this->matchers[] = static function (array &$nodes, array &$patterns, array &$captures) use ($relationships): bool {
+ $limit = min(intdiv(count($nodes), 2), count($patterns));
+
+ for ($generations = 0; $generations < $limit; ++$generations) {
+ if (!in_array($patterns[$generations], $relationships, true)) {
+ break;
+ }
+ }
+
+ if ($generations > 0) {
+ $nodes = array_slice($nodes, 2 * $generations);
+ $patterns = array_slice($patterns, $generations);
+ $captures[] = $generations;
+
+ return true;
+ }
+
+ return false;
+ };
+
+ return $this;
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function sibling(): Relationship
+ {
+ return $this->relation(self::SIBLINGS);
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function ancestor(): Relationship
+ {
+ return $this->repeatedRelationship(self::PARENTS);
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function child(): Relationship
+ {
+ return $this->relation(self::CHILDREN);
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function daughter(): Relationship
+ {
+ return $this->relation([self::DAUGHTER]);
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function divorced(): Relationship
+ {
+ return $this->marriageStatus('DIV');
+ }
+
+ /**
+ * Match a marriage status
+ *
+ * @param string $status
+ *
+ * @return Relationship
+ */
+ protected function marriageStatus(string $status): Relationship
+ {
+ $this->matchers[] = static function (array $nodes) use ($status): bool {
+ $family = $nodes[1] ?? null;
+
+ if ($family instanceof Family) {
+ $fact = $family->facts(['ENGA', 'MARR', 'DIV', 'ANUL'], true, Auth::PRIV_HIDE)->last();
+
+ if ($fact instanceof Fact) {
+ switch ($status) {
+ case 'MARR':
+ return $fact->tag() === 'FAM:MARR';
+
+ case 'DIV':
+ return $fact->tag() === 'FAM:DIV' || $fact->tag() === 'FAM:ANUL';
+
+ case 'ENGA':
+ return $fact->tag() === 'FAM:ENGA';
+ }
+ }
+ }
+
+ return false;
+ };
+
+ return $this;
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function engaged(): Relationship
+ {
+ return $this->marriageStatus('ENGA');
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function father(): Relationship
+ {
+ return $this->relation([self::FATHER]);
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function female(): Relationship
+ {
+ return $this->sex('F');
+ }
+
+ /**
+ * Match the sex of the current individual
+ *
+ * @param string $sex
+ *
+ * @return Relationship
+ */
+ protected function sex(string $sex): Relationship
+ {
+ $this->matchers[] = static function (array $nodes) use ($sex): bool {
+ return $nodes[0]->sex() === $sex;
+ };
+
+ return $this;
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function fostered(): Relationship
+ {
+ $this->matchers[] = fn (array $nodes): bool => count($nodes) > 2 && $nodes[2]
+ ->facts(['FAMC'], false, Auth::PRIV_HIDE)
+ ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === 'foster');
+
+ return $this;
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function fostering(): Relationship
+ {
+ $this->matchers[] = fn (array $nodes): bool => $nodes[0]
+ ->facts(['FAMC'], false, Auth::PRIV_HIDE)
+ ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === 'foster');
+
+ return $this;
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function husband(): Relationship
+ {
+ return $this->married()->relation([self::HUSBAND]);
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function married(): Relationship
+ {
+ return $this->marriageStatus('MARR');
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function male(): Relationship
+ {
+ return $this->sex('M');
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function mother(): Relationship
+ {
+ return $this->relation([self::MOTHER]);
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function older(): Relationship
+ {
+ $this->matchers[] = static function (array $nodes): bool {
+ if (count($nodes) > 2) {
+ $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
+ $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
+
+ return Date::compare($date1, $date2) < 0;
+ }
+
+ return false;
+ };
+
+ return $this;
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function parent(): Relationship
+ {
+ return $this->relation(self::PARENTS);
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function sister(): Relationship
+ {
+ return $this->relation([self::SISTER]);
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function son(): Relationship
+ {
+ return $this->relation([self::SON]);
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function spouse(): Relationship
+ {
+ return $this->married()->partner();
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function partner(): Relationship
+ {
+ return $this->relation(self::SPOUSES);
+ }
+
+ /**
+ * The number of ancestors must be the same as the number of descendants
+ *
+ * @return Relationship
+ */
+ public function symmetricCousin(): Relationship
+ {
+ $this->matchers[] = static function (array &$nodes, array &$patterns, array &$captures): bool {
+ $count = count($patterns);
+
+ $n = 0;
+
+ // Ancestors
+ while ($n < $count && in_array($patterns[$n], Relationship::PARENTS, true)) {
+ $n++;
+ }
+
+ // No ancestors? Not enough path left for descendants?
+ if ($n === 0 || $n * 2 + 1 !== $count) {
+ return false;
+ }
+
+ // Siblings
+ if (!in_array($patterns[$n], Relationship::SIBLINGS, true)) {
+ return false;
+ }
+
+ // Descendants
+ for ($descendants = $n + 1; $descendants < $count; ++$descendants) {
+ if (!in_array($patterns[$descendants], Relationship::CHILDREN, true)) {
+ return false;
+ }
+ }
+
+
+ $nodes = array_slice($nodes, 2 * $n);
+ $patterns = [];
+ $captures[] = $n;
+
+ return true;
+ };
+
+ return $this;
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function twin(): Relationship
+ {
+ $this->matchers[] = static function (array $nodes): bool {
+ if (count($nodes) > 2) {
+ $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
+ $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
+
+ return
+ $date1->isOK() &&
+ $date2->isOK() &&
+ abs($date1->julianDay() - $date2->julianDay()) < 2 &&
+ $date1->minimumDate()->day > 0 &&
+ $date2->minimumDate()->day > 0;
+ }
+
+ return false;
+ };
+
+ return $this;
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function wife(): Relationship
+ {
+ return $this->married()->relation([self::WIFE]);
+ }
+
+ /**
+ * @return Relationship
+ */
+ public function younger(): Relationship
+ {
+ $this->matchers[] = static function (array $nodes): bool {
+ if (count($nodes) > 2) {
+ $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
+ $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
+
+ return Date::compare($date1, $date2) > 0;
+ }
+
+ return false;
+ };
+
+ return $this;
+ }
+}
diff --git a/app/Services/RelationshipService.php b/app/Services/RelationshipService.php
new file mode 100644
index 0000000000..2b7d4d4bff
--- /dev/null
+++ b/app/Services/RelationshipService.php
@@ -0,0 +1,2368 @@
+<?php
+
+/**
+ * webtrees: online genealogy
+ * Copyright (C) 2021 webtrees development team
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees\Services;
+
+use Fisharebest\Webtrees\Auth;
+use Fisharebest\Webtrees\Fact;
+use Fisharebest\Webtrees\Family;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Individual;
+use Fisharebest\Webtrees\Module\ModuleLanguageInterface;
+use Fisharebest\Webtrees\Relationship;
+
+use function abs;
+use function array_key_exists;
+use function array_merge;
+use function array_reduce;
+use function array_slice;
+use function count;
+use function implode;
+use function intdiv;
+use function min;
+use function preg_match;
+use function sprintf;
+use function strlen;
+use function substr;
+
+/**
+ * Names for relationships.
+ */
+class RelationshipService
+{
+ private const COMPONENTS = [
+ 'CHIL' => [
+ 'CHIL' => Relationship::SIBLINGS,
+ 'HUSB' => Relationship::PARENTS,
+ 'WIFE' => Relationship::PARENTS,
+ ],
+ 'HUSB' => [
+ 'CHIL' => Relationship::CHILDREN,
+ 'HUSB' => Relationship::SPOUSES,
+ 'WIFE' => Relationship::SPOUSES,
+ ],
+ 'WIFE' => [
+ 'CHIL' => Relationship::CHILDREN,
+ 'HUSB' => Relationship::SPOUSES,
+ 'WIFE' => Relationship::SPOUSES,
+ ],
+ ];
+
+ /**
+ * For close family relationships, such as the families tab, associates, and the family navigator.
+ *
+ * @param Individual $individual1
+ * @param Individual $individual2
+ *
+ * @return string
+ */
+ public function getCloseRelationshipName(Individual $individual1, Individual $individual2): string
+ {
+ $language = app(ModuleService::class)
+ ->findByInterface(ModuleLanguageInterface::class, true)
+ ->first(fn (ModuleLanguageInterface $language): bool => $language->locale()->languageTag() === I18N::languageTag());
+
+ $path = $this->getCloseRelationship($individual1, $individual2);
+
+ // No relationship found?
+ if ($path === []) {
+ return '';
+ }
+
+ return $this->nameFromPath($path, $language);
+ }
+
+ /**
+ * Get relationship between two individuals in the gedcom. This function
+ * takes account of pending changes, so we can display names of newly added
+ * relations.
+ *
+ * @param Individual $individual1
+ * @param Individual $individual2
+ * @param int $maxlength
+ *
+ * @return array<Individual|Family> An array of nodes on the relationship path
+ */
+ private function getCloseRelationship(Individual $individual1, Individual $individual2, int $maxlength = 4): array
+ {
+ if ($individual1 === $individual2) {
+ return [$individual1];
+ }
+
+ // Only examine each individual once
+ $visited = [
+ $individual1->xref() => true,
+ ];
+
+ // Build paths out from the first individual
+ $paths = [
+ [$individual1],
+ ];
+
+ // Loop over paths of length 1, 2, 3, ...
+ while ($maxlength >= 0) {
+ $maxlength--;
+
+ foreach ($paths as $i => $path) {
+ // Try each new relation from the end of the path
+ $indi = $path[count($path) - 1];
+
+ // Parents and siblings
+ foreach ($indi->childFamilies(Auth::PRIV_HIDE) as $family) {
+ $visited[$family->xref()] = true;
+ foreach ($family->spouses(Auth::PRIV_HIDE) as $spouse) {
+ if (!isset($visited[$spouse->xref()])) {
+ $new_path = $path;
+ $new_path[] = $family;
+ $new_path[] = $spouse;
+ if ($spouse === $individual2) {
+ return $new_path;
+ }
+
+ $paths[] = $new_path;
+ $visited[$spouse->xref()] = true;
+ }
+ }
+ foreach ($family->children(Auth::PRIV_HIDE) as $child) {
+ if (!isset($visited[$child->xref()])) {
+ $new_path = $path;
+ $new_path[] = $family;
+ $new_path[] = $child;
+ if ($child === $individual2) {
+ return $new_path;
+ }
+
+ $paths[] = $new_path;
+ $visited[$child->xref()] = true;
+ }
+ }
+ }
+
+ // Spouses and children
+ foreach ($indi->spouseFamilies(Auth::PRIV_HIDE) as $family) {
+ $visited[$family->xref()] = true;
+ foreach ($family->spouses(Auth::PRIV_HIDE) as $spouse) {
+ if (!isset($visited[$spouse->xref()])) {
+ $new_path = $path;
+ $new_path[] = $family;
+ $new_path[] = $spouse;
+ if ($spouse === $individual2) {
+ return $new_path;
+ }
+
+ $paths[] = $new_path;
+ $visited[$spouse->xref()] = true;
+ }
+ }
+ foreach ($family->children(Auth::PRIV_HIDE) as $child) {
+ if (!isset($visited[$child->xref()])) {
+ $new_path = $path;
+ $new_path[] = $family;
+ $new_path[] = $child;
+ if ($child === $individual2) {
+ return $new_path;
+ }
+
+ $paths[] = $new_path;
+ $visited[$child->xref()] = true;
+ }
+ }
+ }
+ unset($paths[$i]);
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * @param array<Individual|Family> $nodes
+ * @param ModuleLanguageInterface $language
+ *
+ * @return string
+ */
+ public function nameFromPath(array $nodes, ModuleLanguageInterface $language): string
+ {
+ // The relationship matching algorithm could be used for this, but it is more efficient to check it here.
+ if (count($nodes) === 1) {
+ return $this->reflexivePronoun($nodes[0]);
+ }
+
+ // The relationship definitions for the language.
+ $relationships = $language->relationships();
+
+ // We don't strictly need this, as all the information is contained in the nodes.
+ // But it gives us simpler code and better performance.
+ // It is also needed for the legacy algorithm.
+ $pattern = $this->components($nodes);
+
+ // No definitions for this language? Use the legacy algorithm.
+ if ($relationships === []) {
+ return $this->legacyNameAlgorithm(implode('', $pattern), $nodes[0], $nodes[count($nodes) - 1]);
+ }
+
+ // Match the relationship, using a longest-substring algorithm.
+ $relationships = $this->matchRelationships($nodes, $pattern, $relationships);
+
+ // Reduce the genitive-nominative chain to a single string.
+ return array_reduce($relationships, static function (array $carry, array $item): array {
+ return [sprintf($carry[1], $item[0]), sprintf($carry[1], $item[1])];
+ }, [1 => '%s'])[0];
+ }
+
+ /**
+ * Generate a reflexive pronoun for an individual
+ *
+ * @param Individual $individual
+ *
+ * @return string
+ */
+ protected function reflexivePronoun(Individual $individual): string
+ {
+ switch ($individual->sex()) {
+ case 'M':
+ /* I18N: reflexive pronoun */
+ return I18N::translate('himself');
+ case 'F':
+ /* I18N: reflexive pronoun */
+ return I18N::translate('herself');
+ default:
+ /* I18N: reflexive pronoun - gender neutral version of himself/herself */
+ return I18N::translate('themself');
+ }
+ }
+
+ /**
+ * Convert a relationship path into its component pieces; brother, wife, mother, daughter, etc.
+ *
+ * @param array<Individual|Family> $nodes - Alternating list of Individual and Family objects
+ *
+ * @return array<string>
+ */
+ private function components(array $nodes): array
+ {
+ $pattern = [];
+
+ $count = count($nodes);
+
+ for ($i = 1; $i < $count; $i += 2) {
+ $prev = $nodes[$i - 1];
+ $family = $nodes[$i];
+ $next = $nodes[$i + 1];
+
+ preg_match('/\n1 (HUSB|WIFE|CHIL) @' . $prev->xref() . '@/', $family->gedcom(), $match);
+ $rel1 = $match[1] ?? 'xxx';
+
+ preg_match('/\n1 (HUSB|WIFE|CHIL) @' . $next->xref() . '@/', $family->gedcom(), $match);
+ $rel2 = $match[1] ?? 'xxx';
+
+ $pattern[] = self::COMPONENTS[$rel1][$rel2][$next->sex()] ?? 'xxx';
+ }
+
+ return $pattern;
+ }
+
+ /**
+ * @param array<Individual|Family> $nodes
+ * @param array<string> $pattern
+ * @param array<Relationship> $relationships
+ *
+ * @return array<Relationship>
+ */
+ protected function matchRelationships(array $nodes, array $pattern, array $relationships): array
+ {
+ $count = count($pattern);
+
+ // Look for the longest matchable series of components
+ for ($length = $count; $length > 0; $length--) {
+ for ($start = $count - $length; $start >= 0; $start--) {
+ foreach ($relationships as $relationship) {
+ $path_slice = array_slice($nodes, $start * 2, $length * 2 + 1);
+ $pattern_slice = array_slice($pattern, $start, $length);
+ $result = $relationship->match($path_slice, $pattern_slice);
+
+ if ($result !== null) {
+ $nodes_before = array_slice($nodes, 0, $start * 2 + 1);
+ $pattern_before = array_slice($pattern, 0, $start);
+ $result_before = $this->matchRelationships($nodes_before, $pattern_before, $relationships);
+
+ $nodes_after = array_slice($nodes, ($start + $length) * 2);
+ $pattern_after = array_slice($pattern, $start + $length);
+ $result_after = $this->matchRelationships($nodes_after, $pattern_after, $relationships);
+
+ return array_merge($result_before, [$result], $result_after);
+ }
+ }
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ *
+ * @return string
+ *
+ * @deprecated This code was originally Functions::getRelationshipNameFromPath
+ */
+ public function legacyNameAlgorithm(string $path, Individual $person1 = null, Individual $person2 = null): string
+ {
+ // The path does not include the starting person. In some languages, the
+ // translation for a man’s (relative) is different from a woman’s (relative),
+ // due to inflection.
+ $sex1 = $person1 ? $person1->sex() : 'U';
+
+ // The sex of the last person in the relationship determines the name in
+ // many cases. e.g. great-aunt / great-uncle
+ if (preg_match('/(fat|hus|son|bro)$/', $path)) {
+ $sex2 = 'M';
+ } elseif (preg_match('/(mot|wif|dau|sis)$/', $path)) {
+ $sex2 = 'F';
+ } else {
+ $sex2 = 'U';
+ }
+
+ switch ($path) {
+ case '':
+ return I18N::translate('self');
+ // Level One relationships
+ case 'mot':
+ return I18N::translate('mother');
+ case 'fat':
+ return I18N::translate('father');
+ case 'par':
+ return I18N::translate('parent');
+ case 'hus':
+ if ($person1 instanceof Individual && $person2 instanceof Individual) {
+ // We had the linking family earlier, but lost it. Find it again.
+ foreach ($person1->spouseFamilies(Auth::PRIV_HIDE) as $family) {
+ if ($person2 === $family->spouse($person1)) {
+ $event = $family->facts(['ANUL', 'DIV', 'ENGA', 'MARR'], true, Auth::PRIV_HIDE, true)->last();
+
+ if ($event instanceof Fact) {
+ switch ($event->tag()) {
+ case 'FAM:ANUL':
+ case 'FAM:DIV':
+ return I18N::translate('ex-husband');
+ case 'FAM:MARR':
+ return I18N::translate('husband');
+ case 'FAM:ENGA':
+ return I18N::translate('fiancé');
+ }
+ }
+ }
+ }
+ }
+
+ return I18N::translateContext('MALE', 'partner');
+
+ case 'wif':
+ if ($person1 instanceof Individual && $person2 instanceof Individual) {
+ // We had the linking family earlier, but lost it. Find it again.
+ foreach ($person1->spouseFamilies(Auth::PRIV_HIDE) as $family) {
+ if ($person2 === $family->spouse($person1)) {
+ $event = $family->facts(['ANUL', 'DIV', 'ENGA', 'MARR'], true, Auth::PRIV_HIDE, true)->last();
+
+ if ($event instanceof Fact) {
+ switch ($event->tag()) {
+ case 'FAM:ANUL':
+ case 'FAM:DIV':
+ return I18N::translate('ex-wife');
+ case 'FAM:MARR':
+ return I18N::translate('wife');
+ case 'FAM:ENGA':
+ return I18N::translate('fiancée');
+ }
+ }
+ }
+ }
+ }
+
+ return I18N::translateContext('FEMALE', 'partner');
+ case 'spo':
+ if ($person1 instanceof Individual && $person2 instanceof Individual) {
+ // We had the linking family earlier, but lost it. Find it again.
+ foreach ($person1->spouseFamilies(Auth::PRIV_HIDE) as $family) {
+ if ($person2 === $family->spouse($person1)) {
+ $event = $family->facts(['ANUL', 'DIV', 'ENGA', 'MARR'], true, Auth::PRIV_HIDE, true)->last();
+
+ if ($event instanceof Fact) {
+ switch ($event->tag()) {
+ case 'FAM:ANUL':
+ case 'FAM:DIV':
+ return I18N::translate('ex-spouse');
+ case 'FAM:MARR':
+ return I18N::translate('spouse');
+ case 'FAM:ENGA':
+ return I18N::translate('fiancé(e)');
+ }
+ }
+ }
+ }
+ }
+
+ return I18N::translate('partner');
+
+ case 'son':
+ return I18N::translate('son');
+ case 'dau':
+ return I18N::translate('daughter');
+ case 'chi':
+ return I18N::translate('child');
+ case 'bro':
+ if ($person1 && $person2) {
+ $dob1 = $person1->getBirthDate();
+ $dob2 = $person2->getBirthDate();
+ if ($dob1->isOK() && $dob2->isOK()) {
+ if (abs($dob1->julianDay() - $dob2->julianDay()) < 2 && $dob1->minimumDate()->day > 0 && $dob2->minimumDate()->day > 0) {
+ // Exclude BEF, AFT, etc.
+ return I18N::translate('twin brother');
+ }
+
+ if ($dob1->maximumJulianDay() < $dob2->minimumJulianDay()) {
+ return I18N::translate('younger brother');
+ }
+
+ if ($dob1->minimumJulianDay() > $dob2->maximumJulianDay()) {
+ return I18N::translate('elder brother');
+ }
+ }
+ }
+
+ return I18N::translate('brother');
+ case 'sis':
+ if ($person1 && $person2) {
+ $dob1 = $person1->getBirthDate();
+ $dob2 = $person2->getBirthDate();
+ if ($dob1->isOK() && $dob2->isOK()) {
+ if (abs($dob1->julianDay() - $dob2->julianDay()) < 2 && $dob1->minimumDate()->day > 0 && $dob2->minimumDate()->day > 0) {
+ // Exclude BEF, AFT, etc.
+ return I18N::translate('twin sister');
+ }
+
+ if ($dob1->maximumJulianDay() < $dob2->minimumJulianDay()) {
+ return I18N::translate('younger sister');
+ }
+
+ if ($dob1->minimumJulianDay() > $dob2->maximumJulianDay()) {
+ return I18N::translate('elder sister');
+ }
+ }
+ }
+
+ return I18N::translate('sister');
+ case 'sib':
+ if ($person1 && $person2) {
+ $dob1 = $person1->getBirthDate();
+ $dob2 = $person2->getBirthDate();
+ if ($dob1->isOK() && $dob2->isOK()) {
+ if (abs($dob1->julianDay() - $dob2->julianDay()) < 2 && $dob1->minimumDate()->day > 0 && $dob2->minimumDate()->day > 0) {
+ // Exclude BEF, AFT, etc.
+ return I18N::translate('twin sibling');
+ }
+
+ if ($dob1->maximumJulianDay() < $dob2->minimumJulianDay()) {
+ return I18N::translate('younger sibling');
+ }
+
+ if ($dob1->minimumJulianDay() > $dob2->maximumJulianDay()) {
+ return I18N::translate('elder sibling');
+ }
+ }
+ }
+
+ return I18N::translate('sibling');
+
+ // Level Two relationships
+ case 'brochi':
+ return I18N::translateContext('brother’s child', 'nephew/niece');
+ case 'brodau':
+ return I18N::translateContext('brother’s daughter', 'niece');
+ case 'broson':
+ return I18N::translateContext('brother’s son', 'nephew');
+ case 'browif':
+ return I18N::translateContext('brother’s wife', 'sister-in-law');
+ case 'chichi':
+ return I18N::translateContext('child’s child', 'grandchild');
+ case 'chidau':
+ return I18N::translateContext('child’s daughter', 'granddaughter');
+ case 'chihus':
+ return I18N::translateContext('child’s husband', 'son-in-law');
+ case 'chison':
+ return I18N::translateContext('child’s son', 'grandson');
+ case 'chispo':
+ return I18N::translateContext('child’s spouse', 'son/daughter-in-law');
+ case 'chiwif':
+ return I18N::translateContext('child’s wife', 'daughter-in-law');
+ case 'dauchi':
+ return I18N::translateContext('daughter’s child', 'grandchild');
+ case 'daudau':
+ return I18N::translateContext('daughter’s daughter', 'granddaughter');
+ case 'dauhus':
+ return I18N::translateContext('daughter’s husband', 'son-in-law');
+ case 'dauson':
+ return I18N::translateContext('daughter’s son', 'grandson');
+ case 'fatbro':
+ return I18N::translateContext('father’s brother', 'uncle');
+ case 'fatchi':
+ return I18N::translateContext('father’s child', 'half-sibling');
+ case 'fatdau':
+ return I18N::translateContext('father’s daughter', 'half-sister');
+ case 'fatfat':
+ return I18N::translateContext('father’s father', 'paternal grandfather');
+ case 'fatmot':
+ return I18N::translateContext('father’s mother', 'paternal grandmother');
+ case 'fatpar':
+ return I18N::translateContext('father’s parent', 'paternal grandparent');
+ case 'fatsib':
+ return I18N::translateContext('father’s sibling', 'aunt/uncle');
+ case 'fatsis':
+ return I18N::translateContext('father’s sister', 'aunt');
+ case 'fatson':
+ return I18N::translateContext('father’s son', 'half-brother');
+ case 'fatwif':
+ return I18N::translateContext('father’s wife', 'step-mother');
+ case 'husbro':
+ return I18N::translateContext('husband’s brother', 'brother-in-law');
+ case 'huschi':
+ return I18N::translateContext('husband’s child', 'step-child');
+ case 'husdau':
+ return I18N::translateContext('husband’s daughter', 'step-daughter');
+ case 'husfat':
+ return I18N::translateContext('husband’s father', 'father-in-law');
+ case 'husmot':
+ return I18N::translateContext('husband’s mother', 'mother-in-law');
+ case 'hussib':
+ return I18N::translateContext('husband’s sibling', 'brother/sister-in-law');
+ case 'hussis':
+ return I18N::translateContext('husband’s sister', 'sister-in-law');
+ case 'husson':
+ return I18N::translateContext('husband’s son', 'step-son');
+ case 'motbro':
+ return I18N::translateContext('mother’s brother', 'uncle');
+ case 'motchi':
+ return I18N::translateContext('mother’s child', 'half-sibling');
+ case 'motdau':
+ return I18N::translateContext('mother’s daughter', 'half-sister');
+ case 'motfat':
+ return I18N::translateContext('mother’s father', 'maternal grandfather');
+ case 'mothus':
+ return I18N::translateContext('mother’s husband', 'step-father');
+ case 'motmot':
+ return I18N::translateContext('mother’s mother', 'maternal grandmother');
+ case 'motpar':
+ return I18N::translateContext('mother’s parent', 'maternal grandparent');
+ case 'motsib':
+ return I18N::translateContext('mother’s sibling', 'aunt/uncle');
+ case 'motsis':
+ return I18N::translateContext('mother’s sister', 'aunt');
+ case 'motson':
+ return I18N::translateContext('mother’s son', 'half-brother');
+ case 'parbro':
+ return I18N::translateContext('parent’s brother', 'uncle');
+ case 'parchi':
+ return I18N::translateContext('parent’s child', 'half-sibling');
+ case 'pardau':
+ return I18N::translateContext('parent’s daughter', 'half-sister');
+ case 'parfat':
+ return I18N::translateContext('parent’s father', 'grandfather');
+ case 'parmot':
+ return I18N::translateContext('parent’s mother', 'grandmother');
+ case 'parpar':
+ return I18N::translateContext('parent’s parent', 'grandparent');
+ case 'parsib':
+ return I18N::translateContext('parent’s sibling', 'aunt/uncle');
+ case 'parsis':
+ return I18N::translateContext('parent’s sister', 'aunt');
+ case 'parson':
+ return I18N::translateContext('parent’s son', 'half-brother');
+ case 'parspo':
+ return I18N::translateContext('parent’s spouse', 'step-parent');
+ case 'sibchi':
+ return I18N::translateContext('sibling’s child', 'nephew/niece');
+ case 'sibdau':
+ return I18N::translateContext('sibling’s daughter', 'niece');
+ case 'sibson':
+ return I18N::translateContext('sibling’s son', 'nephew');
+ case 'sibspo':
+ return I18N::translateContext('sibling’s spouse', 'brother/sister-in-law');
+ case 'sischi':
+ return I18N::translateContext('sister’s child', 'nephew/niece');
+ case 'sisdau':
+ return I18N::translateContext('sister’s daughter', 'niece');
+ case 'sishus':
+ return I18N::translateContext('sister’s husband', 'brother-in-law');
+ case 'sisson':
+ return I18N::translateContext('sister’s son', 'nephew');
+ case 'sonchi':
+ return I18N::translateContext('son’s child', 'grandchild');
+ case 'sondau':
+ return I18N::translateContext('son’s daughter', 'granddaughter');
+ case 'sonson':
+ return I18N::translateContext('son’s son', 'grandson');
+ case 'sonwif':
+ return I18N::translateContext('son’s wife', 'daughter-in-law');
+ case 'spobro':
+ return I18N::translateContext('spouse’s brother', 'brother-in-law');
+ case 'spochi':
+ return I18N::translateContext('spouse’s child', 'step-child');
+ case 'spodau':
+ return I18N::translateContext('spouse’s daughter', 'step-daughter');
+ case 'spofat':
+ return I18N::translateContext('spouse’s father', 'father-in-law');
+ case 'spomot':
+ return I18N::translateContext('spouse’s mother', 'mother-in-law');
+ case 'sposis':
+ return I18N::translateContext('spouse’s sister', 'sister-in-law');
+ case 'sposon':
+ return I18N::translateContext('spouse’s son', 'step-son');
+ case 'spopar':
+ return I18N::translateContext('spouse’s parent', 'mother/father-in-law');
+ case 'sposib':
+ return I18N::translateContext('spouse’s sibling', 'brother/sister-in-law');
+ case 'wifbro':
+ return I18N::translateContext('wife’s brother', 'brother-in-law');
+ case 'wifchi':
+ return I18N::translateContext('wife’s child', 'step-child');
+ case 'wifdau':
+ return I18N::translateContext('wife’s daughter', 'step-daughter');
+ case 'wiffat':
+ return I18N::translateContext('wife’s father', 'father-in-law');
+ case 'wifmot':
+ return I18N::translateContext('wife’s mother', 'mother-in-law');
+ case 'wifsib':
+ return I18N::translateContext('wife’s sibling', 'brother/sister-in-law');
+ case 'wifsis':
+ return I18N::translateContext('wife’s sister', 'sister-in-law');
+ case 'wifson':
+ return I18N::translateContext('wife’s son', 'step-son');
+
+ // Level Three relationships
+ case 'brochichi':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s child’s child', 'great-nephew/niece');
+ }
+
+ return I18N::translateContext('(a woman’s) brother’s child’s child', 'great-nephew/niece');
+ case 'brochidau':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s child’s daughter', 'great-niece');
+ }
+
+ return I18N::translateContext('(a woman’s) brother’s child’s daughter', 'great-niece');
+ case 'brochison':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s child’s son', 'great-nephew');
+ }
+
+ return I18N::translateContext('(a woman’s) brother’s child’s son', 'great-nephew');
+ case 'brodauchi':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s daughter’s child', 'great-nephew/niece');
+ }
+
+ return I18N::translateContext('(a woman’s) brother’s daughter’s child', 'great-nephew/niece');
+ case 'brodaudau':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s daughter’s daughter', 'great-niece');
+ }
+
+ return I18N::translateContext('(a woman’s) brother’s daughter’s daughter', 'great-niece');
+ case 'brodauhus':
+ return I18N::translateContext('brother’s daughter’s husband', 'nephew-in-law');
+ case 'brodauson':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s daughter’s son', 'great-nephew');
+ }
+
+ return I18N::translateContext('(a woman’s) brother’s daughter’s son', 'great-nephew');
+ case 'brosonchi':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s son’s child', 'great-nephew/niece');
+ }
+
+ return I18N::translateContext('(a woman’s) brother’s son’s child', 'great-nephew/niece');
+ case 'brosondau':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s son’s daughter', 'great-niece');
+ }
+
+ return I18N::translateContext('(a woman’s) brother’s son’s daughter', 'great-niece');
+ case 'brosonson':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s son’s son', 'great-nephew');
+ }
+
+ return I18N::translateContext('(a woman’s) brother’s son’s son', 'great-nephew');
+ case 'brosonwif':
+ return I18N::translateContext('brother’s son’s wife', 'niece-in-law');
+ case 'browifbro':
+ return I18N::translateContext('brother’s wife’s brother', 'brother-in-law');
+ case 'browifsib':
+ return I18N::translateContext('brother’s wife’s sibling', 'brother/sister-in-law');
+ case 'browifsis':
+ return I18N::translateContext('brother’s wife’s sister', 'sister-in-law');
+ case 'chichichi':
+ return I18N::translateContext('child’s child’s child', 'great-grandchild');
+ case 'chichidau':
+ return I18N::translateContext('child’s child’s daughter', 'great-granddaughter');
+ case 'chichison':
+ return I18N::translateContext('child’s child’s son', 'great-grandson');
+ case 'chidauchi':
+ return I18N::translateContext('child’s daughter’s child', 'great-grandchild');
+ case 'chidaudau':
+ return I18N::translateContext('child’s daughter’s daughter', 'great-granddaughter');
+ case 'chidauhus':
+ return I18N::translateContext('child’s daughter’s husband', 'granddaughter’s husband');
+ case 'chidauson':
+ return I18N::translateContext('child’s daughter’s son', 'great-grandson');
+ case 'chisonchi':
+ return I18N::translateContext('child’s son’s child', 'great-grandchild');
+ case 'chisondau':
+ return I18N::translateContext('child’s son’s daughter', 'great-granddaughter');
+ case 'chisonson':
+ return I18N::translateContext('child’s son’s son', 'great-grandson');
+ case 'chisonwif':
+ return I18N::translateContext('child’s son’s wife', 'grandson’s wife');
+ case 'dauchichi':
+ return I18N::translateContext('daughter’s child’s child', 'great-grandchild');
+ case 'dauchidau':
+ return I18N::translateContext('daughter’s child’s daughter', 'great-granddaughter');
+ case 'dauchison':
+ return I18N::translateContext('daughter’s child’s son', 'great-grandson');
+ case 'daudauchi':
+ return I18N::translateContext('daughter’s daughter’s child', 'great-grandchild');
+ case 'daudaudau':
+ return I18N::translateContext('daughter’s daughter’s daughter', 'great-granddaughter');
+ case 'daudauhus':
+ return I18N::translateContext('daughter’s daughter’s husband', 'granddaughter’s husband');
+ case 'daudauson':
+ return I18N::translateContext('daughter’s daughter’s son', 'great-grandson');
+ case 'dauhusfat':
+ return I18N::translateContext('daughter’s husband’s father', 'son-in-law’s father');
+ case 'dauhusmot':
+ return I18N::translateContext('daughter’s husband’s mother', 'son-in-law’s mother');
+ case 'dauhuspar':
+ return I18N::translateContext('daughter’s husband’s parent', 'son-in-law’s parent');
+ case 'dausonchi':
+ return I18N::translateContext('daughter’s son’s child', 'great-grandchild');
+ case 'dausondau':
+ return I18N::translateContext('daughter’s son’s daughter', 'great-granddaughter');
+ case 'dausonson':
+ return I18N::translateContext('daughter’s son’s son', 'great-grandson');
+ case 'dausonwif':
+ return I18N::translateContext('daughter’s son’s wife', 'grandson’s wife');
+ case 'fatbrochi':
+ return I18N::translateContext('father’s brother’s child', 'first cousin');
+ case 'fatbrodau':
+ return I18N::translateContext('father’s brother’s daughter', 'first cousin');
+ case 'fatbroson':
+ return I18N::translateContext('father’s brother’s son', 'first cousin');
+ case 'fatbrowif':
+ return I18N::translateContext('father’s brother’s wife', 'aunt');
+ case 'fatfatbro':
+ return I18N::translateContext('father’s father’s brother', 'great-uncle');
+ case 'fatfatfat':
+ return I18N::translateContext('father’s father’s father', 'great-grandfather');
+ case 'fatfatmot':
+ return I18N::translateContext('father’s father’s mother', 'great-grandmother');
+ case 'fatfatpar':
+ return I18N::translateContext('father’s father’s parent', 'great-grandparent');
+ case 'fatfatsib':
+ return I18N::translateContext('father’s father’s sibling', 'great-aunt/uncle');
+ case 'fatfatsis':
+ return I18N::translateContext('father’s father’s sister', 'great-aunt');
+ case 'fatmotbro':
+ return I18N::translateContext('father’s mother’s brother', 'great-uncle');
+ case 'fatmotfat':
+ return I18N::translateContext('father’s mother’s father', 'great-grandfather');
+ case 'fatmotmot':
+ return I18N::translateContext('father’s mother’s mother', 'great-grandmother');
+ case 'fatmotpar':
+ return I18N::translateContext('father’s mother’s parent', 'great-grandparent');
+ case 'fatmotsib':
+ return I18N::translateContext('father’s mother’s sibling', 'great-aunt/uncle');
+ case 'fatmotsis':
+ return I18N::translateContext('father’s mother’s sister', 'great-aunt');
+ case 'fatparbro':
+ return I18N::translateContext('father’s parent’s brother', 'great-uncle');
+ case 'fatparfat':
+ return I18N::translateContext('father’s parent’s father', 'great-grandfather');
+ case 'fatparmot':
+ return I18N::translateContext('father’s parent’s mother', 'great-grandmother');
+ case 'fatparpar':
+ return I18N::translateContext('father’s parent’s parent', 'great-grandparent');
+ case 'fatparsib':
+ return I18N::translateContext('father’s parent’s sibling', 'great-aunt/uncle');
+ case 'fatparsis':
+ return I18N::translateContext('father’s parent’s sister', 'great-aunt');
+ case 'fatsischi':
+ return I18N::translateContext('father’s sister’s child', 'first cousin');
+ case 'fatsisdau':
+ return I18N::translateContext('father’s sister’s daughter', 'first cousin');
+ case 'fatsishus':
+ return I18N::translateContext('father’s sister’s husband', 'uncle');
+ case 'fatsisson':
+ return I18N::translateContext('father’s sister’s son', 'first cousin');
+ case 'fatwifchi':
+ return I18N::translateContext('father’s wife’s child', 'step-sibling');
+ case 'fatwifdau':
+ return I18N::translateContext('father’s wife’s daughter', 'step-sister');
+ case 'fatwifson':
+ return I18N::translateContext('father’s wife’s son', 'step-brother');
+ case 'husbrowif':
+ return I18N::translateContext('husband’s brother’s wife', 'sister-in-law');
+ case 'hussishus':
+ return I18N::translateContext('husband’s sister’s husband', 'brother-in-law');
+ case 'hussibchi':
+ return I18N::translateContext('husband’s sibling’s child', 'nephew/niece');
+ case 'hussischi':
+ return I18N::translateContext('husband’s sister’s child', 'nephew/niece');
+ case 'husbrochi':
+ return I18N::translateContext('husband’s brother’s child', 'nephew/niece');
+ case 'hussibdau':
+ return I18N::translateContext('husband’s sibling’s daughter', 'niece');
+ case 'hussisdau':
+ return I18N::translateContext('husband’s sister’s daughter', 'niece');
+ case 'husbrodau':
+ return I18N::translateContext('husband’s brother’s daughter', 'niece');
+ case 'hussibson':
+ return I18N::translateContext('husband’s sibling’s son', 'nephew');
+ case 'hussisson':
+ return I18N::translateContext('husband’s sister’s son', 'nephew');
+ case 'husbroson':
+ return I18N::translateContext('husband’s brother’s son', 'nephew');
+ case 'motbrochi':
+ return I18N::translateContext('mother’s brother’s child', 'first cousin');
+ case 'motbrodau':
+ return I18N::translateContext('mother’s brother’s daughter', 'first cousin');
+ case 'motbroson':
+ return I18N::translateContext('mother’s brother’s son', 'first cousin');
+ case 'motbrowif':
+ return I18N::translateContext('mother’s brother’s wife', 'aunt');
+ case 'motfatbro':
+ return I18N::translateContext('mother’s father’s brother', 'great-uncle');
+ case 'motfatfat':
+ return I18N::translateContext('mother’s father’s father', 'great-grandfather');
+ case 'motfatmot':
+ return I18N::translateContext('mother’s father’s mother', 'great-grandmother');
+ case 'motfatpar':
+ return I18N::translateContext('mother’s father’s parent', 'great-grandparent');
+ case 'motfatsib':
+ return I18N::translateContext('mother’s father’s sibling', 'great-aunt/uncle');
+ case 'motfatsis':
+ return I18N::translateContext('mother’s father’s sister', 'great-aunt');
+ case 'mothuschi':
+ return I18N::translateContext('mother’s husband’s child', 'step-sibling');
+ case 'mothusdau':
+ return I18N::translateContext('mother’s husband’s daughter', 'step-sister');
+ case 'mothusson':
+ return I18N::translateContext('mother’s husband’s son', 'step-brother');
+ case 'motmotbro':
+ return I18N::translateContext('mother’s mother’s brother', 'great-uncle');
+ case 'motmotfat':
+ return I18N::translateContext('mother’s mother’s father', 'great-grandfather');
+ case 'motmotmot':
+ return I18N::translateContext('mother’s mother’s mother', 'great-grandmother');
+ case 'motmotpar':
+ return I18N::translateContext('mother’s mother’s parent', 'great-grandparent');
+ case 'motmotsib':
+ return I18N::translateContext('mother’s mother’s sibling', 'great-aunt/uncle');
+ case 'motmotsis':
+ return I18N::translateContext('mother’s mother’s sister', 'great-aunt');
+ case 'motparbro':
+ return I18N::translateContext('mother’s parent’s brother', 'great-uncle');
+ case 'motparfat':
+ return I18N::translateContext('mother’s parent’s father', 'great-grandfather');
+ case 'motparmot':
+ return I18N::translateContext('mother’s parent’s mother', 'great-grandmother');
+ case 'motparpar':
+ return I18N::translateContext('mother’s parent’s parent', 'great-grandparent');
+ case 'motparsib':
+ return I18N::translateContext('mother’s parent’s sibling', 'great-aunt/uncle');
+ case 'motparsis':
+ return I18N::translateContext('mother’s parent’s sister', 'great-aunt');
+ case 'motsischi':
+ return I18N::translateContext('mother’s sister’s child', 'first cousin');
+ case 'motsisdau':
+ return I18N::translateContext('mother’s sister’s daughter', 'first cousin');
+ case 'motsishus':
+ return I18N::translateContext('mother’s sister’s husband', 'uncle');
+ case 'motsisson':
+ return I18N::translateContext('mother’s sister’s son', 'first cousin');
+ case 'parbrowif':
+ return I18N::translateContext('parent’s brother’s wife', 'aunt');
+ case 'parfatbro':
+ return I18N::translateContext('parent’s father’s brother', 'great-uncle');
+ case 'parfatfat':
+ return I18N::translateContext('parent’s father’s father', 'great-grandfather');
+ case 'parfatmot':
+ return I18N::translateContext('parent’s father’s mother', 'great-grandmother');
+ case 'parfatpar':
+ return I18N::translateContext('parent’s father’s parent', 'great-grandparent');
+ case 'parfatsib':
+ return I18N::translateContext('parent’s father’s sibling', 'great-aunt/uncle');
+ case 'parfatsis':
+ return I18N::translateContext('parent’s father’s sister', 'great-aunt');
+ case 'parmotbro':
+ return I18N::translateContext('parent’s mother’s brother', 'great-uncle');
+ case 'parmotfat':
+ return I18N::translateContext('parent’s mother’s father', 'great-grandfather');
+ case 'parmotmot':
+ return I18N::translateContext('parent’s mother’s mother', 'great-grandmother');
+ case 'parmotpar':
+ return I18N::translateContext('parent’s mother’s parent', 'great-grandparent');
+ case 'parmotsib':
+ return I18N::translateContext('parent’s mother’s sibling', 'great-aunt/uncle');
+ case 'parmotsis':
+ return I18N::translateContext('parent’s mother’s sister', 'great-aunt');
+ case 'parparbro':
+ return I18N::translateContext('parent’s parent’s brother', 'great-uncle');
+ case 'parparfat':
+ return I18N::translateContext('parent’s parent’s father', 'great-grandfather');
+ case 'parparmot':
+ return I18N::translateContext('parent’s parent’s mother', 'great-grandmother');
+ case 'parparpar':
+ return I18N::translateContext('parent’s parent’s parent', 'great-grandparent');
+ case 'parparsib':
+ return I18N::translateContext('parent’s parent’s sibling', 'great-aunt/uncle');
+ case 'parparsis':
+ return I18N::translateContext('parent’s parent’s sister', 'great-aunt');
+ case 'parsishus':
+ return I18N::translateContext('parent’s sister’s husband', 'uncle');
+ case 'parspochi':
+ return I18N::translateContext('parent’s spouse’s child', 'step-sibling');
+ case 'parspodau':
+ return I18N::translateContext('parent’s spouse’s daughter', 'step-sister');
+ case 'parsposon':
+ return I18N::translateContext('parent’s spouse’s son', 'step-brother');
+ case 'sibchichi':
+ return I18N::translateContext('sibling’s child’s child', 'great-nephew/niece');
+ case 'sibchidau':
+ return I18N::translateContext('sibling’s child’s daughter', 'great-niece');
+ case 'sibchison':
+ return I18N::translateContext('sibling’s child’s son', 'great-nephew');
+ case 'sibdauchi':
+ return I18N::translateContext('sibling’s daughter’s child', 'great-nephew/niece');
+ case 'sibdaudau':
+ return I18N::translateContext('sibling’s daughter’s daughter', 'great-niece');
+ case 'sibdauhus':
+ return I18N::translateContext('sibling’s daughter’s husband', 'nephew-in-law');
+ case 'sibdauson':
+ return I18N::translateContext('sibling’s daughter’s son', 'great-nephew');
+ case 'sibsonchi':
+ return I18N::translateContext('sibling’s son’s child', 'great-nephew/niece');
+ case 'sibsondau':
+ return I18N::translateContext('sibling’s son’s daughter', 'great-niece');
+ case 'sibsonson':
+ return I18N::translateContext('sibling’s son’s son', 'great-nephew');
+ case 'sibsonwif':
+ return I18N::translateContext('sibling’s son’s wife', 'niece-in-law');
+ case 'sischichi':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s child’s child', 'great-nephew/niece');
+ }
+
+ return I18N::translateContext('(a woman’s) sister’s child’s child', 'great-nephew/niece');
+ case 'sischidau':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s child’s daughter', 'great-niece');
+ }
+
+ return I18N::translateContext('(a woman’s) sister’s child’s daughter', 'great-niece');
+ case 'sischison':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s child’s son', 'great-nephew');
+ }
+
+ return I18N::translateContext('(a woman’s) sister’s child’s son', 'great-nephew');
+ case 'sisdauchi':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s daughter’s child', 'great-nephew/niece');
+ }
+
+ return I18N::translateContext('(a woman’s) sister’s daughter’s child', 'great-nephew/niece');
+ case 'sisdaudau':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s daughter’s daughter', 'great-niece');
+ }
+
+ return I18N::translateContext('(a woman’s) sister’s daughter’s daughter', 'great-niece');
+ case 'sisdauhus':
+ return I18N::translateContext('sisters’s daughter’s husband', 'nephew-in-law');
+ case 'sisdauson':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s daughter’s son', 'great-nephew');
+ }
+
+ return I18N::translateContext('(a woman’s) sister’s daughter’s son', 'great-nephew');
+ case 'sishusbro':
+ return I18N::translateContext('sister’s husband’s brother', 'brother-in-law');
+ case 'sishussib':
+ return I18N::translateContext('sister’s husband’s sibling', 'brother/sister-in-law');
+ case 'sishussis':
+ return I18N::translateContext('sister’s husband’s sister', 'sister-in-law');
+ case 'sissonchi':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s son’s child', 'great-nephew/niece');
+ }
+
+ return I18N::translateContext('(a woman’s) sister’s son’s child', 'great-nephew/niece');
+ case 'sissondau':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s son’s daughter', 'great-niece');
+ }
+
+ return I18N::translateContext('(a woman’s) sister’s son’s daughter', 'great-niece');
+ case 'sissonson':
+ if ($sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s son’s son', 'great-nephew');
+ }
+
+ return I18N::translateContext('(a woman’s) sister’s son’s son', 'great-nephew');
+ case 'sissonwif':
+ return I18N::translateContext('sisters’s son’s wife', 'niece-in-law');
+ case 'sonchichi':
+ return I18N::translateContext('son’s child’s child', 'great-grandchild');
+ case 'sonchidau':
+ return I18N::translateContext('son’s child’s daughter', 'great-granddaughter');
+ case 'sonchison':
+ return I18N::translateContext('son’s child’s son', 'great-grandson');
+ case 'sondauchi':
+ return I18N::translateContext('son’s daughter’s child', 'great-grandchild');
+ case 'sondaudau':
+ return I18N::translateContext('son’s daughter’s daughter', 'great-granddaughter');
+ case 'sondauhus':
+ return I18N::translateContext('son’s daughter’s husband', 'granddaughter’s husband');
+ case 'sondauson':
+ return I18N::translateContext('son’s daughter’s son', 'great-grandson');
+ case 'sonsonchi':
+ return I18N::translateContext('son’s son’s child', 'great-grandchild');
+ case 'sonsondau':
+ return I18N::translateContext('son’s son’s daughter', 'great-granddaughter');
+ case 'sonsonson':
+ return I18N::translateContext('son’s son’s son', 'great-grandson');
+ case 'sonsonwif':
+ return I18N::translateContext('son’s son’s wife', 'grandson’s wife');
+ case 'sonwiffat':
+ return I18N::translateContext('son’s wife’s father', 'daughter-in-law’s father');
+ case 'sonwifmot':
+ return I18N::translateContext('son’s wife’s mother', 'daughter-in-law’s mother');
+ case 'sonwifpar':
+ return I18N::translateContext('son’s wife’s parent', 'daughter-in-law’s parent');
+ case 'wifbrowif':
+ return I18N::translateContext('wife’s brother’s wife', 'sister-in-law');
+ case 'wifsishus':
+ return I18N::translateContext('wife’s sister’s husband', 'brother-in-law');
+ case 'wifsibchi':
+ return I18N::translateContext('wife’s sibling’s child', 'nephew/niece');
+ case 'wifsischi':
+ return I18N::translateContext('wife’s sister’s child', 'nephew/niece');
+ case 'wifbrochi':
+ return I18N::translateContext('wife’s brother’s child', 'nephew/niece');
+ case 'wifsibdau':
+ return I18N::translateContext('wife’s sibling’s daughter', 'niece');
+ case 'wifsisdau':
+ return I18N::translateContext('wife’s sister’s daughter', 'niece');
+ case 'wifbrodau':
+ return I18N::translateContext('wife’s brother’s daughter', 'niece');
+ case 'wifsibson':
+ return I18N::translateContext('wife’s sibling’s son', 'nephew');
+ case 'wifsisson':
+ return I18N::translateContext('wife’s sister’s son', 'nephew');
+ case 'wifbroson':
+ return I18N::translateContext('wife’s brother’s son', 'nephew');
+
+ // Some “special case” level four relationships that have specific names in certain languages
+ case 'fatfatbrowif':
+ return I18N::translateContext('father’s father’s brother’s wife', 'great-aunt');
+ case 'fatfatsibspo':
+ return I18N::translateContext('father’s father’s sibling’s spouse', 'great-aunt/uncle');
+ case 'fatfatsishus':
+ return I18N::translateContext('father’s father’s sister’s husband', 'great-uncle');
+ case 'fatmotbrowif':
+ return I18N::translateContext('father’s mother’s brother’s wife', 'great-aunt');
+ case 'fatmotsibspo':
+ return I18N::translateContext('father’s mother’s sibling’s spouse', 'great-aunt/uncle');
+ case 'fatmotsishus':
+ return I18N::translateContext('father’s mother’s sister’s husband', 'great-uncle');
+ case 'fatparbrowif':
+ return I18N::translateContext('father’s parent’s brother’s wife', 'great-aunt');
+ case 'fatparsibspo':
+ return I18N::translateContext('father’s parent’s sibling’s spouse', 'great-aunt/uncle');
+ case 'fatparsishus':
+ return I18N::translateContext('father’s parent’s sister’s husband', 'great-uncle');
+ case 'motfatbrowif':
+ return I18N::translateContext('mother’s father’s brother’s wife', 'great-aunt');
+ case 'motfatsibspo':
+ return I18N::translateContext('mother’s father’s sibling’s spouse', 'great-aunt/uncle');
+ case 'motfatsishus':
+ return I18N::translateContext('mother’s father’s sister’s husband', 'great-uncle');
+ case 'motmotbrowif':
+ return I18N::translateContext('mother’s mother’s brother’s wife', 'great-aunt');
+ case 'motmotsibspo':
+ return I18N::translateContext('mother’s mother’s sibling’s spouse', 'great-aunt/uncle');
+ case 'motmotsishus':
+ return I18N::translateContext('mother’s mother’s sister’s husband', 'great-uncle');
+ case 'motparbrowif':
+ return I18N::translateContext('mother’s parent’s brother’s wife', 'great-aunt');
+ case 'motparsibspo':
+ return I18N::translateContext('mother’s parent’s sibling’s spouse', 'great-aunt/uncle');
+ case 'motparsishus':
+ return I18N::translateContext('mother’s parent’s sister’s husband', 'great-uncle');
+ case 'parfatbrowif':
+ return I18N::translateContext('parent’s father’s brother’s wife', 'great-aunt');
+ case 'parfatsibspo':
+ return I18N::translateContext('parent’s father’s sibling’s spouse', 'great-aunt/uncle');
+ case 'parfatsishus':
+ return I18N::translateContext('parent’s father’s sister’s husband', 'great-uncle');
+ case 'parmotbrowif':
+ return I18N::translateContext('parent’s mother’s brother’s wife', 'great-aunt');
+ case 'parmotsibspo':
+ return I18N::translateContext('parent’s mother’s sibling’s spouse', 'great-aunt/uncle');
+ case 'parmotsishus':
+ return I18N::translateContext('parent’s mother’s sister’s husband', 'great-uncle');
+ case 'parparbrowif':
+ return I18N::translateContext('parent’s parent’s brother’s wife', 'great-aunt');
+ case 'parparsibspo':
+ return I18N::translateContext('parent’s parent’s sibling’s spouse', 'great-aunt/uncle');
+ case 'parparsishus':
+ return I18N::translateContext('parent’s parent’s sister’s husband', 'great-uncle');
+ case 'fatfatbrodau':
+ return I18N::translateContext('father’s father’s brother’s daughter', 'first cousin once removed ascending');
+ case 'fatfatbroson':
+ return I18N::translateContext('father’s father’s brother’s son', 'first cousin once removed ascending');
+ case 'fatfatbrochi':
+ return I18N::translateContext('father’s father’s brother’s child', 'first cousin once removed ascending');
+ case 'fatfatsisdau':
+ return I18N::translateContext('father’s father’s sister’s daughter', 'first cousin once removed ascending');
+ case 'fatfatsisson':
+ return I18N::translateContext('father’s father’s sister’s son', 'first cousin once removed ascending');
+ case 'fatfatsischi':
+ return I18N::translateContext('father’s father’s sister’s child', 'first cousin once removed ascending');
+ case 'fatmotbrodau':
+ return I18N::translateContext('father’s mother’s brother’s daughter', 'first cousin once removed ascending');
+ case 'fatmotbroson':
+ return I18N::translateContext('father’s mother’s brother’s son', 'first cousin once removed ascending');
+ case 'fatmotbrochi':
+ return I18N::translateContext('father’s mother’s brother’s child', 'first cousin once removed ascending');
+ case 'fatmotsisdau':
+ return I18N::translateContext('father’s mother’s sister’s daughter', 'first cousin once removed ascending');
+ case 'fatmotsisson':
+ return I18N::translateContext('father’s mother’s sister’s son', 'first cousin once removed ascending');
+ case 'fatmotsischi':
+ return I18N::translateContext('father’s mother’s sister’s child', 'first cousin once removed ascending');
+ case 'motfatbrodau':
+ return I18N::translateContext('mother’s father’s brother’s daughter', 'first cousin once removed ascending');
+ case 'motfatbroson':
+ return I18N::translateContext('mother’s father’s brother’s son', 'first cousin once removed ascending');
+ case 'motfatbrochi':
+ return I18N::translateContext('mother’s father’s brother’s child', 'first cousin once removed ascending');
+ case 'motfatsisdau':
+ return I18N::translateContext('mother’s father’s sister’s daughter', 'first cousin once removed ascending');
+ case 'motfatsisson':
+ return I18N::translateContext('mother’s father’s sister’s son', 'first cousin once removed ascending');
+ case 'motfatsischi':
+ return I18N::translateContext('mother’s father’s sister’s child', 'first cousin once removed ascending');
+ case 'motmotbrodau':
+ return I18N::translateContext('mother’s mother’s brother’s daughter', 'first cousin once removed ascending');
+ case 'motmotbroson':
+ return I18N::translateContext('mother’s mother’s brother’s son', 'first cousin once removed ascending');
+ case 'motmotbrochi':
+ return I18N::translateContext('mother’s mother’s brother’s child', 'first cousin once removed ascending');
+ case 'motmotsisdau':
+ return I18N::translateContext('mother’s mother’s sister’s daughter', 'first cousin once removed ascending');
+ case 'motmotsisson':
+ return I18N::translateContext('mother’s mother’s sister’s son', 'first cousin once removed ascending');
+ case 'motmotsischi':
+ return I18N::translateContext('mother’s mother’s sister’s child', 'first cousin once removed ascending');
+ }
+
+ // Some “special case” level five relationships that have specific names in certain languages
+ if (preg_match('/^(mot|fat|par)fatbro(son|dau|chi)dau$/', $path)) {
+ return I18N::translateContext('grandfather’s brother’s granddaughter', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)fatbro(son|dau|chi)son$/', $path)) {
+ return I18N::translateContext('grandfather’s brother’s grandson', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)fatbro(son|dau|chi)chi$/', $path)) {
+ return I18N::translateContext('grandfather’s brother’s grandchild', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)fatsis(son|dau|chi)dau$/', $path)) {
+ return I18N::translateContext('grandfather’s sister’s granddaughter', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)fatsis(son|dau|chi)son$/', $path)) {
+ return I18N::translateContext('grandfather’s sister’s grandson', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)fatsis(son|dau|chi)chi$/', $path)) {
+ return I18N::translateContext('grandfather’s sister’s grandchild', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)fatsib(son|dau|chi)dau$/', $path)) {
+ return I18N::translateContext('grandfather’s sibling’s granddaughter', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)fatsib(son|dau|chi)son$/', $path)) {
+ return I18N::translateContext('grandfather’s sibling’s grandson', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)fatsib(son|dau|chi)chi$/', $path)) {
+ return I18N::translateContext('grandfather’s sibling’s grandchild', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)motbro(son|dau|chi)dau$/', $path)) {
+ return I18N::translateContext('grandmother’s brother’s granddaughter', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)motbro(son|dau|chi)son$/', $path)) {
+ return I18N::translateContext('grandmother’s brother’s grandson', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)motbro(son|dau|chi)chi$/', $path)) {
+ return I18N::translateContext('grandmother’s brother’s grandchild', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)motsis(son|dau|chi)dau$/', $path)) {
+ return I18N::translateContext('grandmother’s sister’s granddaughter', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)motsis(son|dau|chi)son$/', $path)) {
+ return I18N::translateContext('grandmother’s sister’s grandson', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)motsis(son|dau|chi)chi$/', $path)) {
+ return I18N::translateContext('grandmother’s sister’s grandchild', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)motsib(son|dau|chi)dau$/', $path)) {
+ return I18N::translateContext('grandmother’s sibling’s granddaughter', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)motsib(son|dau|chi)son$/', $path)) {
+ return I18N::translateContext('grandmother’s sibling’s grandson', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)motsib(son|dau|chi)chi$/', $path)) {
+ return I18N::translateContext('grandmother’s sibling’s grandchild', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)parbro(son|dau|chi)dau$/', $path)) {
+ return I18N::translateContext('grandparent’s brother’s granddaughter', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)parbro(son|dau|chi)son$/', $path)) {
+ return I18N::translateContext('grandparent’s brother’s grandson', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)parbro(son|dau|chi)chi$/', $path)) {
+ return I18N::translateContext('grandparent’s brother’s grandchild', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)parsis(son|dau|chi)dau$/', $path)) {
+ return I18N::translateContext('grandparent’s sister’s granddaughter', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)parsis(son|dau|chi)son$/', $path)) {
+ return I18N::translateContext('grandparent’s sister’s grandson', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)parsis(son|dau|chi)chi$/', $path)) {
+ return I18N::translateContext('grandparent’s sister’s grandchild', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)parsib(son|dau|chi)dau$/', $path)) {
+ return I18N::translateContext('grandparent’s sibling’s granddaughter', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)parsib(son|dau|chi)son$/', $path)) {
+ return I18N::translateContext('grandparent’s sibling’s grandson', 'second cousin');
+ }
+
+ if (preg_match('/^(mot|fat|par)parsib(son|dau|chi)chi$/', $path)) {
+ return I18N::translateContext('grandparent’s sibling’s grandchild', 'second cousin');
+ }
+
+ // Look for generic/pattern relationships.
+ if (preg_match('/^((?:mot|fat|par)+)(bro|sis|sib)$/', $path, $match)) {
+ // siblings of direct ancestors
+ $up = intdiv(strlen($match[1]), 3);
+ $bef_last = substr($path, -6, 3);
+ switch ($up) {
+ case 3:
+ if ($sex2 === 'M') {
+ if ($bef_last === 'fat') {
+ return I18N::translateContext('great-grandfather’s brother', 'great-great-uncle');
+ }
+
+ if ($bef_last === 'mot') {
+ return I18N::translateContext('great-grandmother’s brother', 'great-great-uncle');
+ }
+
+ return I18N::translateContext('great-grandparent’s brother', 'great-great-uncle');
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great-great-aunt');
+ }
+
+ return I18N::translate('great-great-aunt/uncle');
+
+ case 4:
+ if ($sex2 === 'M') {
+ if ($bef_last === 'fat') {
+ return I18N::translateContext('great-great-grandfather’s brother', 'great-great-great-uncle');
+ }
+
+ if ($bef_last === 'mot') {
+ return I18N::translateContext('great-great-grandmother’s brother', 'great-great-great-uncle');
+ }
+
+ return I18N::translateContext('great-great-grandparent’s brother', 'great-great-great-uncle');
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great-great-great-aunt');
+ }
+
+ return I18N::translate('great-great-great-aunt/uncle');
+
+ case 5:
+ if ($sex2 === 'M') {
+ if ($bef_last === 'fat') {
+ return I18N::translateContext('great-great-great-grandfather’s brother', 'great ×4 uncle');
+ }
+
+ if ($bef_last === 'mot') {
+ return I18N::translateContext('great-great-great-grandmother’s brother', 'great ×4 uncle');
+ }
+
+ return I18N::translateContext('great-great-great-grandparent’s brother', 'great ×4 uncle');
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×4 aunt');
+ }
+
+ return I18N::translate('great ×4 aunt/uncle');
+
+ case 6:
+ if ($sex2 === 'M') {
+ if ($bef_last === 'fat') {
+ return I18N::translateContext('great ×4 grandfather’s brother', 'great ×5 uncle');
+ }
+
+ if ($bef_last === 'mot') {
+ return I18N::translateContext('great ×4 grandmother’s brother', 'great ×5 uncle');
+ }
+
+ return I18N::translateContext('great ×4 grandparent’s brother', 'great ×5 uncle');
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×5 aunt');
+ }
+
+ return I18N::translate('great ×5 aunt/uncle');
+
+ case 7:
+ if ($sex2 === 'M') {
+ if ($bef_last === 'fat') {
+ return I18N::translateContext('great ×5 grandfather’s brother', 'great ×6 uncle');
+ }
+
+ if ($bef_last === 'mot') {
+ return I18N::translateContext('great ×5 grandmother’s brother', 'great ×6 uncle');
+ }
+
+ return I18N::translateContext('great ×5 grandparent’s brother', 'great ×6 uncle');
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×6 aunt');
+ }
+
+ return I18N::translate('great ×6 aunt/uncle');
+
+ case 8:
+ if ($sex2 === 'M') {
+ if ($bef_last === 'fat') {
+ return I18N::translateContext('great ×6 grandfather’s brother', 'great ×7 uncle');
+ }
+
+ if ($bef_last === 'mot') {
+ return I18N::translateContext('great ×6 grandmother’s brother', 'great ×7 uncle');
+ }
+
+ return I18N::translateContext('great ×6 grandparent’s brother', 'great ×7 uncle');
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×7 aunt');
+ }
+
+ return I18N::translate('great ×7 aunt/uncle');
+
+ default:
+ // Different languages have different rules for naming generations.
+ // An English great ×12 uncle is a Danish great ×10 uncle.
+ //
+ // Need to find out which languages use which rules.
+ switch (I18N::languageTag()) {
+ case 'da':
+ if ($sex2 === 'M') {
+ return I18N::translate('great ×%s uncle', I18N::number($up - 4));
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×%s aunt', I18N::number($up - 4));
+ }
+
+ return I18N::translate('great ×%s aunt/uncle', I18N::number($up - 4));
+
+ case 'pl':
+ if ($sex2 === 'M') {
+ if ($bef_last === 'fat') {
+ return I18N::translateContext('great ×(%s-1) grandfather’s brother', 'great ×%s uncle', I18N::number($up - 2));
+ }
+
+ if ($bef_last === 'mot') {
+ return I18N::translateContext('great ×(%s-1) grandmother’s brother', 'great ×%s uncle', I18N::number($up - 2));
+ }
+
+ return I18N::translateContext('great ×(%s-1) grandparent’s brother', 'great ×%s uncle', I18N::number($up - 2));
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×%s aunt', I18N::number($up - 2));
+ }
+
+ return I18N::translate('great ×%s aunt/uncle', I18N::number($up - 2));
+
+ case 'hi': // Source: MrQD
+ if ($sex2 === 'M') {
+ // I18N: if you need a different number for %s, contact the developers, as a code-change is required
+ return I18N::translate('great ×%s uncle', I18N::number($up - 2));
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×%s aunt', I18N::number($up - 2));
+ }
+
+ return I18N::translate('great ×%s aunt/uncle', I18N::number($up - 2));
+
+ case 'zh-Hans': // Source: xmlf
+ case 'zh-Hant':
+ if ($sex2 === 'M') {
+ return I18N::translate('great ×%s uncle', I18N::number($up));
+ }
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×%s aunt', I18N::number($up));
+ }
+
+ return I18N::translate('great ×%s aunt/uncle', I18N::number($up));
+
+ case 'it': // Source: Michele Locati
+ case 'en_AU':
+ case 'en_GB':
+ case 'en_US':
+ default:
+ if ($sex2 === 'M') {
+ // I18N: if you need a different number for %s, contact the developers, as a code-change is required
+ return I18N::translate('great ×%s uncle', I18N::number($up - 1));
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×%s aunt', I18N::number($up - 1));
+ }
+
+ return I18N::translate('great ×%s aunt/uncle', I18N::number($up - 1));
+ }
+ }
+ }
+ if (preg_match('/^(?:bro|sis|sib)((?:son|dau|chi)+)$/', $path, $match)) {
+ // direct descendants of siblings
+ $down = intdiv(strlen($match[1]), 3) + 1; // Add one, as we count generations from the common ancestor
+ $first = substr($path, 0, 3);
+ switch ($down) {
+ case 4:
+ if ($sex2 === 'M') {
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great-grandson', 'great-great-nephew');
+ }
+
+ if ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great-grandson', 'great-great-nephew');
+ }
+
+ return I18N::translateContext('(a woman’s) great-great-nephew', 'great-great-nephew');
+ }
+
+ if ($sex2 === 'F') {
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great-granddaughter', 'great-great-niece');
+ }
+
+ if ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great-granddaughter', 'great-great-niece');
+ }
+
+ return I18N::translateContext('(a woman’s) great-great-niece', 'great-great-niece');
+ }
+
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great-grandchild', 'great-great-nephew/niece');
+ }
+
+ if ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great-grandchild', 'great-great-nephew/niece');
+ }
+
+ return I18N::translateContext('(a woman’s) great-great-nephew/niece', 'great-great-nephew/niece');
+
+ case 5:
+ if ($sex2 === 'M') {
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great-great-grandson', 'great-great-great-nephew');
+ }
+
+ if ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great-great-grandson', 'great-great-great-nephew');
+ }
+
+ return I18N::translateContext('(a woman’s) great-great-great-nephew', 'great-great-great-nephew');
+ }
+
+ if ($sex2 === 'F') {
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great-great-granddaughter', 'great-great-great-niece');
+ }
+
+ if ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great-great-granddaughter', 'great-great-great-niece');
+ }
+
+ return I18N::translateContext('(a woman’s) great-great-great-niece', 'great-great-great-niece');
+ }
+
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great-great-grandchild', 'great-great-great-nephew/niece');
+ }
+
+ if ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great-great-grandchild', 'great-great-great-nephew/niece');
+ }
+
+ return I18N::translateContext('(a woman’s) great-great-great-nephew/niece', 'great-great-great-nephew/niece');
+
+ case 6:
+ if ($sex2 === 'M') {
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great-great-great-grandson', 'great ×4 nephew');
+ }
+
+ if ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great-great-great-grandson', 'great ×4 nephew');
+ }
+
+ return I18N::translateContext('(a woman’s) great ×4 nephew', 'great ×4 nephew');
+ }
+
+ if ($sex2 === 'F') {
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great-great-great-granddaughter', 'great ×4 niece');
+ }
+
+ if ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great-great-great-granddaughter', 'great ×4 niece');
+ }
+
+ return I18N::translateContext('(a woman’s) great ×4 niece', 'great ×4 niece');
+ }
+
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great-great-great-grandchild', 'great ×4 nephew/niece');
+ }
+
+ if ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great-great-great-grandchild', 'great ×4 nephew/niece');
+ }
+
+ return I18N::translateContext('(a woman’s) great ×4 nephew/niece', 'great ×4 nephew/niece');
+
+ case 7:
+ if ($sex2 === 'M') {
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great ×4 grandson', 'great ×5 nephew');
+ }
+
+ if ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great ×4 grandson', 'great ×5 nephew');
+ }
+
+ return I18N::translateContext('(a woman’s) great ×5 nephew', 'great ×5 nephew');
+ }
+
+ if ($sex2 === 'F') {
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great ×4 granddaughter', 'great ×5 niece');
+ }
+
+ if ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great ×4 granddaughter', 'great ×5 niece');
+ }
+
+ return I18N::translateContext('(a woman’s) great ×5 niece', 'great ×5 niece');
+ }
+
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great ×4 grandchild', 'great ×5 nephew/niece');
+ }
+
+ if ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great ×4 grandchild', 'great ×5 nephew/niece');
+ }
+
+ return I18N::translateContext('(a woman’s) great ×5 nephew/niece', 'great ×5 nephew/niece');
+
+ default:
+ // Different languages have different rules for naming generations.
+ // An English great ×12 nephew is a Polish great ×11 nephew.
+ //
+ // Need to find out which languages use which rules.
+ switch (I18N::languageTag()) {
+ case 'pl': // Source: Lukasz Wilenski
+ if ($sex2 === 'M') {
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great ×(%s-1) grandson', 'great ×%s nephew', I18N::number($down - 3));
+ }
+
+ if ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great ×(%s-1) grandson', 'great ×%s nephew', I18N::number($down - 3));
+ }
+
+ return I18N::translateContext('(a woman’s) great ×%s nephew', 'great ×%s nephew', I18N::number($down - 3));
+ }
+
+ if ($sex2 === 'F') {
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great ×(%s-1) granddaughter', 'great ×%s niece', I18N::number($down - 3));
+ }
+
+ if ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great ×(%s-1) granddaughter', 'great ×%s niece', I18N::number($down - 3));
+ }
+
+ return I18N::translateContext('(a woman’s) great ×%s niece', 'great ×%s niece', I18N::number($down - 3));
+ }
+
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great ×(%s-1) grandchild', 'great ×%s nephew/niece', I18N::number($down - 3));
+ }
+
+ if ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great ×(%s-1) grandchild', 'great ×%s nephew/niece', I18N::number($down - 3));
+ }
+
+ return I18N::translateContext('(a woman’s) great ×%s nephew/niece', 'great ×%s nephew/niece', I18N::number($down - 3));
+
+ case 'zh-Hans': // Source: xmlf
+ case 'zh-Hant':
+ if ($sex2 === 'M') {
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great ×(%s-1) grandson', 'great ×%s nephew', I18N::number($down - 1));
+ }
+ if ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great ×(%s-1) grandson', 'great ×%s nephew', I18N::number($down - 1));
+ }
+
+ return I18N::translateContext('(a woman’s) great ×%s nephew', 'great ×%s nephew', I18N::number($down - 1));
+ }
+ if ($sex2 === 'F') {
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great ×(%s-1) granddaughter', 'great ×%s niece', I18N::number($down - 1));
+ }
+ if ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great ×(%s-1) granddaughter', 'great ×%s niece', I18N::number($down - 1));
+ }
+
+ return I18N::translateContext('(a woman’s) great ×%s niece', 'great ×%s niece', I18N::number($down - 1));
+ }
+ if ($first === 'bro' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) brother’s great ×(%s-1) grandchild', 'great ×%s nephew/niece', I18N::number($down - 1));
+ }
+ if ($first === 'sis' && $sex1 === 'M') {
+ return I18N::translateContext('(a man’s) sister’s great ×(%s-1) grandchild', 'great ×%s nephew/niece', I18N::number($down - 1));
+ }
+
+ return I18N::translateContext('(a woman’s) great ×%s nephew/niece', 'great ×%s nephew/niece', I18N::number($down - 1));
+
+ case 'he': // Source: Meliza Amity
+ if ($sex2 === 'M') {
+ return I18N::translate('great ×%s nephew', I18N::number($down - 1));
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×%s niece', I18N::number($down - 1));
+ }
+
+ return I18N::translate('great ×%s nephew/niece', I18N::number($down - 1));
+
+ case 'hi': // Source: MrQD.
+ if ($sex2 === 'M') {
+ // I18N: if you need a different number for %s, contact the developers, as a code-change is required
+ return I18N::translate('great ×%s nephew', I18N::number($down - 3));
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×%s niece', I18N::number($down - 3));
+ }
+
+ return I18N::translate('great ×%s nephew/niece', I18N::number($down - 3));
+
+ case 'it': // Source: Michele Locati.
+ case 'en_AU':
+ case 'en_GB':
+ case 'en_US':
+ default:
+ if ($sex2 === 'M') {
+ // I18N: if you need a different number for %s, contact the developers, as a code-change is required
+ return I18N::translate('great ×%s nephew', I18N::number($down - 2));
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×%s niece', I18N::number($down - 2));
+ }
+
+ return I18N::translate('great ×%s nephew/niece', I18N::number($down - 2));
+ }
+ }
+ }
+ if (preg_match('/^((?:mot|fat|par)*)$/', $path, $match)) {
+ // direct ancestors
+ $up = intdiv(strlen($match[1]), 3);
+ switch ($up) {
+ case 4:
+ if ($sex2 === 'M') {
+ return I18N::translate('great-great-grandfather');
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great-great-grandmother');
+ }
+
+ return I18N::translate('great-great-grandparent');
+
+ case 5:
+ if ($sex2 === 'M') {
+ return I18N::translate('great-great-great-grandfather');
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great-great-great-grandmother');
+ }
+
+ return I18N::translate('great-great-great-grandparent');
+
+ case 6:
+ if ($sex2 === 'M') {
+ return I18N::translate('great ×4 grandfather');
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×4 grandmother');
+ }
+
+ return I18N::translate('great ×4 grandparent');
+
+ case 7:
+ if ($sex2 === 'M') {
+ return I18N::translate('great ×5 grandfather');
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×5 grandmother');
+ }
+
+ return I18N::translate('great ×5 grandparent');
+
+ case 8:
+ if ($sex2 === 'M') {
+ return I18N::translate('great ×6 grandfather');
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×6 grandmother');
+ }
+
+ return I18N::translate('great ×6 grandparent');
+
+ case 9:
+ if ($sex2 === 'M') {
+ return I18N::translate('great ×7 grandfather');
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×7 grandmother');
+ }
+
+ return I18N::translate('great ×7 grandparent');
+
+ default:
+ // Different languages have different rules for naming generations.
+ // An English great ×12 grandfather is a Danish great ×11 grandfather.
+ //
+ // Need to find out which languages use which rules.
+ switch (I18N::languageTag()) {
+ case 'da': // Source: Patrick Sorensen
+ if ($sex2 === 'M') {
+ return I18N::translate('great ×%s grandfather', I18N::number($up - 3));
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×%s grandmother', I18N::number($up - 3));
+ }
+
+ return I18N::translate('great ×%s grandparent', I18N::number($up - 3));
+
+ case 'it': // Source: Michele Locati
+ case 'zh-Hans': // Source: xmlf
+ case 'zh-Hant':
+ case 'es': // Source: Wes Groleau
+ if ($sex2 === 'M') {
+ return I18N::translate('great ×%s grandfather', I18N::number($up));
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×%s grandmother', I18N::number($up));
+ }
+
+ return I18N::translate('great ×%s grandparent', I18N::number($up));
+
+ case 'fr': // Source: Jacqueline Tetreault
+ case 'fr_CA':
+ if ($sex2 === 'M') {
+ return I18N::translate('great ×%s grandfather', I18N::number($up - 1));
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×%s grandmother', I18N::number($up - 1));
+ }
+
+ return I18N::translate('great ×%s grandparent', I18N::number($up - 1));
+
+ case 'nn': // Source: Hogne Røed Nilsen (https://bugs.launchpad.net/webtrees/+bug/1168553)
+ case 'nb':
+ if ($sex2 === 'M') {
+ // I18N: if you need a different number for %s, contact the developers, as a code-change is required
+ return I18N::translate('great ×%s grandfather', I18N::number($up - 3));
+ }
+
+ if ($sex2 === 'F') {
+ // I18N: if you need a different number for %s, contact the developers, as a code-change is required
+ return I18N::translate('great ×%s grandmother', I18N::number($up - 3));
+ }
+
+ // I18N: if you need a different number for %s, contact the developers, as a code-change is required
+ return I18N::translate('great ×%s grandparent', I18N::number($up - 3));
+ case 'en_AU':
+ case 'en_GB':
+ case 'en_US':
+ default:
+ if ($sex2 === 'M') {
+ // I18N: if you need a different number for %s, contact the developers, as a code-change is required
+ return I18N::translate('great ×%s grandfather', I18N::number($up - 2));
+ }
+
+ if ($sex2 === 'F') {
+ // I18N: if you need a different number for %s, contact the developers, as a code-change is required
+ return I18N::translate('great ×%s grandmother', I18N::number($up - 2));
+ }
+
+ // I18N: if you need a different number for %s, contact the developers, as a code-change is required
+ return I18N::translate('great ×%s grandparent', I18N::number($up - 2));
+ }
+ }
+ }
+ if (preg_match('/^((?:son|dau|chi)*)$/', $path, $match)) {
+ // direct descendants
+ $up = intdiv(strlen($match[1]), 3);
+ switch ($up) {
+ case 4:
+ if ($sex2 === 'M') {
+ return I18N::translate('great-great-grandson');
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great-great-granddaughter');
+ }
+
+ return I18N::translate('great-great-grandchild');
+
+ case 5:
+ if ($sex2 === 'M') {
+ return I18N::translate('great-great-great-grandson');
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great-great-great-granddaughter');
+ }
+
+ return I18N::translate('great-great-great-grandchild');
+
+ case 6:
+ if ($sex2 === 'M') {
+ return I18N::translate('great ×4 grandson');
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×4 granddaughter');
+ }
+
+ return I18N::translate('great ×4 grandchild');
+
+ case 7:
+ if ($sex2 === 'M') {
+ return I18N::translate('great ×5 grandson');
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×5 granddaughter');
+ }
+
+ return I18N::translate('great ×5 grandchild');
+
+ case 8:
+ if ($sex2 === 'M') {
+ return I18N::translate('great ×6 grandson');
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×6 granddaughter');
+ }
+
+ return I18N::translate('great ×6 grandchild');
+
+ case 9:
+ if ($sex2 === 'M') {
+ return I18N::translate('great ×7 grandson');
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×7 granddaughter');
+ }
+
+ return I18N::translate('great ×7 grandchild');
+
+ default:
+ // Different languages have different rules for naming generations.
+ // An English great ×12 grandson is a Danish great ×11 grandson.
+ //
+ // Need to find out which languages use which rules.
+ switch (I18N::languageTag()) {
+ case 'nn': // Source: Hogne Røed Nilsen
+ case 'nb':
+ case 'da': // Source: Patrick Sorensen
+ if ($sex2 === 'M') {
+ return I18N::translate('great ×%s grandson', I18N::number($up - 3));
+ }
+
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×%s granddaughter', I18N::number($up - 3));
+ }
+
+ return I18N::translate('great ×%s grandchild', I18N::number($up - 3));
+
+ case 'zh-Hans': // Source: xmlf
+ case 'zh-Hant':
+ if ($sex2 === 'M') {
+ return I18N::translate('great ×%s grandson', I18N::number($up));
+ }
+ if ($sex2 === 'F') {
+ return I18N::translate('great ×%s granddaughter', I18N::number($up));
+ }
+
+ return I18N::translate('great ×%s grandchild', I18N::number($up));
+
+ case 'it':
+ // Source: Michele Locati
+ case 'es':
+ // Source: Wes Groleau (adding doesn’t change behavior, but needs to be better researched)
+ case 'en_AU':
+ case 'en_GB':
+ case 'en_US':
+ default:
+ if ($sex2 === 'M') {
+ // I18N: if you need a different number for %s, contact the developers, as a code-change is required
+ return I18N::translate('great ×%s grandson', I18N::number($up - 2));
+ }
+
+ if ($sex2 === 'F') {
+ // I18N: if you need a different number for %s, contact the developers, as a code-change is required
+ return I18N::translate('great ×%s granddaughter', I18N::number($up - 2));
+ }
+
+ // I18N: if you need a different number for %s, contact the developers, as a code-change is required
+ return I18N::translate('great ×%s grandchild', I18N::number($up - 2));
+ }
+ }
+ }
+ if (preg_match('/^((?:mot|fat|par)+)(?:bro|sis|sib)((?:son|dau|chi)+)$/', $path, $match)) {
+ // cousins in English
+ $ascent = $match[1];
+ $descent = $match[2];
+ $up = intdiv(strlen($ascent), 3);
+ $down = intdiv(strlen($descent), 3);
+ $cousin = min($up, $down); // Moved out of switch (en/default case) so that
+ $removed = abs($down - $up); // Spanish (and other languages) can use it, too.
+
+ // Different languages have different rules for naming cousins. For example,
+ // an English “second cousin once removed” is a Polish “cousin of 7th degree”.
+ //
+ // Need to find out which languages use which rules.
+ switch (I18N::languageTag()) {
+ case 'pl': // Source: Lukasz Wilenski
+ return self::legacyCousinName($up + $down + 2, $sex2);
+ case 'it':
+ // Source: Michele Locati. See italian_cousins_names.zip
+ // https://webtrees.net/forums/8-translation/1200-great-xn-grandparent?limit=6&start=6
+ return self::legacyCousinName($up + $down - 3, $sex2);
+ case 'es':
+ // Source: Wes Groleau. See http://UniGen.us/Parentesco.html & http://UniGen.us/Parentesco-D.html
+ if ($down === $up) {
+ return self::legacyCousinName($cousin, $sex2);
+ }
+
+ if ($down < $up) {
+ return self::legacyCousinName2($cousin + 1, $sex2, $this->legacyNameAlgorithm('sib' . $descent));
+ }
+
+ if ($sex2 === 'M') {
+ return self::legacyCousinName2($cousin + 1, $sex2, $this->legacyNameAlgorithm('bro' . $descent));
+ }
+
+ if ($sex2 === 'F') {
+ return self::legacyCousinName2($cousin + 1, $sex2, $this->legacyNameAlgorithm('sis' . $descent));
+ }
+
+ return self::legacyCousinName2($cousin + 1, $sex2, $this->legacyNameAlgorithm('sib' . $descent));
+
+ case 'en_AU': // See: http://en.wikipedia.org/wiki/File:CousinTree.svg
+ case 'en_GB':
+ case 'en_US':
+ default:
+ switch ($removed) {
+ case 0:
+ return self::legacyCousinName($cousin, $sex2);
+ case 1:
+ if ($up > $down) {
+ /* I18N: %s=“fifth cousin”, etc. http://www.ancestry.com/learn/library/article.aspx?article=2856 */
+ return I18N::translate('%s once removed ascending', self::legacyCousinName($cousin, $sex2));
+ }
+
+ /* I18N: %s=“fifth cousin”, etc. http://www.ancestry.com/learn/library/article.aspx?article=2856 */
+
+ return I18N::translate('%s once removed descending', self::legacyCousinName($cousin, $sex2));
+ case 2:
+ if ($up > $down) {
+ /* I18N: %s=“fifth cousin”, etc. */
+ return I18N::translate('%s twice removed ascending', self::legacyCousinName($cousin, $sex2));
+ }
+
+ /* I18N: %s=“fifth cousin”, etc. */
+
+ return I18N::translate('%s twice removed descending', self::legacyCousinName($cousin, $sex2));
+ case 3:
+ if ($up > $down) {
+ /* I18N: %s=“fifth cousin”, etc. */
+ return I18N::translate('%s three times removed ascending', self::legacyCousinName($cousin, $sex2));
+ }
+
+ /* I18N: %s=“fifth cousin”, etc. */
+
+ return I18N::translate('%s three times removed descending', self::legacyCousinName($cousin, $sex2));
+ default:
+ if ($up > $down) {
+ /* I18N: %1$s=“fifth cousin”, etc., %2$s>=4 */
+ return I18N::translate('%1$s %2$s times removed ascending', self::legacyCousinName($cousin, $sex2), I18N::number($removed));
+ }
+
+ /* I18N: %1$s=“fifth cousin”, etc., %2$s>=4 */
+
+ return I18N::translate('%1$s %2$s times removed descending', self::legacyCousinName($cousin, $sex2), I18N::number($removed));
+ }
+ }
+ }
+
+ // Split the relationship into sub-relationships, e.g., third-cousin’s great-uncle.
+ // Try splitting at every point, and choose the path with the shorted translated name.
+ // But before starting to recursively go through all combinations, do a cache look-up
+
+ static $relationshipsCache;
+ $relationshipsCache ??= [];
+ if (array_key_exists($path, $relationshipsCache)) {
+ return $relationshipsCache[$path];
+ }
+
+ $relationship = null;
+ $path1 = substr($path, 0, 3);
+ $path2 = substr($path, 3);
+ while ($path2) {
+ // I18N: A complex relationship, such as “third-cousin’s great-uncle”
+ $tmp = I18N::translate(
+ '%1$s’s %2$s',
+ $this->legacyNameAlgorithm($path1),
+ $this->legacyNameAlgorithm($path2)
+ );
+ if (!$relationship || strlen($tmp) < strlen($relationship)) {
+ $relationship = $tmp;
+ }
+ $path1 .= substr($path2, 0, 3);
+ $path2 = substr($path2, 3);
+ }
+ // and store the result in the cache
+ $relationshipsCache[$path] = $relationship;
+
+ return $relationship;
+ }
+
+ /**
+ * Calculate the name of a cousin.
+ *
+ * @param int $n
+ * @param string $sex
+ *
+ * @return string
+ *
+ * @deprecated
+ */
+ private static function legacyCousinName(int $n, string $sex): string
+ {
+ if ($sex === 'M') {
+ switch ($n) {
+ case 1:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'first cousin');
+ case 2:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'second cousin');
+ case 3:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'third cousin');
+ case 4:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'fourth cousin');
+ case 5:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'fifth cousin');
+ case 6:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'sixth cousin');
+ case 7:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'seventh cousin');
+ case 8:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'eighth cousin');
+ case 9:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'ninth cousin');
+ case 10:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'tenth cousin');
+ case 11:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'eleventh cousin');
+ case 12:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'twelfth cousin');
+ case 13:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'thirteenth cousin');
+ case 14:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'fourteenth cousin');
+ case 15:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', 'fifteenth cousin');
+ default:
+ /* I18N: Note that for Italian and Polish, “N’th cousins” are different from English “N’th cousins”, and the software has already generated the correct “N” for your language. You only need to translate - you do not need to convert. For other languages, if your cousin rules are different from English, please contact the developers. */
+ return I18N::translateContext('MALE', '%s × cousin', I18N::number($n));
+ }
+ }
+
+ if ($sex === 'F') {
+ switch ($n) {
+ case 1:
+ return I18N::translateContext('FEMALE', 'first cousin');
+ case 2:
+ return I18N::translateContext('FEMALE', 'second cousin');
+ case 3:
+ return I18N::translateContext('FEMALE', 'third cousin');
+ case 4:
+ return I18N::translateContext('FEMALE', 'fourth cousin');
+ case 5:
+ return I18N::translateContext('FEMALE', 'fifth cousin');
+ case 6:
+ return I18N::translateContext('FEMALE', 'sixth cousin');
+ case 7:
+ return I18N::translateContext('FEMALE', 'seventh cousin');
+ case 8:
+ return I18N::translateContext('FEMALE', 'eighth cousin');
+ case 9:
+ return I18N::translateContext('FEMALE', 'ninth cousin');
+ case 10:
+ return I18N::translateContext('FEMALE', 'tenth cousin');
+ case 11:
+ return I18N::translateContext('FEMALE', 'eleventh cousin');
+ case 12:
+ return I18N::translateContext('FEMALE', 'twelfth cousin');
+ case 13:
+ return I18N::translateContext('FEMALE', 'thirteenth cousin');
+ case 14:
+ return I18N::translateContext('FEMALE', 'fourteenth cousin');
+ case 15:
+ return I18N::translateContext('FEMALE', 'fifteenth cousin');
+ default:
+ return I18N::translateContext('FEMALE', '%s × cousin', I18N::number($n));
+ }
+ }
+
+ switch ($n) {
+ case 1:
+ return I18N::translate('first cousin');
+ case 2:
+ return I18N::translate('second cousin');
+ case 3:
+ return I18N::translate('third cousin');
+ case 4:
+ return I18N::translate('fourth cousin');
+ case 5:
+ return I18N::translate('fifth cousin');
+ case 6:
+ return I18N::translate('sixth cousin');
+ case 7:
+ return I18N::translate('seventh cousin');
+ case 8:
+ return I18N::translate('eighth cousin');
+ case 9:
+ return I18N::translate('ninth cousin');
+ case 10:
+ return I18N::translate('tenth cousin');
+ case 11:
+ return I18N::translate('eleventh cousin');
+ case 12:
+ return I18N::translate('twelfth cousin');
+ case 13:
+ return I18N::translate('thirteenth cousin');
+ case 14:
+ return I18N::translate('fourteenth cousin');
+ case 15:
+ return I18N::translate('fifteenth cousin');
+ default:
+ return I18N::translate('%s × cousin', I18N::number($n));
+ }
+ }
+
+ /**
+ * A variation on cousin_name(), for constructs such as “sixth great-nephew”
+ * Currently used only by Spanish relationship names.
+ *
+ * @param int $n
+ * @param string $sex
+ * @param string $relation
+ *
+ * @return string
+ *
+ * @deprecated
+ */
+ private static function legacyCousinName2(int $n, string $sex, string $relation): string
+ {
+ if ($sex === 'M') {
+ switch ($n) {
+ case 1:
+ /* I18N: A Spanish relationship name, such as third great-nephew */
+ return I18N::translateContext('MALE', 'first %s', $relation);
+ case 2:
+ /* I18N: A Spanish relationship name, such as third great-nephew */
+ return I18N::translateContext('MALE', 'second %s', $relation);
+ case 3:
+ /* I18N: A Spanish relationship name, such as third great-nephew */
+ return I18N::translateContext('MALE', 'third %s', $relation);
+ case 4:
+ /* I18N: A Spanish relationship name, such as third great-nephew */
+ return I18N::translateContext('MALE', 'fourth %s', $relation);
+ case 5:
+ /* I18N: A Spanish relationship name, such as third great-nephew */
+ return I18N::translateContext('MALE', 'fifth %s', $relation);
+ default:
+ /* I18N: A Spanish relationship name, such as third great-nephew */
+ return I18N::translateContext('MALE', '%1$s × %2$s', I18N::number($n), $relation);
+ }
+ }
+
+ if ($sex === 'F') {
+ switch ($n) {
+ case 1:
+ /* I18N: A Spanish relationship name, such as third great-nephew */
+ return I18N::translateContext('FEMALE', 'first %s', $relation);
+ case 2:
+ /* I18N: A Spanish relationship name, such as third great-nephew */
+ return I18N::translateContext('FEMALE', 'second %s', $relation);
+ case 3:
+ /* I18N: A Spanish relationship name, such as third great-nephew */
+ return I18N::translateContext('FEMALE', 'third %s', $relation);
+ case 4:
+ /* I18N: A Spanish relationship name, such as third great-nephew */
+ return I18N::translateContext('FEMALE', 'fourth %s', $relation);
+ case 5:
+ /* I18N: A Spanish relationship name, such as third great-nephew */
+ return I18N::translateContext('FEMALE', 'fifth %s', $relation);
+ default:
+ /* I18N: A Spanish relationship name, such as third great-nephew */
+ return I18N::translateContext('FEMALE', '%1$s × %2$s', I18N::number($n), $relation);
+ }
+ }
+
+ switch ($n) {
+ case 1:
+ /* I18N: A Spanish relationship name, such as third great-nephew */
+ return I18N::translate('first %s', $relation);
+ case 2:
+ /* I18N: A Spanish relationship name, such as third great-nephew */
+ return I18N::translate('second %s', $relation);
+ case 3:
+ /* I18N: A Spanish relationship name, such as third great-nephew */
+ return I18N::translate('third %s', $relation);
+ case 4:
+ /* I18N: A Spanish relationship name, such as third great-nephew */
+ return I18N::translate('fourth %s', $relation);
+ case 5:
+ /* I18N: A Spanish relationship name, such as third great-nephew */
+ return I18N::translate('fifth %s', $relation);
+ default:
+ /* I18N: A Spanish relationship name, such as third great-nephew */
+ return I18N::translate('%1$s × %2$s', I18N::number($n), $relation);
+ }
+ }
+}
diff --git a/resources/views/modules/family_nav/sidebar-family.phtml b/resources/views/modules/family_nav/sidebar-family.phtml
index 7478a0ed58..6eb389e89c 100644
--- a/resources/views/modules/family_nav/sidebar-family.phtml
+++ b/resources/views/modules/family_nav/sidebar-family.phtml
@@ -4,6 +4,7 @@ use Fisharebest\Webtrees\Family;
use Fisharebest\Webtrees\Functions\Functions;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
+use Fisharebest\Webtrees\Services\RelationshipService;
/**
* @var Individual $individual
@@ -24,12 +25,12 @@ use Fisharebest\Webtrees\Individual;
<tr class="text-center wt-family-navigator-parent wt-gender-<?= $spouse->sex() ?>">
<th class="align-middle wt-family-navigator-label" scope="row">
<?php if ($spouse === $individual) : ?>
- <?= Functions::getCloseRelationshipName($individual, $spouse) ?>
+ <?= app(RelationshipService::class)->getCloseRelationshipName($individual, $spouse) ?>
<i class="icon-selected"></i>
<?php elseif ($spouse->childFamilies()->isNotEmpty()) : ?>
<div class="dropdown">
<a class="dropdown-toggle" href="#" role="button" id="dropdown-<?= e($spouse->xref()) ?>" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
- <?= Functions::getCloseRelationshipName($individual, $spouse) ?>
+ <?= app(RelationshipService::class)->getCloseRelationshipName($individual, $spouse) ?>
</a>
<div class="dropdown-menu wt-family-navigator-dropdown">
@@ -49,7 +50,7 @@ use Fisharebest\Webtrees\Individual;
</div>
</div>
<?php else : ?>
- <?= Functions::getCloseRelationshipName($individual, $spouse) ?>
+ <?= app(RelationshipService::class)->getCloseRelationshipName($individual, $spouse) ?>
<?php endif ?>
</th>
@@ -72,12 +73,12 @@ use Fisharebest\Webtrees\Individual;
<tr class="text-center wt-family-navigator-child wt-gender-<?= $child->sex() ?>">
<th class="align-middle wt-family-navigator-label" scope="row">
<?php if ($child === $individual) : ?>
- <?= Functions::getCloseRelationshipName($individual, $child) ?>
+ <?= app(RelationshipService::class)->getCloseRelationshipName($individual, $child) ?>
<i class="icon-selected"></i>
<?php elseif ($child->spouseFamilies()->isNotEmpty()) : ?>
<div class="dropdown">
<a class="dropdown-toggle" href="#" role="button" id="dropdown-<?= e($child->xref()) ?>" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
- <?= Functions::getCloseRelationshipName($individual, $child) ?>
+ <?= app(RelationshipService::class)->getCloseRelationshipName($individual, $child) ?>
</a>
<div class="dropdown-menu">
@@ -110,7 +111,7 @@ use Fisharebest\Webtrees\Individual;
</div>
</div>
<?php else : ?>
- <?= Functions::getCloseRelationshipName($individual, $child) ?>
+ <?= app(RelationshipService::class)->getCloseRelationshipName($individual, $child) ?>
<?php endif ?>
</th>
diff --git a/resources/views/modules/relatives/family.phtml b/resources/views/modules/relatives/family.phtml
index 22722717a9..f961502611 100644
--- a/resources/views/modules/relatives/family.phtml
+++ b/resources/views/modules/relatives/family.phtml
@@ -10,6 +10,7 @@ use Fisharebest\Webtrees\Http\RequestHandlers\AddSpouseToFamilyPage;
use Fisharebest\Webtrees\Http\RequestHandlers\ReorderChildrenPage;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
+use Fisharebest\Webtrees\Services\RelationshipService;
/**
* @var Family $family
@@ -44,7 +45,7 @@ use Fisharebest\Webtrees\Individual;
<tr class="<?= $row_class ?>">
<th scope="row">
<?= $individual === $person ? '<i class="icon-selected"></i>' : '' ?>
- <?= Functions::getCloseRelationshipName($individual, $person) ?>
+ <?= app(RelationshipService::class)->getCloseRelationshipName($individual, $person) ?>
</th>
<td class="border-0 p-0">
<?= view('chart-box', ['individual' => $person]) ?>
@@ -82,7 +83,7 @@ use Fisharebest\Webtrees\Individual;
<tr class="<?= $row_class ?>">
<th scope="row">
<?= $individual === $person ? '<i class="icon-selected"></i>' : '' ?>
- <?= Functions::getCloseRelationshipName($individual, $person) ?>
+ <?= app(RelationshipService::class)->getCloseRelationshipName($individual, $person) ?>
</th>
<td class="border-0 p-0">
<?= view('chart-box', ['individual' => $person]) ?>
@@ -197,7 +198,7 @@ use Fisharebest\Webtrees\Individual;
</div>
<?php endif ?>
- <?= Functions::getCloseRelationshipName($individual, $person) ?>
+ <?= app(RelationshipService::class)->getCloseRelationshipName($individual, $person) ?>
</th>
<td class="border-0 p-0">
<?= view('chart-box', ['individual' => $person]) ?>
diff --git a/tests/feature/RelationshipNamesTest.php b/tests/feature/RelationshipNamesTest.php
new file mode 100644
index 0000000000..4e91778174
--- /dev/null
+++ b/tests/feature/RelationshipNamesTest.php
@@ -0,0 +1,185 @@
+<?php
+
+/**
+ * webtrees: online genealogy
+ * Copyright (C) 2021 webtrees development team
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees;
+
+use Fisharebest\Webtrees\Factories\FamilyFactory;
+use Fisharebest\Webtrees\Factories\IndividualFactory;
+use Fisharebest\Webtrees\Module\LanguageEnglishUnitedStates;
+use Fisharebest\Webtrees\Services\RelationshipService;
+
+/**
+ * Test the user functions
+ *
+ * @covers \Fisharebest\Webtrees\Relationship
+ * @covers \Fisharebest\Webtrees\Services\RelationshipService
+ * @covers \Fisharebest\Webtrees\Module\LanguageEnglishGreatBritain
+ * @covers \Fisharebest\Webtrees\Module\LanguageModuleTrait
+ */
+class RelationshipNamesTest extends TestCase
+{
+ /**
+ * @return void
+ */
+ public function testRelationshipNames(): void
+ {
+ // i22m===f10===i23f
+ // |
+ // +-----+-----+
+ // |
+ // i20m===f9===i21f
+ // |
+ // i19f===f8===i18m
+ // |
+ // i16m===f7===i17f
+ // |
+ // i12f===f4m===i11m i13m===f5m===i14f
+ // | |
+ // i1m===f1m==========i2f===f2d===i6m
+ // | |
+ // +---+---+ +---+---+
+ // | | | | | |
+ // i10f===f3e===i3m i4f i5u i7ma i8f i9u===f6===i15u
+ //
+ // Individual suffixes - m(ale), f(emale), u(nknown), a(dopted)
+ // Family suffixes - m(arried), d(ivorced), e(ngaged)
+ //
+ $tree = $this->createMock(Tree::class);
+
+ $individual_factory = $this->createStub(IndividualFactory::class);
+ $family_factory = $this->createStub(FamilyFactory::class);
+
+ Registry::familyFactory($family_factory);
+ Registry::individualFactory($individual_factory);
+
+ $i1m = new Individual('i1m', "0 @i1m@ INDI\n1 SEX M\n1 FAMS @f1m@\n1 FAMC @f4m@", null, $tree);
+ $i2f = new Individual('i2f', "0 @i2f@ INDI\n1 SEX F\n1 FAMS @f1m@\n1 FAMS @f2d@\n2 FAMC @f5m@", null, $tree);
+ $i3m = new Individual('i3m', "0 @i3m@ INDI\n1 SEX M\n1 FAMC @f1m@\n1 FAMS @f3e@\n1 BIRT\n2 DATE 2000", null, $tree);
+ $i4f = new Individual('i4f', "0 @i4f@ INDI\n1 SEX F\n1 FAMC @f1m@\n1 BIRT\n2 DATE 2001", null, $tree);
+ $i5u = new Individual('i5u', "0 @i5u@ INDI\n1 SEX U\n1 FAMC @f1m@\n1 BIRT\n2 DATE 2002", null, $tree);
+ $i6m = new Individual('i6m', "0 @i6m@ INDI\n1 SEX M\n1 FAMS @f2d@", null, $tree);
+ $i7ma = new Individual('i7ma', "0 @i7ma@ INDI\n1 SEX M\n1 FAMC @f2d@\n2 PEDI adopted", null, $tree);
+ $i8f = new Individual('i8f', "0 @i8f@ INDI\n1 SEX F\n1 FAMC @f2d@", null, $tree);
+ $i9u = new Individual('i9u', "0 @i9u@ INDI\n1 SEX U\n1 FAMC @f2d@\n1 FAMS @f6@", null, $tree);
+ $i10f = new Individual('i10f', "0 @i10f@ INDI\n1 SEX F\n1 FAMS @f3e@", null, $tree);
+ $i11m = new Individual('i11m', "0 @i11f@ INDI\n1 SEX M\n1 FAMS @f4m@\n1 FAMC @f7@", null, $tree);
+ $i12f = new Individual('i12f', "0 @i12f@ INDI\n1 SEX F\n1 FAMS @f4m@", null, $tree);
+ $i13m = new Individual('i13m', "0 @i13f@ INDI\n1 SEX M\n1 FAMS @f5m@", null, $tree);
+ $i14f = new Individual('i14f', "0 @i14f@ INDI\n1 SEX F\n1 FAMS @f5m@", null, $tree);
+ $i15u = new Individual('i15u', "0 @i15u@ INDI\n1 SEX U\n1 FAMS @f6@", null, $tree);
+ $i16m = new Individual('i16m', "0 @i16m@ INDI\n1 SEX M\n1 FAMS @f7@", null, $tree);
+ $i17f = new Individual('i17f', "0 @i17f@ INDI\n1 SEX F\n1 FAMS @f7@\n1 FAMC @f8@", null, $tree);
+ $i18m = new Individual('i18m', "0 @i18m@ INDI\n1 SEX M\n1 FAMS @f8@\n1 FAMC @f9@", null, $tree);
+ $i19f = new Individual('i19f', "0 @i19f@ INDI\n1 SEX F\n1 FAMS @f8@", null, $tree);
+ $i20m = new Individual('i20m', "0 @i20m@ INDI\n1 SEX M\n1 FAMS @f9@", null, $tree);
+ $i21f = new Individual('i21f', "0 @i21f@ INDI\n1 SEX F\n1 FAMS @f9@\n1 FAMC @f10@", null, $tree);
+ $i22m = new Individual('i22m', "0 @i22m@ INDI\n1 SEX M\n1 FAMS @f10@", null, $tree);
+ $i23f = new Individual('i23f', "0 @i23f@ INDI\n1 SEX F\n1 FAMS @f10@", null, $tree);
+
+ $individual_factory->method('make')->will($this->returnValueMap([
+ 'i1m' => $i1m,
+ 'i2f' => $i2f,
+ 'i3m' => $i3m,
+ 'i4f' => $i4f,
+ 'i5u' => $i5u,
+ 'i6m' => $i6m,
+ 'i7ma' => $i7ma,
+ 'i8f' => $i8f,
+ 'i9u' => $i9u,
+ 'i10f' => $i10f,
+ 'i11m' => $i11m,
+ 'i12f' => $i12f,
+ 'i13m' => $i13m,
+ 'i14f' => $i14f,
+ 'i15u' => $i15u,
+ 'i16m' => $i16m,
+ 'i17f' => $i17f,
+ 'i18m' => $i18m,
+ 'i19f' => $i19f,
+ 'i20m' => $i20m,
+ 'i21f' => $i21f,
+ ]));
+
+ $f1m = new Family('f1m', "0 @f1m@ FAM\n1 MARR Y\n1 HUSB @i1m@\n1 WIFE @i2f@\n1 CHIL @i3m@\n1 CHIL @i4f@\n1 CHIL @i5u@", null, $tree);
+ $f2d = new Family('f2d', "0 @f2d@ FAM\n1 DIV Y\n1 HUSB @i6m@\n1 WIFE @i2f@\n1 CHIL @i7ma@\n1 CHIL @i8f@\n1 CHIL @i9u@", null, $tree);
+ $f3e = new Family('f3e', "0 @f3e@ FAM\n1 ENGA Y\n1 HUSB @i3m@\n1 WIFE @i10f@", null, $tree);
+ $f4m = new Family('f4m', "0 @f4m@ FAM\n1 MARR Y\n1 HUSB @i11m@\n1 WIFE @i12f@\n1 CHIL @i1m@", null, $tree);
+ $f5m = new Family('f5m', "0 @f5m@ FAM\n1 MARR Y\n1 HUSB @i13m@\n1 WIFE @i14f@\n1 CHIL @i2f@", null, $tree);
+ $f6 = new Family('f6', "0 @f6@ FAM\n1 HUSB @i9u@\n1 WIFE @i15u@", null, $tree);
+ $f7 = new Family('f7', "0 @f7@ FAM\n1 HUSB @i16m@\n1 WIFE @i17f@\n1 CHIL @i11m@", null, $tree);
+ $f8 = new Family('f8', "0 @f8@ FAM\n1 HUSB @i18m@\n1 WIFE @i19f@\n1 CHIL @i17f@", null, $tree);
+ $f9 = new Family('f9', "0 @f9@ FAM\n1 HUSB @i20m@\n1 WIFE @i21f@\n1 CHIL @i18m@", null, $tree);
+ $f10 = new Family('f10', "0 @f10@ FAM\n1 HUSB @i22m@\n1 WIFE @i23f@\n1 CHIL @i21f@", null, $tree);
+
+ $family_factory->method('make')->will($this->returnValueMap([
+ 'f1m' => $f1m,
+ 'f2d' => $f2d,
+ 'f3e' => $f3e,
+ 'f4m' => $f4m,
+ 'f5m' => $f5m,
+ 'f6' => $f6,
+ 'f7' => $f7,
+ 'f8' => $f8,
+ 'f9' => $f9,
+ 'f10' => $f10,
+ ]));
+
+ $service = new RelationshipService();
+
+ // ENGLISH
+ $en_gb = new LanguageEnglishUnitedStates();
+ // Static relationships
+ self::assertSame('wife', $service->nameFromPath([$i1m, $f1m, $i2f], $en_gb));
+ self::assertSame('husband', $service->nameFromPath([$i2f, $f1m, $i1m], $en_gb));
+ self::assertSame('partner', $service->nameFromPath([$i9u, $f6, $i15u], $en_gb));
+ self::assertSame('ex-husband', $service->nameFromPath([$i2f, $f2d, $i6m], $en_gb));
+ self::assertSame('ex-wife', $service->nameFromPath([$i6m, $f2d, $i2f], $en_gb));
+ self::assertSame('fiancé', $service->nameFromPath([$i10f, $f3e, $i3m], $en_gb));
+ self::assertSame('fiancée', $service->nameFromPath([$i3m, $f3e, $i10f], $en_gb));
+ self::assertSame('son', $service->nameFromPath([$i1m, $f1m, $i3m], $en_gb));
+ self::assertSame('daughter', $service->nameFromPath([$i1m, $f1m, $i4f], $en_gb));
+ self::assertSame('child', $service->nameFromPath([$i1m, $f1m, $i5u], $en_gb));
+ self::assertSame('younger brother', $service->nameFromPath([$i4f, $f1m, $i3m], $en_gb));
+ self::assertSame('elder sister', $service->nameFromPath([$i3m, $f1m, $i4f], $en_gb));
+ self::assertSame('elder sibling', $service->nameFromPath([$i3m, $f1m, $i5u], $en_gb));
+ self::assertSame('brother', $service->nameFromPath([$i8f, $f2d, $i7ma], $en_gb));
+ self::assertSame('sister', $service->nameFromPath([$i7ma, $f2d, $i8f], $en_gb));
+ self::assertSame('sibling', $service->nameFromPath([$i7ma, $f2d, $i9u], $en_gb));
+ self::assertSame('adoptive-mother', $service->nameFromPath([$i7ma, $f2d, $i2f], $en_gb));
+ self::assertSame('adoptive-father', $service->nameFromPath([$i7ma, $f2d, $i6m], $en_gb));
+ self::assertSame('adopted-son', $service->nameFromPath([$i6m, $f2d, $i7ma], $en_gb));
+ self::assertSame('paternal-grandfather', $service->nameFromPath([$i3m, $f1m, $i1m, $f4m, $i11m], $en_gb));
+ self::assertSame('paternal-grandmother', $service->nameFromPath([$i3m, $f1m, $i1m, $f4m, $i12f], $en_gb));
+ self::assertSame('maternal-grandfather', $service->nameFromPath([$i3m, $f1m, $i2f, $f5m, $i13m], $en_gb));
+ self::assertSame('maternal-grandmother', $service->nameFromPath([$i3m, $f1m, $i2f, $f5m, $i14f], $en_gb));
+ // Dynamic relationships
+ self::assertSame('paternal great-grandfather', $service->nameFromPath([$i3m, $f1m, $i1m, $f4m, $i11m, $f7, $i16m], $en_gb));
+ self::assertSame('paternal great-grandmother', $service->nameFromPath([$i3m, $f1m, $i1m, $f4m, $i11m, $f7, $i17f], $en_gb));
+ self::assertSame('paternal great-great-grandfather', $service->nameFromPath([$i3m, $f1m, $i1m, $f4m, $i11m, $f7, $i17f, $f8, $i18m], $en_gb));
+ self::assertSame('paternal great-great-grandmother', $service->nameFromPath([$i3m, $f1m, $i1m, $f4m, $i11m, $f7, $i17f, $f8, $i19f], $en_gb));
+ self::assertSame('paternal great-great-great-grandfather', $service->nameFromPath([$i3m, $f1m, $i1m, $f4m, $i11m, $f7, $i17f, $f8, $i18m, $f9, $i20m], $en_gb));
+ self::assertSame('paternal great-great-great-grandmother', $service->nameFromPath([$i3m, $f1m, $i1m, $f4m, $i11m, $f7, $i17f, $f8, $i18m, $f9, $i21f], $en_gb));
+ self::assertSame('paternal great ×4 grandfather', $service->nameFromPath([$i3m, $f1m, $i1m, $f4m, $i11m, $f7, $i17f, $f8, $i18m, $f9, $i21f, $f10, $i22m], $en_gb));
+ self::assertSame('paternal great ×4 grandmother', $service->nameFromPath([$i3m, $f1m, $i1m, $f4m, $i11m, $f7, $i17f, $f8, $i18m, $f9, $i21f, $f10, $i23f], $en_gb));
+ // Compound relationships
+ self::assertSame('wife’s ex-husband', $service->nameFromPath([$i1m, $f1m, $i2f, $f2d, $i6m], $en_gb));
+
+ // SLOVAK
+ }
+}