mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
4.30.1
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:
parent
958a3956f1
commit
97c96be276
33 changed files with 183 additions and 110 deletions
|
@ -779,6 +779,12 @@
|
|||
"css": "right-open",
|
||||
"code": 59462,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "a2a74f5e7b7d9ba054897d8c795a326a",
|
||||
"css": "list-bullet",
|
||||
"code": 61642,
|
||||
"src": "fontawesome"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.30.0",
|
||||
"version": "4.30.1",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
|
|
Binary file not shown.
|
@ -158,6 +158,8 @@
|
|||
|
||||
<glyph glyph-name="docs" unicode="" 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="" 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="" 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="" 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" />
|
||||
|
@ -209,4 +211,4 @@
|
|||
<glyph glyph-name="window-close" unicode="" d="M656 113l81 81q6 6 6 13t-6 13l-130 130 130 130q6 6 6 13t-6 13l-81 81q-6 6-13 6t-13-6l-130-130-130 130q-6 6-13 6t-13-6l-81-81q-6-6-6-13t6-13l130-130-130-130q-6-6-6-13t6-13l81-81q6-6 13-6t13 6l130 130 130-130q6-6 13-6t13 6z m344 576v-678q0-37-26-63t-63-27h-822q-36 0-63 27t-26 63v678q0 37 26 63t63 27h822q37 0 63-27t26-63z" horiz-adv-x="1000" />
|
||||
</font>
|
||||
</defs>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -294,6 +294,8 @@ export default class AddonManager extends Module {
|
|||
if ( ! addon )
|
||||
throw new Error(`Unknown add-on id: ${id}`);
|
||||
|
||||
await this.i18n.loadChunk(`addon.${id}`);
|
||||
|
||||
let module = this.resolve(`addon.${id}`);
|
||||
if ( module ) {
|
||||
if ( ! module.loaded )
|
||||
|
|
112
src/i18n.js
112
src/i18n.js
|
@ -6,12 +6,15 @@
|
|||
|
||||
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 { getBuster } from 'utilities/time';
|
||||
import Module from 'utilities/module';
|
||||
|
||||
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 STACK_SPLITTER = /\s*at\s+(.+?)\s+\((.+)\)$/,
|
||||
|
@ -78,6 +81,7 @@ export class TranslationManager extends Module {
|
|||
this._seen = new Set;
|
||||
|
||||
this.availableLocales = ['en'];
|
||||
this.chunks = ['client'];
|
||||
|
||||
this.localeData = {
|
||||
en: { name: 'English' }
|
||||
|
@ -350,6 +354,25 @@ export class TranslationManager extends Module {
|
|||
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) {
|
||||
if ( this._broadcaster )
|
||||
this._broadcaster.postMessage(msg);
|
||||
|
@ -448,32 +471,13 @@ export class TranslationManager extends Module {
|
|||
|
||||
this.strings_loading = true;
|
||||
|
||||
const loadPage = async page => {
|
||||
const resp = await fetch(`${API_SERVER}/v2/i18n/strings?page=${page}`);
|
||||
if ( ! resp.ok ) {
|
||||
this.log.warn(`Error Loading Strings -- Status: ${resp.status}`);
|
||||
return {
|
||||
next: false,
|
||||
strings: []
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
const resp = await fetch(`${SERVER}/script/locale/strings.json?_=${getBuster(30)}`);
|
||||
let strings;
|
||||
if (! resp.ok ) {
|
||||
this.log.warn(`Error Loading Strings -- Status: ${resp.status}`);
|
||||
strings = [];
|
||||
} else
|
||||
strings = await resp.json();
|
||||
|
||||
for(const str of strings) {
|
||||
const key = str.id;
|
||||
|
@ -629,19 +633,20 @@ export class TranslationManager extends Module {
|
|||
|
||||
|
||||
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 ) {
|
||||
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 )
|
||||
data = [{
|
||||
id: 'en',
|
||||
name: 'English',
|
||||
coverage: 100,
|
||||
rtl: false
|
||||
rtl: false,
|
||||
hashes: {}
|
||||
}];
|
||||
|
||||
this.localeData = {};
|
||||
|
@ -657,11 +662,46 @@ export class TranslationManager extends Module {
|
|||
}
|
||||
|
||||
|
||||
async loadLocale(locale) {
|
||||
async loadLocale(locale, chunk = null) {
|
||||
if ( locale === 'en' )
|
||||
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.status === 404 ) {
|
||||
this.log.info(`Cannot Load Locale: ${locale}`);
|
||||
|
@ -673,7 +713,7 @@ export class TranslationManager extends Module {
|
|||
}
|
||||
|
||||
const data = await resp.json();
|
||||
return data?.phrases;
|
||||
return data?.phrases;*/
|
||||
}
|
||||
|
||||
async setLocale(new_locale) {
|
||||
|
@ -713,7 +753,7 @@ export class TranslationManager extends Module {
|
|||
}
|
||||
|
||||
async loadDayjsLocale(locale) {
|
||||
if ( locale === 'en' )
|
||||
if ( locale === 'en' || locale === 'en-arrr' )
|
||||
return;
|
||||
|
||||
try {
|
||||
|
|
|
@ -276,11 +276,11 @@ export const timeout = {
|
|||
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-timeout.vue'),
|
||||
|
||||
title: 'Timeout User',
|
||||
description: '{options.duration,number} second{options.duration,en_plural}',
|
||||
description: '{options.duration, plural, one {# second} other {# seconds}}',
|
||||
|
||||
reason_text(data) {
|
||||
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,
|
||||
duration: data.options.duration
|
||||
|
@ -291,7 +291,7 @@ export const timeout = {
|
|||
tooltip(data) {
|
||||
return this.i18n.t(
|
||||
'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,
|
||||
duration: data.options.duration
|
||||
|
|
|
@ -466,18 +466,18 @@ export default class Badges extends Module {
|
|||
if ( d.data ) {
|
||||
if ( d.badge === 'subscriber' ) {
|
||||
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,
|
||||
tier,
|
||||
months: d.data
|
||||
});
|
||||
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,
|
||||
count: d.data
|
||||
});
|
||||
} 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,
|
||||
count: d.data
|
||||
});
|
||||
|
|
|
@ -251,7 +251,7 @@ export const Clips = {
|
|||
|
||||
const extra = {
|
||||
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: {
|
||||
curator,
|
||||
views: clip.viewCount
|
||||
|
|
|
@ -556,7 +556,7 @@ export default {
|
|||
|
||||
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,
|
||||
count: i
|
||||
});
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
<label :for="item.full_key" class="ffz-checkbox__label">
|
||||
<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>
|
||||
</label>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
>
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<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>
|
||||
</label>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
>
|
||||
<div class="tw-flex tw-align-items-start">
|
||||
<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>
|
||||
</label>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
>
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<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>
|
||||
</label>
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template lang="html">
|
||||
<div class="ffz-input">
|
||||
<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>
|
||||
</header>
|
||||
<section
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
>
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<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>
|
||||
</label>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
>
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<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>
|
||||
</label>
|
||||
|
||||
|
|
|
@ -260,6 +260,7 @@ export default class MainMenu extends Module {
|
|||
|
||||
async onEnable() {
|
||||
await this.site.awaitElement(Dialog.EXCLUSIVE);
|
||||
await this.i18n.loadChunk('settings');
|
||||
|
||||
this.on('addons:added', this.scheduleUpdate, this);
|
||||
this.on('experiments:enabled', this.scheduleUpdate, this);
|
||||
|
@ -631,16 +632,16 @@ export default class MainMenu extends Module {
|
|||
];
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
|
@ -670,19 +671,19 @@ export default class MainMenu extends Module {
|
|||
}
|
||||
|
||||
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];
|
||||
|
||||
if ( formatted && formatted.localeCompare(token.key, undefined, {sensitivity: 'base'}) )
|
||||
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));
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
for(const node of ast) {
|
||||
|
|
|
@ -64,10 +64,10 @@
|
|||
</span>
|
||||
<span
|
||||
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"
|
||||
>
|
||||
{{ 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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -824,7 +824,7 @@ export default class EmoteMenu extends Module {
|
|||
|
||||
if ( emote_lock ) {
|
||||
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' ) {
|
||||
sellout = t.i18n.t('emote-menu.emote-follower', 'Follow {user} to unlock this emote in their channel.', emote_lock);
|
||||
} 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">
|
||||
{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')}
|
||||
<div class="ffz--sub-buttons tw-mg-t-05">
|
||||
{locks.map(lock => lock.hide_button ? null : (<a
|
||||
|
|
|
@ -562,7 +562,7 @@ other {# messages were deleted by a moderator.}
|
|||
if ( mystery )
|
||||
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') ?
|
||||
t.i18n.t('chat.sub.anonymous-gifter', 'An anonymous gifter') :
|
||||
e('span', {
|
||||
|
@ -683,7 +683,7 @@ other {# messages were deleted by a moderator.}
|
|||
if ( months <= 1 )
|
||||
sub_msg = t.i18n.tList('chat.sub.mystery', '{user} gifted a {plan} Sub to {recipient}! ', bits);
|
||||
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 )
|
||||
sub_msg.push(t.i18n.t('chat.sub.gift-first', "It's their first time gifting a Sub in the channel!"));
|
||||
|
|
|
@ -32,7 +32,7 @@ export default class SubButton extends Module {
|
|||
|
||||
this.SubButton = this.fine.define(
|
||||
'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']
|
||||
);
|
||||
}
|
||||
|
|
|
@ -103,5 +103,6 @@ export default [
|
|||
"volume-off",
|
||||
"reply",
|
||||
"threads",
|
||||
"right-open"
|
||||
"right-open",
|
||||
"list-bullet"
|
||||
];
|
|
@ -75,6 +75,7 @@
|
|||
.ffz-i-twitter:before { content: '\f099'; } /* '' */
|
||||
.ffz-i-github:before { content: '\f09b'; } /* '' */
|
||||
.ffz-i-docs:before { content: '\f0c5'; } /* '' */
|
||||
.ffz-i-list-bullet:before { content: '\f0ca'; } /* '' */
|
||||
.ffz-i-sort-down:before { content: '\f0dd'; } /* '' */
|
||||
.ffz-i-sort-up:before { content: '\f0de'; } /* '' */
|
||||
.ffz-i-gauge:before { content: '\f0e4'; } /* '' */
|
||||
|
@ -99,4 +100,4 @@
|
|||
.ffz-i-window-maximize:before { content: '\f2d0'; } /* '' */
|
||||
.ffz-i-window-minimize:before { content: '\f2d1'; } /* '' */
|
||||
.ffz-i-window-restore:before { content: '\f2d2'; } /* '' */
|
||||
.ffz-i-window-close:before { content: '\f2d3'; } /* '' */
|
||||
.ffz-i-window-close:before { content: '\f2d3'; } /* '' */
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -75,6 +75,7 @@
|
|||
.ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-github { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-docs { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-list-bullet { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-sort-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-sort-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
@ -99,4 +100,4 @@
|
|||
.ffz-i-window-maximize { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-window-minimize { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-window-restore { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-window-close { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-window-close { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
font-family: 'ffz-fontello';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
|
||||
|
||||
/* fix buttons height */
|
||||
line-height: 1em;
|
||||
|
||||
|
||||
/* you can be more comfortable with increased icons size */
|
||||
/* font-size: 120%; */
|
||||
}
|
||||
|
||||
|
||||
.ffz-i-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-zreknarf { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-search { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
@ -86,6 +86,7 @@
|
|||
.ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-github { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-docs { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-list-bullet { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-sort-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-sort-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
@ -110,4 +111,4 @@
|
|||
.ffz-i-window-maximize { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-window-minimize { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-window-restore { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-window-close { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.ffz-i-window-close { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
@font-face {
|
||||
font-family: 'ffz-fontello';
|
||||
src: url('../font/ffz-fontello.eot?52570921');
|
||||
src: url('../font/ffz-fontello.eot?52570921#iefix') format('embedded-opentype'),
|
||||
url('../font/ffz-fontello.woff2?52570921') format('woff2'),
|
||||
url('../font/ffz-fontello.woff?52570921') format('woff'),
|
||||
url('../font/ffz-fontello.ttf?52570921') format('truetype'),
|
||||
url('../font/ffz-fontello.svg?52570921#ffz-fontello') format('svg');
|
||||
src: url('../font/ffz-fontello.eot?79984461');
|
||||
src: url('../font/ffz-fontello.eot?79984461#iefix') format('embedded-opentype'),
|
||||
url('../font/ffz-fontello.woff2?79984461') format('woff2'),
|
||||
url('../font/ffz-fontello.woff?79984461') format('woff'),
|
||||
url('../font/ffz-fontello.ttf?79984461') format('truetype'),
|
||||
url('../font/ffz-fontello.svg?79984461#ffz-fontello') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
@ -15,46 +15,45 @@
|
|||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
@font-face {
|
||||
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-style: normal;
|
||||
font-weight: normal;
|
||||
speak: never;
|
||||
|
||||
|
||||
display: inline-block;
|
||||
text-decoration: inherit;
|
||||
width: 1em;
|
||||
margin-right: .2em;
|
||||
text-align: center;
|
||||
/* opacity: .8; */
|
||||
|
||||
|
||||
/* For safety - reset parent styles, that can break glyph codes*/
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
|
||||
|
||||
/* fix buttons height, for twitter bootstrap */
|
||||
line-height: 1em;
|
||||
|
||||
|
||||
/* Animation center compensation - margins should be symmetric */
|
||||
/* remove if not needed */
|
||||
margin-left: .2em;
|
||||
|
||||
|
||||
/* you can be more comfortable with increased icons size */
|
||||
/* font-size: 120%; */
|
||||
|
||||
|
||||
/* Font smoothing. That was taken from TWBS */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
|
||||
/* Uncomment for 3D effect */
|
||||
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
|
||||
}
|
||||
|
||||
|
||||
.ffz-i-cancel:before { content: '\e800'; } /* '' */
|
||||
.ffz-i-zreknarf:before { content: '\e801'; } /* '' */
|
||||
.ffz-i-search:before { content: '\e802'; } /* '' */
|
||||
|
@ -131,6 +130,7 @@
|
|||
.ffz-i-twitter:before { content: '\f099'; } /* '' */
|
||||
.ffz-i-github:before { content: '\f09b'; } /* '' */
|
||||
.ffz-i-docs:before { content: '\f0c5'; } /* '' */
|
||||
.ffz-i-list-bullet:before { content: '\f0ca'; } /* '' */
|
||||
.ffz-i-sort-down:before { content: '\f0dd'; } /* '' */
|
||||
.ffz-i-sort-up:before { content: '\f0de'; } /* '' */
|
||||
.ffz-i-gauge:before { content: '\f0e4'; } /* '' */
|
||||
|
@ -155,4 +155,4 @@
|
|||
.ffz-i-window-maximize:before { content: '\f2d0'; } /* '' */
|
||||
.ffz-i-window-minimize:before { content: '\f2d1'; } /* '' */
|
||||
.ffz-i-window-restore:before { content: '\f2d2'; } /* '' */
|
||||
.ffz-i-window-close:before { content: '\f2d3'; } /* '' */
|
||||
.ffz-i-window-close:before { content: '\f2d3'; } /* '' */
|
||||
|
|
|
@ -126,6 +126,10 @@ body {
|
|||
.ffz__tooltip--arrow {
|
||||
top: -3px;
|
||||
border-radius: 2px 0 0;
|
||||
|
||||
&:before {
|
||||
left: -3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,6 +137,10 @@ body {
|
|||
.ffz__tooltip--arrow {
|
||||
bottom: 3px;
|
||||
border-radius: 0 0 2px;
|
||||
|
||||
&:before {
|
||||
left: -3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,13 +148,21 @@ body {
|
|||
.ffz__tooltip--arrow {
|
||||
left: -3px;
|
||||
border-radius: 0 2px 0 0;
|
||||
|
||||
&:before {
|
||||
top: -3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-popper-placement^="left"] {
|
||||
.ffz__tooltip--arrow {
|
||||
right: -3px;
|
||||
right: 3px;
|
||||
border-radius: 0 0 0 2px;
|
||||
|
||||
&:before {
|
||||
top: -3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue