summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-07-12 23:42:23 +0200
committererdgeist <erdgeist@erdgeist.org>2026-07-12 23:42:23 +0200
commitcf93acb8ad44ba9cd486e8f6457d9fd9fbc041cc (patch)
tree71ebf9abed3fe2b96262443e33da767315a5f7d2
parentd320f8509751a886adb3ca723ebfafccaa8c191b (diff)
Add a locale-aware relative-time helper; drop stale locale overrides
Node.recently_changed and the new dashboard need "X ago" rendered correctly in both locales. Rails' own distance_of_time_in_words/ time_ago_in_words can't do this on their own -- the scope option changes which translation key is looked up, not the underlying grammar, and German's "vor" requires dative case, which is a different word form than the nominative plural Rails computes by default. relative_time_phrase/relative_distance is a small, from- scratch bucketing helper instead, with an explicit, hand-checked translation table per unit per locale. Also removes de.yml's datetime.distance_in_words and activerecord. errors blocks. Both used the old {{count}}/{{model}} interpolation syntax the current i18n gem no longer recognizes -- it silently leaves the literal placeholder text in the rendered output rather than erroring, which is why this went unnoticed until now. Both were shadowing rails-i18n's own current, correct translations for exactly these keys; deleting the local override lets the gem's version take over instead of maintaining a second, broken copy.
-rw-r--r--app/helpers/datetime_helper.rb53
-rw-r--r--config/locales/de.yml70
-rw-r--r--config/locales/en.yml4
-rw-r--r--test/models/helpers/datetime_helper_test.rb19
4 files changed, 80 insertions, 66 deletions
diff --git a/app/helpers/datetime_helper.rb b/app/helpers/datetime_helper.rb
new file mode 100644
index 0000000..8497b1c
--- /dev/null
+++ b/app/helpers/datetime_helper.rb
@@ -0,0 +1,53 @@
1module DatetimeHelper
2
3 def relative_distance(time, now = Time.current)
4 seconds = (now - time).abs
5
6 case seconds
7 when 0...90
8 [1, :minute]
9 when 90...45.minutes
10 [(seconds / 60).round, :minute]
11 when 45.minutes...24.hours
12 [(seconds / 1.hour).round, :hour]
13 when 24.hours...30.days
14 [(seconds / 1.day).round, :day]
15 when 30.days...365.days
16 [(seconds / 30.days).round, :month]
17 else
18 [(seconds / 365.days).round, :year]
19 end
20 end
21
22 RELATIVE_TIME_UNIT_FORMS = {
23 en: {
24 minute: { one: "minute", other: "minutes" },
25 hour: { one: "hour", other: "hours" },
26 day: { one: "day", other: "days" },
27 month: { one: "month", other: "months" },
28 year: { one: "year", other: "years" },
29 },
30 de: {
31 # "other" here is the DATIVE plural needed after "vor" -- not the
32 # nominative plural you'd use standing alone ("3 Tage" vs "vor 3
33 # Tagen"). All five happen to be the nominative plural + "n", since
34 # none of them already end in -n or -s -- a property of these five
35 # specific words, not a rule to extend to new units without checking.
36 minute: { one: "Minute", other: "Minuten" },
37 hour: { one: "Stunde", other: "Stunden" },
38 day: { one: "Tag", other: "Tagen" },
39 month: { one: "Monat", other: "Monaten" },
40 year: { one: "Jahr", other: "Jahren" },
41 },
42 }.freeze
43
44 def relative_time_phrase(time, now = Time.current)
45 count, unit = relative_distance(time, now)
46 locale = I18n.locale.to_sym
47 forms = RELATIVE_TIME_UNIT_FORMS.fetch(locale, RELATIVE_TIME_UNIT_FORMS[:en])
48 word = forms.fetch(unit).fetch(count == 1 ? :one : :other)
49
50 locale == :de ? "vor #{count} #{word}" : "#{count} #{word} ago"
51 end
52
53end
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 1f2222c..550ccb3 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -14,6 +14,10 @@ de:
14 open_days_label: "Offene Tage" 14 open_days_label: "Offene Tage"
15 upcoming_events: "Weitere Termine" 15 upcoming_events: "Weitere Termine"
16 open_today: "Das Chaos, lokal und heute offen" 16 open_today: "Das Chaos, lokal und heute offen"
17 published: "veröffentlicht"
18 published_by: "veröffentlicht durch %{editor}"
19 editor_self: "du"
20 publisher_self: "dich"
17 date: 21 date:
18 formats: 22 formats:
19 default: "%d.%m.%Y" 23 default: "%d.%m.%Y"
@@ -29,7 +33,6 @@ de:
29 - :day 33 - :day
30 - :month 34 - :month
31 - :year 35 - :year
32
33 time: 36 time:
34 formats: 37 formats:
35 default: "%A, %e. %B %Y, %H:%M Uhr" 38 default: "%A, %e. %B %Y, %H:%M Uhr"
@@ -41,42 +44,6 @@ de:
41 am: "vormittags" 44 am: "vormittags"
42 pm: "nachmittags" 45 pm: "nachmittags"
43 46
44 datetime:
45 distance_in_words:
46 half_a_minute: 'eine halbe Minute'
47 less_than_x_seconds:
48 zero: 'weniger als 1 Sekunde'
49 one: 'weniger als 1 Sekunde'
50 other: 'weniger als {{count}} Sekunden'
51 x_seconds:
52 one: '1 Sekunde'
53 other: '{{count}} Sekunden'
54 less_than_x_minutes:
55 zero: 'weniger als 1 Minute'
56 one: 'weniger als eine Minute'
57 other: 'weniger als {{count}} Minuten'
58 x_minutes:
59 one: '1 Minute'
60 other: '{{count}} Minuten'
61 about_x_hours:
62 one: 'etwa 1 Stunde'
63 other: 'etwa {{count}} Stunden'
64 x_days:
65 one: '1 Tag'
66 other: '{{count}} Tage'
67 about_x_months:
68 one: 'etwa 1 Monat'
69 other: 'etwa {{count}} Monate'
70 x_months:
71 one: '1 Monat'
72 other: '{{count}} Monate'
73 about_x_years:
74 one: 'etwa 1 Jahr'
75 other: 'etwa {{count}} Jahre'
76 over_x_years:
77 one: 'mehr als 1 Jahr'
78 other: 'mehr als {{count}} Jahre'
79
80 number: 47 number:
81 format: 48 format:
82 precision: 2 49 precision: 2
@@ -105,35 +72,6 @@ de:
105 sentence_connector: "und" 72 sentence_connector: "und"
106 skip_last_comma: true 73 skip_last_comma: true
107 74
108 activerecord:
109 errors:
110 template:
111 header:
112 one: 1 error prohibited this {{model}} from being saved
113 other: "{{count}} errors prohibited this {{model}} from being saved"
114 body: "There were problems with the following fields:"
115
116 messages:
117 inclusion: "is not included in the list"
118 exclusion: "is reserved"
119 invalid: "is invalid"
120 confirmation: "doesn't match confirmation"
121 accepted: "must be accepted"
122 empty: "can't be empty"
123 blank: "can't be blank"
124 too_long: "is too long (maximum is {{count}} characters)"
125 too_short: "is too short (minimum is {{count}} characters)"
126 wrong_length: "is the wrong length (should be {{count}} characters)"
127 taken: "has already been taken"
128 not_a_number: "is not a number"
129 greater_than: "must be greater than {{count}}"
130 greater_than_or_equal_to: "must be greater than or equal to {{count}}"
131 equal_to: "must be equal to {{count}}"
132 less_than: "must be less than {{count}}"
133 less_than_or_equal_to: "must be less than or equal to {{count}}"
134 odd: "must be odd"
135 even: "must be even"
136
137 will_paginate: 75 will_paginate:
138 previous_label: "&#8592; Zurück" 76 previous_label: "&#8592; Zurück"
139 previous_aria_label: "Vorherige Seite" 77 previous_aria_label: "Vorherige Seite"
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 59c7304..73271ed 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -15,6 +15,10 @@ en:
15 open_days_label: "Open days" 15 open_days_label: "Open days"
16 upcoming_events: "Upcoming events" 16 upcoming_events: "Upcoming events"
17 open_today: "Open today" 17 open_today: "Open today"
18 published: "published"
19 published_by: "published by %{editor}"
20 publisher_self: "you"
21 editor_self: "you"
18 22
19 time: 23 time:
20 formats: 24 formats:
diff --git a/test/models/helpers/datetime_helper_test.rb b/test/models/helpers/datetime_helper_test.rb
new file mode 100644
index 0000000..a24e5d3
--- /dev/null
+++ b/test/models/helpers/datetime_helper_test.rb
@@ -0,0 +1,19 @@
1require 'test_helper'
2
3class DatetimeHelperTest < ActionView::TestCase
4 test "relative_time_phrase pluralizes correctly in English and German" do
5 travel_to Time.zone.parse("2026-07-12 12:00:00") do
6 I18n.with_locale(:en) do
7 assert_equal "1 day ago", relative_time_phrase(1.day.ago)
8 assert_equal "3 days ago", relative_time_phrase(3.days.ago)
9 end
10 I18n.with_locale(:de) do
11 assert_equal "vor 1 Tag", relative_time_phrase(1.day.ago)
12 assert_equal "vor 3 Tagen", relative_time_phrase(3.days.ago)
13 assert_equal "vor 1 Monat", relative_time_phrase(30.days.ago)
14 assert_equal "vor 2 Monaten", relative_time_phrase(45.days.ago)
15 assert_equal "vor 2 Jahren", relative_time_phrase(2.years.ago)
16 end
17 end
18 end
19end