audit-messages-no-emoji.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. const fs = require("node:fs");
  2. const path = require("node:path");
  3. const emojiAudit = require("./audit-messages-emoji.js");
  4. const EMOJI_RE =
  5. /(\p{Extended_Pictographic}|\p{Regional_Indicator}{2}|[0-9#*]\uFE0F?\u20E3)/gu;
  6. function loadJson(p) {
  7. return JSON.parse(fs.readFileSync(p, "utf8"));
  8. }
  9. function normalizeLocales(messagesRoot, locales) {
  10. if (typeof locales === "string") return normalizeLocales(messagesRoot, [locales]);
  11. if (Array.isArray(locales) && locales.length > 0) {
  12. return locales
  13. .flatMap((s) => String(s).split(","))
  14. .map((s) => s.trim())
  15. .filter(Boolean);
  16. }
  17. const dirs = fs.readdirSync(messagesRoot, { withFileTypes: true });
  18. return dirs
  19. .filter((d) => d.isDirectory() && !d.name.startsWith("."))
  20. .map((d) => d.name)
  21. .sort((a, b) => a.localeCompare(b));
  22. }
  23. function toCodepoint(cp) {
  24. const hex = cp.toString(16).toUpperCase();
  25. return `U+${hex.padStart(4, "0")}`;
  26. }
  27. function listEmojiCodepoints(value) {
  28. EMOJI_RE.lastIndex = 0;
  29. const out = [];
  30. for (const m of value.matchAll(EMOJI_RE)) {
  31. const seq = Array.from(m[0])
  32. .map((ch) => ch.codePointAt(0))
  33. .filter((cp) => typeof cp === "number")
  34. .map((cp) => toCodepoint(cp))
  35. .join("+");
  36. if (seq) out.push(seq);
  37. }
  38. return out;
  39. }
  40. function findMessagesEmojiMatches({ messagesDir, locales }) {
  41. const root = messagesDir || path.join(process.cwd(), "messages");
  42. const targets = normalizeLocales(root, locales);
  43. const rows = [];
  44. for (const locale of targets) {
  45. const localeDir = path.join(root, locale);
  46. if (!fs.existsSync(localeDir) || !fs.statSync(localeDir).isDirectory()) continue;
  47. const files = emojiAudit.listJsonFiles(localeDir);
  48. for (const file of files) {
  49. const relFileNative = path.relative(localeDir, file);
  50. const relFilePosix = relFileNative.replaceAll(path.sep, "/");
  51. const keyPrefix = emojiAudit.fileToKeyPrefix(relFileNative);
  52. const obj = loadJson(file);
  53. for (const leaf of emojiAudit.flattenLeafStrings(obj)) {
  54. if (typeof leaf.value !== "string") continue;
  55. const emojiCount = emojiAudit.countEmojiCodepoints(leaf.value);
  56. if (emojiCount === 0) continue;
  57. const fullKey = keyPrefix
  58. ? leaf.key
  59. ? `${keyPrefix}.${leaf.key}`
  60. : keyPrefix
  61. : leaf.key;
  62. const codepoints = Array.from(new Set(listEmojiCodepoints(leaf.value))).sort((a, b) =>
  63. a.localeCompare(b)
  64. );
  65. rows.push({
  66. file: path.posix.join("messages", locale, relFilePosix),
  67. key: fullKey,
  68. emojiCount,
  69. codepoints,
  70. });
  71. }
  72. }
  73. }
  74. return rows.sort((a, b) => a.file.localeCompare(b.file) || a.key.localeCompare(b.key));
  75. }
  76. function run(argv) {
  77. const fail = argv.includes("--fail");
  78. const messagesDirArg = argv.find((a) => a.startsWith("--messagesDir="));
  79. const messagesDir = messagesDirArg ? messagesDirArg.split("=", 2)[1] : undefined;
  80. const localesArg = argv.find((a) => a.startsWith("--locales="));
  81. const locales = localesArg ? localesArg.split("=", 2)[1] : undefined;
  82. const formatArg = argv.find((a) => a.startsWith("--format="));
  83. const format = formatArg ? formatArg.split("=", 2)[1] : "text";
  84. const rows = findMessagesEmojiMatches({ messagesDir, locales });
  85. const total = rows.length;
  86. if (total === 0) {
  87. return { exitCode: 0, lines: ["OK: no emoji found in messages JSON."] };
  88. }
  89. const exitCode = fail ? 1 : 0;
  90. if (format === "json") {
  91. return { exitCode, lines: [JSON.stringify(rows, null, 2)] };
  92. }
  93. if (format === "tsv") {
  94. const lines = ["file\tkey\temojiCount\tcodepoints"];
  95. for (const r of rows) {
  96. lines.push(`${r.file}\t${r.key}\t${r.emojiCount}\t${r.codepoints.join(",")}`);
  97. }
  98. return { exitCode, lines };
  99. }
  100. const lines = [`Found ${total} messages strings containing emoji:`];
  101. for (const r of rows) lines.push(`${r.file}\t${r.key}\t${r.codepoints.join(",")}`);
  102. return { exitCode, lines };
  103. }
  104. module.exports = {
  105. findMessagesEmojiMatches,
  106. listEmojiCodepoints,
  107. run,
  108. toCodepoint,
  109. };
  110. if (require.main === module) {
  111. const out = run(process.argv.slice(2));
  112. for (const line of out.lines) console.log(line); // eslint-disable-line no-console
  113. process.exit(out.exitCode);
  114. }