diff --git a/app/assets/javascripts/app/lib/app_post/i18n.coffee b/app/assets/javascripts/app/lib/app_post/i18n.coffee index 84b67e380..5704bdce6 100644 --- a/app/assets/javascripts/app/lib/app_post/i18n.coffee +++ b/app/assets/javascripts/app/lib/app_post/i18n.coffee @@ -348,6 +348,9 @@ class _i18nSingleton extends Spine.Module S = timeObject.getSeconds() M = timeObject.getMinutes() H = timeObject.getHours() + l = (H + 11) % 12 + 1 + l = ' ' + l if l < 10 + format = format .replace(/dd/, @formatNumber(d, 2)) .replace(/d/, d) @@ -358,4 +361,6 @@ class _i18nSingleton extends Spine.Module .replace(/SS/, @formatNumber(S, 2)) .replace(/MM/, @formatNumber(M, 2)) .replace(/HH/, @formatNumber(H, 2)) + .replace(/l/, l) + .replace(/P/, if H >= 12 then 'pm' else 'am') format diff --git a/app/models/translation.rb b/app/models/translation.rb index cf8916e26..909592e6e 100644 --- a/app/models/translation.rb +++ b/app/models/translation.rb @@ -164,6 +164,8 @@ or record.sub!('SS', format('%02d', second: timestamp.sec.to_s)) record.sub!('MM', format('%02d', minute: timestamp.min.to_s)) record.sub!('HH', format('%02d', hour: timestamp.hour.to_s)) + record.sub!('l', timestamp.strftime('%l')) + record.sub!('P', timestamp.strftime('%P')) "#{record} (#{timezone})" end diff --git a/i18n/zammad.pot b/i18n/zammad.pot index 8f46ac03c..b5b9155b0 100644 --- a/i18n/zammad.pot +++ b/i18n/zammad.pot @@ -20,7 +20,9 @@ msgstr "" #. - 'yy' - last 2 digits of year #. - 'SS' - 2-digit second #. - 'MM' - 2-digit minute -#. - 'HH' - 2-digit hour +#. - 'HH' - 2-digit hour (24h) +#. - 'l' - hour (12h) +#. - 'P' - Meridian indicator ('am' or 'pm') msgid "FORMAT_DATE" msgstr "mm/dd/yyyy" @@ -34,9 +36,11 @@ msgstr "mm/dd/yyyy" #. - 'yy' - last 2 digits of year #. - 'SS' - 2-digit second #. - 'MM' - 2-digit minute -#. - 'HH' - 2-digit hour +#. - 'HH' - 2-digit hour (24h) +#. - 'l' - hour (12h) +#. - 'P' - Meridian indicator ('am' or 'pm') msgid "FORMAT_DATETIME" -msgstr "mm/dd/yyyy HH:MM" +msgstr "mm/dd/yyyy l:MM P" #: db/seeds/settings.rb msgid "\"Database\" stores all attachments in the database (not recommended for storing large amounts of data). \"Filesystem\" stores the data in the filesystem. You can switch between the modules even on a system that is already in production without any loss of data." diff --git a/lib/generators/translation_catalog/translation_catalog_generator.rb b/lib/generators/translation_catalog/translation_catalog_generator.rb index 8db53c9c3..b1281dd87 100644 --- a/lib/generators/translation_catalog/translation_catalog_generator.rb +++ b/lib/generators/translation_catalog/translation_catalog_generator.rb @@ -32,7 +32,9 @@ class Generators::TranslationCatalog::TranslationCatalogGenerator < Rails::Gener #. - 'yy' - last 2 digits of year #. - 'SS' - 2-digit second #. - 'MM' - 2-digit minute - #. - 'HH' - 2-digit hour + #. - 'HH' - 2-digit hour (24h) + #. - 'l' - hour (12h) + #. - 'P' - Meridian indicator ('am' or 'pm') LEGEND def extract_strings(path) @@ -91,6 +93,9 @@ class Generators::TranslationCatalog::TranslationCatalogGenerator < Rails::Gener POT_HEADER + # Add the default date/time format strings for 'en_US' as translations to + # the source catalog file. They will be read into the Translation model + # and can be customized via the translations admin GUI. pot += <<~FORMAT_STRINGS if !options['addon_path'] #. Default date format to use for the current locale. #{DATE_FORMAT_LEGEND} @@ -100,7 +105,7 @@ class Generators::TranslationCatalog::TranslationCatalogGenerator < Rails::Gener #. Default date/time format to use for the current locale. #{DATE_FORMAT_LEGEND} msgid "FORMAT_DATETIME" - msgstr "mm/dd/yyyy HH:MM" + msgstr "mm/dd/yyyy l:MM P" FORMAT_STRINGS diff --git a/public/assets/tests/qunit/html_utils.js b/public/assets/tests/qunit/html_utils.js index ee77d5112..ecc32fd0c 100644 --- a/public/assets/tests/qunit/html_utils.js +++ b/public/assets/tests/qunit/html_utils.js @@ -1362,7 +1362,13 @@ QUnit.test("check replace tags", assert => { yfull = localTime.getFullYear() M = formatNumber(localTime.getMinutes(), 2) H = formatNumber(localTime.getHours(), 2) - return m + '/' + d + '/' + yfull + ' ' + H + ':' + M + l = (H + 11) % 12 + 1 + if (l < 10) { + l = ' ' + l + } + P = H >= 12 ? 'pm' : 'am' + + return m + '/' + d + '/' + yfull + ' ' + l + ':' + M + ' ' + P } var message = "
#{user.firstname} #{user.lastname}
" diff --git a/public/assets/tests/qunit/i18n.js b/public/assets/tests/qunit/i18n.js index 5e7f93618..9164eb25c 100644 --- a/public/assets/tests/qunit/i18n.js +++ b/public/assets/tests/qunit/i18n.js @@ -217,8 +217,11 @@ QUnit.test('i18n', assert => { translated = App.i18n.translateContent('Enables user authentication via %s. Register your app first at [%s](%s).', 'XXX', 'YYY', 'http://lalala') assert.equal(translated, 'Enables user authentication via XXX. Register your app first at YYY.', 'en-us - link') - timestamp = App.i18n.translateTimestamp('2012-11-06T21:07:24Z', offset) - assert.equal(timestamp, '11/06/2012 21:07', 'en - timestamp translated correctly') + timestamp = App.i18n.translateTimestamp('2012-11-06T09:07:24Z', offset) + assert.equal(timestamp, '11/06/2012 9:07 am', 'en - timestamp translated correctly (pm)') + + timestamp = App.i18n.translateTimestamp('2012-11-06T22:07:24Z', offset) + assert.equal(timestamp, '11/06/2012 10:07 pm', 'en - timestamp translated correctly (pm)') timestamp = App.i18n.translateTimestamp('', offset); assert.equal(timestamp, '', 'en - timestamp translated correctly') @@ -242,7 +245,7 @@ QUnit.test('i18n', assert => { assert.equal(date, undefined, 'en - date translated correctly') date = App.i18n.timeFormat() - assert.deepEqual(date, { "FORMAT_DATE": "mm/dd/yyyy", "FORMAT_DATETIME": "mm/dd/yyyy HH:MM" }, 'timeFormat property') + assert.deepEqual(date, { "FORMAT_DATE": "mm/dd/yyyy", "FORMAT_DATETIME": "mm/dd/yyyy l:MM P" }, 'timeFormat property') // Verify that the datepicker gets the correct format too. el_date = App.UiElement.date.render({name: 'test', value: '2018-07-06'}) diff --git a/public/assets/tests/qunit/model_ui.js b/public/assets/tests/qunit/model_ui.js index 393c58a42..f64160c15 100644 --- a/public/assets/tests/qunit/model_ui.js +++ b/public/assets/tests/qunit/model_ui.js @@ -54,7 +54,7 @@ QUnit.test( "model ui basic tests", assert => { assert.equal( App.viewPrint( ticket, 'state' ), 'open') assert.equal( App.viewPrint( ticket, 'state_id' ), 'open') assert.equal( App.viewPrint( ticket, 'not_existing' ), '-') - assert.equal( App.viewPrint( ticket, 'updated_at' ), '') + assert.equal( App.viewPrint( ticket, 'updated_at' ), '') assert.equal( App.viewPrint( ticket, 'date' ), '02/07/2015') assert.equal( App.viewPrint( ticket, 'textarea' ), '
some new
line
') assert.equal( App.viewPrint( ticket, 'link1' ), 'closed') @@ -65,7 +65,7 @@ QUnit.test( "model ui basic tests", assert => { let attr = App.Ticket.configure_attributes.find(e => { return e.name == 'updated_at' }) attr.include_timezone = true - assert.equal( App.viewPrint( ticket, 'updated_at' ), '') + assert.equal( App.viewPrint( ticket, 'updated_at' ), '') attr.include_timezone = false stub.restore() diff --git a/public/assets/tests/qunit/ui.js b/public/assets/tests/qunit/ui.js index df1048b60..bf733c165 100644 --- a/public/assets/tests/qunit/ui.js +++ b/public/assets/tests/qunit/ui.js @@ -362,9 +362,14 @@ QUnit.test("check pretty date", assert => { yshort = date.getYear()-100 M = date.getMinutes() H = date.getHours() + l = (H + 11) % 12 + 1 + if (l < 10) { + l = ' ' + l + } + P = H >= 12 ? 'pm' : 'am' // YYYY-MM-DD HH::MM - return (m < 10 ? '0':'') + m + '/' + (d < 10 ? '0':'') + d + '/' + (yfull) + ' ' + (H < 10 ? '0':'') + H + ':' + (M < 10 ? '0':'') + M + return (m < 10 ? '0':'') + m + '/' + (d < 10 ? '0':'') + d + '/' + (yfull) + ' ' + l + ':' + (M < 10 ? '0':'') + M + ' ' + P } }); diff --git a/spec/lib/notification_factory/slack_spec.rb b/spec/lib/notification_factory/slack_spec.rb index 52cef2bd5..5adba7fb5 100644 --- a/spec/lib/notification_factory/slack_spec.rb +++ b/spec/lib/notification_factory/slack_spec.rb @@ -60,7 +60,7 @@ RSpec.describe NotificationFactory::Slack do .to match(%r{Updated by #{current_user.fullname}}) .and match(%r{state: aaa -> bbb}) .and match(%r{group: xxx -> yyy}) - .and match(%r{pending_time: 04/01/2019 12:00 \(Europe/Berlin\) -> 04/02/2019 01:00 \(Europe/Berlin\)}) + .and match(%r{pending_time: 04/01/2019 12:00 pm \(Europe/Berlin\) -> 04/02/2019 1:00 am \(Europe/Berlin\)}) .and match(%r{#{article.body}\z}) end end @@ -90,7 +90,7 @@ RSpec.describe NotificationFactory::Slack do it 'returns a hash with body: ' do expect(template[:body]) .to match(%r{A ticket \(#{ticket.title}\) from "#{ticket.customer.fullname}"}) - .and match(%r{is escalated since "04/01/2019 12:00 \(Europe/Berlin\)"!}) + .and match(%r{is escalated since "04/01/2019 12:00 pm \(Europe/Berlin\)"!}) .and match(%r{#{article.body}\z}) end end diff --git a/spec/models/translation/translation_synchronizes_from_po_spec.rb b/spec/models/translation/translation_synchronizes_from_po_spec.rb index d4fc8a863..5eb7ea692 100644 --- a/spec/models/translation/translation_synchronizes_from_po_spec.rb +++ b/spec/models/translation/translation_synchronizes_from_po_spec.rb @@ -56,7 +56,7 @@ RSpec.describe Translation do end it 'contains the translation for "FORMAT_DATE_TIME"' do - expect(described_class.strings_for_locale('en-us')['FORMAT_DATETIME']).to have_attributes(translation: 'mm/dd/yyyy HH:MM', translation_file: 'i18n/zammad.pot') + expect(described_class.strings_for_locale('en-us')['FORMAT_DATETIME']).to have_attributes(translation: 'mm/dd/yyyy l:MM P', translation_file: 'i18n/zammad.pot') end end @@ -210,7 +210,7 @@ RSpec.describe Translation do end it 'adds the en-us FORMAT_DATETIME entry' do - expect(described_class.find_source('en-us', 'FORMAT_DATETIME')).to have_attributes(is_synchronized_from_codebase: true, synchronized_from_translation_file: 'i18n/zammad.pot', target: 'mm/dd/yyyy HH:MM') + expect(described_class.find_source('en-us', 'FORMAT_DATETIME')).to have_attributes(is_synchronized_from_codebase: true, synchronized_from_translation_file: 'i18n/zammad.pot', target: 'mm/dd/yyyy l:MM P') end it 'adds the custom translated entry with content' do diff --git a/spec/models/translation_spec.rb b/spec/models/translation_spec.rb index 80bb4b26f..632c1fb78 100644 --- a/spec/models/translation_spec.rb +++ b/spec/models/translation_spec.rb @@ -69,12 +69,20 @@ RSpec.describe Translation do expect(described_class.timestamp('en-us', 'Invalid/TimeZone', '2018-10-10T10:00:00Z0')).to eq(Time.zone.parse('2018-10-10T10:00:00Z0').to_s) end - it 'en-us with timestamp as string' do - expect(described_class.timestamp('en-us', 'Europe/Berlin', '2018-10-10T10:00:00Z0')).to eq('10/10/2018 12:00 (Europe/Berlin)') + it 'en-us with timestamp as string (am)' do + expect(described_class.timestamp('en-us', 'Europe/Berlin', '2018-10-10T01:00:00Z0')).to eq('10/10/2018 3:00 am (Europe/Berlin)') end - it 'en-us with time object' do - expect(described_class.timestamp('en-us', 'Europe/Berlin', Time.zone.parse('2018-10-10T10:00:00Z0'))).to eq('10/10/2018 12:00 (Europe/Berlin)') + it 'en-us with timestamp as string (pm)' do + expect(described_class.timestamp('en-us', 'Europe/Berlin', '2018-10-10T10:00:00Z0')).to eq('10/10/2018 12:00 pm (Europe/Berlin)') + end + + it 'en-us with time object (am)' do + expect(described_class.timestamp('en-us', 'Europe/Berlin', Time.zone.parse('2018-10-10T01:00:00Z0'))).to eq('10/10/2018 3:00 am (Europe/Berlin)') + end + + it 'en-us with time object (pm)' do + expect(described_class.timestamp('en-us', 'Europe/Berlin', Time.zone.parse('2018-10-10T10:00:00Z0'))).to eq('10/10/2018 12:00 pm (Europe/Berlin)') end it 'de-de with timestamp as string' do diff --git a/spec/system/ticket/update/full_quote_header_spec.rb b/spec/system/ticket/update/full_quote_header_spec.rb index 141215944..655336710 100644 --- a/spec/system/ticket/update/full_quote_header_spec.rb +++ b/spec/system/ticket/update/full_quote_header_spec.rb @@ -375,7 +375,7 @@ RSpec.describe 'Ticket > Update > Full Quote Header', current_user_id: -> { curr expected .created_at .in_time_zone('Europe/London') - .strftime('%m/%d/%Y %H:%M') + .strftime('%m/%d/%Y %1I:%M %P') end end end diff --git a/test/unit/notification_factory_renderer_test.rb b/test/unit/notification_factory_renderer_test.rb index 74621f53e..c94599c66 100644 --- a/test/unit/notification_factory_renderer_test.rb +++ b/test/unit/notification_factory_renderer_test.rb @@ -58,7 +58,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase timezone: 'Europe/Berlin', template: template, ).render - assert_equal('11/12/2016 13:00 (Europe/Berlin)', result) + assert_equal('11/12/2016 1:00 pm (Europe/Berlin)', result) template = "\#{ticket.created_by.firstname}" result = described_class.new( @@ -80,7 +80,7 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase timezone: 'Europe/Berlin', template: template, ).render - assert_equal('11/12/2016 15:00 (Europe/Berlin)', result) + assert_equal('11/12/2016 3:00 pm (Europe/Berlin)', result) template = "\#{ticket.updated_by.firstname}" result = described_class.new( diff --git a/test/unit/ticket_notification_test.rb b/test/unit/ticket_notification_test.rb index 9e82db339..bb81d536b 100644 --- a/test/unit/ticket_notification_test.rb +++ b/test/unit/ticket_notification_test.rb @@ -1194,7 +1194,7 @@ class TicketNotificationTest < ActiveSupport::TestCase assert_match(%r{1 low}, result[:body]) assert_match(%r{2 normal}, result[:body]) assert_match(%r{Pending till}, result[:body]) - assert_match('01/11/2015 19:33 (America/St_Lucia)', result[:body]) + assert_match('01/11/2015 7:33 pm (America/St_Lucia)', result[:body]) assert_match(%r{update}, result[:body]) assert_no_match(%r{pending_till}, result[:body]) assert_no_match(%r{i18n}, result[:body]) @@ -1322,7 +1322,7 @@ class TicketNotificationTest < ActiveSupport::TestCase ) assert_match('Ticket is escalated (some notification template test 1 Bobs\'s resumé', result[:subject]) - assert_match('is escalated since "04/01/2019 06:00 (America/St_Lucia)"!', result[:body]) + assert_match('is escalated since "04/01/2019 6:00 am (America/St_Lucia)"!', result[:body]) end