Fix pluralization issues in translations

This commit is contained in:
Jamie Kyle 2023-04-04 12:05:50 -07:00 committed by GitHub
parent 808c0beae7
commit 9e28f4dbe0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 108 additions and 60 deletions

View file

@ -100,7 +100,7 @@
"description": "(deleted 03/29/2023) Shown below the group name when selecting a group to invite a contact to" "description": "(deleted 03/29/2023) Shown below the group name when selecting a group to invite a contact to"
}, },
"icu:GroupListItem__message-default": { "icu:GroupListItem__message-default": {
"messageformat": "{count, plural, one {1 member} other {# members}}", "messageformat": "{count, plural, one {# member} other {# members}}",
"description": "Shown below the group name when selecting a group to invite a contact to" "description": "Shown below the group name when selecting a group to invite a contact to"
}, },
"GroupListItem__message-already-member": { "GroupListItem__message-already-member": {
@ -768,7 +768,7 @@
"description": "(deleted 03/25/2023) Message shown on the loading screen when we're catching up on the backlog of messages" "description": "(deleted 03/25/2023) Message shown on the loading screen when we're catching up on the backlog of messages"
}, },
"icu:loadingMessages--other": { "icu:loadingMessages--other": {
"messageformat": "Loading messages from {daysAgo, plural, one {1 day} other {# days}} ago...", "messageformat": "Loading messages from {daysAgo, plural, one {# day} other {# days}} ago...",
"description": "Message shown on the loading screen when we're catching up on the backlog of messages from day before yesterday and earlier" "description": "Message shown on the loading screen when we're catching up on the backlog of messages from day before yesterday and earlier"
}, },
"icu:loadingMessages--yesterday": { "icu:loadingMessages--yesterday": {
@ -832,7 +832,7 @@
"description": "(deleted 03/29/2023) Text for unread message separator, with count" "description": "(deleted 03/29/2023) Text for unread message separator, with count"
}, },
"icu:unreadMessages": { "icu:unreadMessages": {
"messageformat": "{count, plural, one {1 Unread Message} other {# Unread Messages}}", "messageformat": "{count, plural, one {# Unread Message} other {# Unread Messages}}",
"description": "Text for unread message separator, with count" "description": "Text for unread message separator, with count"
}, },
"messageHistoryUnsynced": { "messageHistoryUnsynced": {
@ -916,7 +916,7 @@
"description": "Shown to enter 'review' mode if more than five contacts have changed safety numbers" "description": "Shown to enter 'review' mode if more than five contacts have changed safety numbers"
}, },
"icu:safetyNumberChangeDialog__many-contacts": { "icu:safetyNumberChangeDialog__many-contacts": {
"messageformat": "You have {count, plural, one {1 connection} other {# connections}} who may have reinstalled Signal or changed devices. You can optionally review their safety numbers before sending.", "messageformat": "You have {count, plural, one {# connection} other {# connections}} who may have reinstalled Signal or changed devices. You can optionally review their safety numbers before sending.",
"description": "Shown during an attempted send when more than five contacts have changed their safety numbers" "description": "Shown during an attempted send when more than five contacts have changed their safety numbers"
}, },
"safetyNumberChangeDialog__post-review": { "safetyNumberChangeDialog__post-review": {
@ -928,7 +928,7 @@
"description": "Shown after reviewing large number of contacts" "description": "Shown after reviewing large number of contacts"
}, },
"icu:safetyNumberChangeDialog__confirm-remove-all": { "icu:safetyNumberChangeDialog__confirm-remove-all": {
"messageformat": "Are you sure you want to remove {count, plural, one {1 recipient} other {# recipients}} from story {story}?", "messageformat": "Are you sure you want to remove {count, plural, one {# recipient} other {# recipients}} from story {story}?",
"description": "Shown if user selects 'remove all' option to remove all potentially untrusted contacts from a given story" "description": "Shown if user selects 'remove all' option to remove all potentially untrusted contacts from a given story"
}, },
"safetyNumberChangeDialog__remove-all": { "safetyNumberChangeDialog__remove-all": {
@ -4384,7 +4384,7 @@
"description": "(deleted 03/29/2023) Aria label for the conversation list item" "description": "(deleted 03/29/2023) Aria label for the conversation list item"
}, },
"icu:ConversationList__aria-label": { "icu:ConversationList__aria-label": {
"messageformat": "Conversation with {title}, {unreadCount, plural, one {1 new message} other {# new messages}}, last message: {lastMessage}.", "messageformat": "Conversation with {title}, {unreadCount, plural, one {# new message} other {# new messages}}, last message: {lastMessage}.",
"description": "Aria label for the conversation list item" "description": "Aria label for the conversation list item"
}, },
"ConversationList__last-message-undefined": { "ConversationList__last-message-undefined": {
@ -4568,15 +4568,15 @@
"description": "Shown to label a donation badge you've replied to." "description": "Shown to label a donation badge you've replied to."
}, },
"icu:message--donation--remaining--days": { "icu:message--donation--remaining--days": {
"messageformat": "{days, plural, one {1 day} other {# days}} remaining", "messageformat": "{days, plural, one {# day} other {# days}} remaining",
"description": "Describes how long remains for the donation badge you've redeemed on another device (only rendered for days > 1)." "description": "Describes how long remains for the donation badge you've redeemed on another device (only rendered for days > 1)."
}, },
"icu:message--donation--remaining--hours": { "icu:message--donation--remaining--hours": {
"messageformat": "{hours, plural, one {1 hour} other {# hours}} remaining", "messageformat": "{hours, plural, one {# hour} other {# hours}} remaining",
"description": "Describes how long remains for the donation badge you've redeemed on another device (only rendered for hours > 1)" "description": "Describes how long remains for the donation badge you've redeemed on another device (only rendered for hours > 1)"
}, },
"icu:message--donation--remaining--minutes": { "icu:message--donation--remaining--minutes": {
"messageformat": "{minutes, plural, one {1 minute} other {# minutes}} remaining", "messageformat": "{minutes, plural, one {# minute} other {# minutes}} remaining",
"description": "Describes how long remains for the donation badge you've redeemed on another device." "description": "Describes how long remains for the donation badge you've redeemed on another device."
}, },
"icu:message--donation--expired": { "icu:message--donation--expired": {
@ -5716,7 +5716,7 @@
"description": "(deleted 03/15/2023) Text which is shared to social media platforms for sticker packs" "description": "(deleted 03/15/2023) Text which is shared to social media platforms for sticker packs"
}, },
"icu:StickerCreator--Toasts--imagesAdded": { "icu:StickerCreator--Toasts--imagesAdded": {
"messageformat": "{count, plural, one {1 image} other {# images}} added", "messageformat": "{count, plural, one {# image} other {# images}} added",
"description": "(deleted 03/15/2023) Text for the toast when images are added to the sticker creator" "description": "(deleted 03/15/2023) Text for the toast when images are added to the sticker creator"
}, },
"StickerCreator--Toasts--animated": { "StickerCreator--Toasts--animated": {
@ -6028,7 +6028,7 @@
"description": "(deleted 03/29/2023) Shown at the end of profile sharing messages as a link." "description": "(deleted 03/29/2023) Shown at the end of profile sharing messages as a link."
}, },
"icu:ConversationHero--members": { "icu:ConversationHero--members": {
"messageformat": "{count, plural, one {1 member} other {# members}}", "messageformat": "{count, plural, one {# member} other {# members}}",
"description": "Specifies the number of members in a group conversation" "description": "Specifies the number of members in a group conversation"
}, },
"member-of-1-group": { "member-of-1-group": {
@ -6268,7 +6268,7 @@
"description": "(deleted 03/29/2023) Shown in the incoming call bar when someone is ringing you for a group call" "description": "(deleted 03/29/2023) Shown in the incoming call bar when someone is ringing you for a group call"
}, },
"icu:incomingGroupCall__ringing-many": { "icu:incomingGroupCall__ringing-many": {
"messageformat": "{ringer} is calling you, {first}, {second}, and {remaining, plural, one {1 other} other {# others}}", "messageformat": "{ringer} is calling you, {first}, {second}, and {remaining, plural, one {# other} other {# others}}",
"description": "Shown in the incoming call bar when someone is ringing you for a group call" "description": "Shown in the incoming call bar when someone is ringing you for a group call"
}, },
"outgoingCallRinging": { "outgoingCallRinging": {
@ -6816,7 +6816,7 @@
"description": "(deleted 03/29/2023) A holder for two pieces of information - the type of conversation, and the member count" "description": "(deleted 03/29/2023) A holder for two pieces of information - the type of conversation, and the member count"
}, },
"icu:GroupV2--join--group-metadata--full": { "icu:GroupV2--join--group-metadata--full": {
"messageformat": "Group · {memberCount, plural, one {1 member} other {# members}}", "messageformat": "Group · {memberCount, plural, one {# member} other {# members}}",
"description": "A holder for two pieces of information - the type of conversation, and the member count" "description": "A holder for two pieces of information - the type of conversation, and the member count"
}, },
"GroupV2--join--requested": { "GroupV2--join--requested": {
@ -8544,7 +8544,7 @@
"description": "(deleted 03/29/2023) This is the number of members in a group" "description": "(deleted 03/29/2023) This is the number of members in a group"
}, },
"icu:ConversationDetailsHeader--members": { "icu:ConversationDetailsHeader--members": {
"messageformat": "{number, plural, one {1 member} other {# members}}", "messageformat": "{number, plural, one {# member} other {# members}}",
"description": "This is the number of members in a group" "description": "This is the number of members in a group"
}, },
"ConversationDetailsMediaList--shared-media": { "ConversationDetailsMediaList--shared-media": {
@ -8568,7 +8568,7 @@
"description": "(deleted 03/29/2023) The title of the membership list panel" "description": "(deleted 03/29/2023) The title of the membership list panel"
}, },
"icu:ConversationDetailsMembershipList--title": { "icu:ConversationDetailsMembershipList--title": {
"messageformat": "{number, plural, one {1 member} other {# members}}", "messageformat": "{number, plural, one {# member} other {# members}}",
"description": "The title of the membership list panel" "description": "The title of the membership list panel"
}, },
"ConversationDetailsMembershipList--add-members": { "ConversationDetailsMembershipList--add-members": {
@ -8588,7 +8588,7 @@
"description": "This is a button on the conversation details to show all members" "description": "This is a button on the conversation details to show all members"
}, },
"icu:ConversationDetailsGroups--title": { "icu:ConversationDetailsGroups--title": {
"messageformat": "{count, plural, one {1 group} other {# groups}} in common", "messageformat": "{count, plural, one {# group} other {# groups}} in common",
"description": "Title of the groups-in-common panel, in the contact details" "description": "Title of the groups-in-common panel, in the contact details"
}, },
"icu:ConversationDetailsGroups--title--with-zero-groups-in-common": { "icu:ConversationDetailsGroups--title--with-zero-groups-in-common": {
@ -8796,7 +8796,7 @@
"description": "(deleted 03/29/2023) This is the modal content when confirming revoking multiple invites" "description": "(deleted 03/29/2023) This is the modal content when confirming revoking multiple invites"
}, },
"icu:PendingInvites--revoke-from": { "icu:PendingInvites--revoke-from": {
"messageformat": "Revoke {number, plural, one {1 invite} other {# invites}} sent by \"{name}\"?", "messageformat": "Revoke {number, plural, one {# invite} other {# invites}} sent by \"{name}\"?",
"description": "This is the modal content when confirming revoking multiple invites" "description": "This is the modal content when confirming revoking multiple invites"
}, },
"PendingInvites--revoke": { "PendingInvites--revoke": {
@ -9260,11 +9260,11 @@
"description": "(deleted 03/29/2023) Shown in the timeline warning when you multiple group members have the same name" "description": "(deleted 03/29/2023) Shown in the timeline warning when you multiple group members have the same name"
}, },
"icu:ContactSpoofing__same-name-in-group": { "icu:ContactSpoofing__same-name-in-group": {
"messageformat": "{count, plural, one {1 group member} other {# group members}} have the same name. {link}", "messageformat": "{count, plural, one {# group member has} other {# group members have}} the same name. {link}",
"description": "Shown in the timeline warning when you multiple group members have the same name" "description": "(deleted 04/04/2023) Shown in the timeline warning when you multiple group members have the same name"
}, },
"icu:ContactSpoofing__same-name-in-group--link": { "icu:ContactSpoofing__same-name-in-group--link": {
"messageformat": "{count, plural, one {1 group member} other {# group members}} have the same name. <reviewRequestLink>Review request</reviewRequestLink>", "messageformat": "{count, plural, one {# group member has} other {# group members have}} the same name. <reviewRequestLink>Review request</reviewRequestLink>",
"description": "Shown in the timeline warning when you multiple group members have the same name" "description": "Shown in the timeline warning when you multiple group members have the same name"
}, },
"ContactSpoofing__same-name__link": { "ContactSpoofing__same-name__link": {
@ -9328,7 +9328,7 @@
"description": "(deleted 03/29/2023) Description for the group contact spoofing review dialog" "description": "(deleted 03/29/2023) Description for the group contact spoofing review dialog"
}, },
"icu:ContactSpoofingReviewDialog__group__description": { "icu:ContactSpoofingReviewDialog__group__description": {
"messageformat": "{count, plural, one {1 group member} other {# group members}} have similar names. Review the members below or choose to take action.", "messageformat": "{count, plural, one {# group member} other {# group members}} have similar names. Review the members below or choose to take action.",
"description": "Description for the group contact spoofing review dialog" "description": "Description for the group contact spoofing review dialog"
}, },
"ContactSpoofingReviewDialog__group__members-header": { "ContactSpoofingReviewDialog__group__members-header": {
@ -9456,7 +9456,7 @@
"description": "(deleted 03/29/2023) Confirm message for deleting custom color" "description": "(deleted 03/29/2023) Confirm message for deleting custom color"
}, },
"icu:ChatColorPicker__delete--message": { "icu:ChatColorPicker__delete--message": {
"messageformat": "This custom color is used in {num, plural, one {1 chat} other {# chats}}. Do you want to delete it for all chats?", "messageformat": "This custom color is used in {num, plural, one {# chat} other {# chats}}. Do you want to delete it for all chats?",
"description": "Confirm message for deleting custom color" "description": "Confirm message for deleting custom color"
}, },
"ChatColorPicker__global-chat-color": { "ChatColorPicker__global-chat-color": {
@ -10360,7 +10360,7 @@
"description": "(deleted 03/29/2023) Number of contacts blocked plural" "description": "(deleted 03/29/2023) Number of contacts blocked plural"
}, },
"icu:Preferences--blocked-count": { "icu:Preferences--blocked-count": {
"messageformat": "{num, plural, one {1 contact} other {# contacts}}", "messageformat": "{num, plural, one {# contact} other {# contacts}}",
"description": "Number of contacts blocked plural" "description": "Number of contacts blocked plural"
}, },
"Preferences__privacy--description": { "Preferences__privacy--description": {
@ -10900,11 +10900,11 @@
"description": "(deleted 03/29/2023) Number of views your story has" "description": "(deleted 03/29/2023) Number of views your story has"
}, },
"icu:MyStories__views": { "icu:MyStories__views": {
"messageformat": "{views, plural, one {1 view} other {# views}}", "messageformat": "{views, plural, one {# view} other {# views}}",
"description": "Number of views your story has" "description": "Number of views your story has"
}, },
"icu:MyStories__views--strong": { "icu:MyStories__views--strong": {
"messageformat": "{views, plural, one {<strong>1</strong> view} other {<strong>#</strong> views}}", "messageformat": "{views, plural, one {<strong>#</strong> view} other {<strong>#</strong> views}}",
"description": "Number of views your story has" "description": "Number of views your story has"
}, },
"icu:MyStories__views-off": { "icu:MyStories__views-off": {
@ -11204,7 +11204,7 @@
"description": "Story settings modal group story selection subtitle" "description": "Story settings modal group story selection subtitle"
}, },
"icu:StoriesSettings__viewers": { "icu:StoriesSettings__viewers": {
"messageformat": "{count, plural, one {1 viewer} other {# viewers}}", "messageformat": "{count, plural, one {# viewer} other {# viewers}}",
"description": "The number of viewers for a story distribution list" "description": "The number of viewers for a story distribution list"
}, },
"StoriesSettings__who-can-see": { "StoriesSettings__who-can-see": {
@ -11484,7 +11484,7 @@
"description": "Subtitle for My Story when the user has chosen an exclude list" "description": "Subtitle for My Story when the user has chosen an exclude list"
}, },
"icu:SendStoryModal__excluded": { "icu:SendStoryModal__excluded": {
"messageformat": "{count, plural, one {1 excluded} other {# excluded}}", "messageformat": "{count, plural, one {# excluded} other {# excluded}}",
"description": "Label for excluded count for My Story as an exclude list" "description": "Label for excluded count for My Story as an exclude list"
}, },
"SendStoryModal__new": { "SendStoryModal__new": {
@ -11588,19 +11588,19 @@
"description": "Alert body for groups that non-admins cannot send stories to" "description": "Alert body for groups that non-admins cannot send stories to"
}, },
"icu:SendStoryModal__my-stories-description-all": { "icu:SendStoryModal__my-stories-description-all": {
"messageformat": "All Signal connections · {viewersCount, plural, one {1 viewer} other {# viewers}}", "messageformat": "All Signal connections · {viewersCount, plural, one {# viewer} other {# viewers}}",
"description": "Shown as a subtitle under My Stories option in the send-story-to dialog when not exluding anyone" "description": "Shown as a subtitle under My Stories option in the send-story-to dialog when not exluding anyone"
}, },
"icu:SendStoryModal__my-stories-description-excluding": { "icu:SendStoryModal__my-stories-description-excluding": {
"messageformat": "All Signal connections · {excludedCount, plural, one {1 excluded} other {# excluded}}", "messageformat": "All Signal connections · {excludedCount, plural, one {# excluded} other {# excluded}}",
"description": "Shown as a subtitle under My Stories option in the send-story-to dialog when excluding some" "description": "Shown as a subtitle under My Stories option in the send-story-to dialog when excluding some"
}, },
"icu:SendStoryModal__private-story-description": { "icu:SendStoryModal__private-story-description": {
"messageformat": "Private story · {viewersCount, plural, one {1 viewer} other {# viewers}}", "messageformat": "Private story · {viewersCount, plural, one {# viewer} other {# viewers}}",
"description": "Shown as a subtitle of each private story in the send-story-to dialog" "description": "Shown as a subtitle of each private story in the send-story-to dialog"
}, },
"icu:SendStoryModal__group-story-description": { "icu:SendStoryModal__group-story-description": {
"messageformat": "Group story · {membersCount, plural, one {1 member} other {# members}}", "messageformat": "Group story · {membersCount, plural, one {# member} other {# members}}",
"description": "Shown as a subtitle of each group story in the send-story-to dialog" "description": "Shown as a subtitle of each group story in the send-story-to dialog"
}, },
"Stories__settings-toggle--title": { "Stories__settings-toggle--title": {
@ -11908,7 +11908,7 @@
"description": "Error string for when a video post to story fails" "description": "Error string for when a video post to story fails"
}, },
"icu:StoryCreator__error--video-too-long": { "icu:StoryCreator__error--video-too-long": {
"messageformat": "Cannot post video to story because it is longer than {maxDurationInSec, plural, one {1 second} other {# seconds}}.", "messageformat": "Cannot post video to story because it is longer than {maxDurationInSec, plural, one {# second} other {# seconds}}.",
"description": "Error string for when a video post to story fails because video's duration is too long" "description": "Error string for when a video post to story fails because video's duration is too long"
}, },
"icu:StoryCreator__error--video-too-big": { "icu:StoryCreator__error--video-too-big": {

View file

@ -19,6 +19,7 @@ import noLegacyVariables from './rules/noLegacyVariables';
import noNestedChoice from './rules/noNestedChoice'; import noNestedChoice from './rules/noNestedChoice';
import noOffset from './rules/noOffset'; import noOffset from './rules/noOffset';
import noOrdinal from './rules/noOrdinal'; import noOrdinal from './rules/noOrdinal';
import pluralPound from './rules/pluralPound';
const RULES = [ const RULES = [
icuPrefix, icuPrefix,
@ -27,6 +28,7 @@ const RULES = [
noOffset, noOffset,
noOrdinal, noOrdinal,
onePlural, onePlural,
pluralPound,
]; ];
type Test = { type Test = {
@ -65,6 +67,7 @@ type Report = {
id: string; id: string;
message: string; message: string;
location: Location | void; location: Location | void;
locationOffset: number;
}; };
function lintMessage( function lintMessage(
@ -76,8 +79,8 @@ function lintMessage(
for (const rule of rules) { for (const rule of rules) {
rule.run(elements, { rule.run(elements, {
messageId, messageId,
report(message, location) { report(message, location, locationOffset = 0) {
reports.push({ id: rule.id, message, location }); reports.push({ id: rule.id, message, location, locationOffset });
}, },
}); });
} }
@ -151,11 +154,13 @@ async function lintMessages() {
const line = const line =
icuMesssageLiteral.loc.start.line + (report.location.start.line - 1); icuMesssageLiteral.loc.start.line + (report.location.start.line - 1);
const column = const column =
icuMesssageLiteral.loc.start.column + report.location.start.column; icuMesssageLiteral.loc.start.column +
report.location.start.column +
report.locationOffset;
loc = `:${line}:${column}`; loc = `:${line}:${column}`;
} else if (icuMesssageLiteral.loc != null) { } else if (icuMesssageLiteral.loc != null) {
const { line, column } = icuMesssageLiteral.loc.start; const { line, column } = icuMesssageLiteral.loc.start;
loc = `:${line}:${column}`; loc = `:${line}:${column + report.locationOffset}`;
} }
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View file

@ -0,0 +1,37 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { PluralElement } from '@formatjs/icu-messageformat-parser';
import { rule } from '../utils/rule';
export default rule('pluralPound', context => {
const stack: Array<PluralElement> = [];
return {
enterPlural(element) {
stack.push(element);
},
exitPlural() {
stack.pop();
},
enterLiteral(element, parent) {
// Note: Without the stack this could be turned into a rule to check for
// explicit numbers anywhere in the message.
if (parent == null) {
return;
}
if (parent !== stack.at(-1)) {
return;
}
// Adapted from https://github.com/TalhaAwan/get-numbers
// Checks for explicit whitespace before and after the number.
const index = element.value.search(/(^| )(-\d+|\d+)(,\d+)*(\.\d+)*($| )/);
if (index > -1) {
context.report(
'Use # instead of an explicit number',
element.location,
index
);
}
},
};
});

View file

@ -11,7 +11,11 @@ export { Location };
export type Context = { export type Context = {
messageId: string; messageId: string;
report(message: string, location: Location | void): void; report(
message: string,
location: Location | void,
locationOffset?: number
): void;
}; };
export type RuleFactory = { export type RuleFactory = {
@ -27,7 +31,7 @@ export function rule(id: string, ruleFactory: RuleFactory): Rule {
return { return {
id, id,
run(elements, context) { run(elements, context) {
traverse(elements, ruleFactory(context)); traverse(null, elements, ruleFactory(context));
}, },
}; };
} }

View file

@ -16,7 +16,8 @@ import type {
import { TYPE } from '@formatjs/icu-messageformat-parser'; import { TYPE } from '@formatjs/icu-messageformat-parser';
export type VisitorMethod<T extends MessageFormatElement> = ( export type VisitorMethod<T extends MessageFormatElement> = (
element: T element: T,
parent: MessageFormatElement | null
) => void; ) => void;
export type Visitor = { export type Visitor = {
@ -41,44 +42,45 @@ export type Visitor = {
}; };
export function traverse( export function traverse(
parent: MessageFormatElement | null,
elements: Array<MessageFormatElement>, elements: Array<MessageFormatElement>,
visitor: Visitor visitor: Visitor
): void { ): void {
for (const element of elements) { for (const element of elements) {
if (element.type === TYPE.literal) { if (element.type === TYPE.literal) {
visitor.enterLiteral?.(element); visitor.enterLiteral?.(element, parent);
visitor.exitLiteral?.(element); visitor.exitLiteral?.(element, parent);
} else if (element.type === TYPE.argument) { } else if (element.type === TYPE.argument) {
visitor.enterArgument?.(element); visitor.enterArgument?.(element, parent);
visitor.exitArgument?.(element); visitor.exitArgument?.(element, parent);
} else if (element.type === TYPE.number) { } else if (element.type === TYPE.number) {
visitor.enterNumber?.(element); visitor.enterNumber?.(element, parent);
visitor.exitNumber?.(element); visitor.exitNumber?.(element, parent);
} else if (element.type === TYPE.date) { } else if (element.type === TYPE.date) {
visitor.enterDate?.(element); visitor.enterDate?.(element, parent);
visitor.exitDate?.(element); visitor.exitDate?.(element, parent);
} else if (element.type === TYPE.time) { } else if (element.type === TYPE.time) {
visitor.enterTime?.(element); visitor.enterTime?.(element, parent);
visitor.exitTime?.(element); visitor.exitTime?.(element, parent);
} else if (element.type === TYPE.select) { } else if (element.type === TYPE.select) {
visitor.enterSelect?.(element); visitor.enterSelect?.(element, parent);
for (const node of Object.values(element.options)) { for (const node of Object.values(element.options)) {
traverse(node.value, visitor); traverse(element, node.value, visitor);
} }
visitor.exitSelect?.(element); visitor.exitSelect?.(element, parent);
} else if (element.type === TYPE.plural) { } else if (element.type === TYPE.plural) {
visitor.enterPlural?.(element); visitor.enterPlural?.(element, parent);
for (const node of Object.values(element.options)) { for (const node of Object.values(element.options)) {
traverse(node.value, visitor); traverse(element, node.value, visitor);
} }
visitor.exitPlural?.(element); visitor.exitPlural?.(element, parent);
} else if (element.type === TYPE.pound) { } else if (element.type === TYPE.pound) {
visitor.enterPound?.(element); visitor.enterPound?.(element, parent);
visitor.exitPound?.(element); visitor.exitPound?.(element, parent);
} else if (element.type === TYPE.tag) { } else if (element.type === TYPE.tag) {
visitor.enterTag?.(element); visitor.enterTag?.(element, parent);
traverse(element.children, visitor); traverse(element, element.children, visitor);
visitor.exitTag?.(element); visitor.exitTag?.(element, parent);
} else { } else {
unreachable(element); unreachable(element);
} }