summaryrefslogtreecommitdiff
blob: 731e0d465e729f3f83e50e41bd77b3d5bdb13516 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
<?php

/**
 * A class for querying translated time units from CLDR data.
 *
 * @author Niklas Laxström
 * @author Ryan Kaldari
 * @copyright Copyright © 2007-2013
 * @license GPL-2.0-or-later
 */
class TimeUnits extends CldrNames {

	private static $cache = [];

	/**
	 * Get localized time units for a particular language, using fallback languages for missing
	 * items. The time units are returned as an associative array. The keys are of the form:
	 * <unit>-<tense>-<ordinality> (for example, 'hour-future-two'). The values include a placeholder
	 * for the number (for example, '{0} months ago').
	 *
	 * @param string $code The language to return the list in
	 * @return array an associative array of time unit codes and localized time units
	 */
	public static function getUnits( $code ) {
		// Load time units localized for the requested language
		$units = self::loadLanguage( $code );

		if ( $units ) {
			return $units;
		}
		// Load missing time units from fallback languages
		$fallbacks = Language::getFallbacksFor( $code );
		foreach ( $fallbacks as $fallback ) {
			if ( $units ) {
				break;
			}
			// Get time units from a fallback language
			$units = self::loadLanguage( $fallback );
		}

		return $units;
	}

	/**
	 * Load time units localized for a particular language. Helper function for getUnits.
	 *
	 * @param string $code The language to return the list in
	 * @return array an associative array of time unit codes and localized time units
	 */
	private static function loadLanguage( $code ) {
		if ( !isset( self::$cache[$code] ) ) {
			self::$cache[$code] = [];

			/* Load override for wrong or missing entries in cldr */
			$override = __DIR__ . '/../LocalNames/' . self::getOverrideFileName( $code );
			if ( Language::isValidBuiltInCode( $code ) && file_exists( $override ) ) {
				$timeUnits = false;

				require $override;

				// @phan-suppress-next-line PhanImpossibleCondition
				if ( is_array( $timeUnits ) ) {
					self::$cache[$code] = $timeUnits;
				}
			}

			$filename = __DIR__ . '/../CldrNames/' . self::getFileName( $code );
			if ( Language::isValidBuiltInCode( $code ) && file_exists( $filename ) ) {
				$timeUnits = false;
				require $filename;
				// @phan-suppress-next-line PhanImpossibleCondition
				if ( is_array( $timeUnits ) ) {
					self::$cache[$code] = self::$cache[$code] + $timeUnits;
				}
			} else {
				wfDebug( __METHOD__ . ": Unable to load time units for $filename\n" );
			}
		}

		return self::$cache[$code];
	}

	/**
	 * Handler for GetHumanTimestamp hook.
	 * Converts the given time into a human-friendly relative format, for
	 * example, '6 days ago', 'In 10 months'.
	 *
	 * @param string &$output The output timestamp
	 * @param MWTimestamp $timestamp The current (user-adjusted) timestamp
	 * @param MWTimestamp $relativeTo The relative (user-adjusted) timestamp
	 * @param User $user User whose preferences are being used to make timestamp
	 * @param Language $lang Language that will be used to render the timestamp
	 * @return bool False means the timestamp was overridden so stop further
	 *     processing. True means the timestamp was not overridden.
	 */
	public static function onGetHumanTimestamp( &$output, $timestamp, $relativeTo, $user, $lang ) {
		// Map PHP's DateInterval property codes to CLDR unit names.
		$units = [
			's' => 'second',
			'i' => 'minute',
			'h' => 'hour',
			'd' => 'day',
			'm' => 'month',
			'y' => 'year',
		];

		// Get the difference between the two timestamps (as a DateInterval object).
		$timeDifference = $timestamp->diff( $relativeTo );

		// Figure out if the timestamp is in the future or the past.
		if ( $timeDifference->invert ) {
			$tense = 'future';
		} else {
			$tense = 'past';
		}

		// Figure out which unit (days, months, etc.) it makes sense to display
		// the timestamp in, and get the number of that unit to use.
		$unit = null;
		$number = 0;
		foreach ( $units as $code => $testUnit ) {
			$testNumber = (int)$timeDifference->format( '%' . $code );
			if ( $testNumber > 0 ) {
				$unit = $testUnit;
				$number = $testNumber;
			}
		}

		// If it occurred less than 1 second ago, output 'just now' message.
		if ( !$unit || !$number ) {
			$output = wfMessage( 'just-now' )->inLanguage( $lang )->text();
			return false;
		}

		// Get the CLDR time unit strings for the user's language.
		// If no strings are returned, abandon the timestamp override.
		$timeUnits = self::getUnits( $lang->getCode() );
		if ( !$timeUnits ) {
			return true;
		}

		// Figure out which grammatical number to use.
		// If the template doesn't exist, fall back to 'other' as the default.
		$grammaticalNumber = $lang->getPluralRuleType( $number );
		$timeUnitKey = "{$unit}-{$tense}-{$grammaticalNumber}";
		if ( !isset( $timeUnits[$timeUnitKey] ) ) {
			$timeUnitKey = "{$unit}-{$tense}-other";
		}

		// Not all languages have translations for everything
		if ( !isset( $timeUnits[$timeUnitKey] ) ) {
			return true;
		}

		// Select the appropriate template for the timestamp.
		$timeUnit = $timeUnits[$timeUnitKey];
		// Replace the placeholder with the number.
		$output = str_replace( '{0}', $lang->formatNum( $number ), $timeUnit );

		return false;
	}
}