Преглед изворни кода

- Moving all human-facing strings to L10n class

- Better time and date formatting
- Cancel link on edit
- Form styling
master
Rocketsoup пре 3 година
родитељ
комит
5fc17fdd07
2 измењених фајлова са 352 додато и 102 уклоњено
  1. 308
    98
      htdocs/index.php
  2. 44
    4
      htdocs/journal.css

+ 308
- 98
htdocs/index.php Прегледај датотеку

@@ -58,7 +58,11 @@ define('BASE_URL', 'https://example.com/path/index.php');
58 58
 /// For detecting database migrations.
59 59
 define('SCHEMA_VERSION', '20230107');
60 60
 define('RECENT_POSTS_PER_PAGE', 50);
61
-define('SESSION_COOKIE_TTL_SECONDS', 14 * 24 * 60 * 60); // 14 days
61
+define('SECOND', 1);
62
+define('MINUTE', 60 * SECOND);
63
+define('HOUR', 60 * MINUTE);
64
+define('DAY', 24 * HOUR);
65
+define('SESSION_COOKIE_TTL_SECONDS', 14 * DAY);
62 66
 define('SESSION_COOKIE_NAME', 'journal_token');
63 67
 /// If a new post matches an existing post created less than this many seconds
64 68
 /// ago, the new duplicate post will be ignored.
@@ -94,6 +98,163 @@ function trace(string $message): void {
94 98
 	// error_log($message, 0);
95 99
 }
96 100
 
101
+// -- Localization ----------------------------------------
102
+
103
+class L10n {
104
+	// General
105
+	public const page_title = "Microjournal";
106
+	public const mobile_app_icon_title = "μJournal";
107
+
108
+	// Nav menu
109
+	public const hamburger_menu_symbol = "☰";
110
+	public const menu_search = "Search";
111
+	public const menu_log_out = "Log out";
112
+
113
+	// Create post form
114
+	public const post_form_placeholder = "Your journal post here…";
115
+	public const post_form_submit_post = "Post";
116
+	public const post_form_submit_update = "Update";
117
+	public const post_form_cancel = "Cancel";
118
+
119
+	// Previous posts
120
+	public const posts_previous_page = "Previous";
121
+	public const posts_next_page = "Next";
122
+	public static function post_created(int $timestamp): string {
123
+		$date = self::date_string($timestamp);
124
+		$date_full = self::date_string($timestamp, full: true);
125
+		$date_iso = self::date_string($timestamp, iso: true);
126
+		return "Posted <time datetime=\"{$date_iso}\" title=\"{$date_full}\">{$date}</time>";
127
+	}
128
+	public static function post_updated(int $timestamp): string {
129
+		$date = self::date_string($timestamp);
130
+		$date_full = self::date_string($timestamp, full: true);
131
+		$date_iso = self::date_string($timestamp, iso: true);
132
+		return "(updated <time datetime=\"{$date_iso}\" title=\"{$date_full}\">{$date}</time>)";
133
+	}
134
+	public const post_menu = "actions";
135
+	public const post_menu_edit = "Edit";
136
+	public const post_menu_delete = "Delete";
137
+
138
+	// Search form
139
+	public const search_submit = "Search";
140
+	public const search_cancel = "Cancel";
141
+
142
+	// Login form
143
+	public const login_username_label = "Username:";
144
+	public const login_password_label = "Password:";
145
+	public const login_submit = "Log in";
146
+
147
+	// Create account form
148
+	public const create_prompt = "Journal is not setup. Please create a " .
149
+		"username and password to secure your data.";
150
+	public const create_username_label = "Username:";
151
+	public const create_password_label = "Password:";
152
+	public const create_time_zone_label = "Time zone:";
153
+	public const create_submit = "Create account";
154
+
155
+	// Error messages
156
+	public const error_login_incorrect = "Login incorrect.";
157
+	public const error_bad_sql_datatype = "Bad datatype in sqlite statement.";
158
+	public const error_not_configured = "Journal not configured. Open " .
159
+		"index.php in an editor and configure the variables at the top of " .
160
+		"the file.";
161
+	public const error_database_not_found = "Database file cannot be found. " .
162
+		"Check configuration at the top of index.php.";
163
+	public static function error_database_not_writable(
164
+		string $user, string $group): string {
165
+		return "Database file exists but is not writable by web server " .
166
+			"user '{$user}' (user group '{$group}'). Check file permissions.";
167
+	}
168
+	public const error_no_schema_version = "No schema version in database. " .
169
+		"Corrupted?";
170
+	public static function error_unexpected_schema_version(
171
+		string $schema_version): string {
172
+		return "Unexpected schema version {$schema_version}.";
173
+	}
174
+	public const error_database_web_accessible = "Journal database is " .
175
+		"accessible from the web! Either alter your .htaccess permissions " .
176
+		"or rename the database with a UUID filename.";
177
+	public static function error_parameter_required(string $name): string {
178
+		return "Parameter {$name} is required.";
179
+	}
180
+	public static function error_parameter_empty(string $name): string {
181
+		return "Parameter {$name} cannot be empty.";
182
+	}
183
+	public static function error_parameter_integer(string $name): string {
184
+		return "Parameter {$name} must be integer.";
185
+	}
186
+	public const error_invalid_username = "Invalid username. Can be letters, " .
187
+		"numbers, underscores, and dashes, or an email address.";
188
+	public const error_sign_in_to_post = "Please sign in to post.";
189
+	public const error_sign_in_to_edit = "Please sign in to edit.";
190
+
191
+	public static function date_string(int $timestamp, bool $full=false, bool $iso=false): string {
192
+		static $fmtTime = null;
193
+		static $fmtDayOfWeek = null;
194
+		static $fmtMonthDay = null;
195
+		static $fmtLong = null;
196
+		static $fmtFull = null;
197
+		static $fmtIso = null;
198
+		$locale_code = Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']) ?: 'en-US';
199
+		$timezone = User::$current->timezone;
200
+		if (!$fmtTime) {
201
+			$fmtTime = IntlDateFormatter::create(
202
+				locale: $locale_code,
203
+				dateType: IntlDateFormatter::NONE,
204
+				timeType: IntlDateFormatter::MEDIUM,
205
+				timezone: $timezone);
206
+			$fmtDayOfWeek = IntlDateFormatter::create(
207
+				locale: $locale_code,
208
+				dateType: IntlDateFormatter::FULL,
209
+				timeType: IntlDateFormatter::NONE,
210
+				timezone: $timezone,
211
+				pattern: "cccc");
212
+			$fmtMonthDay = IntlDateFormatter::create(
213
+				locale: $locale_code,
214
+				dateType: IntlDateFormatter::MEDIUM,
215
+				timeType: IntlDateFormatter::NONE,
216
+				timezone: $timezone,
217
+				pattern: "MMMM d");
218
+			$fmtLong = IntlDateFormatter::create(
219
+				locale: $locale_code,
220
+				dateType: IntlDateFormatter::MEDIUM,
221
+				timeType: IntlDateFormatter::MEDIUM,
222
+				timezone: $timezone);
223
+			$fmtFull = IntlDateFormatter::create(
224
+				locale: $locale_code,
225
+				dateType: IntlDateFormatter::FULL,
226
+				timeType: IntlDateFormatter::FULL,
227
+				timezone: $timezone);
228
+			$fmtIso = IntlDateFormatter::create(
229
+				locale: $locale_code,
230
+				dateType: IntlDateFormatter::FULL,
231
+				timeType: IntlDateFormatter::FULL,
232
+				timezone: $timezone,
233
+				pattern: "yyyy-MM-dd'T'HH:mm:ssxxx");
234
+		}
235
+		$now = time();
236
+		if ($full) {
237
+			return $fmtFull->format($timestamp);
238
+		}
239
+		if ($iso) {
240
+			return $fmtIso->format($timestamp);
241
+		}
242
+		if ($timestamp > $now) { // future
243
+			return $fmtLong->format($timestamp);
244
+		}
245
+		if ($now - $timestamp < 12 * HOUR) {
246
+			return $fmtTime->format($timestamp);
247
+		}
248
+		if ($now - $timestamp < 4 * DAY) {
249
+			return $fmtDayOfWeek->format($timestamp) . ', ' . $time->format($timestamp);
250
+		}
251
+		if ($now - $timestamp < 180 * DAY) {
252
+			return $fmtMonthDay->format($timestamp) . ', ' . $time->format($timestamp);
253
+		}
254
+		return $fmtLong->format($timestamp);
255
+	}
256
+}
257
+
97 258
 // -- Data classes ----------------------------------------
98 259
 
99 260
 /// Represents a journal post.
@@ -134,8 +295,13 @@ class Post {
134 295
 			trace("Same post already created recently. Skipping.");
135 296
 			return;
136 297
 		}
137
-		$sql = 'INSERT INTO posts (body, author_id, created) VALUES (:body, :author_id, :created);';
138
-		$args = array(':body' => $body, ':author_id' => $author_id, ':created' => $created);
298
+		$sql = 'INSERT INTO posts (body, author_id, created) VALUES ' .
299
+			'(:body, :author_id, :created);';
300
+		$args = array(
301
+			':body' => $body,
302
+			':author_id' => $author_id,
303
+			':created' => $created,
304
+		);
139 305
 		Database::query($sql, $args);
140 306
 	}
141 307
 
@@ -149,7 +315,8 @@ class Post {
149 315
 			string $body,
150 316
 			int $author_id,
151 317
 			int $created): bool {
152
-		$sql = 'SELECT * FROM posts WHERE body=:body AND author_id=:author_id AND ABS(created - :created) < :seconds LIMIT 1;';
318
+		$sql = 'SELECT * FROM posts WHERE body=:body AND author_id=:author_id ' .
319
+			'AND ABS(created - :created) < :seconds LIMIT 1;';
153 320
 		$args = array(
154 321
 			':body' => $body,
155 322
 			':author_id' => $author_id,
@@ -165,8 +332,8 @@ class Post {
165 332
 	/// @param ?int $before_time  If provided, only posts older than this Unix
166 333
 	///                           timestamp are returned.
167 334
 	/// @return array             Three-element tuple - 0: array of Posts,
168
-	///                           1: relative URL to show previous page, or null if none
169
-	///                           2: relative URL to show next page, or null if none
335
+	///                           1: relative URL to show previous page, or null
336
+	///                           2: relative URL to show next page, or null
170 337
 	public static function get_posts(
171 338
 			int $user_id,
172 339
 			int $count = RECENT_POSTS_PER_PAGE,
@@ -203,7 +370,8 @@ class Post {
203 370
 		if ($before_time) {
204 371
 			// We're paged forward. Check if there are newer posts.
205 372
 			$sql = 'SELECT rowid, * FROM posts WHERE author_id=:author_id AND ' .
206
-				'created >= :before_time ' . $search_where . ' ORDER BY created ASC LIMIT :count;';
373
+				'created >= :before_time ' . $search_where .
374
+				' ORDER BY created ASC LIMIT :count;';
207 375
 			// Reusing same $args
208 376
 			$newer_posts = Database::query_objects('Post', $sql, $args);
209 377
 			if (sizeof($newer_posts) > $count) {
@@ -325,12 +493,12 @@ class User {
325 493
 		$user = self::get_by_username($username);
326 494
 		if ($user === null) {
327 495
 			trace("Login failed. No such user {$username}.");
328
-			fatal_error("Login incorrect.");
496
+			fatal_error(L10n::error_login_incorrect);
329 497
 			return null;
330 498
 		}
331 499
 		if (!password_verify($password, $user->password_hash)) {
332 500
 			trace("Login failed. Bad password for {$username}.");
333
-			fatal_error("Login incorrect.");
501
+			fatal_error(L10n::error_login_incorrect);
334 502
 			return null;
335 503
 		}
336 504
 		trace("Login succeeded for {$username}.");
@@ -342,7 +510,9 @@ class User {
342 510
 
343 511
 	/// Updates self::$current from $_SESSION.
344 512
 	public static function update_current(): void {
345
-		if (self::any_exist() && array_key_exists(SESSION_KEY_USER_ID, $_SESSION) && is_int($_SESSION[SESSION_KEY_USER_ID])) {
513
+		if (self::any_exist() &&
514
+			array_key_exists(SESSION_KEY_USER_ID, $_SESSION) &&
515
+			is_int($_SESSION[SESSION_KEY_USER_ID])) {
346 516
 			$id = $_SESSION[SESSION_KEY_USER_ID];
347 517
 			self::$current = User::get_by_id($id);
348 518
 		} else {
@@ -414,12 +584,15 @@ class Session {
414 584
 		$this->updated = time();
415 585
 		$secure = str_starts_with(BASE_URL, 'https:');
416 586
 		$expires = intval(time()) + SESSION_COOKIE_TTL_SECONDS;
417
-		trace('Updating cookie to ' . localized_date_string($expires));
587
+		trace('Updating cookie to ' . L10n::date_string($expires));
418 588
 		setcookie(SESSION_COOKIE_NAME, $this->token, $expires, path: '/',
419 589
 			secure: $secure, httponly: true);
420 590
 		if ($update_table) {
421 591
 			$sql = 'UPDATE sessions SET updated=:updated WHERE rowid=:rowid;';
422
-			$args = array(':updated' => $this->updated, ':rowid' => $this->rowid);
592
+			$args = array(
593
+				':updated' => $this->updated,
594
+				':rowid' => $this->rowid,
595
+			);
423 596
 			Database::query($sql, $args);
424 597
 		}
425 598
 	}
@@ -447,7 +620,8 @@ class Session {
447 620
 		$args = array(':rowid' => $this->rowid);
448 621
 		Database::query($sql, $args);
449 622
 		$secure = str_starts_with(BASE_URL, 'https:');
450
-		setcookie(SESSION_COOKIE_NAME, '', 1, path: '/', secure: $secure, httponly: true);
623
+		setcookie(SESSION_COOKIE_NAME, '', 1, path: '/', secure: $secure,
624
+			httponly: true);
451 625
 		unset($_COOKIE[SESSION_COOKIE_NAME]);
452 626
 	}
453 627
 }
@@ -492,7 +666,10 @@ class Database {
492 666
 	/// @param $params     Map of SQL placeholders to values.
493 667
 	/// @return ?object    Object of given class from first result row, or null
494 668
 	///                    if no rows matched.
495
-	public static function query_object(string $classname, string $sql, array $params = array()): ?object {
669
+	public static function query_object(
670
+		string $classname,
671
+		string $sql,
672
+		array $params = array()): ?object {
496 673
 		$objs = self::query_objects($classname, $sql, $params);
497 674
 		return (sizeof($objs) > 0) ? $objs[0] : null;
498 675
 	}
@@ -505,7 +682,10 @@ class Database {
505 682
 	/// @param $sql        Query to execute.
506 683
 	/// @param $params     Map of SQL placeholders to values.
507 684
 	/// @return array      Array of records of the given class.
508
-	public static function query_objects(string $classname, string $sql, array $params = array()): array {
685
+	public static function query_objects(
686
+		string $classname,
687
+		string $sql,
688
+		array $params = array()): array {
509 689
 		$rows = self::query($sql, $params);
510 690
 		$objs = array();
511 691
 		if ($rows !== false) {
@@ -524,7 +704,9 @@ class Database {
524 704
 	/// @param $params  Map of SQL placeholders to values.
525 705
 	/// @return array   Array of arrays. The inner arrays contain both indexed
526 706
 	///                 and named column values.
527
-	public static function query(string $sql, array $params = array()): bool|array {
707
+	public static function query(
708
+		string $sql,
709
+		array $params = array()): bool|array {
528 710
 		$db = new SQLite3(DB_PATH);
529 711
 		trace('SQL: ' . $sql);
530 712
 		$stmt = $db->prepare($sql);
@@ -563,7 +745,7 @@ class Database {
563 745
 			case 'NULL':
564 746
 				return SQLITE3_NULL;
565 747
 			default:
566
-				fatal_error("Bad datatype in sqlite statement");
748
+				fatal_error(L10n::error_bad_sql_datatype);
567 749
 		}
568 750
 	}
569 751
 
@@ -617,7 +799,8 @@ class Config {
617 799
 	private static function set_config_value(string $name, $value): void {
618 800
 		$args = array(':name' => $name, ':value' => $value);
619 801
 		Database::query('UPDATE config SET value=:value WHERE name=:name;', $args);
620
-		Database::query('INSERT OR IGNORE INTO config (name, value) VALUES (:name, :value);', $args);
802
+		Database::query('INSERT OR IGNORE INTO config (name, value) ' .
803
+			'VALUES (:name, :value);', $args);
621 804
 	}
622 805
 }
623 806
 
@@ -627,27 +810,27 @@ class Config {
627 810
 function check_setup(): void {
628 811
 	// Check user-configurable variables to make sure they aren't default values.
629 812
 	if (str_contains(BASE_URL, 'example.com') || DB_PATH == '/path/to/journal.db') {
630
-		fatal_error("Journal not configured. Open index.php in an editor and configure the variables at the top of the file.");
813
+		fatal_error(L10n::error_not_configured);
631 814
 	}
632 815
 
633 816
 	if (!Database::exists()) {
634
-		fatal_error("Database file cannot be found. Check configuration at the top of index.php.");
817
+		fatal_error(L10n::error_database_not_found);
635 818
 	}
636 819
 
637 820
 	if (!Database::is_writable()) {
638 821
 		$user = trim(shell_exec('whoami'));
639 822
 		$group = trim(shell_exec('id -gn'));
640
-		fatal_error("Database file exists but is not writable by web server user '{$user}' (user group '{$group}'). Check file permissions.");
823
+		fatal_error(L10n::error_database_not_writable($user, $group));
641 824
 	}
642 825
 
643 826
 	$schema_version = Config::get_schema_version();
644 827
 	if ($schema_version === null) {
645
-		fatal_error("No schema version in database. Corrupted?");
828
+		fatal_error(L10n::error_no_schema_version);
646 829
 	}
647 830
 
648 831
 	if ($schema_version != SCHEMA_VERSION) {
649 832
 		// TODO: If schema changes, migration paths will go here. For now just fail.
650
-		fatal_error("Unexpected schema version $schema_version.");
833
+		fatal_error(L10n::error_unexpected_schema_version($schema_version));
651 834
 	}
652 835
 
653 836
 	if (Config::get_is_configured()) {
@@ -658,7 +841,7 @@ function check_setup(): void {
658 841
 	// If using default database name, make sure it's not accessible from the
659 842
 	// web. It'd be far too easy to access.
660 843
 	if (Database::is_accessible_by_web()) {
661
-		fatal_error("Journal database is accessible from the web! Either alter your .htaccess permissions or rename the database with a UUID filename.");
844
+		fatal_error(L10n::error_database_web_accessible);
662 845
 	}
663 846
 
664 847
 	// Everything looks good!
@@ -705,7 +888,7 @@ define('INPUT_TYPE_USERNAME', 0x4 | INPUT_TYPE_TRIMMED | INPUT_TYPE_NONEMPTY);
705 888
 function validate(array $source, string $name, int $type, bool $required = true) {
706 889
 	if (!array_key_exists($name, $source)) {
707 890
 		if ($required) {
708
-			fatal_error("Parameter {$name} is required.");
891
+			fatal_error(L10n::error_parameter_required($name));
709 892
 		}
710 893
 		return null;
711 894
 	}
@@ -715,44 +898,34 @@ function validate(array $source, string $name, int $type, bool $required = true)
715 898
 	}
716 899
 	if (strlen($val) == 0) {
717 900
 		if ($type & INPUT_TYPE_NONEMPTY) {
718
-			fatal_error("Parameter {$name} cannot be empty.");
901
+			fatal_error(L10n::error_parameter_empty($name));
719 902
 		}
720 903
 	}
721 904
 	if (($type & INPUT_TYPE_INT) == INPUT_TYPE_INT) {
722 905
 		if (is_numeric($val)) return intval($val);
723
-		fatal_error("Parameter {$name} must be integer.");
906
+		fatal_error(L10n::error_parameter_integer($name));
724 907
 	}
725 908
 	if (($type & INPUT_TYPE_STRING) == INPUT_TYPE_STRING) {
726 909
 		return $val;
727 910
 	}
728 911
 	if (($type & INPUT_TYPE_USERNAME) == INPUT_TYPE_USERNAME) {
729 912
 		if (preg_match('/[a-zA-Z0-9_@+-]+/', $val)) return $val;
730
-		fatal_error("Invalid username. Can be letters, numbers, underscores, and dashes, or an email address.");
913
+		fatal_error(L10n::error_invalid_username);
731 914
 	}
732 915
 	return null;
733 916
 }
734 917
 
735
-function localized_date_string(int $timestamp): string {
736
-	$locale_code = Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']) ?: 'en-US';
737
-	$timezone = User::$current->timezone;
738
-	$formatter = IntlDateFormatter::create(
739
-		$locale_code,
740
-		dateType: IntlDateFormatter::MEDIUM,
741
-		timeType: IntlDateFormatter::MEDIUM,
742
-		timezone: $timezone,
743
-		calendar: IntlDateFormatter::GREGORIAN);
744
-	return $formatter->format($timestamp);
745
-}
746
-
747 918
 // -- HTML renderers --------------------------------------
748 919
 
749 920
 class HTMLPage {
750 921
 	public static function render_page_start(): void {
922
+		$str_title = L10n::page_title;
923
+		$str_app_icon_title = L10n::mobile_app_icon_title;
751 924
 		print(<<<HTML
752 925
 			<!DOCTYPE html>
753 926
 			<html>
754 927
 				<head>
755
-					<title>Microjournal</title>
928
+					<title>{$str_title}</title>
756 929
 					<meta charset="UTF-8" />
757 930
 					<link rel="stylesheet" type="text/css" href="journal.css" />
758 931
 					<link rel="apple-touch-icon" sizes="58x58" href="apple-touch-icon-58.png" />
@@ -764,30 +937,33 @@ class HTMLPage {
764 937
 					<link rel="apple-touch-icon" sizes="152x152" href="apple-touch-icon-152.png" />
765 938
 					<link rel="apple-touch-icon" sizes="167x167" href="apple-touch-icon-167.png" />
766 939
 					<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon-180.png" />
767
-					<meta name="apple-mobile-web-app-title" content="μJournal" />
940
+					<meta name="apple-mobile-web-app-title" content="{$str_app_icon_title}" />
768 941
 					<meta name="apple-mobile-web-app-capable" content="yes" />
769 942
 					<meta name="theme-color" content="#fff" media="(prefers-color-scheme: light)" />
770 943
 					<meta name="theme-color" content="#444" media="(prefers-color-scheme: dark)" />
771
-					<meta name="viewport" content="user-scalable=no,width=device-width,viewport-fit=cover" />
944
+					<meta name="viewport" content="user-scalable=no, width=device-width, viewport-fit=cover, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no" />
772 945
 					<link rel="icon" sizes="16x16" type="image/png" href="favicon-16.png" />
773 946
 					<link rel="icon" sizes="32x32" type="image/png" href="favicon-32.png" />
774 947
 				</head>
775 948
 				<body>
776 949
 					<nav>
777 950
 						<div class="top-nav">
778
-							<span class="title">Microjournal</span>
951
+							<span class="title">{$str_title}</span>
779 952
 			HTML
780 953
 		);
781 954
 		if (User::$current) {
955
+			$str_hamburger = L10n::hamburger_menu_symbol;
956
+			$str_search = L10n::menu_search;
957
+			$str_log_out = L10n::menu_log_out;
782 958
 			print(<<<HTML
783 959
 							<div class="menu-container">
784 960
 								<details class="menu">
785
-									<summary class="no-indicator menu-button"><span></span></summary>
961
+									<summary class="no-indicator menu-button"><span>{$str_hamburger}</span></summary>
786 962
 
787 963
 									<ul>
788
-										<li><a href="?search">Search</a></li>
964
+										<li><a href="?search">{$str_search}</a></li>
789 965
 										<li class="menu-divider"></li>
790
-										<li class="logout-item destructive"><a href="?logout">Log out</a></li>
966
+										<li class="logout-item destructive"><a href="?logout">{$str_log_out}</a></li>
791 967
 									</ul>
792 968
 								</details>
793 969
 							</div>
@@ -825,13 +1001,13 @@ class HTMLPage {
825 1001
 
826 1002
 	public static function render_post_form(): void {
827 1003
 		$action = 'post';
828
-		$verb = 'Post';
1004
+		$str_submit = L10n::post_form_submit_post;
829 1005
 		$body = '';
830 1006
 		if ($edit_id = validate($_GET, 'edit', INPUT_TYPE_INT, required: false)) {
831 1007
 			if ($post = Post::get_by_id($edit_id)) {
832 1008
 				$body = $post->body;
833 1009
 				$action = 'edit';
834
-				$verb = 'Update';
1010
+				$str_submit = L10n::post_form_submit_update;
835 1011
 			} else {
836 1012
 				unset($edit_id);
837 1013
 			}
@@ -840,17 +1016,24 @@ class HTMLPage {
840 1016
 			unset($_SESSION[SESSION_KEY_POST_BODY]);
841 1017
 		}
842 1018
 		$body_html = htmlentities($body);
1019
+		$str_placeholder = L10n::post_form_placeholder;
843 1020
 		print(<<<HTML
844 1021
 			<form id="post-form" method="POST">
845
-				<div class="text-container"><textarea name="body" placeholder="Your journal post here…">$body_html</textarea></div>
1022
+				<div class="text-container"><textarea name="body" placeholder="$str_placeholder">$body_html</textarea></div>
846 1023
 				<input type="hidden" name="action" value="{$action}" />
847 1024
 			HTML
848 1025
 		);
849 1026
 		if ($edit_id) {
850 1027
 			print("<input type=\"hidden\" name=\"edit_id\" value=\"{$edit_id}\" />");
851 1028
 		}
1029
+		print("<div class=\"submit-post form-buttons\">");
1030
+		if ($edit_id) {
1031
+			$url = BASE_URL;
1032
+			$str_cancel = L10n::post_form_cancel;
1033
+			print("<a class=\"cancel-edit\" href=\"{$url}\">{$str_cancel}</a> ");
1034
+		}
852 1035
 		print(<<<HTML
853
-				<div class="submit-post"><input type="submit" value="{$verb}" /></div>
1036
+				<input type="submit" value="{$str_submit}" /></div>
854 1037
 			</form>
855 1038
 			HTML
856 1039
 		);
@@ -862,13 +1045,15 @@ class HTMLPage {
862 1045
 		[ $posts, $prev_url, $next_url ] = Post::get_posts(User::$current->user_id, query: $query, before_time: $before);
863 1046
 		print("<div class=\"post-container\">\n");
864 1047
 		if ($prev_url !== null) {
865
-			print("<div class=\"previous\"><a href=\"{$prev_url}\">Previous</a></div>\n");
1048
+			$str_prev = L10n::posts_previous_page;
1049
+			print("<div class=\"previous\"><a href=\"{$prev_url}\">{$str_prev}</a></div>\n");
866 1050
 		}
867 1051
 		foreach ($posts as $post) {
868 1052
 			self::render_post($post);
869 1053
 		}
870 1054
 		if ($next_url !== null) {
871
-			print("<div class=\"next\"><a href=\"{$next_url}\">Next</a></div>\n");
1055
+			$str_next = L10n::posts_next_page;
1056
+			print("<div class=\"next\"><a href=\"{$next_url}\">{$str_next}</a></div>\n");
872 1057
 		}
873 1058
 		print("</div>\n");
874 1059
 	}
@@ -893,7 +1078,7 @@ class HTMLPage {
893 1078
 	public static function render_post(Post $post): void {
894 1079
 		$body_html = MARKDOWN_ENABLED ? Markdown::markdown_to_html($post->body) :
895 1080
 			self::post_body_html($post->body);
896
-		$date = localized_date_string($post->created);
1081
+		$str_posted = L10n::post_created($post->created);
897 1082
 		print(<<<HTML
898 1083
 			<div class="post">
899 1084
 				<article>
@@ -901,21 +1086,25 @@ class HTMLPage {
901 1086
 					<div class="post-footer">
902 1087
 						<footer class="secondary-text">
903 1088
 							<div class="post-date">
904
-								Posted {$date}
1089
+								{$str_posted}
905 1090
 			HTML
906 1091
 		);
907 1092
 		if ($post->updated && $post->updated != $post->created) {
908
-			print('<br/>(updated ' . localized_date_string($post->updated) . ')');
1093
+			$str_updated = L10n::post_updated($post->updated);
1094
+			print("<br/>\n$str_updated");
909 1095
 		}
1096
+		$str_menu = L10n::post_menu;
1097
+		$str_edit = L10n::post_menu_edit;
1098
+		$str_delete = L10n::post_menu_delete;
910 1099
 		print(<<<HTML
911 1100
 							</div>
912 1101
 							<details class="post-actions menu">
913
-								<summary class="no-indicator menu-button"><span>actions</span></summary>
1102
+								<summary class="no-indicator menu-button"><span>$str_menu</span></summary>
914 1103
 
915 1104
 								<ul>
916
-									<li><a href="?edit={$post->post_id}">Edit</a></li>
1105
+									<li><a href="?edit={$post->post_id}">{$str_edit}</a></li>
917 1106
 									<li class="menu-divider"></li>
918
-									<li class="post-action-delete destructive"><a href="?delete={$post->post_id}">Delete</a></li>
1107
+									<li class="post-action-delete destructive"><a href="?delete={$post->post_id}">{$str_delete}</a></li>
919 1108
 								</ul>
920 1109
 							</details>
921 1110
 						</footer>
@@ -930,52 +1119,69 @@ class HTMLPage {
930 1119
 		$q = validate($_GET, 'search', INPUT_TYPE_STRING | INPUT_TYPE_TRIMMED, required: false);
931 1120
 		$q_html = htmlentities($q);
932 1121
 		$cancel = BASE_URL;
1122
+		$str_submit = L10n::search_submit;
1123
+		$str_cancel = L10n::search_cancel;
933 1124
 		print(<<<HTML
934
-			<form id="search-form" method="GET">
935
-				<div>
936
-					<label for="search">Search:</label>
937
-					<input type="text" name="search" id="search" value="{$q_html}" autocapitalize="off" />
938
-					<input type="submit" value="Search" />
939
-					<a href="{$cancel}">Cancel</a>
940
-				</div>
941
-			</form>
1125
+			<div class="form-container">
1126
+				<form id="search-form" method="GET">
1127
+					<div>
1128
+						<input type="search" name="search" id="search" value="{$q_html}" size="40" autocapitalize="off" />
1129
+					</div>
1130
+					<div class="form-buttons">
1131
+						<a class="cancel-search" href="{$cancel}">{$str_cancel}</a>
1132
+						<input type="submit" value="{$str_submit}" />
1133
+					</div>
1134
+				</form>
1135
+			</div>
942 1136
 			HTML
943 1137
 		);
944 1138
 	}
945 1139
 
946 1140
 	public static function render_sign_in_form(): void {
1141
+		$str_username = L10n::login_username_label;
1142
+		$str_password = L10n::login_password_label;
1143
+		$str_submit = L10n::login_submit;
947 1144
 		print(<<<HTML
948
-			<form id="signin-form" method="POST">
949
-				<div>
950
-					<label for="username">Username:</label>
951
-					<input type="text" name="username" id="username" autocapitalize="off" autocorrect="off" />
952
-				</div>
953
-				<div>
954
-					<label for="password">Password:</label>
955
-					<input type="password" name="password" id="password" />
956
-				</div>
957
-				<input type="hidden" name="action" value="signin" />
958
-				<input type="submit" value="Sign in" />
959
-			</form>
1145
+			<div class="form-container">
1146
+				<form id="signin-form" method="POST">
1147
+					<div style="text-align: end;">
1148
+						<label for="username">{$str_username}</label>
1149
+						<input type="text" name="username" id="username" autocapitalize="off" autocorrect="off" />
1150
+					</div>
1151
+					<div style="text-align: end;">
1152
+						<label for="password">{$str_password}</label>
1153
+						<input type="password" name="password" id="password" />
1154
+					</div>
1155
+					<input type="hidden" name="action" value="signin" />
1156
+					<div class="form-buttons">
1157
+						<input type="submit" value="{$str_submit}" />
1158
+					</div>
1159
+				</form>
1160
+			</div>
960 1161
 			HTML
961 1162
 		);
962 1163
 	}
963 1164
 
964 1165
 	public static function render_setup_form(): void {
1166
+		$str_prompt = L10n::create_prompt;
1167
+		$str_username = L10n::create_username_label;
1168
+		$str_password = L10n::create_password_label;
1169
+		$str_time_zone = L10n::create_time_zone_label;
965 1170
 		print(<<<HTML
966
-			<div class="important">Journal is not setup. Please create a username and password to secure your data.</div>
967
-			<form id="setup-form" method="POST">
968
-				<div>
969
-					<label for="username">Username:</label>
970
-					<input type="text" id="username" name="username" autocapitalize="off" autocorrect="off" />
971
-				</div>
972
-				<div>
973
-					<label for="password">Password:</label>
974
-					<input type="password" id="password" name="password" />
975
-				</div>
976
-				<div>
977
-					<label for="timezone">Time zone:</label>
978
-					<select name="timezone" id="timezone">
1171
+			<div class="important">{$str_prompt}</div>
1172
+			<div class="form-container">
1173
+				<form id="setup-form" method="POST">
1174
+					<div style="text-align: end;">
1175
+						<label for="username">{$str_username}</label>
1176
+						<input type="text" id="username" name="username" autocapitalize="off" autocorrect="off" />
1177
+					</div>
1178
+					<div style="text-align: end;">
1179
+						<label for="password">{$str_password}</label>
1180
+						<input type="password" id="password" name="password" />
1181
+					</div>
1182
+					<div>
1183
+						<label for="timezone">{$str_time_zone}</label>
1184
+						<select name="timezone" id="timezone">
979 1185
 			HTML
980 1186
 		);
981 1187
 		foreach (DateTimeZone::listIdentifiers() as $timezone) {
@@ -985,11 +1191,15 @@ class HTMLPage {
985 1191
 			}
986 1192
 			print(">" . htmlentities($timezone) . "</option>\n");
987 1193
 		}
1194
+		$str_submit = L10n::create_submit;
988 1195
 		print(<<<HTML
989
-					</select></div>
990
-				<input type="hidden" name="action" value="createaccount" />
991
-				<div><input type="submit" value="Create account" /></div>
992
-			</form>
1196
+						</select></div>
1197
+					<input type="hidden" name="action" value="createaccount" />
1198
+					<div class="form-buttons">
1199
+						<input type="submit" value="{$str_submit}" />
1200
+					</div>
1201
+				</form>
1202
+			</div>
993 1203
 			HTML
994 1204
 		);
995 1205
 	}
@@ -1249,7 +1459,7 @@ switch ($_SERVER['REQUEST_METHOD']) {
1249 1459
 				if (!User::$current) {
1250 1460
 					// Not logged in. Save body for populating once they sign in.
1251 1461
 					$_SESSION[SESSION_KEY_POST_BODY] = $body;
1252
-					fatal_error('Please sign in to post.');
1462
+					fatal_error(L10n::error_sign_in_to_post);
1253 1463
 				}
1254 1464
 				Post::create($body, $author, $created);
1255 1465
 				break;
@@ -1258,7 +1468,7 @@ switch ($_SERVER['REQUEST_METHOD']) {
1258 1468
 				$edit_id = validate($_POST, 'edit_id', INPUT_TYPE_INT);
1259 1469
 				if (!User::$current) {
1260 1470
 					// Not logged in. Save body for populating once they sign in.
1261
-					fatal_error('Please sign in to edit.');
1471
+					fatal_error(L10n::error_sign_in_to_edit);
1262 1472
 				}
1263 1473
 				Post::get_by_id($edit_id)?->update($body);
1264 1474
 				break;

+ 44
- 4
htdocs/journal.css Прегледај датотеку

@@ -9,6 +9,7 @@
9 9
 	--menu-border: #ccc;
10 10
 
11 11
 	--highlight: #d83;
12
+	--highlight-contrast: #000;
12 13
 	--error-text: #d00;
13 14
 	--error-background: #fcc;
14 15
 	--error-border: #400;
@@ -17,6 +18,8 @@
17 18
 	--callout-border: #886;
18 19
 	--destructive: #e00;
19 20
 	--destructive-contrast: #fff;
21
+
22
+	--font-size: 12pt;
20 23
 }
21 24
 
22 25
 @media(prefers-color-scheme: dark) {
@@ -35,7 +38,7 @@
35 38
 :root, input, textarea {
36 39
 	font-family: Garamond, Times New Roman, serif;
37 40
 	line-height: 1.25;
38
-	font-size: 12pt;
41
+	font-size: var(--font-size);
39 42
 }
40 43
 body, textarea, input, select {
41 44
 	background-color: var(--page-background);
@@ -79,7 +82,7 @@ details.menu li a {
79 82
 }
80 83
 details.menu li a:hover {
81 84
 	background-color: var(--highlight);
82
-	color: black;
85
+	color: var(--highlight-contrast);
83 86
 }
84 87
 details.menu li {
85 88
 }
@@ -114,7 +117,17 @@ summary.menu-button > span:first-child {
114 117
 }
115 118
 details[open] summary.menu-button > span:first-child {
116 119
 	background-color: var(--highlight);
117
-	color: white;
120
+	color: var(--highlight-contrast);
121
+}
122
+.form-container {
123
+	position: relative;
124
+	text-align: center;
125
+	margin-left: auto;
126
+	margin-right: auto;
127
+}
128
+.form-container form {
129
+	display: inline-block;
130
+	text-align: start;
118 131
 }
119 132
 
120 133
 
@@ -188,9 +201,36 @@ textarea::placeholder {
188 201
 .submit-post {
189 202
 	text-align: right;
190 203
 }
204
+.cancel-edit {
205
+	display: inline-block;
206
+	padding-inline-end: 2em;
207
+}
208
+.form-buttons {
209
+	text-align: end;
210
+}
191 211
 input[type="submit"] {
192 212
 	margin-top: 0.5em;
193 213
 	min-width: 15ch;
214
+	padding: 0.3em 1.5em;
215
+	color: var(--highlight);
216
+	border: 1px solid var(--highlight);
217
+	border-radius: 0.2em;
218
+	font-size: 1rem;
219
+	-webkit-appearance: none;
220
+}
221
+input[type="submit"]:hover {
222
+	background-color: var(--highlight);
223
+	color: var(--highlight-contrast);
224
+}
225
+
226
+/* Search */
227
+
228
+#search-form input[type="search"] {
229
+	width: 100%;
230
+}
231
+.cancel-search {
232
+	display: inline-block;
233
+	padding-inline-end: 1em;
194 234
 }
195 235
 
196 236
 /* Posts */
@@ -239,6 +279,6 @@ details.menu li.post-action-delete a:hover {
239 279
 
240 280
 @media screen and (max-width: 450px) {
241 281
 	:root {
242
-		font-size: 16pt;
282
+		--font-size: 16px;
243 283
 	}
244 284
 }

Loading…
Откажи
Сачувај