fix(i18n): add zh_TW snooze parser locale (#13822)

This commit is contained in:
salmonumbrella
2026-03-25 04:24:18 -07:00
committed by GitHub
parent 775b73d1f9
commit 6ff643b045
5 changed files with 127 additions and 55 deletions

View File

@@ -166,6 +166,8 @@ const TOD_TO_MERIDIEM = {
evening: 'pm', evening: 'pm',
night: 'pm', night: 'pm',
}; };
const CJK_CHAR_RE =
/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
// ─── Translation Cache ────────────────────────────────────────────────────── // ─── Translation Cache ──────────────────────────────────────────────────────
@@ -278,8 +280,13 @@ const escapeRegex = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const substituteLocalTokens = (text, pairs) => { const substituteLocalTokens = (text, pairs) => {
let r = text; let r = text;
pairs.forEach(([local, en]) => { pairs.forEach(([local, en]) => {
const re = new RegExp(`(?<=^|\\s)${escapeRegex(local)}(?=\\s|$)`, 'g'); if (CJK_CHAR_RE.test(local)) {
r = r.replace(re, en); const re = new RegExp(escapeRegex(local), 'g');
r = r.replace(re, ` ${en} `);
} else {
const re = new RegExp(`(?<=^|\\s)${escapeRegex(local)}(?=\\s|$)`, 'g');
r = r.replace(re, en);
}
}); });
return r; return r;
}; };

View File

@@ -82,6 +82,9 @@ const ORDINAL_RE = `(\\d{1,2}(?:st|nd|rd|th)?|${ORDINAL_WORDS})`;
const HALF_UNIT_RE = /^(?:in\s+)?half\s+(?:an?\s+)?(hour|day|week|month|year)$/; const HALF_UNIT_RE = /^(?:in\s+)?half\s+(?:an?\s+)?(hour|day|week|month|year)$/;
const RELATIVE_DURATION_RE = new RegExp(`^(?:in\\s+)?${NUM_RE}\\s+${UNIT_RE}$`); const RELATIVE_DURATION_RE = new RegExp(`^(?:in\\s+)?${NUM_RE}\\s+${UNIT_RE}$`);
const RELATIVE_DURATION_AFTER_RE = new RegExp(
`^(?:in\\s+)?${NUM_RE}\\s+${UNIT_RE}\\s+after$`
);
const DURATION_FROM_NOW_RE = new RegExp( const DURATION_FROM_NOW_RE = new RegExp(
`^${NUM_RE}\\s+${UNIT_RE}\\s+from\\s+now$` `^${NUM_RE}\\s+${UNIT_RE}\\s+from\\s+now$`
); );
@@ -89,6 +92,9 @@ const RELATIVE_DAY_ONLY_RE = new RegExp(`^(${RELATIVE_DAYS})$`);
const RELATIVE_DAY_TOD_RE = new RegExp( const RELATIVE_DAY_TOD_RE = new RegExp(
`^(${RELATIVE_DAYS})\\s+(?:at\\s+)?(${TIME_OF_DAY_NAMES})$` `^(${RELATIVE_DAYS})\\s+(?:at\\s+)?(${TIME_OF_DAY_NAMES})$`
); );
const RELATIVE_DAY_MERIDIEM_RE = new RegExp(
`^(${RELATIVE_DAYS})\\s+(?:at\\s+)?(am|pm)$`
);
const RELATIVE_DAY_TOD_TIME_RE = new RegExp( const RELATIVE_DAY_TOD_TIME_RE = new RegExp(
`^(${RELATIVE_DAYS})\\s+(?:at\\s+)?(${TIME_OF_DAY_NAMES})\\s+(\\d{1,2}(?::\\d{2})?)$` `^(${RELATIVE_DAYS})\\s+(?:at\\s+)?(${TIME_OF_DAY_NAMES})\\s+(\\d{1,2}(?::\\d{2})?)$`
); );
@@ -245,6 +251,7 @@ const matchDuration = (text, now) => {
return ( return (
parseDuration(text.match(DURATION_FROM_NOW_RE), now) || parseDuration(text.match(DURATION_FROM_NOW_RE), now) ||
parseDuration(text.match(RELATIVE_DURATION_AFTER_RE), now) ||
parseDuration(text.match(RELATIVE_DURATION_RE), now) parseDuration(text.match(RELATIVE_DURATION_RE), now)
); );
}; };
@@ -303,6 +310,13 @@ const matchRelativeDay = (text, now) => {
); );
} }
const dayMeridiemMatch = text.match(RELATIVE_DAY_MERIDIEM_RE);
if (dayMeridiemMatch) {
const [, dayKey, meridiem] = dayMeridiemMatch;
const hours = meridiem === 'am' ? 9 : 14;
return applyTimeWithRollover(RELATIVE_DAY_MAP[dayKey], hours, 0, now);
}
const dayAtTimeMatch = text.match(RELATIVE_DAY_AT_TIME_RE); const dayAtTimeMatch = text.match(RELATIVE_DAY_AT_TIME_RE);
if (dayAtTimeMatch) { if (dayAtTimeMatch) {
const [, dayKey, timeRaw] = dayAtTimeMatch; const [, dayKey, timeRaw] = dayAtTimeMatch;

View File

@@ -1626,6 +1626,24 @@ describe('generateDateSuggestions — localized input regressions', () => {
}, },
}; };
const zhTWSnoozeTranslations = {
UNITS: {
HOUR: '小時',
HOURS: '小時',
DAY: '天',
DAYS: '天',
},
HALF: '半',
RELATIVE: {
TOMORROW: '明天',
},
MERIDIEM: {
AM: '上午',
PM: '下午',
},
AFTER: '後',
};
describe('P1: short non-English tokens must NOT produce spurious half-duration suggestions', () => { describe('P1: short non-English tokens must NOT produce spurious half-duration suggestions', () => {
it('Arabic "غد" does not produce half-duration suggestions', () => { it('Arabic "غد" does not produce half-duration suggestions', () => {
const results = generateDateSuggestions('غد', now, { const results = generateDateSuggestions('غد', now, {
@@ -1721,6 +1739,37 @@ describe('generateDateSuggestions — localized input regressions', () => {
expect(results[0].date.getHours()).toBe(6); expect(results[0].date.getHours()).toBe(6);
}); });
}); });
describe('zh_TW compact CJK inputs', () => {
const options = {
translations: zhTWSnoozeTranslations,
locale: 'zh-TW',
};
it('parses "2小時後" (2 hours from now) without spaces', () => {
const results = generateDateSuggestions('2小時後', now, options);
expect(results.length).toBeGreaterThan(0);
expect(results[0].date.getDate()).toBe(16);
expect(results[0].date.getHours()).toBe(12);
expect(results[0].date.getMinutes()).toBe(0);
});
it('parses "半天" (half day) without spaces', () => {
const results = generateDateSuggestions('半天', now, options);
expect(results.length).toBeGreaterThan(0);
expect(results[0].date.getDate()).toBe(16);
expect(results[0].date.getHours()).toBe(22);
expect(results[0].date.getMinutes()).toBe(0);
});
it('parses "明天 上午" (tomorrow AM) into tomorrow 9am', () => {
const results = generateDateSuggestions('明天 上午', now, options);
expect(results.length).toBeGreaterThan(0);
expect(results[0].date.getDate()).toBe(17);
expect(results[0].date.getHours()).toBe(9);
expect(results[0].date.getMinutes()).toBe(0);
});
});
}); });
describe('no-space duration suggestions', () => { describe('no-space duration suggestions', () => {

View File

@@ -34,6 +34,7 @@ import setNewPassword from './setNewPassword.json';
import settings from './settings.json'; import settings from './settings.json';
import signup from './signup.json'; import signup from './signup.json';
import sla from './sla.json'; import sla from './sla.json';
import snooze from './snooze.json';
import teamsSettings from './teamsSettings.json'; import teamsSettings from './teamsSettings.json';
import whatsappTemplates from './whatsappTemplates.json'; import whatsappTemplates from './whatsappTemplates.json';
@@ -74,6 +75,7 @@ export default {
...settings, ...settings,
...signup, ...signup,
...sla, ...sla,
...snooze,
...teamsSettings, ...teamsSettings,
...whatsappTemplates, ...whatsappTemplates,
}; };

View File

@@ -1,72 +1,72 @@
{ {
"SNOOZE_PARSER": { "SNOOZE_PARSER": {
"UNITS": { "UNITS": {
"MINUTE": "minute", "MINUTE": "分鐘",
"MINUTES": "minutes", "MINUTES": "分鐘",
"HOUR": "hour", "HOUR": "小時",
"HOURS": "小時", "HOURS": "小時",
"DAY": "day", "DAY": "",
"DAYS": "days", "DAYS": "",
"WEEK": "week", "WEEK": "",
"WEEKS": "weeks", "WEEKS": "",
"MONTH": "month", "MONTH": "",
"MONTHS": "months", "MONTHS": "",
"YEAR": "month", "YEAR": "",
"YEARS": "years" "YEARS": ""
}, },
"HALF": "half", "HALF": "",
"NEXT": "next", "NEXT": "下一個",
"THIS": "this", "THIS": "這個",
"AT": "at", "AT": "",
"IN": "in", "IN": "",
"FROM_NOW": "from now", "FROM_NOW": "之後",
"NEXT_YEAR": "next year", "NEXT_YEAR": "明年",
"MERIDIEM": { "MERIDIEM": {
"AM": "am", "AM": "上午",
"PM": "pm" "PM": "下午"
}, },
"RELATIVE": { "RELATIVE": {
"TOMORROW": "明天", "TOMORROW": "明天",
"DAY_AFTER_TOMORROW": "day after tomorrow", "DAY_AFTER_TOMORROW": "後天",
"NEXT_WEEK": "下週", "NEXT_WEEK": "下週",
"NEXT_MONTH": "next month", "NEXT_MONTH": "下個月",
"THIS_WEEKEND": "this weekend", "THIS_WEEKEND": "這個週末",
"NEXT_WEEKEND": "next weekend" "NEXT_WEEKEND": "下個週末"
}, },
"TIME_OF_DAY": { "TIME_OF_DAY": {
"MORNING": "morning", "MORNING": "早上",
"AFTERNOON": "afternoon", "AFTERNOON": "下午",
"EVENING": "evening", "EVENING": "晚上",
"NIGHT": "night", "NIGHT": "夜晚",
"NOON": "noon", "NOON": "中午",
"MIDNIGHT": "midnight" "MIDNIGHT": "午夜"
}, },
"WORD_NUMBERS": { "WORD_NUMBERS": {
"ONE": "one", "ONE": "",
"TWO": "two", "TWO": "",
"THREE": "three", "THREE": "",
"FOUR": "four", "FOUR": "",
"FIVE": "five", "FIVE": "",
"SIX": "six", "SIX": "",
"SEVEN": "seven", "SEVEN": "",
"EIGHT": "eight", "EIGHT": "",
"NINE": "nine", "NINE": "",
"TEN": "ten", "TEN": "",
"TWELVE": "twelve", "TWELVE": "十二",
"FIFTEEN": "fifteen", "FIFTEEN": "十五",
"TWENTY": "twenty", "TWENTY": "二十",
"THIRTY": "thirty" "THIRTY": "三十"
}, },
"ORDINALS": { "ORDINALS": {
"FIRST": "first", "FIRST": "第一",
"SECOND": "second", "SECOND": "第二",
"THIRD": "third", "THIRD": "第三",
"FOURTH": "fourth", "FOURTH": "第四",
"FIFTH": "fifth" "FIFTH": "第五"
}, },
"OF": "of", "OF": "",
"AFTER": "after", "AFTER": "",
"WEEK": "week", "WEEK": "",
"DAY": "day" "DAY": ""
} }
} }