Repository: iamkun/dayjs (js)

Issue:
dayjs.diff 错误
**Describe the bug**
A clear and concise description of what the bug is.
```js
ERROR
Cannot read properties of undefined (reading '$localOffset')
```

**Expected behavior**

通过调试发现这种情况下 `endTime`对象内部没有 $x 属性，所以导致，获取 $x.$localOffset 时报错

```js
const timezoneOffset = 8
const endTime = dayjs('2023/08/25 10:35', 'YYYY/MM/DD HH:mm').utcOffset(8, true)
const now = dayjs().utcOffset(timezoneOffset)

const diff = endTime.diff(now, 'second')
```

但是只需要改成下面这样，去掉 dayjs 的时间格式，`endTime`对象就会有一个为空对象`{}`的`$x`属性。就能正常使用

```js
const timezoneOffset = 8
const endTime = dayjs('2023/08/25 10:35').utcOffset(8, true)
const now = dayjs().utcOffset(timezoneOffset)

const diff = endTime.diff(now, 'second')
```
**Information**
 - Day.js Version v1.11.9
 - OS: macOS
 - Browser chrome 116
 - Time zone: GMT+8:00


Pull request:
Fixed issue #2428
Move the initialization of the $x attribute to after the parse method so it won't be affected by other plugins

fix #2428

Patch:
diff --git a/README.md b/README.md
index 3a855a9..3e514dc 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@ English | [简体中文](./docs/zh-cn/README.zh-CN.md) | [日本語](./docs/ja/R
 
 <p align="center"><a href="https://day.js.org/" target="_blank" rel="noopener noreferrer"><img width="550"
                                                                              src="https://user-images.githubusercontent.com/17680888/39081119-3057bbe2-456e-11e8-862c-646133ad4b43.png"
-                                                                             alt="Day.js"></a></p>
+                                                                             alt="Day.js" /></a></p>
 <p align="center">Fast <b>2kB</b> alternative to Moment.js with the same modern API</p>
 <p align="center">
     <a href="https://unpkg.com/dayjs/dayjs.min.js"><img
@@ -41,7 +41,7 @@ dayjs().startOf('month').add(1, 'day').set('year', 2018).format('YYYY-MM-DD HH:m
 
 ### Documentation
 
-You can find for more details, API, and other docs on [day.js.org](https://day.js.org/) website.
+You can find more details, API, and other docs on [day.js.org](https://day.js.org/) website.
 
 ### Installation
 
@@ -82,6 +82,7 @@ dayjs.locale('es') // use Spanish locale globally
 
 dayjs('2018-05-05').locale('zh-cn').format() // use Chinese Simplified locale in a specific instance
 ```
+
 📚[Internationalization](https://day.js.org/docs/en/i18n/i18n)
 
 ### Plugin
@@ -100,7 +101,7 @@ dayjs().format('Q Do k kk X x') // more available formats
 
 ## Sponsors
 
-Support this project by becoming a sponsor. Your logo will show up here with a link to your website. 
+Support this project by becoming a sponsor. Your logo will show up here with a link to your website.
 
 [[Become a sponsor via Github](https://github.com/sponsors/iamkun/)] [[Become a sponsor via OpenCollective](https://opencollective.com/dayjs#sponsor)]
 
diff --git a/docs/ko/README-ko.md b/docs/ko/README-ko.md
index 4389cc3..cb1f77a 100644
--- a/docs/ko/README-ko.md
+++ b/docs/ko/README-ko.md
@@ -23,7 +23,7 @@
     </a>
 </p>
 
-> Day.js는 Moment.js와 호환되는 대부분의 API를 사용하며, 최신 브라우저에서 날짜와 시간에 대한 구문 분석, 유효성 검사, 조작, 출력하는 경량 JavaScript 라이브러리입니다. Moment.js를 사용하고 있다면, Day.js는 껌입니다.
+> Day.js는 대부분의 API가 Moment.js와 호환되며 최신 브라우저에서 날짜와 시간에 대한 구문 분석, 유효성 검사, 조작, 출력을 간편하게 처리하는 경량 JavaScript 라이브러리 입니다. Moment.js를 사용해본 경험이 있다면, Day.js도 쉽게 사용하실 수 있습니다.
 
 ```js
 dayjs()
@@ -46,7 +46,7 @@ dayjs()
 
 ### 문서
 
-더 많은 세부 사항과 API, 그리고 다른 문서를 [day.js.org](https://day.js.org/) 웹사이트에서 볼 수 있습니다.
+더 많은 세부 사항과 API 및 다른 문서는 [day.js.org](https://day.js.org/) 웹사이트에서 확인하실 수 있습니다.
 
 ### 설치
 
@@ -58,7 +58,7 @@ npm install dayjs --save
 
 ### API
 
-Day.js API를 사용해서 날짜와 시간에 대한 구문 분석, 유효성 검사, 조작, 출력을 쉽게 할 수 있습니다.
+Day.js API를 사용하여 날짜와 시간의 구문 분석, 검증, 조작, 출력을 간편하게 처리할 수 있습니다.
 
 ```javascript
 dayjs('2018-08-08') // parse
@@ -78,9 +78,9 @@ dayjs().isBefore(dayjs()) // query
 
 ### I18n
 
-Day.js는 국제화에 대해 많은 지원을 합니다.
+Day.js는 국제화에 대한 많은 지원을 제공하고 있습니다.
 
-그러나 그것을 사용하지 않는다면, 그 누구도 당신의 빌드에 포함되지 않습니다.
+하지만 이 기능을 사용하지 않는 경우, 빌드에 포함되지 않습니다.
 
 ```javascript
 import 'dayjs/locale/es' // load on demand
@@ -96,7 +96,7 @@ dayjs('2018-05-05')
 
 ### Plugin
 
-플러그인은 기능을 확장하거나 새로운 기능을 추가하기 위해 Day.js에 추가할 수 있는 독립적인 모듈입니다.
+플러그인은 Day.js의 기능을 확장하거나 새로운 기능을 도입하기 위한 독립적인 모듈입니다.
 
 ```javascript
 import advancedFormat from 'dayjs/plugin/advancedFormat' // load on demand
diff --git a/src/index.js b/src/index.js
index d26a401..061ade1 100644
--- a/src/index.js
+++ b/src/index.js
@@ -86,12 +86,12 @@ class Dayjs {
   constructor(cfg) {
     this.$L = parseLocale(cfg.locale, null, true)
     this.parse(cfg) // for plugin
+    this.$x = this.$x || cfg.x || {}
     this[IS_DAYJS] = true
   }
 
   parse(cfg) {
     this.$d = parseDate(cfg)
-    this.$x = cfg.x || {}
     this.init()
   }
 
diff --git a/src/locale/ar.js b/src/locale/ar.js
index a7025c4..f418379 100644
--- a/src/locale/ar.js
+++ b/src/locale/ar.js
@@ -2,6 +2,7 @@
 import dayjs from 'dayjs'
 
 const months = 'يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر'.split('_')
+
 const symbolMap = {
   1: '١',
   2: '٢',
@@ -36,6 +37,7 @@ const locale = {
   months,
   monthsShort: months,
   weekStart: 6,
+  meridiem: hour => (hour > 12 ? 'م' : 'ص'),
   relativeTime: {
     future: 'بعد %s',
     past: 'منذ %s',
diff --git a/src/locale/fa.js b/src/locale/fa.js
index cbcd13c..cf33827 100644
--- a/src/locale/fa.js
+++ b/src/locale/fa.js
@@ -4,11 +4,11 @@ import dayjs from 'dayjs'
 const locale = {
   name: 'fa',
   weekdays: 'یک‌شنبه_دوشنبه_سه‌شنبه_چهارشنبه_پنج‌شنبه_جمعه_شنبه'.split('_'),
-  weekdaysShort: 'یک‌_دو_سه‌_چه_پن_جم_شن'.split('_'),
+  weekdaysShort: 'یک\u200cشنبه_دوشنبه_سه\u200cشنبه_چهارشنبه_پنج\u200cشنبه_جمعه_شنبه'.split('_'),
   weekdaysMin: 'ی_د_س_چ_پ_ج_ش'.split('_'),
   weekStart: 6,
-  months: 'فروردین_اردیبهشت_خرداد_تیر_مرداد_شهریور_مهر_آبان_آذر_دی_بهمن_اسفند'.split('_'),
-  monthsShort: 'فرو_ارد_خرد_تیر_مرد_شهر_مهر_آبا_آذر_دی_بهم_اسف'.split('_'),
+  months: 'ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر'.split('_'),
+  monthsShort: 'ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر'.split('_'),
   ordinal: n => n,
   formats: {
     LT: 'HH:mm',
@@ -20,7 +20,7 @@ const locale = {
   },
   relativeTime: {
     future: 'در %s',
-    past: '%s قبل',
+    past: '%s پیش',
     s: 'چند ثانیه',
     m: 'یک دقیقه',
     mm: '%d دقیقه',
diff --git a/src/locale/ko.js b/src/locale/ko.js
index 8a8e8ea..7053e83 100644
--- a/src/locale/ko.js
+++ b/src/locale/ko.js
@@ -8,7 +8,7 @@ const locale = {
   weekdaysMin: '일_월_화_수_목_금_토'.split('_'),
   months: '1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월'.split('_'),
   monthsShort: '1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월'.split('_'),
-  ordinal: n => n,
+  ordinal: n => `${n}일`,
   formats: {
     LT: 'A h:mm',
     LTS: 'A h:mm:ss',
diff --git a/src/locale/sl.js b/src/locale/sl.js
index f09e3bf..1e0e4f8 100644
--- a/src/locale/sl.js
+++ b/src/locale/sl.js
@@ -1,6 +1,71 @@
 // Slovenian [sl]
 import dayjs from 'dayjs'
 
+function dual(n) {
+  return (n % 100) == 2 // eslint-disable-line
+}
+
+function threeFour(n) {
+  return (n % 100) == 3 || (n % 100) == 4 // eslint-disable-line
+}
+
+/* eslint-disable */
+function translate(number, withoutSuffix, key, isFuture) {
+  const result = `${number} `
+  switch (key) {
+    case 's': // a few seconds / in a few seconds / a few seconds ago
+      return (withoutSuffix || isFuture) ? 'nekaj sekund' : 'nekaj sekundami'
+    case 'm': // a minute / in a minute / a minute ago
+      return withoutSuffix ? 'ena minuta' : 'eno minuto'
+    case 'mm': // 9 minutes / in 9 minutes / 9 minutes ago
+      if (dual(number)) {
+        return result + ((withoutSuffix || isFuture) ? 'minuti' : 'minutama')
+      }
+      if (threeFour(number)) {
+        return result + ((withoutSuffix || isFuture) ? 'minute' : 'minutami')
+      }
+      return result + ((withoutSuffix || isFuture) ? 'minut' : 'minutami')
+    case 'h': // an hour / in an hour / an hour ago
+      return withoutSuffix ? 'ena ura' : (isFuture ? 'eno uro' : 'eno uro')
+    case 'hh': // 9 hours / in 9 hours / 9 hours ago
+      if (dual(number)) {
+        return result + ((withoutSuffix || isFuture) ? 'uri' : 'urama')
+      }
+      if (threeFour(number)) {
+        return result + ((withoutSuffix || isFuture) ? 'ure' : 'urami')
+      }
+      return result + ((withoutSuffix || isFuture) ? 'ur' : 'urami')
+    case 'd': // a day / in a day / a day ago
+      return (withoutSuffix || isFuture) ? 'en dan' : 'enim dnem'
+    case 'dd': // 9 days / in 9 days / 9 days ago
+      if (dual(number)) {
+        return result + ((withoutSuffix || isFuture) ? 'dneva' : 'dnevoma')
+      }
+      return result + ((withoutSuffix || isFuture) ? 'dni' : 'dnevi')
+    case 'M': // a month / in a month / a month ago
+      return (withoutSuffix || isFuture) ? 'en mesec' : 'enim mesecem'
+    case 'MM': // 9 months / in 9 months / 9 months ago
+      if (dual(number)) { // 2 minutes / in 2 minutes
+        return result + ((withoutSuffix || isFuture) ? 'meseca' : 'mesecema')
+      }
+      if (threeFour(number)) {
+        return result + ((withoutSuffix || isFuture) ? 'mesece' : 'meseci')
+      }
+      return result + ((withoutSuffix || isFuture) ? 'mesecev' : 'meseci')
+    case 'y': // a year / in a year / a year ago
+      return (withoutSuffix || isFuture) ? 'eno leto' : 'enim letom'
+    case 'yy': // 9 years / in 9 years / 9 years ago
+      if (dual(number)) { // 2 minutes / in 2 minutes
+        return result + ((withoutSuffix || isFuture) ? 'leti' : 'letoma')
+      }
+      if (threeFour(number)) {
+        return result + ((withoutSuffix || isFuture) ? 'leta' : 'leti')
+      }
+      return result + ((withoutSuffix || isFuture) ? 'let' : 'leti')
+  }
+}
+
+/* eslint-enable */
 const locale = {
   name: 'sl',
   weekdays: 'nedelja_ponedeljek_torek_sreda_četrtek_petek_sobota'.split('_'),
@@ -16,26 +81,26 @@ const locale = {
     L: 'DD.MM.YYYY',
     LL: 'D. MMMM YYYY',
     LLL: 'D. MMMM YYYY H:mm',
-    LLLL: 'dddd, D. MMMM YYYY H:mm'
+    LLLL: 'dddd, D. MMMM YYYY H:mm',
+    l: 'D. M. YYYY'
   },
   relativeTime: {
     future: 'čez %s',
     past: 'pred %s',
-    s: 'nekaj sekund',
-    m: 'minuta',
-    mm: '%d minut',
-    h: 'ura',
-    hh: '%d ur',
-    d: 'dan',
-    dd: '%d dni',
-    M: 'mesec',
-    MM: '%d mesecev',
-    y: 'leto',
-    yy: '%d let'
+    s: translate,
+    m: translate,
+    mm: translate,
+    h: translate,
+    hh: translate,
+    d: translate,
+    dd: translate,
+    M: translate,
+    MM: translate,
+    y: translate,
+    yy: translate
   }
 }
 
 dayjs.locale(locale, null, true)
 
 export default locale
-
diff --git a/src/locale/uz-latn.js b/src/locale/uz-latn.js
index 220d5d4..7d60219 100644
--- a/src/locale/uz-latn.js
+++ b/src/locale/uz-latn.js
@@ -20,7 +20,7 @@ const locale = {
   },
   relativeTime: {
     future: 'Yaqin %s ichida',
-    past: 'Bir necha %s oldin',
+    past: '%s oldin',
     s: 'soniya',
     m: 'bir daqiqa',
     mm: '%d daqiqa',
diff --git a/src/locale/uz.js b/src/locale/uz.js
index 23f4042..e705f68 100644
--- a/src/locale/uz.js
+++ b/src/locale/uz.js
@@ -20,7 +20,7 @@ const locale = {
   },
   relativeTime: {
     future: 'Якин %s ичида',
-    past: 'Бир неча %s олдин',
+    past: '%s олдин',
     s: 'фурсат',
     m: 'бир дакика',
     mm: '%d дакика',
diff --git a/src/plugin/timezone/index.js b/src/plugin/timezone/index.js
index c8f8a46..832fb4a 100644
--- a/src/plugin/timezone/index.js
+++ b/src/plugin/timezone/index.js
@@ -97,7 +97,7 @@ export default (o, c, d) => {
     const date = this.toDate()
     const target = date.toLocaleString('en-US', { timeZone: timezone })
     const diff = Math.round((date - new Date(target)) / 1000 / 60)
-    let ins = d(target).$set(MS, this.$ms)
+    let ins = d(target, { locale: this.$L }).$set(MS, this.$ms)
       .utcOffset((-Math.round(date.getTimezoneOffset() / 15) * 15) - diff, true)
     if (keepLocalTime) {
       const newOffset = ins.utcOffset()
@@ -120,7 +120,7 @@ export default (o, c, d) => {
       return oldStartOf.call(this, units, startOf)
     }
 
-    const withoutTz = d(this.format('YYYY-MM-DD HH:mm:ss:SSS'))
+    const withoutTz = d(this.format('YYYY-MM-DD HH:mm:ss:SSS'), { locale: this.$L })
     const startOfWithoutTz = oldStartOf.call(withoutTz, units, startOf)
     return startOfWithoutTz.tz(this.$x.$timezone, true)
   }


Test patch:
diff --git a/test/locale/ar.test.js b/test/locale/ar.test.js
index 3f9f60f..2b5a57d 100644
--- a/test/locale/ar.test.js
+++ b/test/locale/ar.test.js
@@ -50,3 +50,13 @@ it('RelativeTime: Time from X gets formatted', () => {
       .toBe(t[2])
   })
 })
+
+it('Format meridiem with locale function', () => {
+  for (let i = 0; i <= 23; i += 1) {
+    const hour = dayjs()
+      .startOf('day')
+      .add(i, 'hour')
+    const meridiem = i > 12 ? 'م' : 'ص'
+    expect(hour.locale('ar').format('A')).toBe(`${meridiem}`)
+  }
+})
diff --git a/test/locale/sl.test.js b/test/locale/sl.test.js
new file mode 100644
index 0000000..672d606
--- /dev/null
+++ b/test/locale/sl.test.js
@@ -0,0 +1,82 @@
+import MockDate from 'mockdate'
+import dayjs from '../../src'
+import relativeTime from '../../src/plugin/relativeTime'
+import '../../src/locale/sl'
+
+dayjs.extend(relativeTime)
+
+beforeEach(() => {
+  MockDate.set(new Date())
+})
+
+afterEach(() => {
+  MockDate.reset()
+})
+
+it('Slovenian locale relative time in past and future', () => {
+  const cases = [
+    [1, 's', 'čez nekaj sekund', 'nekaj sekund'],
+    [-1, 's', 'pred nekaj sekundami', 'nekaj sekund'],
+    [1, 'm', 'čez eno minuto', 'ena minuta'],
+    [-1, 'm', 'pred eno minuto', 'ena minuta'],
+    [2, 'm', 'čez 2 minuti', '2 minuti'],
+    [-2, 'm', 'pred 2 minutama', '2 minuti'],
+    [3, 'm', 'čez 3 minute', '3 minute'],
+    [-3, 'm', 'pred 3 minutami', '3 minute'],
+    [5, 'm', 'čez 5 minut', '5 minut'],
+    [-5, 'm', 'pred 5 minutami', '5 minut'],
+    [1, 'h', 'čez eno uro', 'ena ura'],
+    [-1, 'h', 'pred eno uro', 'ena ura'],
+    [2, 'h', 'čez 2 uri', '2 uri'],
+    [-2, 'h', 'pred 2 urama', '2 uri'],
+    [3, 'h', 'čez 3 ure', '3 ure'],
+    [-3, 'h', 'pred 3 urami', '3 ure'],
+    [5, 'h', 'čez 5 ur', '5 ur'],
+    [-5, 'h', 'pred 5 urami', '5 ur'],
+    [1, 'd', 'čez en dan', 'en dan'],
+    [-1, 'd', 'pred enim dnem', 'en dan'],
+    [2, 'd', 'čez 2 dneva', '2 dneva'],
+    [-2, 'd', 'pred 2 dnevoma', '2 dneva'],
+    [3, 'd', 'čez 3 dni', '3 dni'],
+    [-3, 'd', 'pred 3 dnevi', '3 dni'],
+    [5, 'd', 'čez 5 dni', '5 dni'],
+    [-5, 'd', 'pred 5 dnevi', '5 dni'],
+    [1, 'M', 'čez en mesec', 'en mesec'],
+    [-1, 'M', 'pred enim mesecem', 'en mesec'],
+    [2, 'M', 'čez 2 meseca', '2 meseca'],
+    [-2, 'M', 'pred 2 mesecema', '2 meseca'],
+    [3, 'M', 'čez 3 mesece', '3 mesece'],
+    [-3, 'M', 'pred 3 meseci', '3 mesece'],
+    [5, 'M', 'čez 5 mesecev', '5 mesecev'],
+    [-5, 'M', 'pred 5 meseci', '5 mesecev'],
+    [1, 'y', 'čez eno leto', 'eno leto'],
+    [-1, 'y', 'pred enim letom', 'eno leto'],
+    [2, 'y', 'čez 2 leti', '2 leti'],
+    [-2, 'y', 'pred 2 letoma', '2 leti'],
+    [3, 'y', 'čez 3 leta', '3 leta'],
+    [-3, 'y', 'pred 3 leti', '3 leta'],
+    [5, 'y', 'čez 5 let', '5 let'],
+    [-5, 'y', 'pred 5 leti', '5 let']
+    // these are rounded
+    // if user decides to change rounding then it would be good to test them also
+    // [102, 's', 'čez 102 sekundi', '102 sekundi'],
+    // [-102, 's', 'pred 102 sekundama', '102 sekundi'],
+    // [103, 's', 'čez 103 sekunde', '103 sekunde'],
+    // [-103, 's', 'pred 103 sekundami', '103 sekunde'],
+    // [114, 's', 'čez 114 sekund', '114 sekund'],
+    // [-114, 's', 'pred 114 sekundami', '114 sekund'],
+    // [-102, 'm', 'čez 102 minuti', '102 minuti'],
+    // [-102, 'm', 'pred 102 minutama', '102 minuti'],
+    // [103, 'm', 'čez 103 minute', '103 minute'],
+    // [-103, 'm', 'pred 103 minutami', '103 minute'],
+    // [114, 'm', 'čez 114 minut', '114 minut'],
+    // [-114, 'm', 'pred 114 minutami', '114 minut']
+  ]
+
+  cases.forEach((c) => {
+    // With suffix
+    expect(dayjs().add(c[0], c[1]).locale('sl').fromNow()).toBe(c[2])
+    // Without suffix
+    expect(dayjs().add(c[0], c[1]).locale('sl').fromNow(true)).toBe(c[3])
+  })
+})
diff --git a/test/plugin/timezone.test.js b/test/plugin/timezone.test.js
index 2b28cc3..c4529c6 100644
--- a/test/plugin/timezone.test.js
+++ b/test/plugin/timezone.test.js
@@ -318,4 +318,15 @@ describe('startOf and endOf', () => {
     const endOfDay = originalDay.endOf('day')
     expect(endOfDay.valueOf()).toEqual(originalDay.valueOf())
   })
+
+  it('preserves locality when tz is called', () => {
+    const tzWithoutLocality = dayjs.tz('2023-02-17 00:00:00', NY)
+    const tzWithLocality = dayjs.tz('2023-02-17 00:00:00', NY).locale({
+      name: 'locale_test',
+      weekStart: 3
+    })
+
+    expect(tzWithoutLocality.startOf('week').format('YYYY-MM-DD')).toEqual('2023-02-12')
+    expect(tzWithLocality.startOf('week').format('YYYY-MM-DD')).toEqual('2023-02-15')
+  })
 })