diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index fe250f7f3..361f3b1cd 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -493,7 +493,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) @@ -735,13 +735,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 1fb57b2d7..5b4faf245 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -234,6 +234,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 e00b3686e..8d66c02ef 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 8f732032a..0e2436072 100644 --- a/app/views/issues/show.html.erb +++ b/app/views/issues/show.html.erb @@ -43,7 +43,7 @@
diff --git a/app/views/settings/_display.html.erb b/app/views/settings/_display.html.erb index 62c53dfbb..37ff904d2 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 aa15095c6..3f4aceec9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -447,6 +447,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 @@ -879,6 +880,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 @@ -940,6 +944,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 5b234a19d..9b6bbdafe 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 48da91a10..5a38e353a 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -174,6 +174,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 a9cd1dd0d..3a95bc6fa 100644 --- a/lib/redmine/i18n.rb +++ b/lib/redmine/i18n.rb @@ -89,6 +89,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 9f2eb8405..ea0734254 100644 --- a/test/helpers/application_helper_test.rb +++ b/test/helpers/application_helper_test.rb @@ -1798,12 +1798,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 c32861d1c..49cfc51f9 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 72643b9e6..c5fb6a605 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