Skip to content
This repository was archived by the owner on Dec 31, 2024. It is now read-only.

Commit 3282075

Browse files
committed
⭐ new(datetime): add datetime localization
1 parent d328c61 commit 3282075

7 files changed

Lines changed: 258 additions & 4 deletions

File tree

decls/i18n.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,41 @@
1+
declare var Intl: any;
2+
13
declare type Path = string;
24
declare type Locale = string;
35
declare type LocaleMessage = string | LocaleMessageObject | LocaleMessageArray;
46
declare type LocaleMessageObject = { [key: Path]: LocaleMessage };
57
declare type LocaleMessageArray = Array<LocaleMessage>;
68
declare type LocaleMessages = { [key: Locale]: LocaleMessageObject };
79

10+
// This options is the same as Intl.DateTimeFormat constructor options:
11+
// http://www.ecma-international.org/ecma-402/2.0/#sec-intl-datetimeformat-constructor
12+
declare type DateTimeFormatOptions = {
13+
year?: 'numeric' | '2-digit',
14+
month?: 'numeric' | '2-digit' | 'narrow' | 'short' | 'long',
15+
day?: 'numeric' | '2-digit',
16+
hour?: 'numeric' | '2-digit',
17+
minute?: 'numeric' | '2-digit',
18+
second?: 'numeric' | '2-digit',
19+
weekday?: 'narrow' | 'short' | 'long',
20+
hour12?: boolean,
21+
era?: 'narrow' | 'short' | 'long',
22+
timeZone?: string, // IANA time zone
23+
timeZoneName?: 'short' | 'long',
24+
localeMatcher?: 'lookup' | 'best fit',
25+
formatMatcher?: 'basic' | 'best fit'
26+
};
27+
declare type DateTimeFormat = { [key: string]: DateTimeFormatOptions };
28+
declare type DateTimeFormats = { [key: Locale]: DateTimeFormat };
29+
830
declare type TranslateResult = string | Array<string>;
31+
declare type DateTimeFormatResult = string;
932
declare type MissingHandler = (locale: Locale, key: Path, vm?: any) => void;
1033

1134
declare type I18nOptions = {
1235
locale?: Locale,
1336
fallbackLocale?: Locale,
1437
messages?: LocaleMessages,
38+
dateTimeFormats?: DateTimeFormats,
1539
formatter?: Formatter,
1640
missing?: MissingHandler,
1741
root?: I18n, // for internal
@@ -20,15 +44,21 @@ declare type I18nOptions = {
2044
silentTranslationWarn?: boolean
2145
};
2246

47+
declare type IntlAvailability = {
48+
dateTimeFormat: boolean
49+
};
50+
2351
declare interface I18n {
2452
static install: () => void, // for Vue plugin interface
2553
static version: string,
54+
static availabilities: IntlAvailability,
2655
get vm (): any, // for internal
2756
get locale (): Locale,
2857
set locale (locale: Locale): void,
2958
get fallbackLocale (): Locale,
3059
set fallbackLocale (locale: Locale): void,
3160
get messages (): LocaleMessages,
61+
get dateTimeFormats (): DateTimeFormats,
3262
get missing (): ?MissingHandler,
3363
set missing (handler: MissingHandler): void,
3464
get formatter (): Formatter,
@@ -40,7 +70,11 @@ declare interface I18n {
4070
mergeLocaleMessage (locale: Locale, message: LocaleMessage): void,
4171
t (key: Path, ...values: any): TranslateResult,
4272
tc (key: Path, choice?: number, ...values: any): TranslateResult,
43-
te (key: Path, locale?: Locale): boolean
73+
te (key: Path, locale?: Locale): boolean,
74+
getDateTimeFormat (locale: Locale): DateTimeFormat,
75+
setDateTimeFormat (locale: Locale, format: DateTimeFormat): void,
76+
mergeDateTimeFormat (locale: Locale, format: DateTimeFormat): void,
77+
d (value: number | Date, ...args: any): DateTimeFormatResult
4478
};
4579

4680
declare type FormatterOptions = { [key: string]: any };

src/extend.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,9 @@ export default function extend (Vue: any): void {
1515
const i18n = this.$i18n
1616
return i18n._te(key, i18n.locale, i18n.messages, locale)
1717
}
18+
19+
Vue.prototype.$d = function (value: Date, ...args: any): DateTimeFormatResult {
20+
const i18n = this.$i18n
21+
return i18n.d(value, ...args)
22+
}
1823
}

src/index.js

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
/* @flow */
22

33
import { install, Vue } from './install'
4-
import { warn, isNull, parseArgs, fetchChoice, isPlainObject, looseClone } from './util'
4+
import {
5+
warn,
6+
isNull,
7+
parseArgs,
8+
fetchChoice,
9+
isPlainObject,
10+
isObject,
11+
looseClone,
12+
canUseDateTimeFormat
13+
} from './util'
514
import BaseFormatter from './format'
615
import getPathValue from './path'
716

@@ -10,6 +19,7 @@ import type { PathValue } from './path'
1019
export default class VueI18n {
1120
static install: () => void
1221
static version: string
22+
static availabilities: IntlAvailability
1323

1424
_vm: any
1525
_formatter: Formatter
@@ -21,11 +31,13 @@ export default class VueI18n {
2131
_watcher: any
2232
_i18nWatcher: Function
2333
_silentTranslationWarn: boolean
34+
_dateTimeFormatters: Object
2435

2536
constructor (options: I18nOptions = {}) {
2637
const locale: Locale = options.locale || 'en-US'
2738
const fallbackLocale: Locale = options.fallbackLocale || 'en-US'
2839
const messages: LocaleMessages = options.messages || {}
40+
const dateTimeFormats = options.dateTimeFormats || {}
2941
this._vm = null
3042
this._formatter = options.formatter || new BaseFormatter()
3143
this._missing = options.missing || null
@@ -37,17 +49,21 @@ export default class VueI18n {
3749
this._silentTranslationWarn = options.silentTranslationWarn === undefined
3850
? false
3951
: !!options.silentTranslationWarn
52+
this._dateTimeFormatters = {}
4053

4154
this._exist = (message: Object, key: Path): boolean => {
4255
if (!message || !key) { return false }
4356
return !isNull(getPathValue(message, key))
4457
}
4558

46-
this._initVM({ locale, fallbackLocale, messages })
59+
this._initVM({ locale, fallbackLocale, messages, dateTimeFormats })
4760
}
4861

4962
_initVM (data: {
50-
locale: Locale, fallbackLocale: Locale, messages: LocaleMessages
63+
locale: Locale,
64+
fallbackLocale: Locale,
65+
messages: LocaleMessages,
66+
dateTimeFormats: DateTimeFormats
5167
}): void {
5268
const silent = Vue.config.silent
5369
Vue.config.silent = true
@@ -92,6 +108,7 @@ export default class VueI18n {
92108
get vm (): any { return this._vm }
93109

94110
get messages (): LocaleMessages { return looseClone(this._vm.messages) }
111+
get dateTimeFormats (): DateTimeFormats { return looseClone(this._vm.dateTimeFormats) }
95112

96113
get locale (): Locale { return this._vm.locale }
97114
set locale (locale: Locale): void {
@@ -256,8 +273,80 @@ export default class VueI18n {
256273
mergeLocaleMessage (locale: Locale, message: LocaleMessage): void {
257274
this._vm.messages[locale] = Vue.util.extend(this.getLocaleMessage(locale), message)
258275
}
276+
277+
getDateTimeFormat (locale: Locale): DateTimeFormat {
278+
return looseClone(this._vm.dateTimeFormats[locale])
279+
}
280+
281+
setDateTimeFormat (locale: Locale, format: DateTimeFormat): void {
282+
this._vm.dateTimeFormats[locale] = format
283+
}
284+
285+
mergeDateTimeFormat (locale: Locale, format: DateTimeFormat): void {
286+
this._vm.dateTimeFormats[locale] = Vue.util.extend(this.getDateTimeFormat(locale), format)
287+
}
288+
289+
_d (value: number | Date, _locale: Locale, key: ?string): DateTimeFormatResult {
290+
if (process.env.NODE_ENV !== 'production' && !VueI18n.availabilities.dateTimeFormat) {
291+
warn('Cannot format a Date value due to not support Intl.DateTimeFormat.')
292+
return ''
293+
}
294+
295+
let ret = ''
296+
const dateTimeFormats = this.dateTimeFormats
297+
if (key) {
298+
let locale: Locale = _locale
299+
if (isNull(dateTimeFormats[_locale][key])) {
300+
if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) {
301+
warn(`Fall back to the dateTimeFormat of key '${key}' with '${this.fallbackLocale}' locale.`)
302+
}
303+
locale = this.fallbackLocale
304+
}
305+
const id = `${locale}__${key}`
306+
let formatter = this._dateTimeFormatters[id]
307+
const format = dateTimeFormats[locale][key]
308+
if (!formatter) {
309+
formatter = this._dateTimeFormatters[id] = Intl.DateTimeFormat(locale, format)
310+
}
311+
ret = formatter.format(value)
312+
} else {
313+
ret = Intl.DateTimeFormat(_locale).format(value)
314+
}
315+
316+
return ret
317+
}
318+
319+
d (value: number | Date, ...args: any): DateTimeFormatResult {
320+
let locale: Locale = this.locale
321+
let key: ?string = null
322+
323+
if (args.length === 1) {
324+
if (typeof args[0] === 'string') {
325+
key = args[0]
326+
} else if (isObject(args[0])) {
327+
if (args[0].locale) {
328+
locale = args[0].locale
329+
}
330+
if (args[0].key) {
331+
key = args[0].key
332+
}
333+
}
334+
} else if (args.length === 2) {
335+
if (typeof args[0] === 'string') {
336+
key = args[0]
337+
}
338+
if (typeof args[1] === 'string') {
339+
locale = args[1]
340+
}
341+
}
342+
343+
return this._d(value, locale, key)
344+
}
259345
}
260346

347+
VueI18n.availabilities = {
348+
dateTimeFormat: canUseDateTimeFormat
349+
}
261350
VueI18n.install = install
262351
VueI18n.version = '__VERSION__'
263352

src/util.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,6 @@ export function fetchChoice (message: string, choice: number): ?string {
9595
export function looseClone (obj: Object): Object {
9696
return JSON.parse(JSON.stringify(obj))
9797
}
98+
99+
export const canUseDateTimeFormat: boolean =
100+
typeof Intl !== 'undefined' && typeof Intl.DateTimeFormat !== 'undefined'

test/unit/basic.test.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import messages from './fixture/index'
2+
import dateTimeFormats from './fixture/datetime'
23

34
describe('basic', () => {
45
let i18n
@@ -575,4 +576,55 @@ describe('basic', () => {
575576
}).then(done)
576577
})
577578
})
579+
580+
const desc = VueI18n.availabilities.dateTimeFormat ? describe : describe.skip
581+
desc('i18n#d', () => {
582+
let dt
583+
beforeEach(() => {
584+
i18n = new VueI18n({
585+
locale: 'en-US',
586+
fallbackLocale: 'ja-JP',
587+
dateTimeFormats
588+
})
589+
dt = new Date(Date.UTC(2012, 11, 20, 3, 0, 0))
590+
})
591+
592+
describe('arguments nothing', () => {
593+
it('should be formatted', () => {
594+
assert.equal(i18n.d(dt), '12/20/2012')
595+
})
596+
})
597+
598+
describe('number value', () => {
599+
it('should be formatted', () => {
600+
assert.equal(i18n.d(dt.getTime()), '12/20/2012')
601+
})
602+
})
603+
604+
describe('key argument', () => {
605+
it('should be formatted', () => {
606+
assert.equal(i18n.d(dt, 'short'), '12/20/2012, 12:00 PM')
607+
})
608+
})
609+
610+
describe('locale argument', () => {
611+
describe('with second argument', () => {
612+
it('should be formatted', () => {
613+
assert.equal(i18n.d(dt, 'short', 'ja-JP'), '2012/12/20 12:00')
614+
})
615+
})
616+
617+
describe('with object argument', () => {
618+
it('should be formatted', () => {
619+
assert.equal(i18n.d(dt, { key: 'short', locale: 'ja-JP' }), '2012/12/20 12:00')
620+
})
621+
})
622+
})
623+
624+
describe('fallback', () => {
625+
it('should be formatted', () => {
626+
assert.equal(i18n.d(dt, 'long'), '2012/12/20 12:00:00')
627+
})
628+
})
629+
})
578630
})

test/unit/datetime.test.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import dateTimeFormats from './fixture/datetime'
2+
3+
describe('datetime format', () => {
4+
describe('getDateTimeFormat / setDateTimeFormat', () => {
5+
it('should be worked', done => {
6+
const i18n = new VueI18n({
7+
locale: 'en-US',
8+
dateTimeFormats
9+
})
10+
const el = document.createElement('div')
11+
document.body.appendChild(el)
12+
13+
const dt = new Date(Date.UTC(2012, 11, 20, 3, 0, 0))
14+
const vm = new Vue({
15+
i18n,
16+
render (h) {
17+
return h('p', { ref: 'text' }, [this.$d(dt, 'short')])
18+
}
19+
}).$mount(el)
20+
21+
const { text } = vm.$refs
22+
const zhFormat = {
23+
short: {
24+
year: 'numeric', month: '2-digit', day: '2-digit',
25+
hour: '2-digit', minute: '2-digit'
26+
}
27+
}
28+
nextTick(() => {
29+
assert.equal(text.textContent, '12/20/2012, 12:00 PM')
30+
i18n.setDateTimeFormat('zh-CN', zhFormat)
31+
assert.deepEqual(i18n.getDateTimeFormat('zh-CN'), zhFormat)
32+
i18n.locale = 'zh-CN'
33+
}).then(() => {
34+
assert.equal(text.textContent, '2012/12/20 下午12:00')
35+
}).then(done)
36+
})
37+
})
38+
39+
describe('mergeDateTimeFormat', () => {
40+
it('should be merged', () => {
41+
const i18n = new VueI18n({
42+
locale: 'ja-JP',
43+
dateTimeFormats
44+
})
45+
const short = {
46+
year: 'numeric', month: '2-digit', day: '2-digit',
47+
hour: '2-digit', minute: '2-digit'
48+
}
49+
i18n.mergeDateTimeFormat('en-US', { short })
50+
assert.deepEqual(short, i18n.getDateTimeFormat('en-US').short)
51+
})
52+
})
53+
})

test/unit/fixture/datetime.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export default {
2+
'en-US': {
3+
short: { // DD/MM/YYYY, hh:mm (AM|PM)
4+
year: 'numeric', month: '2-digit', day: '2-digit',
5+
hour: '2-digit', minute: '2-digit'
6+
}
7+
},
8+
'ja-JP': {
9+
long: { // YYYY/MM/DD hh:mm:ss
10+
year: 'numeric', month: '2-digit', day: '2-digit',
11+
hour: '2-digit', minute: '2-digit', second: '2-digit'
12+
},
13+
short: { // YYYY/MM/DD hh:mm
14+
year: 'numeric', month: '2-digit', day: '2-digit',
15+
hour: '2-digit', minute: '2-digit'
16+
}
17+
}
18+
}

0 commit comments

Comments
 (0)