adds script to generate locale data for emojis

This commit is contained in:
ChaosExAnima 2025-05-30 18:37:34 +02:00
parent 0663bda64f
commit d5d360b940
No known key found for this signature in database
GPG Key ID: 8F2B333100FB6117
4 changed files with 141 additions and 116 deletions

View File

@ -1,135 +1,84 @@
import type {
Category,
Data as EmojiMartData,
SkinVariation,
} from 'emoji-mart/dist-es/utils/data';
import type { SkinVariation } from 'emoji-mart/dist-es/utils/data';
import { joinShortcodes } from 'emojibase';
import type {
CompactEmoji,
Locale,
MessagesDataset,
ShortcodesDataset,
} from 'emojibase';
import type { CompactEmoji, Locale, ShortcodesDataset } from 'emojibase';
async function fetchEmojibaseData(locale: Locale): Promise<CompactEmoji[]> {
const [emojiData, shortcodes] = await Promise.all([
import(`emojibase-data/${locale}/compact.json`) as Promise<CompactEmoji[]>,
import(
async function fetchEmojibaseData(
locale: Locale,
withShortcodes = true,
): Promise<CompactEmoji[]> {
const { default: emojiData } = (await import(
`emojibase-data/${locale}/compact.json`
)) as { default: CompactEmoji[] };
if (!withShortcodes) {
return emojiData;
}
const shortcodes = (await import(
`emojibase-data/${locale}/shortcodes/cldr.json`
) as Promise<ShortcodesDataset>,
]);
)) as ShortcodesDataset;
return joinShortcodes(emojiData, [shortcodes]);
}
function fetchEmojibaseMessages(locale: Locale): Promise<MessagesDataset> {
return import(`emojibase-data/${locale}/messages.json`);
}
export async function fetchEmojiMartData() {
const { default: data } = await import('./emoji_data.json');
const { default: data } = await import('emoji-datasource/emoji.json');
return data;
}
/*
{
"man-rowing-boat":{
"a":"Man Rowing Boat",
"b":"1F6A3-200D-2642-FE0F",
"f":true,
"k":[
36,
36
],
"c":"1F6A3-200D-2642",
"j":[
"person_rowing_boat",
"sport",
"move"
],
"skin_variations":{
"1F3FB":{
"unified":"1F6A3-1F3FB-200D-2642-FE0F",
"non_qualified":"1F6A3-1F3FB-200D-2642",
"image":"1f6a3-1f3fb-200d-2642-fe0f.png",
"sheet_x":36,
"sheet_y":37,
"added_in":"4.0",
"has_img_apple":true,
"has_img_google":true,
"has_img_twitter":true,
"has_img_facebook":true
},
}
export type UnicodeMapping = Record<string, string>;
export async function emojibaseToUnicodeMapping(
locale: Locale,
): Promise<UnicodeMapping> {
const emojibaseCompactEmojis = await fetchEmojibaseData(locale, false);
const mapping: UnicodeMapping = {};
for (const emoji of emojibaseCompactEmojis) {
mapping[emoji.hexcode] = emoji.label;
}
return mapping;
}
export interface EmojiMartCompactEmoji {
hexcode: string;
x: number;
y: number;
labels?: string[];
skins?: Omit<EmojiMartCompactEmoji, 'skins' | 'labels'>[];
}
*/
export async function emojibaseToEmojiMart(
locale: Locale,
): Promise<EmojiMartData> {
const emojibaseCompactEmojis = await fetchEmojibaseData(locale);
const emojibaseMessages = await fetchEmojibaseMessages(locale);
): Promise<EmojiMartCompactEmoji[]> {
const emojibaseCompactEmojis = await fetchEmojibaseData(locale, true);
const emojiMartData = await fetchEmojiMartData();
const groupNumToEmojiMap = new Map<number, Set<string>>();
const emojis: EmojiMartData['emojis'] = {};
const aliases: EmojiMartData['aliases'] = {};
const emojibaseMap = new Map<string, CompactEmoji>();
for (const emoji of emojibaseCompactEmojis) {
const shortcode = emoji.shortcodes?.at(0);
if (!shortcode || !emoji.shortcodes) {
continue; // Skip emojis without shortcodes
emojibaseMap.set(emoji.hexcode, emoji);
}
for (const alias of emoji.shortcodes.slice(1)) {
aliases[alias] = shortcode;
}
if (emoji.group) {
const existingEmojis =
groupNumToEmojiMap.get(emoji.group) ?? new Set<string>();
existingEmojis.add(shortcode);
groupNumToEmojiMap.set(emoji.group, existingEmojis);
}
emojis[shortcode] = {
name: emoji.label,
unified: emoji.hexcode,
has_img_twitter: true,
sheet_x: 0,
sheet_y: 0,
keywords: emoji.tags,
skin_variations: emoji.skins?.reduce(
(acc, skin) => ({
...acc,
[skin.hexcode]: {
unified: skin.hexcode,
non_qualified: skin.hexcode,
image: skin.hexcode,
sheet_x: 0,
sheet_y: 0,
added_in: '1.0', // Placeholder, should be set based on actual version
has_img_apple: true,
has_img_google: true,
has_img_twitter: true,
has_img_facebook: true,
},
}),
{} as Record<string, SkinVariation>,
),
};
}
const categories: Category[] = emojibaseMessages.groups.map(
(group, index) => ({
id: group.key,
name: group.message,
emojis: groupNumToEmojiMap.get(index)?.values().toArray() ?? [],
const emojis: EmojiMartCompactEmoji[] = [];
for (const emoji of emojiMartData) {
const emojibaseEmoji = emojibaseMap.get(emoji.unified);
let skins: EmojiMartCompactEmoji['skins'];
if (emoji.skin_variations) {
skins = Object.values(emoji.skin_variations).map(
(skin: SkinVariation) => ({
hexcode: skin.unified,
x: skin.sheet_x,
y: skin.sheet_y,
}),
);
return {
compressed: false,
categories,
emojis,
aliases,
};
}
emojis.push({
hexcode: emoji.unified,
x: emoji.sheet_x,
y: emoji.sheet_y,
labels: emojibaseEmoji?.shortcodes,
skins,
});
}
return emojis;
}

View File

@ -0,0 +1,67 @@
/* eslint-disable no-console */
import fs from 'node:fs/promises';
import path from 'node:path';
import type { Locale } from 'emojibase';
import { SUPPORTED_LOCALES } from 'emojibase';
import { emojibaseToEmojiMart, emojibaseToUnicodeMapping } from './data';
const projectRoot = process.cwd();
const emojiPath = path.resolve(projectRoot, 'public/emoji');
async function calculateLocales() {
// Get a list of all locales we support.
const localeFilesPath = path.resolve(
projectRoot,
'app/javascript/mastodon/locales',
);
const localeDir = await fs.readdir(localeFilesPath, 'utf-8');
const appLocales: string[] = [];
for (const file of localeDir) {
if (file.endsWith('.json')) {
appLocales.push(path.basename(file, '.json').toLowerCase());
}
}
// Get the intersection of supported locales and app locales and generate emoji JSON files for those.
return SUPPORTED_LOCALES.filter((locale) =>
appLocales.includes(locale.toLowerCase()),
);
}
async function generateAllEmojiJsonFiles() {
const emojiLocalePath = path.resolve(emojiPath, 'locales');
const emojiLocales = await calculateLocales();
await fs.mkdir(emojiLocalePath, { recursive: true }); // Ensure the locales directory exists
await Promise.all(
emojiLocales.map((locale) => writeEmojiLocaleJson(locale, emojiLocalePath)),
);
console.log(
`Generated emoji JSON files for locales: ${emojiLocales.join(', ')}`,
);
}
async function writeEmojiLocaleJson(locale: Locale, dirPath: string) {
const mapping = await emojibaseToUnicodeMapping(locale);
const filePath = path.join(dirPath, `${locale}.json`);
await fs.writeFile(filePath, JSON.stringify(mapping, null, 2), 'utf-8');
}
async function generateEmojiSheetMappingJson() {
const emojiMetaPath = path.resolve(emojiPath, 'meta');
await fs.mkdir(emojiMetaPath, { recursive: true }); // Ensure the meta directory exists
const locales = await calculateLocales();
await Promise.all(
locales.map((locale) => writeEmojiMappingJson(locale, emojiMetaPath)),
);
}
async function writeEmojiMappingJson(locale: Locale, dirPath: string) {
const mapping = await emojibaseToEmojiMart(locale);
const filePath = path.join(dirPath, `${locale}.json`);
await fs.writeFile(filePath, JSON.stringify(mapping, null, 2), 'utf-8');
}
Promise.all([generateAllEmojiJsonFiles(), generateEmojiSheetMappingJson()])
.then(() => process.exit())
.catch(console.error);

View File

@ -65,6 +65,7 @@
"core-js": "^3.30.2",
"cross-env": "^7.0.3",
"detect-passive-events": "^2.0.3",
"emoji-datasource": "15.1.2",
"emoji-mart": "npm:emoji-mart-lazyload@latest",
"emojibase": "^16.0.0",
"emojibase-data": "^16.0.3",

View File

@ -2947,6 +2947,7 @@ __metadata:
core-js: "npm:^3.30.2"
cross-env: "npm:^7.0.3"
detect-passive-events: "npm:^2.0.3"
emoji-datasource: "npm:15.1.2"
emoji-mart: "npm:emoji-mart-lazyload@latest"
emojibase: "npm:^16.0.0"
emojibase-data: "npm:^16.0.3"
@ -6783,6 +6784,13 @@ __metadata:
languageName: node
linkType: hard
"emoji-datasource@npm:15.1.2":
version: 15.1.2
resolution: "emoji-datasource@npm:15.1.2"
checksum: 10c0/32d914ec1c3d1044200eb98bb867ec78d53d5b316bbe6a1780db5490016c82f77277fa2cace0ce0d76f155197265f1d9952c8c7c201ddc59ca4d6d0d8c6ca5b5
languageName: node
linkType: hard
"emoji-mart@npm:emoji-mart-lazyload@latest":
version: 3.0.1-j
resolution: "emoji-mart-lazyload@npm:3.0.1-j"