local_time 2.1.0 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +50 -10
  4. data/app/assets/javascripts/local-time.es2017-esm.js +1 -0
  5. data/app/assets/javascripts/local-time.es2017-umd.js +1 -0
  6. data/app/helpers/local_time_helper.rb +72 -21
  7. data/lib/local_time/version.rb +3 -0
  8. data/test/helpers/local_time_helper_test.rb +60 -31
  9. data/test/javascripts/builds/index.js +26626 -0
  10. data/test/javascripts/fixtures/index.html +20 -0
  11. data/test/javascripts/fixtures/time_zone_check.html +20 -0
  12. data/test/javascripts/server.mjs +41 -0
  13. data/test/javascripts/src/format24_test.js +109 -0
  14. data/test/javascripts/src/i18n_test.js +55 -0
  15. data/test/javascripts/src/index.js +10 -0
  16. data/test/javascripts/src/local_time_test.js +54 -0
  17. data/test/javascripts/src/relative_date_test.js +121 -0
  18. data/test/javascripts/src/strftime_test.js +151 -0
  19. data/test/javascripts/src/test_helpers.js +66 -0
  20. data/test/javascripts/src/time_ago_test.js +72 -0
  21. data/test/javascripts/vendor/moment.js +5684 -2
  22. data/test/javascripts/vendor/sinon.js +20485 -0
  23. metadata +81 -28
  24. data/app/assets/javascripts/local-time.js +0 -1
  25. data/test/javascripts/fixtures/body.html +0 -8
  26. data/test/javascripts/src/i18n_test.coffee +0 -47
  27. data/test/javascripts/src/local_time_test.coffee +0 -37
  28. data/test/javascripts/src/relative_date_test.coffee +0 -89
  29. data/test/javascripts/src/strftime_test.coffee +0 -54
  30. data/test/javascripts/src/test.coffee +0 -43
  31. data/test/javascripts/src/time_ago_test.coffee +0 -56
  32. data/test/javascripts/vendor/sinon-timers.js +0 -385
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02425f459bf86821d0c3edab8e84476a55f6b928b521930b0fce12928e41a3e9
4
- data.tar.gz: de41c8acfee651165b14b14e50df09bd4101b980c3789affb626fa3585ff2331
3
+ metadata.gz: 799fafd7b55d67b64c0038f8d9d88a6e521a858916aff976b3bbc6ddd3a588f7
4
+ data.tar.gz: 48000c9b35e80ae555ae629d75734896e9b78deeeb7443ca828f38903fd6c314
5
5
  SHA512:
6
- metadata.gz: bfa2536cb9ca2a0a3bde70c9eee7ea12a85bd022cc819394fef2d77df6acd3f9a927b3ba38110498cb812f2f8959c1cc0f4c8bcbbc82fb6a7f1685417854eeca
7
- data.tar.gz: 66d6fce6ce42a63e83fa0bc9b7bb652ca17dae9f398c58f26684830a24ff7a62266f6e01d3171d22a0b81fcf4ac44a92989d43ae71de9847769f35612bc5565d
6
+ metadata.gz: c6e47fd634ccdfebd71260e05f49d87d6dc80b1aeef0353988d5f8d1f2bc92188073cef9e0a910d0d97fee6d2bc037507669c95b35769a978c8e7b2907ac960f
7
+ data.tar.gz: 3473c5d757047367067087363875824a472adcacd4221a64965c3b1d4d58da3102b569c326c07f55fbe3152bd2c9d9aec80124a0e1093c1ef16f63b8f5e526aa
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2018 Javan Makhmali, Basecamp
1
+ Copyright 2025 Javan Makhmali, Basecamp
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -4,14 +4,26 @@ Local Time makes it easy to display times and dates to users in their local time
4
4
 
5
5
  ## Installation
6
6
 
7
- 1. Add `gem 'local_time'` to your Gemfile.
8
- 2. Include `local-time.js` in your application's JavaScript bundle.
7
+ ### Importmaps
8
+ 1. Add `gem "local_time"` to your Gemfile.
9
+ 2. Run `bundle install`
10
+ 3. Run `bin/importmap pin local-time` to add the [local-time npm package](https://www.npmjs.com/package/local-time)
11
+ 4. Add this to `app/javascript/application.js`
9
12
 
10
- Using the asset pipeline:
11
13
  ```js
12
- //= require local-time
14
+ import LocalTime from "local-time"
15
+ LocalTime.start()
16
+ document.addEventListener("turbo:morph", () => {
17
+ LocalTime.run()
18
+ })
13
19
  ```
14
- Using the [local-time npm package](https://www.npmjs.com/package/local-time):
20
+
21
+ ### Webpacker
22
+ 1. Add `gem "local_time"` to your Gemfile.
23
+ 2. Run `bundle install`
24
+ 3. Run `yarn add local-time`
25
+ 4. Add this to `app/javascript/packs/application.js`
26
+
15
27
  ```js
16
28
  import LocalTime from "local-time"
17
29
  LocalTime.start()
@@ -78,7 +90,9 @@ To use a strftime format already defined in your app, pass a symbol as the forma
78
90
  <%= local_time(date, :long) %>
79
91
  ```
80
92
 
81
- `I18n.t("time.formats.#{format}")`, `I18n.t("date.formats.#{format}")`, `Time::DATE_FORMATS[format]`, and `Date::DATE_FORMATS[format]` will be scanned (in that order) for your format.
93
+ When using the `local_time` helper `I18n.t("time.formats.#{format}")`, `I18n.t("date.formats.#{format}")`, `Time::DATE_FORMATS[format]`, and `Date::DATE_FORMATS[format]` will be scanned (in that order) for your format.
94
+
95
+ When using the `local_date` helper, `I18n.t("date.formats.#{format}")`, `I18n.t("time.formats.#{format}")`, `Date::DATE_FORMATS[format]`, and `Time::DATE_FORMATS[format]` will be scanned (in that order) for your format.
82
96
 
83
97
  Note: The included strftime JavaScript implementation is not 100% complete. It supports the following directives: `%a %A %b %B %c %d %e %H %I %l %m %M %p %P %S %w %y %Y %Z`
84
98
 
@@ -120,7 +134,7 @@ Preset time and date formats that vary with age. The available types are `date`,
120
134
 
121
135
  **Internationalization (I18n)**
122
136
 
123
- Local Time includes a [set of default `en` translations](lib/assets/javascripts/src/local-time/config/i18n.coffee) which can be updated directly. Or, you can provide an entirely new set in a different locale:
137
+ Local Time includes a [set of default `en` translations](lib/assets/javascripts/src/local-time/config/i18n.js) which can be updated directly. Or, you can provide an entirely new set in a different locale:
124
138
 
125
139
  ```js
126
140
  LocalTime.config.i18n["es"] = {
@@ -140,8 +154,34 @@ LocalTime.config.i18n["es"] = {
140
154
  LocalTime.config.locale = "es"
141
155
  ```
142
156
 
143
- ---
157
+ > [!NOTE]
158
+ > The "default" keys in the i18n configuration object are used for translations in LocalTime's `RelativeTime` module. They are not used to determine which format is rendered when none is provided. See https://github.com/basecamp/local_time/issues/128 for details.
159
+
160
+ **24-hour time formatting**
161
+ Local Time supports 24-hour time formats out of the box.
162
+
163
+ To use this feature, configure the library to favor `data-format24` over `data-format` attributes:
164
+
165
+ ```js
166
+ LocalTime.config.useFormat24 = true
167
+ ```
168
+
169
+ The library will now default to using the `data-format24` attribute on `<time>` elements for formatting.
170
+ But it will still fall back to `data-format` if `data-format24` is not provided.
171
+
172
+ The included Rails helpers will automatically look for 24h variants of named formats.
173
+ They will search for `#{name}_24h` in [the same places](#time-and-date-helpers) the regular name is looked up.
174
+
175
+ This is an example of what your app configuration might look like:
176
+
177
+ ```ruby
178
+ Time::DATE_FORMATS[:simple] = "%-l:%M%P"
179
+ Time::DATE_FORMATS[:simple_24h] = "%H:%M"
180
+ ```
181
+
182
+ When `:type` is set to `time-ago`, the format is obtained from the `I18n` [configuration](#configuration).
144
183
 
145
- [![Build Status](https://travis-ci.org/basecamp/local_time.svg?branch=master)](https://travis-ci.org/basecamp/local_time)
184
+ In practice, you might set `config.useFormat24` to `true` or `false` depending on the current user's configuration, before rendering any `<time>` elements.
146
185
 
147
- [![Sauce Test Status](https://saucelabs.com/browser-matrix/basecamp_local_time.svg)](https://saucelabs.com/u/basecamp_local_time)
186
+ ## Contributing
187
+ Please read [CONTRIBUTING.md](./CONTRIBUTING.md).
@@ -0,0 +1 @@
1
+ const t={config:{},run(){this.getController().processElements()},process(...t){for(const e of t)this.getController().processElement(e);return t.length},getController(){return this.controller?this.controller:this.controller=new t.Controller}};t.config.useFormat24=!1,t.config.i18n={en:{date:{dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbrDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],abbrMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],yesterday:"yesterday",today:"today",tomorrow:"tomorrow",on:"on {date}",formats:{default:"%b %e, %Y",thisYear:"%b %e"}},time:{am:"am",pm:"pm",singular:"a {time}",singularAn:"an {time}",elapsed:"{time} ago",second:"second",seconds:"seconds",minute:"minute",minutes:"minutes",hour:"hour",hours:"hours",formats:{default:"%l:%M%P",default_24h:"%H:%M"}},datetime:{at:"{date} at {time}",formats:{default:"%B %e, %Y at %l:%M%P %Z",default_24h:"%B %e, %Y at %H:%M %Z"}}}},t.config.locale="en",t.config.defaultLocale="en",t.config.timerInterval=6e4;const e=!isNaN(Date.parse("2011-01-01T12:00:00-05:00"));t.parseDate=t=>(t=t.toString(),e||(t=function(t){const e=t.match(r);if(e){let t;const[r,a,s,n,i,o,c,u]=e;return"Z"!==u&&(t=u.replace(":","")),`${a}/${s}/${n} ${i}:${o}:${c} GMT${[t]}`}}(t)),new Date(Date.parse(t)));const r=/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(Z|[-+]?[\d:]+)$/;t.elementMatchesSelector=(()=>{const t=document.documentElement,e=t.matches||t.matchesSelector||t.webkitMatchesSelector||t.mozMatchesSelector||t.msMatchesSelector;return(t,r)=>{if(t?.nodeType===Node.ELEMENT_NODE)return e.call(t,r)}})();const{config:a}=t,{i18n:s}=a;t.getI18nValue=(e="",r={locale:a.locale})=>{const{locale:n}=r,i=function(t,e){let r=t;for(var a of Array.from(e.split("."))){if(!r[a])return null;r=r[a]}return r}(s[n],e);return i||(n!==a.defaultLocale?t.getI18nValue(e,{locale:a.defaultLocale}):void 0)},t.translate=(e,r={},a)=>{let s=t.getI18nValue(e,a);for(const t in r){const e=r[t];s=s.replace(`{${t}}`,e)}return s};const{getI18nValue:n,translate:i}=t,o="function"==typeof Intl?.DateTimeFormat,c={"Central European Standard Time":"CET","Central European Summer Time":"CEST","China Standard Time":"CST","Israel Daylight Time":"IDT","Israel Standard Time":"IST","Moscow Standard Time":"MSK","Peru Standard Time":"PET","Philippine Standard Time":"PHT","Singapore Standard Time":"SGT","Western Indonesia Time":"WIB"};t.knownEdgeCaseTimeZones=c,t.strftime=(()=>{const t=(a,s)=>{const u=a.getDay(),l=a.getDate(),d=a.getMonth(),m=a.getFullYear(),h=a.getHours(),f=a.getMinutes(),g=a.getSeconds();return s.replace(/%(-?)([%aAbBcdeHIlmMpPSwyYZ])/g,((s,p,y)=>{switch(y){case"%":return"%";case"a":return n("date.abbrDayNames")[u];case"A":return n("date.dayNames")[u];case"b":return n("date.abbrMonthNames")[d];case"B":return n("date.monthNames")[d];case"c":return a.toString();case"d":return e(l,p);case"e":return l;case"H":return e(h,p);case"I":return e(t(a,"%l"),p);case"l":return 0===h||12===h?12:(h+12)%12;case"m":return e(d+1,p);case"M":return e(f,p);case"p":return i("time."+(h>11?"pm":"am")).toUpperCase();case"P":return i("time."+(h>11?"pm":"am"));case"S":return e(g,p);case"w":return u;case"y":return e(m%100,p);case"Y":return m;case"Z":return function(t){let e,a,s;return(a=function(t){return Object.keys(c).find((e=>o?new Date(t).toLocaleString("en-US",{timeZoneName:"long"}).includes(e):t.toString().includes(e)))}(t))?c[a]:(s=r(t,{allowGMT:!1}))||(s=function(t){const e=t.toString();let r;if(null!=(r=e.match(/\(([\w\s]+)\)$/))){const t=r[1];return/\s/.test(t)?t.match(/\b(\w)/g).join(""):t}if(null!=(r=e.match(/(\w{3,4})\s\d{4}$/)))return r[1];if(null!=(r=e.match(/(UTC[\+\-]\d+)/)))return r[1]}(t))?s:(e=r(t,{allowGMT:!0}))?e:""}(a)}}))};function e(t,e){return"-"===e?t:`0${t}`.slice(-2)}function r(t,{allowGMT:e}){if(o){const r=new Date(t).toLocaleString("en-US",{timeZoneName:"short"}).split(" ").pop();if(e||!r.includes("GMT"))return r}}return t})(),t.CalendarDate=class{static fromDate(t){return new this(t.getFullYear(),t.getMonth()+1,t.getDate())}static today(){return this.fromDate(new Date)}constructor(t,e,r){this.date=new Date(Date.UTC(t,e-1)),this.date.setUTCDate(r),this.year=this.date.getUTCFullYear(),this.month=this.date.getUTCMonth()+1,this.day=this.date.getUTCDate(),this.value=this.date.getTime()}equals(t){return t?.value===this.value}is(t){return this.equals(t)}isToday(){return this.is(this.constructor.today())}occursOnSameYearAs(t){return this.year===t?.year}occursThisYear(){return this.occursOnSameYearAs(this.constructor.today())}daysSince(t){if(t)return(this.date-t.date)/864e5}daysPassed(){return this.constructor.today().daysSince(this)}};const{strftime:u,translate:l,getI18nValue:d,config:m}=t;t.RelativeTime=class{constructor(e){this.date=e,this.calendarDate=t.CalendarDate.fromDate(this.date)}toString(){let t,e;return(e=this.toTimeElapsedString())?l("time.elapsed",{time:e}):(t=this.toWeekdayString())?(e=this.toTimeString(),l("datetime.at",{date:t,time:e})):l("date.on",{date:this.toDateString()})}toTimeOrDateString(){return this.calendarDate.isToday()?this.toTimeString():this.toDateString()}toTimeElapsedString(){let t;const e=(new Date).getTime()-this.date.getTime(),r=Math.round(e/1e3),a=Math.round(r/60),s=Math.round(a/60);return e<0?null:r<10?(t=l("time.second"),l("time.singular",{time:t})):r<45?`${r} ${l("time.seconds")}`:r<90?(t=l("time.minute"),l("time.singular",{time:t})):a<45?`${a} ${l("time.minutes")}`:a<90?(t=l("time.hour"),l("time.singularAn",{time:t})):s<24?`${s} ${l("time.hours")}`:""}toWeekdayString(){switch(this.calendarDate.daysPassed()){case 0:return l("date.today");case 1:return l("date.yesterday");case-1:return l("date.tomorrow");case 2:case 3:case 4:case 5:case 6:return u(this.date,"%A");default:return""}}toDateString(){const t=this.calendarDate.occursThisYear()?d("date.formats.thisYear"):d("date.formats.default");return u(this.date,t)}toTimeString(){const t=m.useFormat24?"default_24h":"default";return u(this.date,d(`time.formats.${t}`))}};const{elementMatchesSelector:h}=t;t.PageObserver=class{constructor(t,e){this.selector=t,this.callback=e,this.processMutations=this.processMutations.bind(this),this.processInsertion=this.processInsertion.bind(this)}start(){this.started||(this.observeWithMutationObserver()||this.observeWithMutationEvent(),this.started=!0)}observeWithMutationObserver(){if("undefined"!=typeof MutationObserver&&null!==MutationObserver){return new MutationObserver(this.processMutations).observe(document.documentElement,{childList:!0,subtree:!0}),!0}return!1}observeWithMutationEvent(){return addEventListener("DOMNodeInserted",this.processInsertion,!1),!0}findSignificantElements(t){const e=[];return t?.nodeType===Node.ELEMENT_NODE&&(h(t,this.selector)&&e.push(t),e.push(...Array.from(t.querySelectorAll(this.selector)||[]))),e}processMutations(t){const e=[];for(const r of t)if("childList"===r.type)for(const t of r.addedNodes)e.push(...this.findSignificantElements(t)||[]);this.notify(e)}processInsertion(t){const e=this.findSignificantElements(t.target);this.notify(e)}notify(t){t?.length>0&&"function"==typeof this.callback&&this.callback(t)}};const{parseDate:f,strftime:g,getI18nValue:p,config:y}=t,S="time[data-local]:not([data-localized])",T=t=>t.setAttribute("data-localized",""),b=e=>new t.RelativeTime(e);t.Controller=class{constructor(){this.processElements=this.processElements.bind(this),this.pageObserver=new t.PageObserver(S,this.processElements)}start(){this.started||(this.processElements(),this.startTimer(),this.pageObserver.start(),this.started=!0)}startTimer(){let t;(t=y.timerInterval)&&(this.timer||(this.timer=setInterval(this.processElements,t)))}processElements(t){t||(t=document.querySelectorAll(S));for(const e of Array.from(t))this.processElement(e);return t.length}processElement(t){const e=t.getAttribute("datetime"),r=t.getAttribute("data-local"),a=y.useFormat24&&t.getAttribute("data-format24")||t.getAttribute("data-format"),s=f(e);if(!isNaN(s)){if(!t.hasAttribute("title")){const e=y.useFormat24?"default_24h":"default",r=g(s,p(`datetime.formats.${e}`));t.setAttribute("title",r)}(t=>{t.setAttribute("data-processed-at",(new Date).toISOString())})(t),t.textContent=(()=>{switch(r){case"time":return T(t),g(s,a);case"date":return T(t),b(s).toDateString();case"time-ago":return b(s).toString();case"time-or-date":return b(s).toTimeOrDateString();case"weekday":return b(s).toWeekdayString();case"weekday-or-date":return b(s).toWeekdayString()||b(s).toDateString()}})()}}};let M=!1;function D(){t.getController().start()}t.start=()=>{var e;M?t.run():(M=!0,"undefined"!=typeof MutationObserver&&null!==MutationObserver||(document.attachEvent?"complete"===document.readyState:"loading"!==document.readyState)?D():(e=D,"function"==typeof requestAnimationFrame?requestAnimationFrame(e):setTimeout(e,17)))},t.processing=()=>t.getController().started,window.LocalTime===t&&t.start();export{t as default};
@@ -0,0 +1 @@
1
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).LocalTime=e()}(this,(function(){"use strict";const t={config:{},run(){this.getController().processElements()},process(...t){for(const e of t)this.getController().processElement(e);return t.length},getController(){return this.controller?this.controller:this.controller=new t.Controller}};t.config.useFormat24=!1,t.config.i18n={en:{date:{dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbrDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],abbrMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],yesterday:"yesterday",today:"today",tomorrow:"tomorrow",on:"on {date}",formats:{default:"%b %e, %Y",thisYear:"%b %e"}},time:{am:"am",pm:"pm",singular:"a {time}",singularAn:"an {time}",elapsed:"{time} ago",second:"second",seconds:"seconds",minute:"minute",minutes:"minutes",hour:"hour",hours:"hours",formats:{default:"%l:%M%P",default_24h:"%H:%M"}},datetime:{at:"{date} at {time}",formats:{default:"%B %e, %Y at %l:%M%P %Z",default_24h:"%B %e, %Y at %H:%M %Z"}}}},t.config.locale="en",t.config.defaultLocale="en",t.config.timerInterval=6e4;const e=!isNaN(Date.parse("2011-01-01T12:00:00-05:00"));t.parseDate=t=>(t=t.toString(),e||(t=function(t){const e=t.match(r);if(e){let t;const[r,a,s,n,i,o,c,u]=e;return"Z"!==u&&(t=u.replace(":","")),`${a}/${s}/${n} ${i}:${o}:${c} GMT${[t]}`}}(t)),new Date(Date.parse(t)));const r=/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(Z|[-+]?[\d:]+)$/;t.elementMatchesSelector=(()=>{const t=document.documentElement,e=t.matches||t.matchesSelector||t.webkitMatchesSelector||t.mozMatchesSelector||t.msMatchesSelector;return(t,r)=>{if(t?.nodeType===Node.ELEMENT_NODE)return e.call(t,r)}})();const{config:a}=t,{i18n:s}=a;t.getI18nValue=(e="",r={locale:a.locale})=>{const{locale:n}=r,i=function(t,e){let r=t;for(var a of Array.from(e.split("."))){if(!r[a])return null;r=r[a]}return r}(s[n],e);return i||(n!==a.defaultLocale?t.getI18nValue(e,{locale:a.defaultLocale}):void 0)},t.translate=(e,r={},a)=>{let s=t.getI18nValue(e,a);for(const t in r){const e=r[t];s=s.replace(`{${t}}`,e)}return s};const{getI18nValue:n,translate:i}=t,o="function"==typeof Intl?.DateTimeFormat,c={"Central European Standard Time":"CET","Central European Summer Time":"CEST","China Standard Time":"CST","Israel Daylight Time":"IDT","Israel Standard Time":"IST","Moscow Standard Time":"MSK","Peru Standard Time":"PET","Philippine Standard Time":"PHT","Singapore Standard Time":"SGT","Western Indonesia Time":"WIB"};t.knownEdgeCaseTimeZones=c,t.strftime=(()=>{const t=(a,s)=>{const u=a.getDay(),l=a.getDate(),d=a.getMonth(),m=a.getFullYear(),h=a.getHours(),f=a.getMinutes(),g=a.getSeconds();return s.replace(/%(-?)([%aAbBcdeHIlmMpPSwyYZ])/g,((s,p,y)=>{switch(y){case"%":return"%";case"a":return n("date.abbrDayNames")[u];case"A":return n("date.dayNames")[u];case"b":return n("date.abbrMonthNames")[d];case"B":return n("date.monthNames")[d];case"c":return a.toString();case"d":return e(l,p);case"e":return l;case"H":return e(h,p);case"I":return e(t(a,"%l"),p);case"l":return 0===h||12===h?12:(h+12)%12;case"m":return e(d+1,p);case"M":return e(f,p);case"p":return i("time."+(h>11?"pm":"am")).toUpperCase();case"P":return i("time."+(h>11?"pm":"am"));case"S":return e(g,p);case"w":return u;case"y":return e(m%100,p);case"Y":return m;case"Z":return function(t){let e,a,s;return(a=function(t){return Object.keys(c).find((e=>o?new Date(t).toLocaleString("en-US",{timeZoneName:"long"}).includes(e):t.toString().includes(e)))}(t))?c[a]:(s=r(t,{allowGMT:!1}))||(s=function(t){const e=t.toString();let r;if(null!=(r=e.match(/\(([\w\s]+)\)$/))){const t=r[1];return/\s/.test(t)?t.match(/\b(\w)/g).join(""):t}if(null!=(r=e.match(/(\w{3,4})\s\d{4}$/)))return r[1];if(null!=(r=e.match(/(UTC[\+\-]\d+)/)))return r[1]}(t))?s:(e=r(t,{allowGMT:!0}))?e:""}(a)}}))};function e(t,e){return"-"===e?t:`0${t}`.slice(-2)}function r(t,{allowGMT:e}){if(o){const r=new Date(t).toLocaleString("en-US",{timeZoneName:"short"}).split(" ").pop();if(e||!r.includes("GMT"))return r}}return t})(),t.CalendarDate=class{static fromDate(t){return new this(t.getFullYear(),t.getMonth()+1,t.getDate())}static today(){return this.fromDate(new Date)}constructor(t,e,r){this.date=new Date(Date.UTC(t,e-1)),this.date.setUTCDate(r),this.year=this.date.getUTCFullYear(),this.month=this.date.getUTCMonth()+1,this.day=this.date.getUTCDate(),this.value=this.date.getTime()}equals(t){return t?.value===this.value}is(t){return this.equals(t)}isToday(){return this.is(this.constructor.today())}occursOnSameYearAs(t){return this.year===t?.year}occursThisYear(){return this.occursOnSameYearAs(this.constructor.today())}daysSince(t){if(t)return(this.date-t.date)/864e5}daysPassed(){return this.constructor.today().daysSince(this)}};const{strftime:u,translate:l,getI18nValue:d,config:m}=t;t.RelativeTime=class{constructor(e){this.date=e,this.calendarDate=t.CalendarDate.fromDate(this.date)}toString(){let t,e;return(e=this.toTimeElapsedString())?l("time.elapsed",{time:e}):(t=this.toWeekdayString())?(e=this.toTimeString(),l("datetime.at",{date:t,time:e})):l("date.on",{date:this.toDateString()})}toTimeOrDateString(){return this.calendarDate.isToday()?this.toTimeString():this.toDateString()}toTimeElapsedString(){let t;const e=(new Date).getTime()-this.date.getTime(),r=Math.round(e/1e3),a=Math.round(r/60),s=Math.round(a/60);return e<0?null:r<10?(t=l("time.second"),l("time.singular",{time:t})):r<45?`${r} ${l("time.seconds")}`:r<90?(t=l("time.minute"),l("time.singular",{time:t})):a<45?`${a} ${l("time.minutes")}`:a<90?(t=l("time.hour"),l("time.singularAn",{time:t})):s<24?`${s} ${l("time.hours")}`:""}toWeekdayString(){switch(this.calendarDate.daysPassed()){case 0:return l("date.today");case 1:return l("date.yesterday");case-1:return l("date.tomorrow");case 2:case 3:case 4:case 5:case 6:return u(this.date,"%A");default:return""}}toDateString(){const t=this.calendarDate.occursThisYear()?d("date.formats.thisYear"):d("date.formats.default");return u(this.date,t)}toTimeString(){const t=m.useFormat24?"default_24h":"default";return u(this.date,d(`time.formats.${t}`))}};const{elementMatchesSelector:h}=t;t.PageObserver=class{constructor(t,e){this.selector=t,this.callback=e,this.processMutations=this.processMutations.bind(this),this.processInsertion=this.processInsertion.bind(this)}start(){this.started||(this.observeWithMutationObserver()||this.observeWithMutationEvent(),this.started=!0)}observeWithMutationObserver(){if("undefined"!=typeof MutationObserver&&null!==MutationObserver){return new MutationObserver(this.processMutations).observe(document.documentElement,{childList:!0,subtree:!0}),!0}return!1}observeWithMutationEvent(){return addEventListener("DOMNodeInserted",this.processInsertion,!1),!0}findSignificantElements(t){const e=[];return t?.nodeType===Node.ELEMENT_NODE&&(h(t,this.selector)&&e.push(t),e.push(...Array.from(t.querySelectorAll(this.selector)||[]))),e}processMutations(t){const e=[];for(const r of t)if("childList"===r.type)for(const t of r.addedNodes)e.push(...this.findSignificantElements(t)||[]);this.notify(e)}processInsertion(t){const e=this.findSignificantElements(t.target);this.notify(e)}notify(t){t?.length>0&&"function"==typeof this.callback&&this.callback(t)}};const{parseDate:f,strftime:g,getI18nValue:p,config:y}=t,S="time[data-local]:not([data-localized])",T=t=>t.setAttribute("data-localized",""),b=e=>new t.RelativeTime(e);t.Controller=class{constructor(){this.processElements=this.processElements.bind(this),this.pageObserver=new t.PageObserver(S,this.processElements)}start(){this.started||(this.processElements(),this.startTimer(),this.pageObserver.start(),this.started=!0)}startTimer(){let t;(t=y.timerInterval)&&(this.timer||(this.timer=setInterval(this.processElements,t)))}processElements(t){t||(t=document.querySelectorAll(S));for(const e of Array.from(t))this.processElement(e);return t.length}processElement(t){const e=t.getAttribute("datetime"),r=t.getAttribute("data-local"),a=y.useFormat24&&t.getAttribute("data-format24")||t.getAttribute("data-format"),s=f(e);if(!isNaN(s)){if(!t.hasAttribute("title")){const e=y.useFormat24?"default_24h":"default",r=g(s,p(`datetime.formats.${e}`));t.setAttribute("title",r)}(t=>{t.setAttribute("data-processed-at",(new Date).toISOString())})(t),t.textContent=(()=>{switch(r){case"time":return T(t),g(s,a);case"date":return T(t),b(s).toDateString();case"time-ago":return b(s).toString();case"time-or-date":return b(s).toTimeOrDateString();case"weekday":return b(s).toWeekdayString();case"weekday-or-date":return b(s).toWeekdayString()||b(s).toDateString()}})()}}};let M=!1;function D(){t.getController().start()}return t.start=()=>{var e;M?t.run():(M=!0,"undefined"!=typeof MutationObserver&&null!==MutationObserver||(document.attachEvent?"complete"===document.readyState:"loading"!==document.readyState)?D():(e=D,"function"==typeof requestAnimationFrame?requestAnimationFrame(e):setTimeout(e,17)))},t.processing=()=>t.getController().started,window.LocalTime===t&&t.start(),t}));
@@ -2,19 +2,19 @@ module LocalTimeHelper
2
2
  def local_time(time, options = nil)
3
3
  time = utc_time(time)
4
4
 
5
- options, format = extract_options_and_value(options, :format)
6
- format = find_time_format(format)
5
+ options, format12 = extract_options_and_value(options, :format)
6
+ format12, format24 = find_12h_and_24h_formats(format12)
7
7
 
8
8
  options[:data] ||= {}
9
- options[:data].merge! local: :time, format: format
9
+ options[:data].merge! local: :time, format: format12, format24: format24
10
10
 
11
- time_tag time, time.strftime(format), options
11
+ time_tag time, time.strftime(format12), options
12
12
  end
13
13
 
14
14
  def local_date(time, options = nil)
15
15
  options, format = extract_options_and_value(options, :format)
16
- options[:format] = format || LocalTime.default_date_format
17
- local_time time, options
16
+ format, _ = find_12h_and_24h_formats(format, prefer: :date)
17
+ local_time time, options.merge(format: format)
18
18
  end
19
19
 
20
20
  def local_relative_time(time, options = nil)
@@ -29,7 +29,7 @@ module LocalTimeHelper
29
29
 
30
30
  def local_time_ago(time, options = nil)
31
31
  options, * = extract_options_and_value(options, :type)
32
- options[:type] = 'time-ago'
32
+ options[:type] = "time-ago"
33
33
  local_relative_time time, options
34
34
  end
35
35
 
@@ -42,20 +42,6 @@ module LocalTimeHelper
42
42
  end
43
43
 
44
44
  private
45
- def find_time_format(format)
46
- if format.is_a?(Symbol)
47
- if (i18n_format = I18n.t("time.formats.#{format}", default: [:"date.formats.#{format}", ''])).present?
48
- i18n_format
49
- elsif (date_format = Time::DATE_FORMATS[format] || Date::DATE_FORMATS[format])
50
- date_format.is_a?(Proc) ? LocalTime.default_time_format : date_format
51
- else
52
- LocalTime.default_time_format
53
- end
54
- else
55
- format.presence || LocalTime.default_time_format
56
- end
57
- end
58
-
59
45
  def extract_options_and_value(options, value_key = nil)
60
46
  case options
61
47
  when Hash
@@ -67,4 +53,69 @@ module LocalTimeHelper
67
53
  [ {}, options ]
68
54
  end
69
55
  end
56
+
57
+ def find_12h_and_24h_formats(format12, prefer: :time)
58
+ if format12.is_a?(Symbol)
59
+ find_time_formats_by_name(format12, prefer: prefer)
60
+ else
61
+ [ format12.presence || default_time_format(prefer), nil ]
62
+ end
63
+ end
64
+
65
+ def find_time_formats_by_name(name, prefer:)
66
+ if use_i18n_time_formats?(name)
67
+ find_i18_time_formats(name, prefer: prefer)
68
+ elsif use_ruby_time_formats?(name)
69
+ find_ruby_time_formats(name, prefer: prefer)
70
+ else
71
+ [ default_time_format(prefer), nil ]
72
+ end
73
+ end
74
+
75
+ def default_time_format(prefer = :time)
76
+ if prefer == :time
77
+ LocalTime.default_time_format
78
+ else
79
+ LocalTime.default_date_format
80
+ end
81
+ end
82
+
83
+ def use_i18n_time_formats?(name)
84
+ i18n_time_or_date_format(name).present?
85
+ end
86
+
87
+ def i18n_time_or_date_format(name, prefer: :time)
88
+ if prefer == :time
89
+ I18n.t("time.formats.#{name}", default: [ :"date.formats.#{name}", "" ]).presence
90
+ else
91
+ I18n.t("date.formats.#{name}", default: [ :"time.formats.#{name}", "" ]).presence
92
+ end
93
+ end
94
+
95
+ def find_i18_time_formats(name, prefer:)
96
+ [ i18n_time_or_date_format(name, prefer: prefer),
97
+ i18n_time_or_date_format("#{name}_24h", prefer: prefer) ]
98
+ end
99
+
100
+ def use_ruby_time_formats?(name)
101
+ ruby_time_or_date_format(name).present?
102
+ end
103
+
104
+ def ruby_time_or_date_format(name, prefer: :time)
105
+ if prefer == :time
106
+ Time::DATE_FORMATS.with_indifferent_access[name] || Date::DATE_FORMATS.with_indifferent_access[name]
107
+ else
108
+ Date::DATE_FORMATS.with_indifferent_access[name] || Time::DATE_FORMATS.with_indifferent_access[name]
109
+ end
110
+ end
111
+
112
+ def find_ruby_time_formats(name, prefer:)
113
+ format12 = ruby_time_or_date_format(name, prefer: prefer)
114
+
115
+ if format12.is_a?(Proc)
116
+ [ default_time_format(prefer), nil ]
117
+ else
118
+ [ format12, ruby_time_or_date_format("#{name}_24h", prefer: prefer) ]
119
+ end
120
+ end
70
121
  end
@@ -0,0 +1,3 @@
1
+ module LocalTime
2
+ VERSION = "3.0.3"
3
+ end
@@ -1,18 +1,13 @@
1
- require 'rails'
2
- require 'action_view'
3
- require 'rails-dom-testing'
4
-
5
- require 'local_time'
6
- require_relative '../../app/helpers/local_time_helper'
7
-
8
- require 'minitest/autorun'
9
- begin
10
- # 2.0.0
11
- class TestCase < MiniTest::Test; end
12
- rescue NameError
13
- # 1.9.3
14
- class TestCase < MiniTest::Unit::TestCase; end
15
- end
1
+ require "rails"
2
+ require "active_support/all"
3
+ require "action_view"
4
+ require "rails-dom-testing"
5
+
6
+ require "local_time"
7
+ require_relative "../../app/helpers/local_time_helper"
8
+
9
+ require "minitest/autorun"
10
+ class TestCase < Minitest::Test; end
16
11
 
17
12
  I18n.enforce_available_locales = false
18
13
 
@@ -26,10 +21,14 @@ class LocalTimeHelperTest < TestCase
26
21
  @original_zone = Time.zone
27
22
  Time.zone = ActiveSupport::TimeZone["Central Time (US & Canada)"]
28
23
  I18n.backend.store_translations(:en, {
29
- time: { formats: { simple_time: "%b %e" } },
30
- date: { formats: { simple_date: "%b %e" } } })
31
- Time::DATE_FORMATS[:time_formats_simple_time] = '%b %e'
32
- Date::DATE_FORMATS[:date_formats_simple_date] = '%b %e'
24
+ time: { formats: { simple_time: "%-l:%M%P", simple_time_24h: "%H:%M", time_with_context: "%b %e, %-l:%M%P", ambiguous_format: "%Y-%m-%d %H:%M:%S" } },
25
+ date: { formats: { simple_date: "%b %e", ambiguous_format: "%Y-%m-%d" } } })
26
+ Time::DATE_FORMATS[:time_formats_simple_time] = "%-l:%M%P"
27
+ Time::DATE_FORMATS[:time_formats_simple_time_24h] = "%H:%M"
28
+ Time::DATE_FORMATS[:time_formats_time_with_context] = "%b %e, %-l:%M%P"
29
+ Time::DATE_FORMATS[:ruby_ambiguous_format] = "%Y-%m-%d %H:%M:%S"
30
+ Date::DATE_FORMATS[:ruby_ambiguous_format] = "%Y-%m-%d"
31
+ Date::DATE_FORMATS[:date_formats_simple_date] = "%b %e"
33
32
 
34
33
  @date = "2013-11-21"
35
34
  @time = Time.zone.parse(@date)
@@ -61,25 +60,35 @@ class LocalTimeHelperTest < TestCase
61
60
 
62
61
  def test_local_time_with_format
63
62
  expected = %Q(<time data-format="%b %e" data-local="time" datetime="#{@time_js}">Nov 21</time>)
64
- assert_dom_equal expected, local_time(@time, format: '%b %e')
63
+ assert_dom_equal expected, local_time(@time, format: "%b %e")
65
64
  end
66
65
 
67
66
  def test_local_time_with_format_as_string
68
67
  expected = %Q(<time data-format="%b %e" data-local="time" datetime="#{@time_js}">Nov 21</time>)
69
- assert_dom_equal expected, local_time(@time, '%b %e')
68
+ assert_dom_equal expected, local_time(@time, "%b %e")
70
69
  end
71
70
 
72
71
  def test_local_time_with_i18n_format
73
- expected = %Q(<time data-format="%b %e" data-local="time" datetime="#{@time_js}">Nov 21</time>)
72
+ expected = %Q(<time data-format="%-l:%M%P" data-format24="%H:%M" data-local="time" datetime="#{@time_js}">6:00am</time>)
74
73
  assert_dom_equal expected, local_time(@time, format: :simple_time)
75
74
  end
76
75
 
77
- def test_local_time_with_date_formats_format
78
- expected = %Q(<time data-format="%b %e" data-local="time" datetime="#{@time_js}">Nov 21</time>)
76
+ def test_local_time_with_i18n_format_missing_24h
77
+ expected = %Q(<time data-format="%b %e, %-l:%M%P" data-local="time" datetime="#{@time_js}">Nov 21, 6:00am</time>)
78
+ assert_dom_equal expected, local_time(@time, format: :time_with_context)
79
+ end
80
+
81
+ def test_local_time_with_ruby_format
82
+ expected = %Q(<time data-format="%-l:%M%P" data-format24="%H:%M" data-local="time" datetime="#{@time_js}">6:00am</time>)
79
83
  assert_dom_equal expected, local_time(@time, format: :time_formats_simple_time)
80
84
  end
81
85
 
82
- def test_local_time_with_missing_i18n_and_date_formats_format
86
+ def test_local_time_with_ruby_format_missing_24h
87
+ expected = %Q(<time data-format="%b %e, %-l:%M%P" data-local="time" datetime="#{@time_js}">Nov 21, 6:00am</time>)
88
+ assert_dom_equal expected, local_time(@time, format: :time_formats_time_with_context)
89
+ end
90
+
91
+ def test_local_time_with_missing_i18n_and_ruby_format
83
92
  expected = %Q(<time data-format="%B %e, %Y %l:%M%P" data-local="time" datetime="#{@time_js}">November 21, 2013 6:00am</time>)
84
93
  assert_dom_equal expected, local_time(@time, format: :missing_format)
85
94
  end
@@ -92,7 +101,27 @@ class LocalTimeHelperTest < TestCase
92
101
 
93
102
  def test_local_time_with_options
94
103
  expected = %Q(<time data-format="%b %e" data-local="time" datetime="#{@time_js}" style="display:none">Nov 21</time>)
95
- assert_dom_equal expected, local_time(@time, format: '%b %e', style: 'display:none')
104
+ assert_dom_equal expected, local_time(@time, format: "%b %e", style: "display:none")
105
+ end
106
+
107
+ def test_local_time_with_ruby_ambiguous_format
108
+ expected = %Q(<time data-format="%Y-%m-%d %H:%M:%S" data-local="time" datetime="#{@time_js}">2013-11-21 06:00:00</time>)
109
+ assert_dom_equal expected, local_time(@time, format: :ruby_ambiguous_format)
110
+ end
111
+
112
+ def test_local_time_with_i18n_ambiguous_format
113
+ expected = %Q(<time data-format="%Y-%m-%d %H:%M:%S" data-local="time" datetime="#{@time_js}">2013-11-21 06:00:00</time>)
114
+ assert_dom_equal expected, local_time(@time, format: :ambiguous_format)
115
+ end
116
+
117
+ def test_local_date_with_ruby_ambiguous_format
118
+ expected = %Q(<time data-format="%Y-%m-%d" data-local="time" datetime="#{@time_js}">2013-11-21</time>)
119
+ assert_dom_equal expected, local_date(@time.to_date, format: :ruby_ambiguous_format)
120
+ end
121
+
122
+ def test_local_date_with_i18n_ambiguous_format
123
+ expected = %Q(<time data-format="%Y-%m-%d" data-local="time" datetime="#{@time_js}">2013-11-21</time>)
124
+ assert_dom_equal expected, local_date(@time.to_date, format: :ambiguous_format)
96
125
  end
97
126
 
98
127
  def test_local_date
@@ -103,12 +132,12 @@ class LocalTimeHelperTest < TestCase
103
132
 
104
133
  def test_local_date_with_format
105
134
  expected = %Q(<time data-format="%b %e" data-local="time" datetime="#{@time_js}">Nov 21</time>)
106
- assert_dom_equal expected, local_date(@time.to_date, format: '%b %e')
135
+ assert_dom_equal expected, local_date(@time.to_date, format: "%b %e")
107
136
  end
108
137
 
109
138
  def test_local_date_with_format_as_string
110
139
  expected = %Q(<time data-format="%b %e" data-local="time" datetime="#{@time_js}">Nov 21</time>)
111
- assert_dom_equal expected, local_date(@time.to_date, '%b %e')
140
+ assert_dom_equal expected, local_date(@time.to_date, "%b %e")
112
141
  end
113
142
 
114
143
  def test_local_date_with_i18n_format
@@ -116,13 +145,13 @@ class LocalTimeHelperTest < TestCase
116
145
  assert_dom_equal expected, local_date(@time.to_date, format: :simple_date)
117
146
  end
118
147
 
119
- def test_local_date_with_date_formats_format
148
+ def test_local_date_with_ruby_format
120
149
  expected = %Q(<time data-format="%b %e" data-local="time" datetime="#{@time_js}">Nov 21</time>)
121
150
  assert_dom_equal expected, local_date(@time.to_date, format: :date_formats_simple_date)
122
151
  end
123
152
 
124
- def test_local_date_with_missing_i18n_and_date_formats_format
125
- expected = %Q(<time data-format="%B %e, %Y %l:%M%P" data-local="time" datetime="#{@time_js}">November 21, 2013 6:00am</time>)
153
+ def test_local_date_with_missing_i18n_and_ruby_format
154
+ expected = %Q(<time data-format="%B %e, %Y" data-local="time" datetime="#{@time_js}">November 21, 2013</time>)
126
155
  assert_dom_equal expected, local_date(@time.to_date, format: :missing_date_format)
127
156
  end
128
157