diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3177d0eb7c..31fa3cf610 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -467,7 +467,7 @@ module ApplicationHelper h(page.pretty_title), href, :title => (if options[:timestamp] && page.updated_on - l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) + l(label_by_timestamp_format(:label_updated_time), format_timestamp(page.updated_on)) else nil end) @@ -690,13 +690,13 @@ module ApplicationHelper end def authoring(created, author, options={}) - l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe + l(label_by_timestamp_format(options[:label] || :label_added_time_by), :author => link_to_user(author), :age => time_tag(created)).html_safe end def time_tag(time) return if time.nil? - text = distance_of_time_in_words(Time.now, time) + text = format_timestamp(time) if @project link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 6fec8fe659..1b7cc0a490 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -224,6 +224,14 @@ module SettingsHelper end end + # Returns the options for the timestamp_format setting + # Convert the date and time three days ago into each format and use it as an example + def timestamp_format_setting_options + %w[relative_time relative_time_with_absolute_time absolute_time].map do |f| + ["#{format_timestamp(Time.now.ago(3.days), f)} (#{l('label_' + f)})", f] + end + end + def gravatar_default_setting_options [['Identicons', 'identicon'], ['Monster ids', 'monsterid'], diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb index 0b5f3b8f4e..2fe3807179 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -73,6 +73,6 @@ module WikiHelper end def wiki_content_update_info(content) - l(:label_updated_time_by, :author => link_to_user(content.author), :age => time_tag(content.updated_on)).html_safe + authoring(content.updated_on, content.author, label: :label_updated_time_by) end end diff --git a/app/views/issues/show.html.erb b/app/views/issues/show.html.erb index 6f0a0a9849..bf6391750b 100644 --- a/app/views/issues/show.html.erb +++ b/app/views/issues/show.html.erb @@ -40,7 +40,7 @@

<%= authoring @issue.created_on, @issue.author %>. <% if @issue.created_on != @issue.updated_on %> - <%= l(:label_updated_time, time_tag(@issue.updated_on)).html_safe %>. + <%= l(label_by_timestamp_format(:label_updated_time), time_tag(@issue.updated_on)).html_safe %>. <% end %>

diff --git a/app/views/settings/_display.html.erb b/app/views/settings/_display.html.erb index 30e6525f70..9f758e401a 100644 --- a/app/views/settings/_display.html.erb +++ b/app/views/settings/_display.html.erb @@ -17,6 +17,8 @@

<%= setting_select :timespan_format, [["%.2f" % 0.75, 'decimal'], ['0:45 h', 'minutes']], :blank => false %>

+

<%= setting_select :timestamp_format, timestamp_format_setting_options %>

+

<%= setting_select :user_format, @options[:user_format] %>

<%= setting_check_box :gravatar_enabled, :data => {:enables => '#settings_gravatar_default'} %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 5397a521d3..72d71cd8b3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -443,6 +443,7 @@ en: setting_date_format: Date format setting_time_format: Time format setting_timespan_format: Time span format + setting_timestamp_format: Timestamp format setting_cross_project_issue_relations: Allow cross-project issue relations setting_cross_project_subtasks: Allow cross-project subtasks setting_issue_list_default_columns: Issues list defaults @@ -869,6 +870,9 @@ en: label_f_hour: "%{value} hour" label_f_hour_plural: "%{value} hours" label_f_hour_short: "%{value} h" + label_relative_time: Relative time + label_relative_time_with_absolute_time: Relative time with absolute time + label_absolute_time: Absolute time label_time_tracking: Time tracking label_change_plural: Changes label_statistics: Statistics @@ -929,6 +933,9 @@ en: label_added_time_by: "Added by %{author} %{age} ago" label_updated_time_by: "Updated by %{author} %{age} ago" label_updated_time: "Updated %{value} ago" + label_added_absolute_time_by: "Added by %{author} %{age}" + label_updated_absolute_time_by: "Updated by %{author} %{age}" + label_updated_absolute_time: "Updated %{value}" label_jump_to_a_project: Jump to a project... label_file_plural: Files label_changeset_plural: Changesets diff --git a/config/locales/ja.yml b/config/locales/ja.yml index aab7542a36..05728e41d9 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -744,6 +744,9 @@ ja: label_added_time_by: "%{author} さんが%{age}前に追加" label_updated_time_by: "%{author} さんが%{age}前に更新" label_updated_time: "%{value}前に更新" + label_added_absolute_time_by: "%{author} さんが%{age}に追加" + label_updated_absolute_time_by: "%{author} さんが%{age}に更新" + label_updated_absolute_time: "%{value}に更新" label_jump_to_a_project: プロジェクトへ移動... label_file_plural: ファイル label_changeset_plural: 更新履歴 diff --git a/config/settings.yml b/config/settings.yml index 6db94a50f9..43169d4f78 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -171,6 +171,8 @@ time_format: default: '' timespan_format: default: 'minutes' +timestamp_format: + default: 'relative_time' user_format: default: :firstname_lastname format: symbol diff --git a/lib/redmine/i18n.rb b/lib/redmine/i18n.rb index 7010437fd7..026cffe4c2 100644 --- a/lib/redmine/i18n.rb +++ b/lib/redmine/i18n.rb @@ -87,6 +87,23 @@ module Redmine (include_date ? "#{format_date(local)} " : "") + ::I18n.l(local, **options) end + def format_timestamp(time, timestamp_format=nil) + case (timestamp_format || Setting.timestamp_format) + when 'relative_time' + distance_of_time_in_words(Time.now, time) + when 'relative_time_with_absolute_time' + "#{distance_of_time_in_words(Time.now, time)}(#{format_time(time)})" + when 'absolute_time' + format_time(time) + end + end + + def label_by_timestamp_format(label_name) + return label_name unless Setting.timestamp_format == 'absolute_time' + + label_name.to_s.gsub('_time', '_absolute_time').to_sym + end + def format_hours(hours) return "" if hours.blank? diff --git a/test/helpers/application_helper_test.rb b/test/helpers/application_helper_test.rb index ddc55dcb8e..3507b36d18 100644 --- a/test/helpers/application_helper_test.rb +++ b/test/helpers/application_helper_test.rb @@ -1774,12 +1774,10 @@ class ApplicationHelperTest < Redmine::HelperTest result = render_page_hierarchy(pages_by_parent_id, nil, :timestamp => true) assert_select_in( result, 'ul.pages-hierarchy li a[title=?]', - l(:label_updated_time, - distance_of_time_in_words(Time.now, parent_page.updated_on))) + l(:label_updated_time, format_timestamp(parent_page.updated_on))) assert_select_in( result, 'ul.pages-hierarchy li ul.pages-hierarchy a[title=?]', - l(:label_updated_time, - distance_of_time_in_words(Time.now, child_page.updated_on))) + l(:label_updated_time, format_timestamp(child_page.updated_on))) end def test_render_page_hierarchy_when_action_is_export diff --git a/test/helpers/settings_helper_test.rb b/test/helpers/settings_helper_test.rb index ef9bb05788..31831df2e8 100644 --- a/test/helpers/settings_helper_test.rb +++ b/test/helpers/settings_helper_test.rb @@ -29,4 +29,13 @@ class SettingsHelperTest < Redmine::HelperTest options = date_format_setting_options('en') assert_include ["2015-07-14 (yyyy-mm-dd)", "%Y-%m-%d"], options end + + def test_timestamp_format_setting_options_should_include_human_readable_format + sample_time = Time.now.ago(3.days) + + options = timestamp_format_setting_options + assert_include ["3 days (Relative time)", 'relative_time'], options + assert_include ["3 days(#{format_time(sample_time)}) (Relative time with absolute time)", 'relative_time_with_absolute_time'], options + assert_include ["#{format_time(sample_time)} (Absolute time)", 'absolute_time'], options + end end diff --git a/test/unit/lib/redmine/i18n_test.rb b/test/unit/lib/redmine/i18n_test.rb index c146549f18..b6d1348737 100644 --- a/test/unit/lib/redmine/i18n_test.rb +++ b/test/unit/lib/redmine/i18n_test.rb @@ -22,6 +22,7 @@ require_relative '../../../test_helper' class Redmine::I18nTest < ActiveSupport::TestCase include Redmine::I18n include ActionView::Helpers::NumberHelper + include ActionView::Helpers::DateHelper def setup User.current = nil @@ -125,6 +126,44 @@ class Redmine::I18nTest < ActiveSupport::TestCase end end + def test_format_timestamp + travel_to Time.utc(2023, 5, 31, 23, 59, 59) # Time.now => 2023/5/31 23:59:59 + sample_time = Time.utc(2022, 1, 1, 0, 0, 0) + + with_settings :timestamp_format => 'relative_time' do + assert_equal 'over 1 year', format_timestamp(sample_time) + end + with_settings :timestamp_format => 'relative_time_with_absolute_time', :date_format => '%d/%m/%Y', :time_format => '%H:%M' do + assert_equal 'over 1 year(01/01/2022 00:00)', format_timestamp(sample_time) + end + with_settings :timestamp_format => 'absolute_time', :date_format => '%d/%m/%Y', :time_format => '%H:%M' do + assert_equal '01/01/2022 00:00', format_timestamp(sample_time) + end + + # If there is an argument, it takes precedence over the set value + with_settings :timestamp_format => 'absolute_time', :date_format => '%d/%m/%Y', :time_format => '%H:%M' do + assert_equal 'over 1 year', format_timestamp(sample_time, 'relative_time') + end + end + + def test_label_by_timestamp_format + with_settings :timestamp_format => 'relative_time' do + assert_equal :label_added_time_by, label_by_timestamp_format(:label_added_time_by) + assert_equal :label_updated_time_by, label_by_timestamp_format(:label_updated_time_by) + assert_equal :label_updated_time, label_by_timestamp_format(:label_updated_time) + end + with_settings :timestamp_format => 'relative_time_with_absolute_time' do + assert_equal :label_added_time_by, label_by_timestamp_format(:label_added_time_by) + assert_equal :label_updated_time_by, label_by_timestamp_format(:label_updated_time_by) + assert_equal :label_updated_time, label_by_timestamp_format(:label_updated_time) + end + with_settings :timestamp_format => 'absolute_time' do + assert_equal :label_added_absolute_time_by, label_by_timestamp_format(:label_added_time_by) + assert_equal :label_updated_absolute_time_by, label_by_timestamp_format(:label_updated_time_by) + assert_equal :label_updated_absolute_time, label_by_timestamp_format(:label_updated_time) + end + end + def test_number_to_human_size_for_each_language valid_languages.each do |lang| set_language_if_valid lang