1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +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",
"code": 59462,
"src": "fontawesome"
},
{
"uid": "a2a74f5e7b7d9ba054897d8c795a326a",
"css": "list-bullet",
"code": 61642,
"src": "fontawesome"
}
]
}

View file

@ -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.

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="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-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" />
@ -209,4 +211,4 @@
<glyph glyph-name="window-close" unicode="&#xf2d3;" 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

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 )
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 )

View file

@ -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 {

View file

@ -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

View file

@ -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
});

View file

@ -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

View file

@ -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
});

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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));
}

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) {
for(const node of ast) {

View file

@ -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>

View file

@ -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

View file

@ -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!"));

View file

@ -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']
);
}

View file

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

View file

@ -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

View file

@ -75,6 +75,7 @@
.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-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-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0de;&nbsp;'); }
.ffz-i-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e4;&nbsp;'); }
@ -99,4 +100,4 @@
.ffz-i-window-maximize { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf2d0;&nbsp;'); }
.ffz-i-window-minimize { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf2d1;&nbsp;'); }
.ffz-i-window-restore { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf2d2;&nbsp;'); }
.ffz-i-window-close { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf2d3;&nbsp;'); }
.ffz-i-window-close { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf2d3;&nbsp;'); }

View file

@ -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 = '&#xe800;&nbsp;'); }
.ffz-i-zreknarf { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
.ffz-i-search { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
@ -86,6 +86,7 @@
.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-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-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0de;&nbsp;'); }
.ffz-i-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e4;&nbsp;'); }
@ -110,4 +111,4 @@
.ffz-i-window-maximize { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf2d0;&nbsp;'); }
.ffz-i-window-minimize { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf2d1;&nbsp;'); }
.ffz-i-window-restore { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf2d2;&nbsp;'); }
.ffz-i-window-close { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf2d3;&nbsp;'); }
.ffz-i-window-close { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf2d3;&nbsp;'); }

View file

@ -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'; } /* '' */

View file

@ -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;
}
}
}
}