feat: intl support for emoji picker (#1910)

* feat: intl support for emoji picker

Fixes #1908

* fix: update emoji-picker-element

* fix: fix typo
This commit is contained in:
Nolan Lawson 2020-12-18 20:02:36 -08:00 committed by GitHub
parent 2de875795b
commit a028a7e880
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 117 additions and 16 deletions

2
.gitignore vendored
View file

@ -8,7 +8,7 @@
/static/icons.svg /static/icons.svg
/static/robots.txt /static/robots.txt
/static/inline-script.js.map /static/inline-script.js.map
/static/emoji-all-en.json /static/emoji-*.json
/src/inline-script/checksum.js /src/inline-script/checksum.js
yarn-error.log yarn-error.log

View file

@ -1,17 +1,48 @@
import path from 'path' import path from 'path'
import fs from 'fs' import fs from 'fs'
import { promisify } from 'util' import { promisify } from 'util'
import { LOCALE } from '../src/routes/_static/intl'
const readFile = promisify(fs.readFile) const readFile = promisify(fs.readFile)
const writeFile = promisify(fs.writeFile) const writeFile = promisify(fs.writeFile)
// Try 'en-US' first, then 'en' if that doesn't exist
const PREFERRED_LOCALES = [LOCALE, LOCALE.split('-')[0]]
// emojibase seems like the most "neutral" shortcodes, but cldr is available in every language
const PREFERRED_SHORTCODES = ['emojibase', 'cldr']
async function getEmojiI18nFile (locale, shortcode) {
const filename = path.resolve(__dirname,
'../node_modules/emoji-picker-element-data',
locale,
shortcode,
'data.json')
try {
return JSON.parse(await readFile(filename, 'utf8'))
} catch (err) { /* ignore */ }
}
async function getFirstExistingEmojiI18nFile () {
for (const locale of PREFERRED_LOCALES) {
for (const shortcode of PREFERRED_SHORTCODES) {
const json = await getEmojiI18nFile(locale, shortcode)
if (json) {
return json
}
}
}
}
async function main () { async function main () {
const json = JSON.parse(await readFile( const json = await getFirstExistingEmojiI18nFile()
path.resolve(__dirname, '../node_modules/emoji-picker-element-data/en/emojibase-legacy/data.json'),
'utf8') if (!json) {
) throw new Error(`Couldn't find i18n data for locale ${LOCALE}. Is it supported in emoji-picker-element-data?`)
}
await writeFile( await writeFile(
path.resolve(__dirname, '../static/emoji-all-en.json'), path.resolve(__dirname, `../static/emoji-${LOCALE}.json`),
JSON.stringify(json), JSON.stringify(json),
'utf8' 'utf8'
) )

View file

@ -49,7 +49,7 @@
"compression": "^1.7.4", "compression": "^1.7.4",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"css-dedoupe": "^0.1.1", "css-dedoupe": "^0.1.1",
"emoji-picker-element": "^1.3.0", "emoji-picker-element": "^1.3.1",
"emoji-picker-element-data": "^1.0.0", "emoji-picker-element-data": "^1.0.0",
"emoji-regex": "^9.0.0", "emoji-regex": "^9.0.0",
"encoding": "^0.1.13", "encoding": "^0.1.13",

View file

@ -0,0 +1,34 @@
export default {
categoriesLabel: 'Catégories',
emojiUnsupportedMessage: 'Votre navigateur ne soutient pas les emojis en couleur.',
favoritesLabel: 'Favoris',
loadingMessage: 'Chargement en cours…',
networkErrorMessage: 'Impossible de charger les emojis. Veuillez essayer de recharger.',
regionLabel: 'Choisir un emoji',
searchDescription: 'Quand les résultats sont disponisbles, appuyez la fleche vers le haut ou le bas et la touche entrée pour choisir.',
searchLabel: 'Rechercher',
searchResultsLabel: 'Résultats',
skinToneDescription: 'Quand disponible, appuyez la fleche vers le haut ou le bas et la touch entrée pour choisir.',
skinToneLabel: 'Choisir une couleur de peau (actuellement {skinTone})',
skinTonesLabel: 'Couleurs de peau',
skinTones: [
'Défaut',
'Clair',
'Moyennement clair',
'Moyen',
'Moyennement sombre',
'Sombre'
],
categories: {
custom: 'Customisé',
'smileys-emotion': 'Les smileyes et les émoticônes',
'people-body': 'Les gens et le corps',
'animals-nature': 'Les animaux et la nature',
'food-drink': 'La nourriture et les boissons',
'travel-places': 'Les voyages et les endroits',
activities: 'Les activités',
objects: 'Les objets',
symbols: 'Les symbols',
flags: 'Les drapeaux'
}
}

View file

@ -8,7 +8,8 @@
<div class="emoji-container"> <div class="emoji-container">
<emoji-picker <emoji-picker
ref:picker ref:picker
data-source="/emoji-all-en.json" locale={emojiPickerLocale}
data-source={emojiPickerDataSource}
class={darkMode ? 'dark' : 'light'} class={darkMode ? 'dark' : 'light'}
on:emoji-click="onEmojiSelected(event)" on:emoji-click="onEmojiSelected(event)"
on:keydown="onPickerKeydown(event)" on:keydown="onPickerKeydown(event)"
@ -67,6 +68,7 @@
import { convertCustomEmojiToEmojiPickerFormat } from '../../../_utils/convertCustomEmojiToEmojiPickerFormat' import { convertCustomEmojiToEmojiPickerFormat } from '../../../_utils/convertCustomEmojiToEmojiPickerFormat'
import { supportsFocusVisible } from '../../../_utils/supportsFocusVisible' import { supportsFocusVisible } from '../../../_utils/supportsFocusVisible'
import { importFocusVisible } from '../../../_utils/asyncPolyfills' import { importFocusVisible } from '../../../_utils/asyncPolyfills'
import { emojiPickerI18n, emojiPickerDataSource, emojiPickerLocale } from '../../../_static/emojiPickerIntl'
export default { export default {
async oncreate () { async oncreate () {
@ -75,6 +77,9 @@
const { enableGrayscale, isUserTouching } = this.store.get() const { enableGrayscale, isUserTouching } = this.store.get()
const { picker } = this.refs const { picker } = this.refs
picker.customEmoji = customEmoji picker.customEmoji = customEmoji
if (emojiPickerI18n) {
picker.i18n = emojiPickerI18n
}
// break into shadow DOM to fix grayscale in Wellness grayscale mode // break into shadow DOM to fix grayscale in Wellness grayscale mode
if (enableGrayscale) { if (enableGrayscale) {
const style = document.createElement('style') const style = document.createElement('style')
@ -100,6 +105,10 @@
ModalDialog ModalDialog
}, },
store: () => store, store: () => store,
data: () => ({
emojiPickerLocale,
emojiPickerDataSource
}),
computed: { computed: {
darkMode: ({ $currentTheme }) => isDarkTheme($currentTheme), darkMode: ({ $currentTheme }) => isDarkTheme($currentTheme),
customEmoji: ({ $currentCustomEmoji, $autoplayGifs }) => ( customEmoji: ({ $currentCustomEmoji, $autoplayGifs }) => (

View file

@ -0,0 +1,11 @@
import { LOCALE } from './intl'
export const emojiPickerDataSource = `/emoji-${LOCALE}.json`
// this should be undefined for English; it's already bundled with emoji-picker-element
export const emojiPickerI18n = process.env.EMOJI_PICKER_I18N
// To avoid creating a new IDB database named emoji-picker-en-US, just
// reuse the existing default "en" one (otherwise people will end up with
// a stale database taking up useless space)
export const emojiPickerLocale = LOCALE === 'en-US' ? 'en' : LOCALE

View file

@ -1,5 +1,6 @@
import Database from 'emoji-picker-element/database' import Database from 'emoji-picker-element/database'
import { lifecycle } from './lifecycle' import { lifecycle } from './lifecycle'
import { emojiPickerLocale, emojiPickerDataSource } from '../_static/emojiPickerIntl'
let database let database
@ -23,7 +24,8 @@ function applySkinToneToEmoji (emoji, skinTone) {
export function init () { export function init () {
if (!database) { if (!database) {
database = new Database({ database = new Database({
dataSource: '/emoji-all-en.json' locale: emojiPickerLocale,
dataSource: emojiPickerDataSource
}) })
} }
} }

View file

@ -27,7 +27,7 @@ const assets = __assets__
.filter(filename => filename !== '/robots.txt') .filter(filename => filename !== '/robots.txt')
.filter(filename => !filename.includes('traineddata.gz')) // cache on-demand .filter(filename => !filename.includes('traineddata.gz')) // cache on-demand
.filter(filename => !filename.endsWith('.webapp')) // KaiOS manifest .filter(filename => !filename.endsWith('.webapp')) // KaiOS manifest
.filter(filename => !filename.includes('emoji-all-en.json')) // useless to cache; it already goes in IndexedDB .filter(filename => !/emoji-.*?\.json$/.test(filename)) // useless to cache; it already goes in IndexedDB
// `shell` is an array of all the files generated by webpack // `shell` is an array of all the files generated by webpack
// also contains '/index.html' for some reason // also contains '/index.html' for some reason

View file

@ -100,6 +100,16 @@ test('autosuggest handles works with regular emoji - clicking', async t => {
.expect(composeInput.value).eql('\ud83c\udf4d @quux ') .expect(composeInput.value).eql('\ud83c\udf4d @quux ')
}) })
test('autosuggest can suggest native emoji', async t => {
await loginAsFoobar(t)
await t
.hover(composeInput)
.typeText(composeInput, ':slight')
.expect(getNthAutosuggestionResult(1).innerText).contains(':slightly_smiling_face:', { timeout })
.click(getNthAutosuggestionResult(1))
.expect(composeInput.value).eql('\ud83d\ude42 ')
})
test('autosuggest only shows for one input', async t => { test('autosuggest only shows for one input', async t => {
await loginAsFoobar(t) await loginAsFoobar(t)
await t await t

View file

@ -1,4 +1,4 @@
import { LOCALE } from '../src/routes/_static/intl' import { DEFAULT_LOCALE, LOCALE } from '../src/routes/_static/intl'
const path = require('path') const path = require('path')
const webpack = require('webpack') const webpack = require('webpack')
@ -19,6 +19,9 @@ const output = Object.assign(config.client.output(), {
chunkFilename: dev ? '[hash]/[id].js' : '[id].[contenthash].js' chunkFilename: dev ? '[hash]/[id].js' : '[id].[contenthash].js'
}) })
const emojiPickerI18n = LOCALE !== DEFAULT_LOCALE &&
require(path.join(__dirname, '../src/intl/emoji-picker/', `${LOCALE}.js`)).default
module.exports = { module.exports = {
entry: config.client.entry(), entry: config.client.entry(),
output, output,
@ -101,7 +104,8 @@ module.exports = {
'process.env.INLINE_SVGS': JSON.stringify(inlineSvgs), 'process.env.INLINE_SVGS': JSON.stringify(inlineSvgs),
'process.env.ALL_SVGS': JSON.stringify(allSvgs), 'process.env.ALL_SVGS': JSON.stringify(allSvgs),
'process.env.URL_REGEX': urlRegex.toString(), 'process.env.URL_REGEX': urlRegex.toString(),
'process.env.LOCALE': JSON.stringify(LOCALE) 'process.env.LOCALE': JSON.stringify(LOCALE),
'process.env.EMOJI_PICKER_I18N': emojiPickerI18n ? JSON.stringify(emojiPickerI18n) : 'undefined'
}), }),
new webpack.NormalModuleReplacementPlugin( new webpack.NormalModuleReplacementPlugin(
/\/_database\/database\.js$/, // this version plays nicer with IDEs /\/_database\/database\.js$/, // this version plays nicer with IDEs

View file

@ -2904,10 +2904,10 @@ emoji-picker-element-data@^1.0.0:
resolved "https://registry.yarnpkg.com/emoji-picker-element-data/-/emoji-picker-element-data-1.0.0.tgz#1e9c4b399ce6e1858514df4c25b65284d981f92f" resolved "https://registry.yarnpkg.com/emoji-picker-element-data/-/emoji-picker-element-data-1.0.0.tgz#1e9c4b399ce6e1858514df4c25b65284d981f92f"
integrity sha512-Ch6Ibuc2DJAh9MMyaH0hxsfkCoGAkYVWf9i1JC30PsaC4L9rmS7LMvu1iR396NHecdMYToJEQEOneatPVGe/IQ== integrity sha512-Ch6Ibuc2DJAh9MMyaH0hxsfkCoGAkYVWf9i1JC30PsaC4L9rmS7LMvu1iR396NHecdMYToJEQEOneatPVGe/IQ==
emoji-picker-element@^1.3.0: emoji-picker-element@^1.3.1:
version "1.3.0" version "1.3.1"
resolved "https://registry.yarnpkg.com/emoji-picker-element/-/emoji-picker-element-1.3.0.tgz#d78deba0ebc4b87731bb2c16f7be00ec458d7647" resolved "https://registry.yarnpkg.com/emoji-picker-element/-/emoji-picker-element-1.3.1.tgz#844e1ed261b6cda431423a1652da977202cb2090"
integrity sha512-Zg+8rtr3vXKuAgBXWpSBghHq+I6o7+35N+25MN3P07pUyk07GXJ6B+gKr8ttUo2LZrLDZVoqKOVMzowkNwwZIg== integrity sha512-+WtNPw28snGd5ZXVS5mAAKsAJ1+hJVRdXNQ9ZCdWSLVCK1mtJjcHl2dMxtB1LxSpX6m79DAAHMmDJRjqbRA+5w==
emoji-regex@^7.0.1: emoji-regex@^7.0.1:
version "7.0.3" version "7.0.3"