diff --git a/src/renderer/components/ft-profile-bubble/ft-profile-bubble.js b/src/renderer/components/ft-profile-bubble/ft-profile-bubble.js index 682057bf80cca..39d4e226e8e54 100644 --- a/src/renderer/components/ft-profile-bubble/ft-profile-bubble.js +++ b/src/renderer/components/ft-profile-bubble/ft-profile-bubble.js @@ -1,6 +1,7 @@ import { defineComponent } from 'vue' import { sanitizeForHtmlId } from '../../helpers/accessibility' import { MAIN_PROFILE_ID } from '../../../constants' +import { getFirstCharacter } from '../../helpers/strings' export default defineComponent({ name: 'FtProfileBubble', @@ -24,6 +25,9 @@ export default defineComponent({ }, emits: ['click'], computed: { + locale: function () { + return this.$i18n.locale.replace('_', '-') + }, isMainProfile: function () { return this.profileId === MAIN_PROFILE_ID }, @@ -31,7 +35,9 @@ export default defineComponent({ return 'profileBubble' + sanitizeForHtmlId(this.profileId) }, profileInitial: function () { - return this?.profileName?.length > 0 ? Array.from(this.translatedProfileName)[0].toUpperCase() : '' + return this.profileName + ? getFirstCharacter(this.translatedProfileName, this.locale).toUpperCase() + : '' }, translatedProfileName: function () { return this.isMainProfile ? this.$t('Profile.All Channels') : this.profileName diff --git a/src/renderer/components/ft-profile-edit/ft-profile-edit.js b/src/renderer/components/ft-profile-edit/ft-profile-edit.js index f89b449f1a795..240501a0f7012 100644 --- a/src/renderer/components/ft-profile-edit/ft-profile-edit.js +++ b/src/renderer/components/ft-profile-edit/ft-profile-edit.js @@ -8,6 +8,7 @@ import FtButton from '../../components/ft-button/ft-button.vue' import { MAIN_PROFILE_ID } from '../../../constants' import { calculateColorLuminance, colors } from '../../helpers/colors' import { showToast } from '../../helpers/utils' +import { getFirstCharacter } from '../../helpers/strings' export default defineComponent({ name: 'FtProfileEdit', @@ -47,11 +48,16 @@ export default defineComponent({ } }, computed: { + locale: function () { + return this.$i18n.locale.replace('_', '-') + }, colorValues: function () { return colors.map(color => color.value) }, profileInitial: function () { - return this?.profileName?.length > 0 ? Array.from(this.translatedProfileName)[0].toUpperCase() : '' + return this.profileName + ? getFirstCharacter(this.translatedProfileName, this.locale).toUpperCase() + : '' }, activeProfile: function () { return this.$store.getters.getActiveProfile diff --git a/src/renderer/components/ft-profile-selector/ft-profile-selector.js b/src/renderer/components/ft-profile-selector/ft-profile-selector.js index 6026abd06e37a..3dc775de48391 100644 --- a/src/renderer/components/ft-profile-selector/ft-profile-selector.js +++ b/src/renderer/components/ft-profile-selector/ft-profile-selector.js @@ -5,6 +5,7 @@ import FtCard from '../../components/ft-card/ft-card.vue' import FtIconButton from '../../components/ft-icon-button/ft-icon-button.vue' import { showToast } from '../../helpers/utils' import { MAIN_PROFILE_ID } from '../../../constants' +import { getFirstCharacter } from '../../helpers/strings' export default defineComponent({ name: 'FtProfileSelector', @@ -19,6 +20,9 @@ export default defineComponent({ } }, computed: { + locale: function () { + return this.$i18n.locale.replace('_', '-') + }, profileList: function () { return this.$store.getters.getProfileList }, @@ -26,12 +30,15 @@ export default defineComponent({ return this.$store.getters.getActiveProfile }, activeProfileInitial: function () { - // use Array.from, so that emojis don't get split up into individual character codes - return this.activeProfile?.name?.length > 0 ? Array.from(this.translatedProfileName(this.activeProfile))[0].toUpperCase() : '' + return this.activeProfile?.name + ? getFirstCharacter(this.translatedProfileName(this.activeProfile), this.locale).toUpperCase() + : '' }, profileInitials: function () { return this.profileList.map((profile) => { - return profile?.name?.length > 0 ? Array.from(this.translatedProfileName(profile))[0].toUpperCase() : '' + return profile?.name + ? getFirstCharacter(this.translatedProfileName(profile), this.locale).toUpperCase() + : '' }) } }, diff --git a/src/renderer/components/ft-subscribe-button/ft-subscribe-button.js b/src/renderer/components/ft-subscribe-button/ft-subscribe-button.js index 806310857a6a9..13f5c65bf4c51 100644 --- a/src/renderer/components/ft-subscribe-button/ft-subscribe-button.js +++ b/src/renderer/components/ft-subscribe-button/ft-subscribe-button.js @@ -6,6 +6,7 @@ import FtButton from '../../components/ft-button/ft-button.vue' import { MAIN_PROFILE_ID } from '../../../constants' import { deepCopy, showToast } from '../../helpers/utils' +import { getFirstCharacter } from '../../helpers/strings' export default defineComponent({ name: 'FtSubscribeButton', @@ -45,9 +46,14 @@ export default defineComponent({ } }, computed: { + locale: function () { + return this.$i18n.locale.replace('_', '-') + }, profileInitials: function () { return this.profileDisplayList.map((profile) => { - return profile?.name?.length > 0 ? Array.from(profile.name)[0].toUpperCase() : '' + return profile.name + ? getFirstCharacter(profile.name, this.locale).toUpperCase() + : '' }) }, diff --git a/src/renderer/helpers/strings.js b/src/renderer/helpers/strings.js index 2556304842533..53221fee31ff3 100644 --- a/src/renderer/helpers/strings.js +++ b/src/renderer/helpers/strings.js @@ -52,3 +52,30 @@ export function translateWindowTitle(title, i18n) { return null } } + +/** + * Returns the first user-perceived character, + * respecting language specific rules and + * emojis made up of multiple codepoints like flags and families. + * @param {string} text + * @param {string} locale + * @returns {string} + */ +export function getFirstCharacter(text, locale) { + if (text.length === 0) { + return '' + } + + // Firefox only received support for Intl.Segmenter support in version 125 (2024-04-16) + // so fallback to Array.from just in case. + // TODO: Remove fallback in the future + if (Intl.Segmenter) { + const segmenter = new Intl.Segmenter([locale, 'en'], { granularity: 'grapheme' }) + + // Use iterator directly as we only need the first segment + const firstSegment = segmenter.segment(text)[Symbol.iterator]().next().value + return firstSegment.segment + } else { + return Array.from(text)[0] + } +}