1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-28 05:15:54 +00:00
This release cleans up some English strings, and moves us to a new back-end for translation. Now, translations are cached in our CDN which should reduce resource usage and allow clients to properly cache things. Translations are also split into chunks so clients can minimize what they need to request at any given time.

* Fixed: Detection of the subscriber button.
* Fixed: Remove references of non-standard ICU format `en_plural`. Use proper `plural` tags.
* API Added: `i18n.loadChunk(name: string) => Promise<void>` for requiring a new chunk.
* API Fixed: Allow settings to have a `null` i18n key to disable translation.
This commit is contained in:
SirStendec 2021-11-12 16:58:35 -05:00
parent 958a3956f1
commit 97c96be276
33 changed files with 183 additions and 110 deletions

View file

@ -779,6 +779,12 @@
"css": "right-open", "css": "right-open",
"code": 59462, "code": 59462,
"src": "fontawesome" "src": "fontawesome"
},
{
"uid": "a2a74f5e7b7d9ba054897d8c795a326a",
"css": "list-bullet",
"code": 61642,
"src": "fontawesome"
} }
] ]
} }

View file

@ -1,7 +1,7 @@
{ {
"name": "frankerfacez", "name": "frankerfacez",
"author": "Dan Salvato LLC", "author": "Dan Salvato LLC",
"version": "4.30.0", "version": "4.30.1",
"description": "FrankerFaceZ is a Twitch enhancement suite.", "description": "FrankerFaceZ is a Twitch enhancement suite.",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",

Binary file not shown.

View file

@ -158,6 +158,8 @@
<glyph glyph-name="docs" unicode="&#xf0c5;" d="M946 636q23 0 38-16t16-38v-678q0-23-16-38t-38-16h-535q-23 0-38 16t-16 38v160h-303q-23 0-38 16t-16 38v375q0 22 11 49t27 42l228 228q15 16 42 27t49 11h232q23 0 38-16t16-38v-183q38 23 71 23h232z m-303-119l-167-167h167v167z m-357 214l-167-167h167v167z m109-361l176 176v233h-214v-233q0-22-15-37t-38-16h-233v-357h286v143q0 22 11 49t27 42z m534-449v643h-215v-232q0-22-15-38t-38-15h-232v-358h500z" horiz-adv-x="1000" /> <glyph glyph-name="docs" unicode="&#xf0c5;" d="M946 636q23 0 38-16t16-38v-678q0-23-16-38t-38-16h-535q-23 0-38 16t-16 38v160h-303q-23 0-38 16t-16 38v375q0 22 11 49t27 42l228 228q15 16 42 27t49 11h232q23 0 38-16t16-38v-183q38 23 71 23h232z m-303-119l-167-167h167v167z m-357 214l-167-167h167v167z m109-361l176 176v233h-214v-233q0-22-15-37t-38-16h-233v-357h286v143q0 22 11 49t27 42z m534-449v643h-215v-232q0-22-15-38t-38-15h-232v-358h500z" horiz-adv-x="1000" />
<glyph glyph-name="list-bullet" unicode="&#xf0ca;" d="M214 64q0-44-31-76t-76-31-76 31-31 76 31 76 76 31 76-31 31-76z m0 286q0-45-31-76t-76-31-76 31-31 76 31 76 76 31 76-31 31-76z m786-232v-107q0-7-5-13t-13-5h-678q-8 0-13 5t-5 13v107q0 7 5 12t13 6h678q7 0 13-6t5-12z m-786 518q0-45-31-76t-76-31-76 31-31 76 31 76 76 31 76-31 31-76z m786-232v-108q0-7-5-12t-13-5h-678q-8 0-13 5t-5 12v108q0 7 5 12t13 5h678q7 0 13-5t5-12z m0 285v-107q0-7-5-12t-13-6h-678q-8 0-13 6t-5 12v107q0 8 5 13t13 5h678q7 0 13-5t5-13z" horiz-adv-x="1000" />
<glyph glyph-name="sort-down" unicode="&#xf0dd;" d="M571 243q0-15-10-25l-250-250q-11-11-25-11t-25 11l-250 250q-11 10-11 25t11 25 25 11h500q14 0 25-11t10-25z" horiz-adv-x="571.4" /> <glyph glyph-name="sort-down" unicode="&#xf0dd;" d="M571 243q0-15-10-25l-250-250q-11-11-25-11t-25 11l-250 250q-11 10-11 25t11 25 25 11h500q14 0 25-11t10-25z" horiz-adv-x="571.4" />
<glyph glyph-name="sort-up" unicode="&#xf0de;" d="M571 457q0-14-10-25t-25-11h-500q-15 0-25 11t-11 25 11 25l250 250q10 11 25 11t25-11l250-250q10-10 10-25z" horiz-adv-x="571.4" /> <glyph glyph-name="sort-up" unicode="&#xf0de;" d="M571 457q0-14-10-25t-25-11h-500q-15 0-25 11t-11 25 11 25l250 250q10 11 25 11t25-11l250-250q10-10 10-25z" horiz-adv-x="571.4" />

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Before After
Before After

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -294,6 +294,8 @@ export default class AddonManager extends Module {
if ( ! addon ) if ( ! addon )
throw new Error(`Unknown add-on id: ${id}`); throw new Error(`Unknown add-on id: ${id}`);
await this.i18n.loadChunk(`addon.${id}`);
let module = this.resolve(`addon.${id}`); let module = this.resolve(`addon.${id}`);
if ( module ) { if ( module ) {
if ( ! module.loaded ) if ( ! module.loaded )

View file

@ -6,12 +6,15 @@
import Parser from '@ffz/icu-msgparser'; import Parser from '@ffz/icu-msgparser';
import {DEBUG} from 'utilities/constants'; import {DEBUG, SERVER} from 'utilities/constants';
import {get, pick_random, shallow_copy, deep_copy} from 'utilities/object'; import {get, pick_random, shallow_copy, deep_copy} from 'utilities/object';
import { getBuster } from 'utilities/time';
import Module from 'utilities/module'; import Module from 'utilities/module';
import NewTransCore from 'utilities/translation-core'; import NewTransCore from 'utilities/translation-core';
const fetchJSON = (url, options) => fetch(url, options).then(r => r.ok ? r.json() : null).catch(() => null);
const API_SERVER = 'https://api-test.frankerfacez.com'; const API_SERVER = 'https://api-test.frankerfacez.com';
const STACK_SPLITTER = /\s*at\s+(.+?)\s+\((.+)\)$/, const STACK_SPLITTER = /\s*at\s+(.+?)\s+\((.+)\)$/,
@ -78,6 +81,7 @@ export class TranslationManager extends Module {
this._seen = new Set; this._seen = new Set;
this.availableLocales = ['en']; this.availableLocales = ['en'];
this.chunks = ['client'];
this.localeData = { this.localeData = {
en: { name: 'English' } en: { name: 'English' }
@ -350,6 +354,25 @@ export class TranslationManager extends Module {
this.locale = this.settings.get('i18n.locale'); this.locale = this.settings.get('i18n.locale');
} }
async loadChunk(name) {
if (this.chunks.includes(name))
return [];
this.chunks.push(name);
const locale = this._.locale;
const phrases = await this.loadLocale(locale, name);
const added = this._.extend(phrases);
if ( added.length ) {
this.log.info(`Loaded Chunk: ${name} -- Phrases: ${added.length}`);
this.emit(':loaded', added);
this.emit(':update');
}
return added;
}
broadcast(msg) { broadcast(msg) {
if ( this._broadcaster ) if ( this._broadcaster )
this._broadcaster.postMessage(msg); this._broadcaster.postMessage(msg);
@ -448,32 +471,13 @@ export class TranslationManager extends Module {
this.strings_loading = true; this.strings_loading = true;
const loadPage = async page => { const resp = await fetch(`${SERVER}/script/locale/strings.json?_=${getBuster(30)}`);
const resp = await fetch(`${API_SERVER}/v2/i18n/strings?page=${page}`); let strings;
if ( ! resp.ok ) { if (! resp.ok ) {
this.log.warn(`Error Loading Strings -- Status: ${resp.status}`); this.log.warn(`Error Loading Strings -- Status: ${resp.status}`);
return { strings = [];
next: false, } else
strings: [] strings = await resp.json();
};
}
const data = await resp.json();
return {
next: data?.pages > page,
strings: data?.strings || []
}
}
let page = 1;
let next = true;
let strings = [];
while(next) {
const data = await loadPage(page++); // eslint-disable-line no-await-in-loop
strings = strings.concat(data.strings);
next = data.next;
}
for(const str of strings) { for(const str of strings) {
const key = str.id; const key = str.id;
@ -629,19 +633,20 @@ export class TranslationManager extends Module {
async loadLocales() { async loadLocales() {
const resp = await fetch(`${API_SERVER}/v2/i18n/locales`); const resp = await fetch(`${SERVER}/script/locale/locales.json?_=${getBuster(30)}`);
let data;
if ( ! resp.ok ) { if ( ! resp.ok ) {
this.log.warn(`Error Populating Locales -- Status: ${resp.status}`); this.log.warn(`Error Populating Locales -- Status: ${resp.status}`);
throw new Error(`http error ${resp.status} loading locales`) } else
} data = await resp.json();
let data = await resp.json();
if ( ! Array.isArray(data) || ! data.length ) if ( ! Array.isArray(data) || ! data.length )
data = [{ data = [{
id: 'en', id: 'en',
name: 'English', name: 'English',
coverage: 100, coverage: 100,
rtl: false rtl: false,
hashes: {}
}]; }];
this.localeData = {}; this.localeData = {};
@ -657,11 +662,46 @@ export class TranslationManager extends Module {
} }
async loadLocale(locale) { async loadLocale(locale, chunk = null) {
if ( locale === 'en' ) if ( locale === 'en' )
return {}; return {};
const resp = await fetch(`${API_SERVER}/v2/i18n/locale/${locale}`); const hashes = this.localeData[locale]?.hashes;
if (! hashes) {
this.log.info(`Cannot Load Locale: ${locale}`);
return {};
}
if (! chunk)
chunk = this.chunks;
else if (! Array.isArray(chunk))
chunk = [chunk];
const id = this.localeData[locale].id;
const promises = [];
for(const chnk of chunk) {
const hash = hashes[chnk];
if (! hash)
continue;
promises.push(fetchJSON(`https://cdn.frankerfacez.com/static/locale/${id}/${chnk}.${hash}.json`));
}
const chunks = await Promise.all(promises);
const result = {};
for(const chunk of chunks) {
if (! chunk)
continue;
for(const [key,val] of Object.entries(chunk))
result[key] = val;
}
return result;
/*const resp = await fetch(`${API_SERVER}/v2/i18n/locale/${locale}`);
if ( ! resp.ok ) { if ( ! resp.ok ) {
if ( resp.status === 404 ) { if ( resp.status === 404 ) {
this.log.info(`Cannot Load Locale: ${locale}`); this.log.info(`Cannot Load Locale: ${locale}`);
@ -673,7 +713,7 @@ export class TranslationManager extends Module {
} }
const data = await resp.json(); const data = await resp.json();
return data?.phrases; return data?.phrases;*/
} }
async setLocale(new_locale) { async setLocale(new_locale) {
@ -713,7 +753,7 @@ export class TranslationManager extends Module {
} }
async loadDayjsLocale(locale) { async loadDayjsLocale(locale) {
if ( locale === 'en' ) if ( locale === 'en' || locale === 'en-arrr' )
return; return;
try { try {

View file

@ -276,11 +276,11 @@ export const timeout = {
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-timeout.vue'), editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-timeout.vue'),
title: 'Timeout User', title: 'Timeout User',
description: '{options.duration,number} second{options.duration,en_plural}', description: '{options.duration, plural, one {# second} other {# seconds}}',
reason_text(data) { reason_text(data) {
return this.i18n.t('chat.actions.timeout-reason', return this.i18n.t('chat.actions.timeout-reason',
'Timeout {user.login} for {duration,number} second{duration,en_plural} for:', 'Timeout {user.login} for {duration, plural, one {# second} other {# seconds}} for:',
{ {
user: data.user, user: data.user,
duration: data.options.duration duration: data.options.duration
@ -291,7 +291,7 @@ export const timeout = {
tooltip(data) { tooltip(data) {
return this.i18n.t( return this.i18n.t(
'chat.actions.timeout.tooltip', 'chat.actions.timeout.tooltip',
'Timeout {user.login} for {duration,number} second{duration,en_plural}', 'Timeout {user.login} for {duration, plural, one {# second} other {# seconds}}',
{ {
user: data.user, user: data.user,
duration: data.options.duration duration: data.options.duration

View file

@ -466,18 +466,18 @@ export default class Badges extends Module {
if ( d.data ) { if ( d.data ) {
if ( d.badge === 'subscriber' ) { if ( d.badge === 'subscriber' ) {
if ( tier > 0 ) if ( tier > 0 )
title = this.i18n.t('badges.subscriber.tier-months', '{title}\n(Tier {tier}, {months,number} Month{months,en_plural})', { title = this.i18n.t('badges.subscriber.tier-months', '{title}\n(Tier {tier}, {months, plural, one {# Month} other {# Months}})', {
title, title,
tier, tier,
months: d.data months: d.data
}); });
else else
title = this.i18n.t('badges.subscriber.months', '{title}\n({count,number} Month{count,en_plural})', { title = this.i18n.t('badges.subscriber.months', '{title}\n({count, plural, one {# Month} other {# Months}})', {
title, title,
count: d.data count: d.data
}); });
} else if ( d.badge === 'founder' ) { } else if ( d.badge === 'founder' ) {
title = this.i18n.t('badges.founder.months', '{title}\n(Subscribed for {count,number} Month{count,en_plural})', { title = this.i18n.t('badges.founder.months', '{title}\n(Subscribed for {count, plural, one {# Month} other {# Months}})', {
title, title,
count: d.data count: d.data
}); });

View file

@ -251,7 +251,7 @@ export const Clips = {
const extra = { const extra = {
type: 'i18n', key: 'clip.desc.2', type: 'i18n', key: 'clip.desc.2',
phrase: 'Clipped by {curator} — {views,number} View{views,en_plural}', phrase: 'Clipped by {curator} — {views, plural, one {# View} other {# Views}}',
content: { content: {
curator, curator,
views: clip.viewCount views: clip.viewCount

View file

@ -556,7 +556,7 @@ export default {
this.resetImport(); this.resetImport();
this.import_message = this.t('setting.backup-restore.imported', 'The profile "{name}" has been successfully imported with {count,number} setting{count,en_plural}.', { this.import_message = this.t('setting.backup-restore.imported', 'The profile "{name}" has been successfully imported with {count, plural, one {# setting} other {# settings}}.', {
name: prof.i18n_key ? this.t(prof.i18n_key, prof.title) : prof.title, name: prof.i18n_key ? this.t(prof.i18n_key, prof.title) : prof.title,
count: i count: i
}); });

View file

@ -15,7 +15,7 @@
<label :for="item.full_key" class="ffz-checkbox__label"> <label :for="item.full_key" class="ffz-checkbox__label">
<span class="tw-mg-l-1"> <span class="tw-mg-l-1">
{{ t(item.i18n_key, item.title) }} {{ item.i18n_key ? t(item.i18n_key, item.title) : item.title }}
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span> <span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
</span> </span>
</label> </label>

View file

@ -5,7 +5,7 @@
> >
<div class="tw-flex tw-align-items-center"> <div class="tw-flex tw-align-items-center">
<label :for="item.full_key"> <label :for="item.full_key">
{{ t(item.i18n_key, item.title) }} {{ item.i18n_key ? t(item.i18n_key, item.title) : item.title }}
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span> <span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
</label> </label>

View file

@ -5,7 +5,7 @@
> >
<div class="tw-flex tw-align-items-start"> <div class="tw-flex tw-align-items-start">
<label :for="item.full_key" class="tw-mg-y-05"> <label :for="item.full_key" class="tw-mg-y-05">
{{ t(item.i18n_key, item.title) }} {{ item.i18n_key ? t(item.i18n_key, item.title) : item.title }}
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span> <span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
</label> </label>

View file

@ -5,7 +5,7 @@
> >
<div class="tw-flex tw-align-items-center"> <div class="tw-flex tw-align-items-center">
<label :for="item.full_key"> <label :for="item.full_key">
{{ t(item.i18n_key, item.title) }} {{ item.i18n_key ? t(item.i18n_key, item.title) : item.title }}
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span> <span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
</label> </label>

View file

@ -1,7 +1,7 @@
<template lang="html"> <template lang="html">
<div class="ffz-input"> <div class="ffz-input">
<header> <header>
{{ t(item.i18n_key, item.title) }} {{ item.i18n_key ? t(item.i18n_key, item.title) : item.title }}
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span> <span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
</header> </header>
<section <section

View file

@ -5,7 +5,7 @@
> >
<div class="tw-flex tw-align-items-center"> <div class="tw-flex tw-align-items-center">
<label :for="item.full_key" class="tw-mg-y-05"> <label :for="item.full_key" class="tw-mg-y-05">
{{ t(item.i18n_key, item.title) }} {{ item.i18n_key ? t(item.i18n_key, item.title) : item.title }}
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span> <span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
</label> </label>

View file

@ -5,7 +5,7 @@
> >
<div class="tw-flex tw-align-items-center"> <div class="tw-flex tw-align-items-center">
<label :for="item.full_key"> <label :for="item.full_key">
{{ t(item.i18n_key, item.title) }} {{ item.i18n_key ? t(item.i18n_key, item.title) : item.title }}
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span> <span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
</label> </label>

View file

@ -260,6 +260,7 @@ export default class MainMenu extends Module {
async onEnable() { async onEnable() {
await this.site.awaitElement(Dialog.EXCLUSIVE); await this.site.awaitElement(Dialog.EXCLUSIVE);
await this.i18n.loadChunk('settings');
this.on('addons:added', this.scheduleUpdate, this); this.on('addons:added', this.scheduleUpdate, this);
this.on('experiments:enabled', this.scheduleUpdate, this); this.on('experiments:enabled', this.scheduleUpdate, this);
@ -631,16 +632,16 @@ export default class MainMenu extends Module {
]; ];
if ( tok.title ) { if ( tok.title ) {
terms.push(this.i18n.t(tok.i18n_key, tok.title, null, true)); terms.push(tok.i18n_key ? this.i18n.t(tok.i18n_key, tok.title, null, true) : tok.title);
if ( have_locale && this.i18n.has(tok.i18n_key) ) if ( have_locale && tok.i18n_key && this.i18n.has(tok.i18n_key) )
terms.push(this.i18n.t(tok.i18n_key, tok.title, null)); terms.push(this.i18n.t(tok.i18n_key, tok.title, null));
} }
if ( tok.description ) { if ( tok.description ) {
terms.push(this.i18n.t(tok.desc_i18n_key, tok.description, null, true)); terms.push(tok.desc_i18n_key ? this.i18n.t(tok.desc_i18n_key, tok.description, null, true) : tok.description);
if ( have_locale && this.i18n.has(tok.desc_i18n_key) ) if ( have_locale && tok.desc_i18n_key && this.i18n.has(tok.desc_i18n_key) )
terms.push(this.i18n.t(tok.desc_i18n_key, tok.description, null)); terms.push(this.i18n.t(tok.desc_i18n_key, tok.description, null));
} }
@ -670,19 +671,19 @@ export default class MainMenu extends Module {
} }
if ( ! token.search_terms ) { if ( ! token.search_terms ) {
const formatted = token.title && this.i18n.t(token.i18n_key, token.title, null, true); const formatted = token.title && (token.i18n_key ? this.i18n.t(token.i18n_key, token.title, null, true) : token.title);
let terms = [token.key]; let terms = [token.key];
if ( formatted && formatted.localeCompare(token.key, undefined, {sensitivity: 'base'}) ) if ( formatted && formatted.localeCompare(token.key, undefined, {sensitivity: 'base'}) )
terms.push(formatted); terms.push(formatted);
if ( have_locale && this.i18n.has(token.i18n_key) ) if ( have_locale && token.i18n_key && this.i18n.has(token.i18n_key) )
terms.push(this.i18n.t(token.i18n_key, token.title, null)); terms.push(this.i18n.t(token.i18n_key, token.title, null));
if ( token.description ) { if ( token.description ) {
terms.push(this.i18n.t(token.desc_i18n_key, token.description, null, true)); terms.push(token.desc_i18n_key ? this.i18n.t(token.desc_i18n_key, token.description, null, true) : token.description);
if ( have_locale && this.i18n.has(token.desc_i18n_key) ) if ( have_locale && token.desc_i18n_key && this.i18n.has(token.desc_i18n_key) )
terms.push(this.i18n.t(token.desc_i18n_key, token.description, null)); terms.push(this.i18n.t(token.desc_i18n_key, token.description, null));
} }

View file

@ -122,7 +122,7 @@ function parse(text) {
} }
} }
const NUMBER_TYPES = ['number', 'plural', 'en_plural', 'selectordinal', 'duration'] const NUMBER_TYPES = ['number', 'plural', 'selectordinal', 'duration']
function extractVariables(ast, out, vars, context) { function extractVariables(ast, out, vars, context) {
for(const node of ast) { for(const node of ast) {

View file

@ -64,10 +64,10 @@
</span> </span>
<span <span
v-if="subscription" v-if="subscription"
:data-title="t('viewer-card.months-tip', 'Subscribed for {months,number} month{months,en_plural}', {months: subscription.months})" :data-title="t('viewer-card.months-tip', 'Subscribed for {months, plural, one {# month} other {# months}}', {months: subscription.months})"
class="ffz-tooltip ffz-i-star viewer-card-drag-cancel" class="ffz-tooltip ffz-i-star viewer-card-drag-cancel"
> >
{{ t('viewer-card.months', '{months,number} month{months,en_plural}', {months: subscription.months}) }} {{ t('viewer-card.months', '{months, plural, one {# month} other {# months}}', {months: subscription.months}) }}
</span> </span>
</div> </div>
</div> </div>

View file

@ -824,7 +824,7 @@ export default class EmoteMenu extends Module {
if ( emote_lock ) { if ( emote_lock ) {
if ( emote_lock.id === 'cheer' ) { if ( emote_lock.id === 'cheer' ) {
sellout = t.i18n.t('emote-menu.emote-cheer', 'Cheer an additional {bits_remaining,number} bit{bits_remaining,en_plural} to unlock this emote.', emote_lock); sellout = t.i18n.t('emote-menu.emote-cheer', 'Cheer an additional {bits_remaining, plural, one {# bit} other {# bits}} to unlock this emote.', emote_lock);
} else if ( emote_lock.id === 'follower' ) { } else if ( emote_lock.id === 'follower' ) {
sellout = t.i18n.t('emote-menu.emote-follower', 'Follow {user} to unlock this emote in their channel.', emote_lock); sellout = t.i18n.t('emote-menu.emote-follower', 'Follow {user} to unlock this emote in their channel.', emote_lock);
} else if ( data.all_locked ) } else if ( data.all_locked )
@ -915,7 +915,7 @@ export default class EmoteMenu extends Module {
return (<div class="tw-mg-1 tw-border-t tw-pd-t-1 tw-mg-b-0"> return (<div class="tw-mg-1 tw-border-t tw-pd-t-1 tw-mg-b-0">
{lock ? {lock ?
t.i18n.t('emote-menu.sub-unlock', 'Subscribe for {price} to unlock {count,number} emote{count,en_plural}', {price: lock.price, count: lock.emotes.size}) : t.i18n.t('emote-menu.sub-unlock', 'Subscribe for {price} to unlock {count, plural, one {# emote} other {# emotes}}', {price: lock.price, count: lock.emotes.size}) :
t.i18n.t('emote-menu.sub-basic', 'Subscribe to unlock some emotes')} t.i18n.t('emote-menu.sub-basic', 'Subscribe to unlock some emotes')}
<div class="ffz--sub-buttons tw-mg-t-05"> <div class="ffz--sub-buttons tw-mg-t-05">
{locks.map(lock => lock.hide_button ? null : (<a {locks.map(lock => lock.hide_button ? null : (<a

View file

@ -562,7 +562,7 @@ other {# messages were deleted by a moderator.}
if ( mystery ) if ( mystery )
msg.mystery.line = this; msg.mystery.line = this;
const sub_msg = t.i18n.tList('chat.sub.gift', "{user} is gifting {count,number} Tier {tier} Sub{count,en_plural} to {channel}'s community! ", { const sub_msg = t.i18n.tList('chat.sub.gift', "{user} is gifting {count, plural, one {# Tier {tier} Sub} other {# Tier {tier} Subs}} to {channel}'s community! ", {
user: (msg.sub_anon || user.username === 'ananonymousgifter') ? user: (msg.sub_anon || user.username === 'ananonymousgifter') ?
t.i18n.t('chat.sub.anonymous-gifter', 'An anonymous gifter') : t.i18n.t('chat.sub.anonymous-gifter', 'An anonymous gifter') :
e('span', { e('span', {
@ -683,7 +683,7 @@ other {# messages were deleted by a moderator.}
if ( months <= 1 ) if ( months <= 1 )
sub_msg = t.i18n.tList('chat.sub.mystery', '{user} gifted a {plan} Sub to {recipient}! ', bits); sub_msg = t.i18n.tList('chat.sub.mystery', '{user} gifted a {plan} Sub to {recipient}! ', bits);
else else
sub_msg = t.i18n.tList('chat.sub.gift-months', '{user} gifted {months,number} month{months,en_plural} of {plan} Sub to {recipient}!', bits); sub_msg = t.i18n.tList('chat.sub.gift-months', '{user} gifted {months, plural, one {# month} other {# months}} of {plan} Sub to {recipient}!', bits);
if ( msg.sub_total === 1 ) if ( msg.sub_total === 1 )
sub_msg.push(t.i18n.t('chat.sub.gift-first', "It's their first time gifting a Sub in the channel!")); sub_msg.push(t.i18n.t('chat.sub.gift-first', "It's their first time gifting a Sub in the channel!"));

View file

@ -32,7 +32,7 @@ export default class SubButton extends Module {
this.SubButton = this.fine.define( this.SubButton = this.fine.define(
'sub-button', 'sub-button',
n => n.handleSubMenuAction && n.isUserDataReady, n => n.handleSubMenuAction && n.openSubModal,
['user', 'user-home', 'user-video', 'user-clip', 'video', 'user-videos', 'user-clips', 'user-collections', 'user-events', 'user-followers', 'user-following'] ['user', 'user-home', 'user-video', 'user-clip', 'video', 'user-videos', 'user-clips', 'user-collections', 'user-events', 'user-followers', 'user-following']
); );
} }

View file

@ -103,5 +103,6 @@ export default [
"volume-off", "volume-off",
"reply", "reply",
"threads", "threads",
"right-open" "right-open",
"list-bullet"
]; ];

View file

@ -75,6 +75,7 @@
.ffz-i-twitter:before { content: '\f099'; } /* '' */ .ffz-i-twitter:before { content: '\f099'; } /* '' */
.ffz-i-github:before { content: '\f09b'; } /* '' */ .ffz-i-github:before { content: '\f09b'; } /* '' */
.ffz-i-docs:before { content: '\f0c5'; } /* '' */ .ffz-i-docs:before { content: '\f0c5'; } /* '' */
.ffz-i-list-bullet:before { content: '\f0ca'; } /* '' */
.ffz-i-sort-down:before { content: '\f0dd'; } /* '' */ .ffz-i-sort-down:before { content: '\f0dd'; } /* '' */
.ffz-i-sort-up:before { content: '\f0de'; } /* '' */ .ffz-i-sort-up:before { content: '\f0de'; } /* '' */
.ffz-i-gauge:before { content: '\f0e4'; } /* '' */ .ffz-i-gauge:before { content: '\f0e4'; } /* '' */

File diff suppressed because one or more lines are too long

View file

@ -75,6 +75,7 @@
.ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf099;&nbsp;'); } .ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf099;&nbsp;'); }
.ffz-i-github { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf09b;&nbsp;'); } .ffz-i-github { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf09b;&nbsp;'); }
.ffz-i-docs { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0c5;&nbsp;'); } .ffz-i-docs { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0c5;&nbsp;'); }
.ffz-i-list-bullet { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0ca;&nbsp;'); }
.ffz-i-sort-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0dd;&nbsp;'); } .ffz-i-sort-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0dd;&nbsp;'); }
.ffz-i-sort-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0de;&nbsp;'); } .ffz-i-sort-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0de;&nbsp;'); }
.ffz-i-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e4;&nbsp;'); } .ffz-i-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e4;&nbsp;'); }

View file

@ -86,6 +86,7 @@
.ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf099;&nbsp;'); } .ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf099;&nbsp;'); }
.ffz-i-github { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf09b;&nbsp;'); } .ffz-i-github { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf09b;&nbsp;'); }
.ffz-i-docs { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0c5;&nbsp;'); } .ffz-i-docs { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0c5;&nbsp;'); }
.ffz-i-list-bullet { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0ca;&nbsp;'); }
.ffz-i-sort-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0dd;&nbsp;'); } .ffz-i-sort-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0dd;&nbsp;'); }
.ffz-i-sort-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0de;&nbsp;'); } .ffz-i-sort-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0de;&nbsp;'); }
.ffz-i-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e4;&nbsp;'); } .ffz-i-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e4;&nbsp;'); }

View file

@ -1,11 +1,11 @@
@font-face { @font-face {
font-family: 'ffz-fontello'; font-family: 'ffz-fontello';
src: url('../font/ffz-fontello.eot?52570921'); src: url('../font/ffz-fontello.eot?79984461');
src: url('../font/ffz-fontello.eot?52570921#iefix') format('embedded-opentype'), src: url('../font/ffz-fontello.eot?79984461#iefix') format('embedded-opentype'),
url('../font/ffz-fontello.woff2?52570921') format('woff2'), url('../font/ffz-fontello.woff2?79984461') format('woff2'),
url('../font/ffz-fontello.woff?52570921') format('woff'), url('../font/ffz-fontello.woff?79984461') format('woff'),
url('../font/ffz-fontello.ttf?52570921') format('truetype'), url('../font/ffz-fontello.ttf?79984461') format('truetype'),
url('../font/ffz-fontello.svg?52570921#ffz-fontello') format('svg'); url('../font/ffz-fontello.svg?79984461#ffz-fontello') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -15,12 +15,11 @@
@media screen and (-webkit-min-device-pixel-ratio:0) { @media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face { @font-face {
font-family: 'ffz-fontello'; font-family: 'ffz-fontello';
src: url('../font/ffz-fontello.svg?52570921#ffz-fontello') format('svg'); src: url('../font/ffz-fontello.svg?79984461#ffz-fontello') format('svg');
} }
} }
*/ */
[class^="ffz-i-"]:before, [class*=" ffz-i-"]:before {
[class^="ffz-i-"]:before, [class*=" ffz-i-"]:before {
font-family: "ffz-fontello"; font-family: "ffz-fontello";
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
@ -131,6 +130,7 @@
.ffz-i-twitter:before { content: '\f099'; } /* '' */ .ffz-i-twitter:before { content: '\f099'; } /* '' */
.ffz-i-github:before { content: '\f09b'; } /* '' */ .ffz-i-github:before { content: '\f09b'; } /* '' */
.ffz-i-docs:before { content: '\f0c5'; } /* '' */ .ffz-i-docs:before { content: '\f0c5'; } /* '' */
.ffz-i-list-bullet:before { content: '\f0ca'; } /* '' */
.ffz-i-sort-down:before { content: '\f0dd'; } /* '' */ .ffz-i-sort-down:before { content: '\f0dd'; } /* '' */
.ffz-i-sort-up:before { content: '\f0de'; } /* '' */ .ffz-i-sort-up:before { content: '\f0de'; } /* '' */
.ffz-i-gauge:before { content: '\f0e4'; } /* '' */ .ffz-i-gauge:before { content: '\f0e4'; } /* '' */

View file

@ -126,6 +126,10 @@ body {
.ffz__tooltip--arrow { .ffz__tooltip--arrow {
top: -3px; top: -3px;
border-radius: 2px 0 0; border-radius: 2px 0 0;
&:before {
left: -3px;
}
} }
} }
@ -133,6 +137,10 @@ body {
.ffz__tooltip--arrow { .ffz__tooltip--arrow {
bottom: 3px; bottom: 3px;
border-radius: 0 0 2px; border-radius: 0 0 2px;
&:before {
left: -3px;
}
} }
} }
@ -140,13 +148,21 @@ body {
.ffz__tooltip--arrow { .ffz__tooltip--arrow {
left: -3px; left: -3px;
border-radius: 0 2px 0 0; border-radius: 0 2px 0 0;
&:before {
top: -3px;
}
} }
} }
&[data-popper-placement^="left"] { &[data-popper-placement^="left"] {
.ffz__tooltip--arrow { .ffz__tooltip--arrow {
right: -3px; right: 3px;
border-radius: 0 0 0 2px; border-radius: 0 0 0 2px;
&:before {
top: -3px;
}
} }
} }
} }