Skip to content

Commit 6a28226

Browse files
authored
feat: Format date and numbers based on locale (#238)
1 parent 9b466a7 commit 6a28226

14 files changed

+134
-90
lines changed

.github/workflows/phpunit-ci-coverage.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ jobs:
1818
- name: PHPUnit Tests
1919
uses: php-actions/phpunit@v3
2020
with:
21+
php_extensions: imagick intl
2122
bootstrap: vendor/autoload.php
2223
configuration: tests/phpunit/phpunit.xml
2324
args: --testdox

composer.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/card.php

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,38 @@
66
* Convert date from Y-M-D to more human-readable format
77
*
88
* @param string $dateString String in Y-M-D format
9-
* @param string $format Date format to use
9+
* @param string|null $format Date format to use, or null to use locale default
10+
* @param string $locale Locale code
1011
* @return string Formatted Date string
1112
*/
12-
function formatDate(string $dateString, string $format): string
13+
function formatDate(string $dateString, string|null $format, string $locale): string
1314
{
1415
$date = new DateTime($dateString);
1516
$formatted = "";
17+
$patternGenerator = new IntlDatePatternGenerator($locale);
1618
// if current year, display only month and day
1719
if (date_format($date, "Y") == date("Y")) {
18-
// remove brackets and all text within them
19-
$formatted = date_format($date, preg_replace("/\[.*?\]/", "", $format));
20+
if ($format) {
21+
// remove brackets and all text within them
22+
$formatted = date_format($date, preg_replace("/\[.*?\]/", "", $format));
23+
} else {
24+
// format without year using locale
25+
$pattern = $patternGenerator->getBestPattern("MMM d");
26+
$dateFormatter = new IntlDateFormatter($locale, IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE, pattern: $pattern);
27+
$formatted = $dateFormatter->format($date);
28+
}
2029
}
21-
// otherwise, display month, day, and year (just brackets removed)
30+
// otherwise, display month, day, and year
2231
else {
23-
$formatted = date_format($date, str_replace(array("[", "]"), "", $format));
32+
if ($format) {
33+
// remove brackets, but leave text within them
34+
$formatted = date_format($date, str_replace(["[", "]"], "", $format));
35+
} else {
36+
// format with year using locale
37+
$pattern = $patternGenerator->getBestPattern("YYYY MMM d");
38+
$dateFormatter = new IntlDateFormatter($locale, IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE, pattern: $pattern);
39+
$formatted = $dateFormatter->format($date);
40+
}
2441
}
2542
// sanitize and return formatted date
2643
return htmlspecialchars($formatted);
@@ -101,35 +118,36 @@ function generateCard(array $stats, array $params = null): string
101118
$theme = getRequestedTheme($params);
102119

103120
// get the labels from the translations file
104-
$labels = include "translations.php";
121+
$translations = include "translations.php";
105122
// get requested locale, default to English
106-
$locale = $params["locale"] ?? "en";
107-
// if the locale does not exist in the first value of the labels array, throw an exception
108-
if (!isset(reset($labels)[$locale])) {
109-
throw new InvalidArgumentException("That locale is not supported. You can help by adding it to the translations file.");
110-
}
123+
$localeCode = $params["locale"] ?? "en";
124+
$localeTranslations = $translations[$localeCode] ?? $translations["en"];
111125

112126
// get date format
113-
$dateFormat = $params["date_format"] ?? "M j[, Y]";
127+
// locale date formatter (used only if date_format is not specified)
128+
$dateFormat = $params["date_format"] ?? $localeTranslations["date_format"] ?? null;
129+
130+
// number formatter
131+
$numFormatter = new NumberFormatter($localeCode, NumberFormatter::DECIMAL);
114132

115133
// total contributions
116-
$totalContributions = $stats["totalContributions"];
117-
$firstContribution = formatDate($stats["firstContribution"], $dateFormat);
134+
$totalContributions = $numFormatter->format($stats["totalContributions"]);
135+
$firstContribution = formatDate($stats["firstContribution"], $dateFormat, $localeCode);
118136
$totalContributionsRange = $firstContribution . " - Present";
119137

120138
// current streak
121-
$currentStreak = $stats["currentStreak"]["length"];
122-
$currentStreakStart = formatDate($stats["currentStreak"]["start"], $dateFormat);
123-
$currentStreakEnd = formatDate($stats["currentStreak"]["end"], $dateFormat);
139+
$currentStreak = $numFormatter->format($stats["currentStreak"]["length"]);
140+
$currentStreakStart = formatDate($stats["currentStreak"]["start"], $dateFormat, $localeCode);
141+
$currentStreakEnd = formatDate($stats["currentStreak"]["end"], $dateFormat, $localeCode);
124142
$currentStreakRange = $currentStreakStart;
125143
if ($currentStreakStart != $currentStreakEnd) {
126144
$currentStreakRange .= " - " . $currentStreakEnd;
127145
}
128146

129147
// longest streak
130-
$longestStreak = $stats["longestStreak"]["length"];
131-
$longestStreakStart = formatDate($stats["longestStreak"]["start"], $dateFormat);
132-
$longestStreakEnd = formatDate($stats["longestStreak"]["end"], $dateFormat);
148+
$longestStreak = $numFormatter->format($stats["longestStreak"]["length"]);
149+
$longestStreakStart = formatDate($stats["longestStreak"]["start"], $dateFormat, $localeCode);
150+
$longestStreakEnd = formatDate($stats["longestStreak"]["end"], $dateFormat, $localeCode);
133151
$longestStreakRange = $longestStreakStart;
134152
if ($longestStreakStart != $longestStreakEnd) {
135153
$longestStreakRange .= " - " . $longestStreakEnd;
@@ -178,7 +196,7 @@ function generateCard(array $stats, array $params = null): string
178196
<g transform='translate(1,84)'>
179197
<rect width='163' height='50' stroke='none' fill='none'></rect>
180198
<text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Segoe UI, Ubuntu, sans-serif;font-weight:400;font-size:14px;font-style:normal;fill:{$theme["sideLabels"]};stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 0.7s;'>
181-
{$labels["totalContributions"][$locale]}
199+
{$localeTranslations["Total Contributions"]}
182200
</text>
183201
</g>
184202
@@ -203,7 +221,7 @@ function generateCard(array $stats, array $params = null): string
203221
<g transform='translate(166,108)'>
204222
<rect width='163' height='50' stroke='none' fill='none'></rect>
205223
<text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Segoe UI, Ubuntu, sans-serif;font-weight:700;font-size:14px;font-style:normal;fill:{$theme["currStreakLabel"]};stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 0.9s;'>
206-
{$labels["currentStreak"][$locale]}
224+
{$localeTranslations["Current Streak"]}
207225
</text>
208226
</g>
209227
@@ -239,7 +257,7 @@ function generateCard(array $stats, array $params = null): string
239257
<g transform='translate(331,84)'>
240258
<rect width='163' height='50' stroke='none' fill='none'></rect>
241259
<text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Segoe UI, Ubuntu, sans-serif;font-weight:400;font-size:14px;font-style:normal;fill:{$theme["sideLabels"]};stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 1.3s;'>
242-
{$labels["longestStreak"][$locale]}
260+
{$localeTranslations["Longest Streak"]}
243261
</text>
244262
</g>
245263
@@ -371,7 +389,7 @@ function renderOutput(string|array $output, int $responseCode = 200): void
371389
// set content type to JSON
372390
header('Content-Type: application/json');
373391
// generate array from output
374-
$data = gettype($output) === "string" ? array("error" => $output) : $output;
392+
$data = gettype($output) === "string" ? ["error" => $output] : $output;
375393
// output as JSON
376394
echo json_encode($data);
377395
}

src/colors.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22

33
// return a list of valid CSS colors
4-
return array(
4+
return [
55
"aliceblue",
66
"antiquewhite",
77
"aqua",
@@ -150,4 +150,4 @@
150150
"whitesmoke",
151151
"yellow",
152152
"yellowgreen",
153-
);
153+
];

src/demo/index.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
$THEMES = include "../themes.php";
44
$TRANSLATIONS = include "../translations.php";
55
// Get the keys of the first value in the translations array
6-
$LOCALES = array_keys(reset($TRANSLATIONS));
6+
$LOCALES = array_keys($TRANSLATIONS);
77

88
?>
99

@@ -78,6 +78,7 @@ function gtag() {
7878

7979
<label for="date_format">Date Format</label>
8080
<select class="param" id="date_format" name="date_format">
81+
<option value="">default</option>
8182
<option value="M j[, Y]">Aug 10, 2016</option>
8283
<option value="j M[ Y]">10 Aug 2016</option>
8384
<option value="[Y ]M j">2016 Aug 10</option>

src/demo/js/script.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ let preview = {
22
// default values
33
defaults: {
44
theme: "default",
5-
locale: "en",
65
hide_border: "false",
6+
date_format: "",
7+
locale: "en",
78
},
89
// update the preview
910
update: function () {

src/stats.php

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ function getContributionGraphs(string $user): array
1414
// Get the years the user has contributed
1515
$contributionYears = getContributionYears($user);
1616
// build a list of individual requests
17-
$requests = array();
17+
$requests = [];
1818
foreach ($contributionYears as $year) {
1919
// create query for year
2020
$start = "$year-01-01T00:00:00Z";
@@ -53,7 +53,7 @@ function getContributionGraphs(string $user): array
5353
}
5454
curl_multi_close($multi);
5555
// collect responses from last to first
56-
$response = array();
56+
$response = [];
5757
foreach ($requests as $request) {
5858
array_unshift($response, json_decode(curl_multi_getcontent($request)));
5959
}
@@ -65,13 +65,14 @@ function getContributionGraphs(string $user): array
6565
*
6666
* @return array<string> List of tokens
6767
*/
68-
function getGitHubTokens() {
68+
function getGitHubTokens()
69+
{
6970
// result is already calculated
7071
if (isset($GLOBALS["ALL_TOKENS"])) {
7172
return $GLOBALS["ALL_TOKENS"];
7273
}
7374
// find all tokens in environment variables
74-
$tokens = array($_SERVER["TOKEN"] ?? "");
75+
$tokens = isset($_SERVER["TOKEN"]) ? [$_SERVER["TOKEN"]] : [];
7576
for ($i = 2; $i < 4; $i++) {
7677
if (isset($_SERVER["TOKEN$i"])) {
7778
// add token to list
@@ -93,13 +94,13 @@ function getGraphQLCurlHandle(string $query)
9394
{
9495
$all_tokens = getGitHubTokens();
9596
$token = $all_tokens[array_rand($all_tokens)];
96-
$headers = array(
97+
$headers = [
9798
"Authorization: bearer $token",
9899
"Content-Type: application/json",
99100
"Accept: application/vnd.github.v4.idl",
100101
"User-Agent: GitHub-Readme-Streak-Stats"
101-
);
102-
$body = array("query" => $query);
102+
];
103+
$body = ["query" => $query];
103104
// create curl request
104105
$ch = curl_init();
105106
curl_setopt($ch, CURLOPT_URL, "https://api.github.com/graphql");
@@ -191,7 +192,7 @@ function getContributionYears(string $user): array
191192
function getContributionDates(array $contributionGraphs): array
192193
{
193194
// get contributions from HTML
194-
$contributions = array();
195+
$contributions = [];
195196
$today = date("Y-m-d");
196197
$tomorrow = date("Y-m-d", strtotime("tomorrow"));
197198
foreach ($contributionGraphs as $graph) {

src/translations.php

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,44 @@
11
<?php
22

3+
/**
4+
* Locales
5+
* -------
6+
* For a list of supported locale codes, see https://gist.github.com/DenverCoder1/f61147ba26bfcf7c3bf605af7d3382d5
7+
*
8+
* Date Format
9+
* -----------
10+
* Supplying a date format is optional and will be used instead of the default locale date format.
11+
* If the default date format for the locale displays correctly, you should omit the date_format parameter.
12+
*
13+
* Different year Same year Format string
14+
* -------------- --------- -------------
15+
* 10/8/2016 10/8 j/n[/Y]
16+
* 8/10/2016 8/10 n/j[/Y]
17+
* 2016.8.10 8.10 [Y.]n.j
18+
*
19+
* For info on valid date_format strings, see https://github.com/DenverCoder1/github-readme-streak-stats#date-formats
20+
*/
21+
322
return [
4-
"currentStreak" => [
5-
"en" => "Current Streak",
6-
"de" => "Aktuelle Serie",
7-
"es" => "Racha Actual",
8-
"ja" => "現在のストリーク",
23+
"en" => [
24+
"Total Contributions" => "Total Contributions",
25+
"Current Streak" => "Current Streak",
26+
"Longest Streak" => "Longest Streak",
27+
],
28+
"de" => [
29+
"Total Contributions" => "Gesamte Beiträge",
30+
"Current Streak" => "Aktuelle Serie",
31+
"Longest Streak" => "Längste Serie",
932
],
10-
"totalContributions" => [
11-
"en" => "Total Contributions",
12-
"de" => "Gesamte Beiträge",
13-
"es" => "Todas Contribuciones",
14-
"ja" => "総コントリビューション数",
33+
"es" => [
34+
"Total Contributions" => "Todas Contribuciones",
35+
"Current Streak" => "Racha Actual",
36+
"Longest Streak" => "Racha Más Larga",
1537
],
16-
"longestStreak" => [
17-
"en" => "Longest Streak",
18-
"de" => "Längste Serie",
19-
"es" => "Racha Más Larga",
20-
"ja" => "最長のストリーク",
38+
"ja" => [
39+
"date_format" => "[Y.]n.j",
40+
"Total Contributions" => "総コントリビューション数",
41+
"Current Streak" => "現在のストリーク",
42+
"Longest Streak" => "最長のストリーク",
2143
],
2244
];

tests/OptionsTest.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
final class OptionsTest extends TestCase
1111
{
12-
private $defaultTheme = array(
12+
private $defaultTheme = [
1313
"background" => "#fffefe",
1414
"border" => "#e4e2e2",
1515
"stroke" => "#e4e2e2",
@@ -20,7 +20,7 @@ final class OptionsTest extends TestCase
2020
"currStreakLabel" => "#fb8c00",
2121
"sideLabels" => "#151515",
2222
"dates" => "#464646",
23-
);
23+
];
2424

2525
/**
2626
* Test theme request parameters return colors for theme
@@ -60,7 +60,7 @@ public function testThemesHaveValidParameters(): void
6060
// check that there are no extra keys in the theme
6161
$this->assertEquals(
6262
array_diff_key($colors, $this->defaultTheme),
63-
array(),
63+
[],
6464
"The theme '$theme' contains invalid parameters."
6565
);
6666
# check that no parameters are missing and all values are valid
@@ -172,7 +172,7 @@ public function testHideBorder(): void
172172
public function testDateFormatSameYear(): void
173173
{
174174
$year = date("Y");
175-
$formatted = formatDate("$year-04-12", "M j[, Y]");
175+
$formatted = formatDate("$year-04-12", "M j[, Y]", "en");
176176
$this->assertEquals("Apr 12", $formatted);
177177
}
178178

@@ -181,7 +181,7 @@ public function testDateFormatSameYear(): void
181181
*/
182182
public function testDateFormatDifferentYear(): void
183183
{
184-
$formatted = formatDate("2000-04-12", "M j[, Y]");
184+
$formatted = formatDate("2000-04-12", "M j[, Y]", "en");
185185
$this->assertEquals("Apr 12, 2000", $formatted);
186186
}
187187

@@ -190,7 +190,7 @@ public function testDateFormatDifferentYear(): void
190190
*/
191191
public function testDateFormatNoBracketsDiffYear(): void
192192
{
193-
$formatted = formatDate("2000-04-12", "Y/m/d");
193+
$formatted = formatDate("2000-04-12", "Y/m/d", "en");
194194
$this->assertEquals("2000/04/12", $formatted);
195195
}
196196

@@ -200,7 +200,7 @@ public function testDateFormatNoBracketsDiffYear(): void
200200
public function testDateFormatNoBracketsSameYear(): void
201201
{
202202
$year = date("Y");
203-
$formatted = formatDate("$year-04-12", "Y/m/d");
203+
$formatted = formatDate("$year-04-12", "Y/m/d", "en");
204204
$this->assertEquals("$year/04/12", $formatted);
205205
}
206206
}

0 commit comments

Comments
 (0)