Files
sky_phone-sky-systems/frontend/src/components/FullEmojiPicker.vue
T
Leon.Schmidt e58f1b6c18 FIX - complete phone locale fallback
Resolve configured and regional locales through a centralized English fallback for Lua and NUI consumers. Complete the German and English app translation keys, localize remaining picker and attachment copy, and add contract tests that detect missing locale entries.
2026-08-17 15:01:57 +02:00

112 lines
3.1 KiB
Vue

<script setup lang="ts">
import deEmojiData from 'emoji-picker-element-data/de/cldr/data.json'
import enEmojiData from 'emoji-picker-element-data/en/cldr/data.json'
import { Search, X } from 'lucide-vue-next'
import { computed, ref } from 'vue'
import { usePhoneStore } from '@/stores/phone'
type EmojiEntry = {
annotation: string
emoji: string
group: number
skins?: Array<{ emoji: string; tone: number }>
shortcodes?: string[]
tags?: string[]
}
const emit = defineEmits<{
close: []
pick: [emoji: string]
}>()
const phone = usePhoneStore()
const query = ref('')
const activeGroup = ref(0)
const skinTone = ref(0)
const groups = ['😀', '👋', '🧑', '🐻', '🍔', '⚽', '🚗', '💡', '❤️', '🏳️']
const skinTones = ['✋', '✋🏻', '✋🏼', '✋🏽', '✋🏾', '✋🏿']
const localizedEmojiData = computed(() =>
phone.lang.toLocaleLowerCase().startsWith('de') ? deEmojiData : enEmojiData,
)
const emojis = computed(() => {
const needle = query.value.trim().toLocaleLowerCase(phone.lang)
return (localizedEmojiData.value as EmojiEntry[])
.filter((entry) => {
if (!needle) return entry.group === activeGroup.value
return `${entry.annotation} ${entry.shortcodes?.join(' ') ?? ''} ${entry.tags?.join(' ') ?? ''}`
.toLocaleLowerCase(phone.lang)
.includes(needle)
})
.map((entry) => ({
...entry,
emoji:
skinTone.value === 0
? entry.emoji
: (entry.skins?.find((skin) => skin.tone === skinTone.value)?.emoji ??
entry.emoji),
}))
})
</script>
<template>
<section
class="messages-full-emoji-picker"
:aria-label="phone.t('Apps.messages.emojiPicker')"
>
<header>
<strong>{{ phone.t('Apps.messages.emoji') }}</strong>
<button
type="button"
:aria-label="phone.t('Common.done')"
@click="emit('close')"
>
<X :size="18" />
</button>
</header>
<label class="messages-full-emoji-picker__search">
<Search :size="15" />
<input
v-model="query"
type="search"
:placeholder="phone.t('Common.search')"
autocomplete="off"
/>
</label>
<nav v-if="!query" :aria-label="phone.t('Apps.messages.emojiCategories')">
<button
v-for="(group, index) in groups"
:key="group"
type="button"
:class="{ active: activeGroup === index }"
@click="activeGroup = index"
>
{{ group }}
</button>
<i aria-hidden="true" />
<button
v-for="(tone, index) in skinTones"
:key="tone"
type="button"
:class="{ active: skinTone === index }"
:title="phone.t('Apps.messages.skinTone', { number: String(index) })"
@click="skinTone = index"
>
{{ tone }}
</button>
</nav>
<div class="messages-full-emoji-picker__grid">
<button
v-for="entry in emojis"
:key="`${entry.emoji}-${entry.annotation}`"
type="button"
:title="entry.annotation"
@click="emit('pick', entry.emoji)"
>
{{ entry.emoji }}
</button>
</div>
</section>
</template>