Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Supports locale options for localeString in amp-date-display. #34359

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions examples/amp-date-display.amp.html
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ <h2>Locales</h2>
en-GB: {{dayName}} {{day}} {{monthName}} {{year}}
</template>
</amp-date-display>
<amp-date-display datetime="now" locale="zh-TW" layout="fixed" width="360" height="20" data-options-time-style="short">
<template type="amp-mustache">
zh-TW: {{localeString}}
</template>
</amp-date-display>
</div>
</body>
</html>
90 changes: 85 additions & 5 deletions extensions/amp-date-display/0.1/amp-date-display.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
import {AmpEvents} from '../../../src/core/constants/amp-events';
import {Services} from '../../../src/services';
import {createCustomEvent} from '../../../src/event-helper';
import {dev, devAssert, userAssert} from '../../../src/log';
import {dashToCamelCase} from '../../../src/core/types/string/index.js';
import {dev, devAssert, user, userAssert} from '../../../src/log';
import {isLayoutSizeDefined} from '../../../src/layout';
import {removeChildren} from '../../../src/dom';

Expand All @@ -30,6 +31,15 @@ const DEFAULT_LOCALE = 'en';
/** @const {number} */
const DEFAULT_OFFSET_SECONDS = 0;

/** @const {!Object<string, *>} */
const DEFAULT_DATETIME_OPTIONS = {
'year': 'numeric',
'month': 'short',
'day': 'numeric',
'hour': 'numeric',
'minute': 'numeric',
};

/** @typedef {{
year: number,
month: number,
Expand Down Expand Up @@ -66,6 +76,7 @@ let VariablesDef;
minuteTwoDigit: string,
secondTwoDigit: string,
dayPeriod: string,
localeString: string,
}} */
let EnhancedVariablesDef;

Expand Down Expand Up @@ -95,6 +106,9 @@ export class AmpDateDisplay extends AMP.BaseElement {
/** @private {string} */
this.locale_ = '';

/** @private {Object<string, *>} */
this.localeOptions_ = null;

/** @private {?../../../src/service/template-impl.Templates} */
this.templates_ = null;

Expand Down Expand Up @@ -129,6 +143,11 @@ export class AmpDateDisplay extends AMP.BaseElement {

this.locale_ = this.element.getAttribute('locale') || DEFAULT_LOCALE;

this.localeOptions_ = this.parseLocaleOptionsAttrs_(
this.element,
'data-options-'
);

const data = /** @type {!JsonObject} */ (this.getDataForTemplate_());
this.templates_
.findAndRenderTemplate(this.element, data)
Expand All @@ -152,8 +171,8 @@ export class AmpDateDisplay extends AMP.BaseElement {
const date = new Date(epoch + offset);
const inUTC = this.displayIn_.toLowerCase() === 'utc';
const basicData = inUTC
? this.getVariablesInUTC_(date, this.locale_)
: this.getVariablesInLocal_(date, this.locale_);
? this.getVariablesInUTC_(date, this.locale_, this.localeOptions_)
: this.getVariablesInLocal_(date, this.locale_, this.localeOptions_);

return this.enhanceBasicVariables_(basicData);
}
Expand Down Expand Up @@ -185,13 +204,67 @@ export class AmpDateDisplay extends AMP.BaseElement {
return epoch;
}

/**
* @param {null|string} attributeName
* @param {string|undefined} attributePrefix
* @return {boolean}
* @private
*/
matchesAttrPrefix_(attributeName, attributePrefix) {
return (
attributeName !== null &&
attributePrefix !== undefined &&
attributeName.startsWith(attributePrefix) &&
attributeName !== attributePrefix
);
}

/**
* @param {!Element} element
* @param {string} attrPrefix
* @return {Object<string, *>|undefined}
* @private
*/
parseLocaleOptionsAttrs_(element, attrPrefix) {
const currObj = {};
let objContains = false;
const attrs = element.attributes;
for (let i = 0; i < attrs.length; i++) {
const attrib = attrs[i];
if (this.matchesAttrPrefix_(attrib.name, attrPrefix)) {
currObj[dashToCamelCase(attrib.name.slice(attrPrefix.length))] =
attrib.value;
objContains = true;
}
}
if (objContains) {
return currObj;
}
}

/**
* @param {!Date} date
* @param {string} locale
* @param {?Object<string, *>} localeOptions
* @return {string}
* @private
*/
getLocaleString_(date, locale, localeOptions) {
try {
return date.toLocaleString(locale, localeOptions);
} catch (e) {
user().error(TAG, 'localeOptions', e);
}
}

/**
* @param {!Date} date
* @param {string} locale
* @param {?Object<string, *>} localeOptions
* @return {!VariablesDef}
* @private
*/
getVariablesInLocal_(date, locale) {
getVariablesInLocal_(date, locale, localeOptions = DEFAULT_DATETIME_OPTIONS) {
return {
year: date.getFullYear(),
month: date.getMonth() + 1,
Expand All @@ -208,16 +281,22 @@ export class AmpDateDisplay extends AMP.BaseElement {
minute: date.getMinutes(),
second: date.getSeconds(),
iso: date.toISOString(),
localeString: this.getLocaleString_(date, locale, localeOptions),
};
}

/**
* @param {!Date} date
* @param {string} locale
* @param {?Object<string, *>} localeOptions
* @return {!VariablesDef}
* @private
*/
getVariablesInUTC_(date, locale) {
getVariablesInUTC_(date, locale, localeOptions = DEFAULT_DATETIME_OPTIONS) {
const localeOptionsInUTC = {
...localeOptions,
timeZone: 'UTC',
};
return {
year: date.getUTCFullYear(),
month: date.getUTCMonth() + 1,
Expand All @@ -242,6 +321,7 @@ export class AmpDateDisplay extends AMP.BaseElement {
minute: date.getUTCMinutes(),
second: date.getUTCSeconds(),
iso: date.toISOString(),
localeString: this.getLocaleString_(date, locale, localeOptionsInUTC),
};
}

Expand Down
10 changes: 10 additions & 0 deletions extensions/amp-date-display/0.1/amp-date-display.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ This table lists the format you can specify in your Mustache template:
| secondTwoDigit | 00, 01, 02, ..., 58, 59 |
| year | 0, 1, 2, ..., 1999, 2000, 2001, etc. |
| yearTwoDigit | 00, 01, 02, ..., 17, 18, 19, ..., 98, 99 |
| localeString | A string with a language sensitive representation. |

## Attributes

Expand Down Expand Up @@ -115,6 +116,15 @@ date to UTC.
The `offset-seconds` attribute specifies an integer number of seconds to shift
the given date.

### data-options-\* (optional)

The `data-options-*` supports all the options under [Intl.DateTimeFormat.options](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#parameters)
parameter that specifies the formatting style to use for `localeString` format.
Valid attributes include: `data-options-date-style`, `data-options-time-style`, etc.

Note that if `display-in` attrubute is set to `utc`, the value of
`data-options-time-zone` will automatically be converted to `UTC`.

## Validation

See [amp-date-display rules](../validator-amp-date-display.protoascii) in the AMP validator specification.
58 changes: 58 additions & 0 deletions extensions/amp-date-display/0.1/test/test-amp-date-display.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import '../amp-date-display';
import * as fakeTimers from '@sinonjs/fake-timers';
import {Services} from '../../../../src/services';
import {expect} from 'chai';
import {user} from '../../../../src/log';

describes.realWin(
'amp-date-display',
Expand Down Expand Up @@ -81,6 +83,7 @@ describes.realWin(
expect(data.second).to.equal(6);
expect(data.secondTwoDigit).to.equal('06');
expect(data.dayPeriod).to.equal('am');
expect(data.localeString).to.equal('Feb 3, 2001, 4:05 AM');
});

it('provides all variables in local and English (default)', async () => {
Expand Down Expand Up @@ -108,6 +111,7 @@ describes.realWin(
expect(data.second).to.equal(6);
expect(data.secondTwoDigit).to.equal('06');
expect(data.dayPeriod).to.equal('am');
expect(data.localeString).to.equal('Feb 3, 2001, 4:05 AM');
});

describe('correctly parses', () => {
Expand Down Expand Up @@ -173,6 +177,31 @@ describes.realWin(
'2001-02-03T04:05:06.007Z'
);
});

it('locale and data-options-time-style', async () => {
element.setAttribute('datetime', '2001-02-03T04:05:06.007Z');
element.setAttribute('display-in', 'UTC');
element.setAttribute('locale', 'zh-TW');
element.setAttribute('data-options-time-style', 'short');
await element.buildInternal();

const data = impl.getDataForTemplate_();

expect(data.localeString).to.equal('上午4:05');
});

it('locale, data-options-time-style, and data-options-date-style', async () => {
element.setAttribute('datetime', '2001-02-03T04:05:06.007Z');
element.setAttribute('display-in', 'UTC');
element.setAttribute('locale', 'zh-TW');
element.setAttribute('data-options-date-style', 'full');
element.setAttribute('data-options-time-style', 'medium');
await element.buildInternal();

const data = impl.getDataForTemplate_();

expect(data.localeString).to.equal('2001年2月3日 星期六 上午4:05:06');
});
});

it('adds offset seconds', async () => {
Expand Down Expand Up @@ -206,5 +235,34 @@ describes.realWin(
expect(data.dayName).to.equal('sobota');
expect(data.dayNameShort).to.equal('so');
});

describe('invalid data-options-* settings', () => {
it('throws error when invalid data-options value is provided', async () => {
const spy = env.sandbox.stub(user(), 'error');
element.setAttribute('datetime', '2001-02-03T04:05:06.007Z');
element.setAttribute('display-in', 'UTC');
element.setAttribute('locale', 'zh-TW');
element.setAttribute('data-options-time-style', 'invalid');

await element.buildInternal();
const data = impl.getDataForTemplate_();

expect(spy.args[0][1]).to.equal('localeOptions');
expect(spy.args[0][2]).to.match(/RangeError/);
expect(data.localeString).to.be.undefined;
});

it('ignores the attr when invalid data-options-attr is provided', async () => {
element.setAttribute('datetime', '2001-02-03T04:05:06.007Z');
element.setAttribute('display-in', 'UTC');
element.setAttribute('locale', 'zh-TW');
element.setAttribute('data-options-invalid', 'invalid');

await element.buildInternal();
const data = impl.getDataForTemplate_();

expect(data.localeString).to.equal('2001/2/3 上午4:05:06');
});
});
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@
<amp-date-display datetime="2017-08-02T15:05:05.+04:00" layout="fixed" width="360" height="20">
<template type="amp-mustache">{{iso}}</template>
</amp-date-display>

<!-- valid, with data-options-time-style -->
<amp-date-display datetime="now" layout="fixed" width="360" height="20" data-options-time-style="short">
<template type="amp-mustache">{{localeString}}</template>
</amp-date-display>
</body>

</html>
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ amp-date-display/0.1/test/validator-amp-date-display.html:168:2 The attribute 'd
amp-date-display/0.1/test/validator-amp-date-display.html:173:2 The attribute 'datetime' in tag 'amp-date-display' is set to the invalid value '2017-08-02T15:05:05.+04:00'. (see https://amp.dev/documentation/components/amp-date-display)
| <template type="amp-mustache">{{iso}}</template>
| </amp-date-display>
|
| <!-- valid, with data-options-time-style -->
| <amp-date-display datetime="now" layout="fixed" width="360" height="20" data-options-time-style="short">
| <template type="amp-mustache">{{localeString}}</template>
| </amp-date-display>
| </body>
|
| </html>
1 change: 1 addition & 0 deletions extensions/amp-date-display/1.0/base-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ BaseElement['props'] = {
},
'displayIn': {attr: 'display-in'},
'locale': {attr: 'locale'},
'localeOptions': {attrPrefix: 'data-options-'},
};

/** @override */
Expand Down
Loading