diff --git a/core/audits/accessibility/aria-meter-name.js b/core/audits/accessibility/aria-meter-name.js index 895739a3cca5..d7ea6fc31dd7 100644 --- a/core/audits/accessibility/aria-meter-name.js +++ b/core/audits/accessibility/aria-meter-name.js @@ -17,8 +17,8 @@ const UIStrings = { title: 'ARIA `meter` elements have accessible names', /** Title of an accessibility audit that evaluates if meter HTML elements do not have accessible names. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */ failureTitle: 'ARIA `meter` elements do not have accessible names.', - /** Description of a Lighthouse audit that tells the user *why* they should have accessible names for HTML elements. This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */ - description: 'When an element doesn\'t have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `meter` elements](https://dequeuniversity.com/rules/axe/4.4/aria-meter-name).', + /** Description of a Lighthouse audit that tells the user *why* they should have accessible names for HTML 'meter' elements. This is displayed after a user expands the section to see more. No character length limits. 'Learn how...' becomes link text to additional documentation. */ + description: 'When a meter element doesn\'t have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `meter` elements](https://dequeuniversity.com/rules/axe/4.4/aria-meter-name).', }; const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings); diff --git a/core/audits/accessibility/aria-tooltip-name.js b/core/audits/accessibility/aria-tooltip-name.js index 3161a18b3b5a..1a4e5683cd5d 100644 --- a/core/audits/accessibility/aria-tooltip-name.js +++ b/core/audits/accessibility/aria-tooltip-name.js @@ -17,8 +17,8 @@ const UIStrings = { title: 'ARIA `tooltip` elements have accessible names', /** Title of an accessibility audit that evaluates if tooltip HTML elements do not have accessible names. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */ failureTitle: 'ARIA `tooltip` elements do not have accessible names.', - /** Description of a Lighthouse audit that tells the user *why* they should have accessible names for HTML elements. This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */ - description: 'When an element doesn\'t have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `tooltip` elements](https://dequeuniversity.com/rules/axe/4.4/aria-tooltip-name).', + /** Description of a Lighthouse audit that tells the user *why* they should have accessible names for HTML 'tooltip' elements. This is displayed after a user expands the section to see more. No character length limits. 'Learn how...' becomes link text to additional documentation. */ + description: 'When a tooltip element doesn\'t have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `tooltip` elements](https://dequeuniversity.com/rules/axe/4.4/aria-tooltip-name).', }; const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings); diff --git a/core/lib/i18n/README.md b/core/lib/i18n/README.md index 606cd5b16baf..bcc936499b8b 100644 --- a/core/lib/i18n/README.md +++ b/core/lib/i18n/README.md @@ -12,11 +12,11 @@ The collection and translation pipeline: ``` Source files: Locale files: +---------------------------+ +---------------------------------------------- -| ++ | shared/localization/locales/en-US.json | -| const UIStrings = { ... };|-+ +---> | shared/localization/locales/en-XL.json | +| ++ | shared/localization/locales/en-US.json | +| const UIStrings = { ... };|-+ +---> | shared/localization/locales/en-XL.json | | |-| | +----------------------------------------------+ +-----------------------------| | | || - +----------------------------| | | shared/localization/locales/*.json |-<+ + +----------------------------| | | shared/localization/locales/*.json |-<+ +---------------------------+ | | || | | | +----------------------------------------------| | $ yarn | | +---------------------------------------------+ | @@ -198,11 +198,11 @@ CTC is a name that is distinct and identifies this as the Chrome translation for ```json { "name": { - "message": "Message text, with optional placeholders.", + "message": "Message text, with optional placeholders, which can be $PLACEHOLDER_TEXT$", "description": "Translator-aimed description of the message.", "meaning": "Description given when a message is duplicated, in order to give context to the message. Lighthouse uses a copy of the description for this.", "placeholders": { - "placeholder_name": { + "PLACEHOLDER_TEXT": { "content": "A string to be placed within the message.", "example": "Translator-aimed example of the placeholder string." }, @@ -211,6 +211,23 @@ CTC is a name that is distinct and identifies this as the Chrome translation for } ``` +### Collisions +Collisions happen when two CTC messages have the same `message`. For Lighthouse, there are two relevant collision types in TC: + - Allowed: the CTC `message`, `description`, and `placeholders` are exactly the same. These collisions are deduped on the TC side and the translation cost is the same as for a single string. + - Disallowed: `message` is the same but one or more of the other properties differ. + +When the `message` needs to be the same as another string but another property must differ, that disallowed collision can be fixed by adding a unique `meaning` property to each colliding CTC message. TC will then consider those as separate strings and not a collision. + +In Lighthouse, this is done by having a different `description` for the strings, which is then copied to `meaning` in `resolveMessageCollisions()`. `meaning` cannot be manually set. + +For instance, the string "Potential Savings" currently refers to both saved KiB and saved milliseconds in different audits. The string is defined twice, each with a different `description` describing the units being saved, in case some locales' translations will use a different word choice depending on the unit. + +Internally, TC uses a message ID that's a hash of `message` and `meaning` to check for collisions. Somewhat confusingly, if two messages do have colliding IDs, then `message`, `meaning`, `description`, and `placeholders` are all required to match or an error is thrown. This is why all message properties could cause a collision but `meaning` is the only way to dedupe them. + +We treat it as an error if `placeholders` differ between messages in a collision: if there is a need for placeholders to differ, then the strings aren't really the same, and at least the `description` should be changed to explain that context. Placeholders must match in user-controlled data (e.g. if a placeholder has an `@example`, it must be the same example in all instances) and in Lighthouse-controlled data (e.g. the token used to replace it in the CTC `message`, like `$PLACEHOLDER_TEXT$` in the example above). + +Finally, identical messages made to not collide by Lighthouse with a `meaning` cost real money and shouldn't be confused with allowed collisions which cost nothing for each additional collision. Fixed collisions are checked against a known list to add a little friction and motivate keeping them few in number. An error is thrown if a collision is fixed that hasn't yet been added to that list. + # Appendix ## Appendix A: How runtime string replacement works diff --git a/core/scripts/i18n/collect-strings.js b/core/scripts/i18n/collect-strings.js index 774d11585e7f..9a8e7a2a7938 100644 --- a/core/scripts/i18n/collect-strings.js +++ b/core/scripts/i18n/collect-strings.js @@ -16,6 +16,7 @@ import {expect} from 'expect'; import tsc from 'typescript'; import MessageParser from 'intl-messageformat-parser'; import esMain from 'es-main'; +import isDeepEqual from 'lodash/isEqual.js'; import {Util} from '../../util.cjs'; import {collectAndBakeCtcStrings} from './bake-ctc-to-lhl.js'; @@ -613,43 +614,90 @@ function writeStringsToCtcFiles(locale, strings) { } /** - * This function does two things: - * - * - Add `meaning` property to ctc messages that have the same message but different descriptions so TC can disambiguate. - * - Throw if the known collisions has changed at all. + * TODO: replace by Array.prototype.groupToMap when available. + * @template {unknown} T + * @template {string} U + * @param {Array} elements + * @param {(element: T) => U} callback + * @return {Map>} + */ +function arrayGroupToMap(elements, callback) { + /** @type {Map>} */ + const elementsByValue = new Map(); + + for (const element of elements) { + const key = callback(element); + const group = elementsByValue.get(key) || []; + group.push(element); + elementsByValue.set(key, group); + } + + return elementsByValue; +} + +/** + * Returns whether all the placeholders in the given CtcMessages match. + * @param {Array<{ctc: CtcMessage}>} strings + * @return {boolean} + */ +function doPlaceholdersMatch(strings) { + // Technically placeholder `content` is not required to match by TC, but since + // `example` must match and any auto-generated `example` is copied from `content`, + // it would be confusing to let it differ when `example` is explicit. + return strings.every(val => isDeepEqual(val.ctc.placeholders, strings[0].ctc.placeholders)); +} + +/** + * If two or more messages have the same `message`: + * - if `description`, and `placeholders` are also the same, TC treats them as a + * single message, the collision is allowed, and no change is made. + * - if `description` is unique for the collision members, the `description` is + * copied to `meaning` to differentiate them for TC. They must be added to the + * `collidingMessages` list for this case as a nudge to not be wasteful. + * - if `description` is the same but `placeholders` differ, an error is thrown. + * Either `placeholders` should be the same, or `message` or `description` + * should be changed to explain the need for different `placeholders`. * + * Modifies `strings` in place to fix collisions. + * @see https://github.com/GoogleChrome/lighthouse/blob/main/core/lib/i18n/README.md#collisions * @param {Record} strings */ function resolveMessageCollisions(strings) { - /** @type {Map>} */ - const stringsByMessage = new Map(); - - // Group all the strings by their message. - for (const ctc of Object.values(strings)) { - const collisions = stringsByMessage.get(ctc.message) || []; - collisions.push(ctc); - stringsByMessage.set(ctc.message, collisions); - } + const entries = Object.entries(strings).map(([key, ctc]) => ({key, ctc})); - /** @type {Array} */ - const allCollisions = []; + const stringsByMessage = arrayGroupToMap(entries, (entry) => entry.ctc.message); for (const messageGroup of stringsByMessage.values()) { // If this message didn't collide with anything else, skip it. if (messageGroup.length <= 1) continue; - // If group shares both message and description, they can be translated as if a single string. - const descriptions = new Set(messageGroup.map(ctc => ctc.description)); - if (descriptions.size <= 1) continue; + // For each group of matching message+description, placeholders must also match. + const stringsByDescription = arrayGroupToMap(messageGroup, (entry) => entry.ctc.description); + for (const descriptionGroup of stringsByDescription.values()) { + if (!doPlaceholdersMatch(descriptionGroup)) { + throw new Error('string collision: `message` and `description` are the same but differ in `placeholders`:\n' + + descriptionGroup.map(entry => `key: ${entry.key}\n`).join('') + + 'make `placeholders` match or update `message` or `description`s to explain why they don\'t match'); + } + } + + // If the entire message group has the same description and placeholders, + // they can be translated as if a single message, no meaning needed. + if (stringsByDescription.size <= 1) continue; // We have duplicate messages with different descriptions. Disambiguate using `meaning` for TC. - for (const ctc of messageGroup) { + for (const {ctc} of messageGroup) { ctc.meaning = ctc.description; } - allCollisions.push(...messageGroup); } +} - // Check that the known collisions match our known list. - const collidingMessages = allCollisions.map(collision => collision.message).sort(); +/** + * Check that fixed collisions match our known list. + * @param {Record} strings + */ +function checkKnownFixedCollisions(strings) { + const fixedCollisions = Object.values(strings).filter(string => string.meaning); + const collidingMessages = fixedCollisions.map(collision => collision.message).sort(); try { expect(collidingMessages).toEqual([ @@ -696,6 +744,7 @@ async function main() { } resolveMessageCollisions(strings); + checkKnownFixedCollisions(strings); writeStringsToCtcFiles('en-US', strings); console.log('Written to disk!', 'en-US.ctc.json'); @@ -734,4 +783,5 @@ export { parseUIStrings, createPsuedoLocaleStrings, convertMessageToCtc, + resolveMessageCollisions, }; diff --git a/core/test/fixtures/fraggle-rock/reports/sample-flow-result.json b/core/test/fixtures/fraggle-rock/reports/sample-flow-result.json index 9a3f4902a43d..90c225583400 100644 --- a/core/test/fixtures/fraggle-rock/reports/sample-flow-result.json +++ b/core/test/fixtures/fraggle-rock/reports/sample-flow-result.json @@ -2532,7 +2532,7 @@ "aria-meter-name": { "id": "aria-meter-name", "title": "ARIA `meter` elements have accessible names", - "description": "When an element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `meter` elements](https://dequeuniversity.com/rules/axe/4.4/aria-meter-name).", + "description": "When a meter element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `meter` elements](https://dequeuniversity.com/rules/axe/4.4/aria-meter-name).", "score": null, "scoreDisplayMode": "notApplicable" }, @@ -2591,7 +2591,7 @@ "aria-tooltip-name": { "id": "aria-tooltip-name", "title": "ARIA `tooltip` elements have accessible names", - "description": "When an element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `tooltip` elements](https://dequeuniversity.com/rules/axe/4.4/aria-tooltip-name).", + "description": "When a tooltip element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `tooltip` elements](https://dequeuniversity.com/rules/axe/4.4/aria-tooltip-name).", "score": null, "scoreDisplayMode": "notApplicable" }, @@ -11172,7 +11172,7 @@ "aria-meter-name": { "id": "aria-meter-name", "title": "ARIA `meter` elements have accessible names", - "description": "When an element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `meter` elements](https://dequeuniversity.com/rules/axe/4.4/aria-meter-name).", + "description": "When a meter element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `meter` elements](https://dequeuniversity.com/rules/axe/4.4/aria-meter-name).", "score": null, "scoreDisplayMode": "notApplicable" }, @@ -11231,7 +11231,7 @@ "aria-tooltip-name": { "id": "aria-tooltip-name", "title": "ARIA `tooltip` elements have accessible names", - "description": "When an element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `tooltip` elements](https://dequeuniversity.com/rules/axe/4.4/aria-tooltip-name).", + "description": "When a tooltip element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `tooltip` elements](https://dequeuniversity.com/rules/axe/4.4/aria-tooltip-name).", "score": null, "scoreDisplayMode": "notApplicable" }, @@ -16505,7 +16505,7 @@ "aria-meter-name": { "id": "aria-meter-name", "title": "ARIA `meter` elements have accessible names", - "description": "When an element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `meter` elements](https://dequeuniversity.com/rules/axe/4.4/aria-meter-name).", + "description": "When a meter element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `meter` elements](https://dequeuniversity.com/rules/axe/4.4/aria-meter-name).", "score": null, "scoreDisplayMode": "notApplicable" }, @@ -16564,7 +16564,7 @@ "aria-tooltip-name": { "id": "aria-tooltip-name", "title": "ARIA `tooltip` elements have accessible names", - "description": "When an element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `tooltip` elements](https://dequeuniversity.com/rules/axe/4.4/aria-tooltip-name).", + "description": "When a tooltip element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `tooltip` elements](https://dequeuniversity.com/rules/axe/4.4/aria-tooltip-name).", "score": null, "scoreDisplayMode": "notApplicable" }, diff --git a/core/test/results/sample_v2.json b/core/test/results/sample_v2.json index 6fa3fd026a5c..a421481896ae 100644 --- a/core/test/results/sample_v2.json +++ b/core/test/results/sample_v2.json @@ -3442,7 +3442,7 @@ "aria-meter-name": { "id": "aria-meter-name", "title": "ARIA `meter` elements have accessible names", - "description": "When an element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `meter` elements](https://dequeuniversity.com/rules/axe/4.4/aria-meter-name).", + "description": "When a meter element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `meter` elements](https://dequeuniversity.com/rules/axe/4.4/aria-meter-name).", "score": null, "scoreDisplayMode": "notApplicable" }, @@ -3501,7 +3501,7 @@ "aria-tooltip-name": { "id": "aria-tooltip-name", "title": "ARIA `tooltip` elements have accessible names", - "description": "When an element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `tooltip` elements](https://dequeuniversity.com/rules/axe/4.4/aria-tooltip-name).", + "description": "When a tooltip element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `tooltip` elements](https://dequeuniversity.com/rules/axe/4.4/aria-tooltip-name).", "score": null, "scoreDisplayMode": "notApplicable" }, diff --git a/core/test/scripts/i18n/collect-strings-test.js b/core/test/scripts/i18n/collect-strings-test.js index b57726700736..f20ba1ec5ffe 100644 --- a/core/test/scripts/i18n/collect-strings-test.js +++ b/core/test/scripts/i18n/collect-strings-test.js @@ -6,6 +6,9 @@ import * as collect from '../../../scripts/i18n/collect-strings.js'; +/** @typedef {collect.CtcMessage & Required>} CtcWithPlaceholders */ + +/** @param {string} justUIStrings */ function evalJustUIStrings(justUIStrings) { return Function(`'use strict'; ${justUIStrings} return UIStrings;`)(); } @@ -590,17 +593,169 @@ describe('Convert Message to Placeholder', () => { }); }); +describe('collisions', () => { + /** + * @template {unknown} T + * @param {T} input + * @return {T} + */ + function deepClone(input) { + return JSON.parse(JSON.stringify(input)); + } + + /** @return {Record<'first'|'second'|'third', CtcWithPlaceholders>} */ + function getStrings() { + const ctcMessage = { + message: 'Need absolute URL ($ICU_0$)', + description: 'Explanatory message.', + placeholders: { + ICU_0: { + content: '{url}', + example: 'https://example.com/', + }, + }, + }; + + return { + first: deepClone(ctcMessage), + second: deepClone(ctcMessage), + third: deepClone(ctcMessage), + }; + } + + it('finds no collisions with three unique ctc messages', () => { + const originalStrings = getStrings(); + originalStrings.first.message += '1'; + originalStrings.second.message += '2'; + originalStrings.third.message += '3'; + + const testStrings = deepClone(originalStrings); + collect.resolveMessageCollisions(testStrings); + expect(testStrings).toEqual(originalStrings); + // No meanings added. + expect(Object.values(testStrings).filter(str => str.meaning)).toHaveLength(0); + }); + + it('finds only allowed collisions and takes no actions for three identical ctc messages', () => { + const originalStrings = getStrings(); + const testStrings = deepClone(originalStrings); + collect.resolveMessageCollisions(testStrings); + expect(testStrings).toEqual(originalStrings); + // No meanings added. + expect(Object.values(testStrings).filter(str => str.meaning)).toHaveLength(0); + }); + + it('uses meaning to disambiguate collisions with different descriptions', () => { + const testStrings = getStrings(); + testStrings.first.description += '1'; + testStrings.second.description += '2'; + testStrings.third.description += '3'; + + const expectedStrings = deepClone(testStrings); + expectedStrings.first.meaning = expectedStrings.first.description; + expectedStrings.second.meaning = expectedStrings.second.description; + expectedStrings.third.meaning = expectedStrings.third.description; + + collect.resolveMessageCollisions(testStrings); + expect(testStrings).toEqual(expectedStrings); + }); + + it('all collisions with different descriptions get a meaning', () => { + const testStrings = getStrings(); + testStrings.third.description += '3'; + + const expectedStrings = deepClone(testStrings); + expectedStrings.first.meaning = expectedStrings.first.description; + expectedStrings.second.meaning = expectedStrings.second.description; + // Even though `third` has a unique description, still gets a `meaning`. + expectedStrings.third.meaning = expectedStrings.third.description; + + collect.resolveMessageCollisions(testStrings); + expect(testStrings).toEqual(expectedStrings); + }); + + it('only alters and returns fixed collisions', () => { + const testStrings = getStrings(); + // `first` will not collide. + testStrings.first.message += '1'; + testStrings.third.description += '3'; + + const expectedStrings = deepClone(testStrings); + expectedStrings.second.meaning = expectedStrings.second.description; + expectedStrings.third.meaning = expectedStrings.third.description; + + collect.resolveMessageCollisions(testStrings); + expect(testStrings).toEqual(expectedStrings); + }); + + describe('placeholders', () => { + it('throws if collisions have different placeholder tokens', () => { + const testStrings = getStrings(); + testStrings.first.message.replace('ICU_0', 'SOMETHING_ELSE'); + testStrings.first.placeholders = {SOMETHING_ELSE: testStrings.first.placeholders.ICU_0}; + + expect(() => collect.resolveMessageCollisions(testStrings)) + .toThrow(/collision: .* differ in `placeholders`.*key: first\nkey: second\nkey: third/s); + }); + + it('throws if collisions have different placeholder content', () => { + const testStrings = getStrings(); + testStrings.first.placeholders.ICU_0.content = 'something else'; + + expect(() => collect.resolveMessageCollisions(testStrings)) + .toThrow(/collision: .* differ in `placeholders`.*key: first\nkey: second\nkey: third/s); + }); + + it('throws if collisions have different placeholder example', () => { + const testStrings = getStrings(); + testStrings.first.placeholders.ICU_0.example = 'notaurl'; + + expect(() => collect.resolveMessageCollisions(testStrings)) + .toThrow(/collision: .* differ in `placeholders`.*key: first\nkey: second\nkey: third/s); + }); + + it('throws only for unfixed collisions with different placeholders', () => { + const testStrings = getStrings(); + // `second` will not collide. + testStrings.second.message += '2'; + // `first` and `third` collide and have different placeholders. + testStrings.first.placeholders.ICU_0.content = 'different'; + + expect(() => collect.resolveMessageCollisions(testStrings)) + .toThrow(/collision: .* differ in `placeholders`.*key: first\nkey: third/s); + }); + + it('does not throw if different placeholders are unique per description', () => { + const testStrings = getStrings(); + // Non-matching placeholder, would cause error. + testStrings.first.placeholders.ICU_0.content = 'something else'; + // But description is also different, so can be fixed with meaning. + testStrings.first.description += '1'; + + const expectedStrings = deepClone(testStrings); + expectedStrings.first.meaning = expectedStrings.first.description; + expectedStrings.second.meaning = expectedStrings.second.description; + expectedStrings.third.meaning = expectedStrings.third.description; + + collect.resolveMessageCollisions(testStrings); + expect(testStrings).toEqual(expectedStrings); + }); + }); +}); + describe('PseudoLocalizer', () => { it('adds cute hats to strings', () => { const strings = { hello: { message: 'world', + description: 'yah', }, }; const res = collect.createPsuedoLocaleStrings(strings); expect(res).toEqual({ hello: { message: 'ŵór̂ĺd̂', + description: 'yah', }, }); }); @@ -609,12 +764,14 @@ describe('PseudoLocalizer', () => { const strings = { hello: { message: '{world}', + description: 'nah', }, }; const res = collect.createPsuedoLocaleStrings(strings); expect(res).toEqual({ hello: { message: '{world}', + description: 'nah', }, }); }); @@ -623,12 +780,14 @@ describe('PseudoLocalizer', () => { const strings = { hello: { message: '{num_worlds, plural, =1{world} other{worlds}}', + description: 'yay', }, }; const res = collect.createPsuedoLocaleStrings(strings); expect(res).toEqual({ hello: { message: '{num_worlds, plural, =1{ŵór̂ĺd̂} other{ẃôŕl̂d́ŝ}}', + description: 'yay', }, }); }); @@ -637,6 +796,7 @@ describe('PseudoLocalizer', () => { const strings = { hello: { message: 'Hello $MARKDOWN_SNIPPET_0$', + description: 'yay', placeholders: { MARKDOWN_SNIPPET_0: { content: '`World`', @@ -649,6 +809,7 @@ describe('PseudoLocalizer', () => { expect(res).toEqual({ hello: { message: 'Ĥél̂ĺô $MARKDOWN_SNIPPET_0$', + description: 'yay', placeholders: { MARKDOWN_SNIPPET_0: { content: '`World`', diff --git a/shared/localization/locales/en-US.json b/shared/localization/locales/en-US.json index 7b4aa827d843..bd967f48d2e4 100644 --- a/shared/localization/locales/en-US.json +++ b/shared/localization/locales/en-US.json @@ -54,7 +54,7 @@ "message": "ARIA input fields have accessible names" }, "core/audits/accessibility/aria-meter-name.js | description": { - "message": "When an element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `meter` elements](https://dequeuniversity.com/rules/axe/4.4/aria-meter-name)." + "message": "When a meter element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `meter` elements](https://dequeuniversity.com/rules/axe/4.4/aria-meter-name)." }, "core/audits/accessibility/aria-meter-name.js | failureTitle": { "message": "ARIA `meter` elements do not have accessible names." @@ -117,7 +117,7 @@ "message": "ARIA toggle fields have accessible names" }, "core/audits/accessibility/aria-tooltip-name.js | description": { - "message": "When an element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `tooltip` elements](https://dequeuniversity.com/rules/axe/4.4/aria-tooltip-name)." + "message": "When a tooltip element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `tooltip` elements](https://dequeuniversity.com/rules/axe/4.4/aria-tooltip-name)." }, "core/audits/accessibility/aria-tooltip-name.js | failureTitle": { "message": "ARIA `tooltip` elements do not have accessible names." diff --git a/shared/localization/locales/en-XL.json b/shared/localization/locales/en-XL.json index dd00870276a7..6fae1535a40c 100644 --- a/shared/localization/locales/en-XL.json +++ b/shared/localization/locales/en-XL.json @@ -54,7 +54,7 @@ "message": "ÂŔÎÁ îńp̂út̂ f́îél̂d́ŝ h́âv́ê áĉćêśŝíb̂ĺê ńâḿêś" }, "core/audits/accessibility/aria-meter-name.js | description": { - "message": "Ŵh́êń âń êĺêḿêńt̂ d́ôéŝń't̂ h́âv́ê án̂ áĉćêśŝíb̂ĺê ńâḿê, śĉŕêén̂ ŕêád̂ér̂ś âńn̂óûńĉé ît́ ŵít̂h́ â ǵêńêŕîć n̂ám̂é, m̂ák̂ín̂ǵ ît́ ûńûśâb́l̂é f̂ór̂ úŝér̂ś ŵh́ô ŕêĺŷ ón̂ śĉŕêén̂ ŕêád̂ér̂ś. [L̂éâŕn̂ h́ôẃ t̂ó n̂ám̂é `meter` êĺêḿêńt̂ś](https://dequeuniversity.com/rules/axe/4.4/aria-meter-name)." + "message": "Ŵh́êń â ḿêt́êŕ êĺêḿêńt̂ d́ôéŝń't̂ h́âv́ê án̂ áĉćêśŝíb̂ĺê ńâḿê, śĉŕêén̂ ŕêád̂ér̂ś âńn̂óûńĉé ît́ ŵít̂h́ â ǵêńêŕîć n̂ám̂é, m̂ák̂ín̂ǵ ît́ ûńûśâb́l̂é f̂ór̂ úŝér̂ś ŵh́ô ŕêĺŷ ón̂ śĉŕêén̂ ŕêád̂ér̂ś. [L̂éâŕn̂ h́ôẃ t̂ó n̂ám̂é `meter` êĺêḿêńt̂ś](https://dequeuniversity.com/rules/axe/4.4/aria-meter-name)." }, "core/audits/accessibility/aria-meter-name.js | failureTitle": { "message": "ÂŔÎÁ `meter` êĺêḿêńt̂ś d̂ó n̂ót̂ h́âv́ê áĉćêśŝíb̂ĺê ńâḿêś." @@ -117,7 +117,7 @@ "message": "ÂŔÎÁ t̂óĝǵl̂é f̂íêĺd̂ś ĥáv̂é âćĉéŝśîb́l̂é n̂ám̂éŝ" }, "core/audits/accessibility/aria-tooltip-name.js | description": { - "message": "Ŵh́êń âń êĺêḿêńt̂ d́ôéŝń't̂ h́âv́ê án̂ áĉćêśŝíb̂ĺê ńâḿê, śĉŕêén̂ ŕêád̂ér̂ś âńn̂óûńĉé ît́ ŵít̂h́ â ǵêńêŕîć n̂ám̂é, m̂ák̂ín̂ǵ ît́ ûńûśâb́l̂é f̂ór̂ úŝér̂ś ŵh́ô ŕêĺŷ ón̂ śĉŕêén̂ ŕêád̂ér̂ś. [L̂éâŕn̂ h́ôẃ t̂ó n̂ám̂é `tooltip` êĺêḿêńt̂ś](https://dequeuniversity.com/rules/axe/4.4/aria-tooltip-name)." + "message": "Ŵh́êń â t́ôól̂t́îṕ êĺêḿêńt̂ d́ôéŝń't̂ h́âv́ê án̂ áĉćêśŝíb̂ĺê ńâḿê, śĉŕêén̂ ŕêád̂ér̂ś âńn̂óûńĉé ît́ ŵít̂h́ â ǵêńêŕîć n̂ám̂é, m̂ák̂ín̂ǵ ît́ ûńûśâb́l̂é f̂ór̂ úŝér̂ś ŵh́ô ŕêĺŷ ón̂ śĉŕêén̂ ŕêád̂ér̂ś. [L̂éâŕn̂ h́ôẃ t̂ó n̂ám̂é `tooltip` êĺêḿêńt̂ś](https://dequeuniversity.com/rules/axe/4.4/aria-tooltip-name)." }, "core/audits/accessibility/aria-tooltip-name.js | failureTitle": { "message": "ÂŔÎÁ `tooltip` êĺêḿêńt̂ś d̂ó n̂ót̂ h́âv́ê áĉćêśŝíb̂ĺê ńâḿêś." diff --git a/tsconfig.json b/tsconfig.json index 4b3470c08a70..1c7794a66559 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -102,7 +102,6 @@ "core/test/runner-test.js", "core/test/scoring-test.js", "core/test/scripts/i18n/bake-ctc-to-lhl-test.js", - "core/test/scripts/i18n/collect-strings-test.js", ], "files": [ // Opt-in to typechecking for some core tests and test-support files.