diff --git a/package-lock.json b/package-lock.json
index 40ed8a15..2a0eeb24 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -176,6 +176,11 @@
}
}
},
+ "@ffz/icu-msgparser": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@ffz/icu-msgparser/-/icu-msgparser-1.0.1.tgz",
+ "integrity": "sha512-0+i29DZUJIqrK02rHcxDXmuLSOyp4EJERu4uQDOh7w39d5TqXHbFP2CRtSXjYYCQZWHoKwn37ZZScn+8+zH56g=="
+ },
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
diff --git a/package.json b/package.json
index 4a9d9831..d934e736 100755
--- a/package.json
+++ b/package.json
@@ -54,6 +54,7 @@
"url": "https://github.com/FrankerFaceZ/FrankerFaceZ.git"
},
"dependencies": {
+ "@ffz/icu-msgparser": "^1.0.1",
"crypto-js": "^3.1.9-1",
"dayjs": "^1.7.7",
"displacejs": "^1.2.4",
diff --git a/src/i18n.js b/src/i18n.js
index 624609b9..9460c8dc 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -2,35 +2,32 @@
// ============================================================================
// Localization
-// This is based on Polyglot, but with some changes to avoid dependencies on
-// additional libraries and with support for Vue.
// ============================================================================
+import Parser from '@ffz/icu-msgparser';
+
import {SERVER} from 'utilities/constants';
import {get, pick_random, has, timeout} from 'utilities/object';
import Module from 'utilities/module';
+import NewTransCore from 'utilities/translation-core';
+
const FACES = ['(・`ω´・)', ';;w;;', 'owo', 'ono', 'oAo', 'oxo', 'ovo;', 'UwU', '>w<', '^w^', '> w >', 'v.v'],
- format_text = (phrase, token_regex, formatter) => {
- const out = [];
+ transformText = (ast, fn) => {
+ return ast.map(node => {
+ if ( typeof node === 'string' )
+ return fn(node);
- let i = 0, match;
- token_regex.lastIndex = 0;
+ else if ( typeof node === 'object' && node.o ) {
+ const out = Object.assign(node, {o: {}});
+ for(const key of Object.keys(node.o))
+ out.o[key] = transformText(node.o[key], fn)
+ }
- while((match = token_regex.exec(phrase))) {
- if ( match.index !== i )
- out.push(formatter(phrase.slice(i, match.index)))
-
- out.push(match[0]);
- i = match.index + match[0].length;
- }
-
- if ( i < phrase.length )
- out.push(formatter(phrase.slice(i)));
-
- return out.join('')
+ return node;
+ })
},
owo = text => text
@@ -44,19 +41,12 @@ const FACES = ['(・`ω´・)', ';;w;;', 'owo', 'ono', 'oAo', 'oxo', 'ovo;', 'Uw
TRANSFORMATIONS = {
- double: (key, text) =>
- `${text} ${text}`,
-
- upper: (key, text, opts, locale, token_regex) =>
- format_text(text, token_regex, t => t.toUpperCase()),
-
- lower: (key, text, opts, locale, token_regex) =>
- format_text(text, token_regex, t => t.toLowerCase()),
-
- append_key: (key, text) => `${text} (${key})`,
-
- owo: (key, text, opts, locale, token_regex) =>
- format_text(text, token_regex, t => owo(t))
+ double: (key, ast) => [...ast, ' ', ...ast],
+ upper: (key, ast) => transformText(ast, n => n.toUpperCase()),
+ lower: (key, ast) => transformText(ast, n => n.toLowerCase()),
+ append_key: (key, ast) => [...ast, ` (${key})`],
+ set_key: (key, ast) => [key],
+ owo: (key, ast) => transformText(ast, owo)
};
@@ -69,14 +59,16 @@ export class TranslationManager extends Module {
super(...args);
this.inject('settings');
+ this.parser = new Parser;
+
this._seen = new Set;
this.availableLocales = ['en']; //, 'de', 'ja'];
this.localeData = {
- en: { name: 'English' },
- //de: { name: 'Deutsch' },
- //ja: { name: '日本語' }
+ en: { name: 'English' }/*,
+ de: { name: 'Deutsch' },
+ ja: { name: '日本語' }*/
}
@@ -92,6 +84,7 @@ export class TranslationManager extends Module {
{value: 'upper', title: 'Upper Case'},
{value: 'lower', title: 'Lower Case'},
{value: 'append_key', title: 'Append Key'},
+ {value: 'set_key', title: 'Set to Key'},
{value: 'double', title: 'Double'},
{value: 'owo', title: "owo what's this"}
]
@@ -137,10 +130,8 @@ export class TranslationManager extends Module {
}
onEnable() {
- this._ = new TranslationCore({
- formatters: {
- 'humanTime': n => this.toHumanTime(n)
- }
+ this._ = new NewTransCore({ //TranslationCore({
+ warn: (...args) => this.log.warn(...args),
});
if ( window.BroadcastChannel ) {
@@ -210,48 +201,8 @@ export class TranslationManager extends Module {
this.broadcast({type: 'seen', key});
}
-
- toLocaleString(thing) {
- if ( thing && thing.toLocaleString )
- return thing.toLocaleString(this._.locale);
- return thing;
- }
-
-
- toHumanTime(duration, factor = 1) {
- // TODO: Make this better. Make all time handling better in fact.
-
- if ( duration instanceof Date )
- duration = (Date.now() - duration.getTime()) / 1000;
-
- duration = Math.floor(duration);
-
- const years = Math.floor((duration * factor) / 31536000) / factor;
- if ( years >= 1 )
- return this.t('human-time.years', '%{count} year%{count|en_plural}', years);
-
- const days = Math.floor((duration %= 31536000) / 86400);
- if ( days >= 1 )
- return this.t('human-time.days', '%{count} day%{count|en_plural}', days);
-
- const hours = Math.floor((duration %= 86400) / 3600);
- if ( hours >= 1 )
- return this.t('human-time.hours', '%{count} hour%{count|en_plural}', hours);
-
- const minutes = Math.floor((duration %= 3600) / 60);
- if ( minutes >= 1 )
- return this.t('human-time.minutes', '%{count} minute%{count|en_plural}', minutes);
-
- const seconds = duration % 60;
- if ( seconds >= 1 )
- return this.t('human-time.seconds', '%{count} second%{count|en_plural}', seconds);
-
- return this.t('human-time.none', 'less than a second');
- }
-
-
async loadLocale(locale) {
- /*if ( locale === 'en' )
+ if ( locale === 'en' )
return {};
if ( locale === 'de' )
@@ -363,7 +314,7 @@ export class TranslationManager extends Module {
support: '対応'
}
}
- }*/
+ }
const resp = await fetch(`${SERVER}/script/i18n/${locale}.json`);
if ( ! resp.ok ) {
@@ -384,6 +335,8 @@ export class TranslationManager extends Module {
if ( new_locale === old_locale )
return [];
+ await this.loadDayjsLocale(new_locale);
+
this._.locale = new_locale;
this._.clear();
this.log.info(`Changed Locale: ${new_locale} -- Old: ${old_locale}`);
@@ -412,14 +365,49 @@ export class TranslationManager extends Module {
return added;
}
+ async loadDayjsLocale(locale) {
+ if ( locale === 'en' )
+ return;
+
+ try {
+ await import(
+ /* webpackMode: 'lazy' */
+ /* webpackChunkName: 'i18n-[index]' */
+ `dayjs/locale/${locale}`
+ );
+ } catch(err) {
+ this.log.warn(`Unable to load day.js locale data for locale "${locale}"`, err);
+ }
+ }
+
has(key) {
return this._.has(key);
}
+ toLocaleString(...args) {
+ return this._.toLocaleString(...args);
+ }
+
+ toHumanTime(...args) {
+ return this._.formatHumanTime(...args);
+ }
+
formatNumber(...args) {
return this._.formatNumber(...args);
}
+ formatDate(...args) {
+ return this._.formatDate(...args)
+ }
+
+ formatTime(...args) {
+ return this._.formatTime(...args)
+ }
+
+ formatDateTime(...args) {
+ return this._.formatDateTime(...args)
+ }
+
t(key, ...args) {
this.see(key);
return this._.t(key, ...args);
@@ -437,7 +425,7 @@ export class TranslationManager extends Module {
// TranslationCore
// ============================================================================
-const REPLACE = String.prototype.replace,
+const REPLACE = String.prototype.replace; /*,
SPLIT = String.prototype.split;
const DEFAULT_FORMATTERS = {
@@ -460,7 +448,7 @@ export default class TranslationCore {
this.onMissingKey = typeof options.onMissingKey === 'function' ? options.onMissingKey : allowMissing;
this.transformPhrase = typeof options.transformPhrase === 'function' ? options.transformPhrase : transformPhrase;
this.transformList = typeof options.transformList === 'function' ? options.transformList : transformList;
- this.delimiter = options.delimiter || /\s*\|\|\|\|\s*/;
+ this.delimiter = options.delimiter || /\s*\|\|\|\|\s/;
this.tokenRegex = options.tokenRegex || /%\{(.*?)(?:\|(.*?))?\}/g;
this.formatters = Object.assign({}, DEFAULT_FORMATTERS, options.formatters || {});
}
@@ -575,7 +563,7 @@ export default class TranslationCore {
return this.transformList(p, opts, locale, this.tokenRegex, this.formatters);
}
-}
+}*/
// ============================================================================
@@ -593,10 +581,7 @@ export function transformList(phrase, substitutions, locale, token_regex, format
const options = typeof substitutions === 'number' ? {count: substitutions} : substitutions;
if ( is_array )
- p = p[pluralTypeIndex(
- locale || 'en',
- has(options, 'count') ? options.count : 1
- )] || p[0];
+ p = p[0];
const result = [];
@@ -642,10 +627,7 @@ export function transformPhrase(phrase, substitutions, locale, token_regex, form
const options = typeof substitutions === 'number' ? {count: substitutions} : substitutions;
if ( is_array )
- result = result[pluralTypeIndex(
- locale || 'en',
- has(options, 'count') ? options.count : 1
- )] || result[0];
+ result = result[0];
if ( typeof result === 'string' )
result = REPLACE.call(result, token_regex, (expr, arg, fmt) => {
@@ -663,65 +645,4 @@ export function transformPhrase(phrase, substitutions, locale, token_regex, form
});
return result;
-}
-
-
-// ============================================================================
-// Plural Nonsense
-// ============================================================================
-
-const PLURAL_TYPE_TO_LANG = {
- arabic: ['ar'],
- chinese: ['fa', 'id', 'ja', 'ko', 'lo', 'ms', 'th', 'tr', 'zh'],
- german: ['da', 'de', 'en', 'es', 'es', 'fi', 'el', 'he', 'hu', 'it', 'nl', 'no', 'pt', 'sv'],
- french: ['fr', 'tl', 'pt-br'],
- russian: ['hr', 'ru', 'lt'],
- czech: ['cs', 'sk'],
- polish: ['pl'],
- icelandic: ['is']
-};
-
-const PLURAL_LANG_TO_TYPE = {};
-
-for(const type in PLURAL_TYPE_TO_LANG) // eslint-disable-line guard-for-in
- for(const lang of PLURAL_TYPE_TO_LANG[type])
- PLURAL_LANG_TO_TYPE[lang] = type;
-
-const PLURAL_TYPES = {
- arabic: n => {
- if ( n < 3 ) return n;
- const n1 = n % 100;
- if ( n1 >= 3 && n1 <= 10 ) return 3;
- return n1 >= 11 ? 4 : 5;
- },
- chinese: () => 0,
- german: n => n !== 1 ? 1 : 0,
- french: n => n > 1 ? 1 : 0,
- russian: n => {
- const n1 = n % 10, n2 = n % 100;
- if ( n1 === 1 && n2 !== 11 ) return 0;
- return n1 >= 2 && n1 <= 4 && (n2 < 10 || n2 >= 20) ? 1 : 2;
- },
- czech: n => {
- if ( n === 1 ) return 0;
- return n >= 2 && n <= 4 ? 1 : 2;
- },
- polish: n => {
- if ( n === 1 ) return 0;
- const n1 = n % 10, n2 = n % 100;
- return n1 >= 2 && n1 <= 4 && (n2 < 10 || n2 >= 20) ? 1 : 2;
- },
- icelandic: n => n % 10 !== 1 || n % 100 === 11 ? 1 : 0
-};
-
-
-export function pluralTypeIndex(locale, n) {
- let type = PLURAL_LANG_TO_TYPE[locale];
- if ( ! type ) {
- const idx = locale.indexOf('-');
- if ( idx !== -1 )
- type = PLURAL_LANG_TO_TYPE[locale.slice(0, idx)]
- }
-
- return PLURAL_TYPES[type || 'german'](n);
}
\ No newline at end of file
diff --git a/src/main.js b/src/main.js
index 522164cc..0b76d4f5 100644
--- a/src/main.js
+++ b/src/main.js
@@ -149,7 +149,7 @@ ${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}`
FrankerFaceZ.Logger = Logger;
const VER = FrankerFaceZ.version_info = {
- major: 4, minor: 0, revision: 0, extra: '-rc18.2',
+ major: 4, minor: 0, revision: 0, extra: '-rc19',
commit: __git_commit__,
build: __webpack_hash__,
toString: () =>
@@ -166,7 +166,9 @@ FrankerFaceZ.utilities = {
logging: require('utilities/logging'),
object: require('utilities/object'),
time: require('utilities/time'),
- tooltip: require('utilities/tooltip')
+ tooltip: require('utilities/tooltip'),
+ i18n: require('utilities/translation-core'),
+ dayjs: require('dayjs')
}
diff --git a/src/modules/chat/actions/components/edit-chat.vue b/src/modules/chat/actions/components/edit-chat.vue
index 3ac31855..ad86997b 100644
--- a/src/modules/chat/actions/components/edit-chat.vue
+++ b/src/modules/chat/actions/components/edit-chat.vue
@@ -14,7 +14,7 @@
>
- {{ t('setting.actions.variables', 'Available Variables: %{vars}', {vars}) }}
+ {{ t('setting.actions.variables', 'Available Variables: {vars}', {vars}) }}
diff --git a/src/modules/chat/actions/components/edit-url.vue b/src/modules/chat/actions/components/edit-url.vue
index b835c186..0618ea62 100644
--- a/src/modules/chat/actions/components/edit-url.vue
+++ b/src/modules/chat/actions/components/edit-url.vue
@@ -14,7 +14,7 @@
>
- {{ t('setting.actions.variables', 'Available Variables: %{vars}', {vars}) }}
+ {{ t('setting.actions.variables', 'Available Variables: {vars}', {vars}) }}
diff --git a/src/modules/chat/actions/index.jsx b/src/modules/chat/actions/index.jsx
index 2a3c1544..cb903bf5 100644
--- a/src/modules/chat/actions/index.jsx
+++ b/src/modules/chat/actions/index.jsx
@@ -40,7 +40,7 @@ export default class Actions extends Module {
always_inherit: true,
ui: {
- path: 'Chat > Actions > Reasons',
+ path: 'Chat > Actions > Reasons >> Custom Reasons',
component: 'chat-reasons',
}
});
@@ -80,6 +80,15 @@ export default class Actions extends Module {
}
});
+ this.settings.add('chat.actions.rules-as-reasons', {
+ default: true,
+ ui: {
+ path: 'Chat > Actions > Reasons >> Rules',
+ component: 'setting-check-box',
+ title: "Include the current room's rules in the list of reasons."
+ }
+ });
+
this.settings.add('chat.actions.viewer-card', {
// Filter out actions
process: (ctx, val) =>
@@ -184,7 +193,7 @@ export default class Actions extends Module {
renderInlineReasons(data, t, tip) {
const reasons = this.parent.context.get('chat.actions.reasons'),
reason_elements = [],
- room = this.parent.getRoom(data.room.id, data.room.login, true),
+ room = this.parent.context.get('chat.actions.rules-as-reasons') && this.parent.getRoom(data.room.id, data.room.login, true),
rules = room && room.rules;
if ( ! reasons && ! rules ) {
@@ -201,7 +210,7 @@ export default class Actions extends Module {
if ( reasons && reasons.length ) {
for(const reason of reasons) {
- const text = this.replaceVariables(reason.i18n ? this.i18n.t(reason.i18n, reason.text) : reason.text, data);
+ const text = this.replaceVariables((typeof reason.i18n === 'string') ? this.i18n.t(reason.i18n, reason.text) : reason.text, data);
reason_elements.push(
import(/* webpackChunkName: 'main-menu' */ './components/edit-timeout.vue'),
title: 'Timeout User',
- description: '%{options.duration} second%{options.duration|en_plural}',
+ description: '{options.duration,number} second%{options.duration,en_plural}',
reason_text(data) {
return this.i18n.t('chat.actions.timeout-reason',
- 'Timeout %{user.login} for %{duration} second%{duration|en_plural} for:',
+ 'Timeout {user.login} for {duration,number} second{duration,en_plural} for:',
{
user: data.user,
duration: data.options.duration
@@ -198,7 +198,7 @@ export const timeout = {
tooltip(data) {
return this.i18n.t(
'chat.actions.timeout',
- 'Timeout %{user.login} for %{duration} second%{duration|en_plural}',
+ 'Timeout {user.login} for {duration,number} second{duration,en_plural}',
{
user: data.user,
duration: data.options.duration
@@ -230,7 +230,7 @@ export const unban = {
title: 'Unban User',
tooltip(data) {
- return this.i18n.t('chat.actions.unban', 'Unban %{user.login}', {user: data.user});
+ return this.i18n.t('chat.actions.unban', 'Unban {user.login}', {user: data.user});
},
click(event, data) {
@@ -257,7 +257,7 @@ export const untimeout = {
title: 'Untimeout User',
tooltip(data) {
- return this.i18n.t('chat.actions.untimeout', 'Untimeout %{user.login}', {user: data.user});
+ return this.i18n.t('chat.actions.untimeout', 'Untimeout {user.login}', {user: data.user});
},
click(event, data) {
@@ -283,7 +283,7 @@ export const whisper = {
title: 'Whisper User',
tooltip(data) {
- return this.i18n.t('chat.actions.whisper', 'Whisper %{user.login}', data);
+ return this.i18n.t('chat.actions.whisper', 'Whisper {user.login}', data);
},
click(event, data) {
@@ -326,7 +326,7 @@ export const gift_sub = {
title: 'Gift Subscription',
tooltip(data) {
- return this.i18n.t('chat.actions.gift_sub', 'Gift a Sub to %{user.login}', data);
+ return this.i18n.t('chat.actions.gift_sub', 'Gift a Sub to {user.login}', data);
},
context() {
diff --git a/src/modules/chat/badges.jsx b/src/modules/chat/badges.jsx
index c2df36bd..79763b6a 100644
--- a/src/modules/chat/badges.jsx
+++ b/src/modules/chat/badges.jsx
@@ -284,7 +284,7 @@ export default class Badges extends Module {
let title = bd.title || global_badge.title;
if ( d.data ) {
if ( d.badge === 'subscriber' ) {
- title = this.i18n.t('badges.subscriber.months', '%{title} (%{count} Month%{count|en_plural})', {
+ title = this.i18n.t('badges.subscriber.months', '{title} ({count,number} Month{count,en_plural})', {
title,
count: d.data
});
@@ -645,16 +645,23 @@ export default class Badges extends Module {
return b;
}
+ hasTwitchBadges() {
+ return !! this.twitch_badges
+ }
+
updateTwitchBadges(badges) {
- if ( ! badges )
+ if ( ! Array.isArray(badges) )
this.twitch_badges = badges;
else {
- const b = {};
- for(const data of badges) {
- const sid = data.setID,
- bs = b[sid] = b[sid] || {__game: /_\d+$/.test(sid)};
+ let b = null;
+ if ( badges.length ) {
+ b = {};
+ for(const data of badges) {
+ const sid = data.setID,
+ bs = b[sid] = b[sid] || {__game: /_\d+$/.test(sid)};
- bs[data.version] = data;
+ bs[data.version] = data;
+ }
}
this.twitch_badges = b;
diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js
index c6dd28db..382996d2 100644
--- a/src/modules/chat/index.js
+++ b/src/modules/chat/index.js
@@ -1168,7 +1168,7 @@ export default class Chat extends Module {
l = parts.length,
emotes = {};
- let idx = 0, ret, last_type = null;
+ let idx = 0, ret, last_type = null, bits = 0;
for(let i=0; i < l; i++) {
const part = parts[i],
@@ -1186,10 +1186,11 @@ export default class Chat extends Module {
else if ( content.url )
ret = content.url;
- else if ( content.cheerAmount )
+ else if ( content.cheerAmount ) {
+ bits += content.cheerAmount;
ret = `${content.alt}${content.cheerAmount}`;
- else if ( content.images ) {
+ } else if ( content.images ) {
const url = (content.images.themed ? content.images.dark : content.images.sources),
match = url && /\/emoticons\/v1\/(\d+)\/[\d.]+$/.exec(url['1x']),
id = match && match[1];
@@ -1219,6 +1220,7 @@ export default class Chat extends Module {
if ( ! emotes_only )
msg.message = out.join('');
+ msg.bits = bits;
msg.ffz_emotes = emotes;
return msg;
}
@@ -1228,8 +1230,15 @@ export default class Chat extends Module {
if (!( time instanceof Date ))
time = new Date(time);
- const fmt = this.context.get('chat.timestamp-format');
- return dayjs(time).locale(this.i18n.locale).format(fmt);
+ const fmt = this.context.get('chat.timestamp-format'),
+ d = dayjs(time);
+
+ try {
+ return d.locale(this.i18n.locale).format(fmt);
+ } catch(err) {
+ // If the locale isn't loaded, this can fail.
+ return d.format(fmt);
+ }
}
diff --git a/src/modules/chat/rich_providers.js b/src/modules/chat/rich_providers.js
index 72f2f00f..193e1408 100644
--- a/src/modules/chat/rich_providers.js
+++ b/src/modules/chat/rich_providers.js
@@ -106,25 +106,25 @@ export const Clips = {
let desc_1;
if ( game_name === 'creative' )
- desc_1 = this.i18n.t('clip.desc.1.creative', '%{user} being Creative', {
+ desc_1 = this.i18n.t('clip.desc.1.creative', '{user} being Creative', {
user
});
else if ( game )
- desc_1 = this.i18n.t('clip.desc.1.playing', '%{user} playing %{game}', {
+ desc_1 = this.i18n.t('clip.desc.1.playing', '{user} playing {game}', {
user,
game: game_display
});
else
- desc_1 = this.i18n.t('clip.desc.1', 'Clip of %{user}', {user});
+ desc_1 = this.i18n.t('clip.desc.1', 'Clip of {user}', {user});
return {
url: token.url,
image: clip.thumbnailURL,
title: clip.title,
desc_1,
- desc_2: this.i18n.t('clip.desc.2', 'Clipped by %{curator} — %{views|number} View%{views|en_plural}', {
+ desc_2: this.i18n.t('clip.desc.2', 'Clipped by {curator} — {views,number} View{views,en_plural}', {
curator: clip.curator ? clip.curator.displayName : this.i18n.t('clip.unknown', 'Unknown'),
views: clip.viewCount
})
@@ -170,25 +170,25 @@ export const Videos = {
let desc_1;
if ( game_name === 'creative' )
- desc_1 = this.i18n.t('clip.desc.1.creative', '%{user} being Creative', {
+ desc_1 = this.i18n.t('clip.desc.1.creative', '{user} being Creative', {
user
});
else if ( game )
- desc_1 = this.i18n.t('clip.desc.1.playing', '%{user} playing %{game}', {
+ desc_1 = this.i18n.t('clip.desc.1.playing', '{user} playing %{game}', {
user,
game: game_display
});
else
- desc_1 = this.i18n.t('video.desc.1', 'Video of %{user}', {user});
+ desc_1 = this.i18n.t('video.desc.1', 'Video of {user}', {user});
return {
url: token.url,
image: video.previewThumbnailURL,
title: video.title,
desc_1,
- desc_2: this.i18n.t('video.desc.2', '%{length} — %{views} Views - %{date}', {
+ desc_2: this.i18n.t('video.desc.2', '{length,duration} — {views,number} Views - {date}', {
length: video.lengthSeconds,
views: video.viewCount,
date: video.publishedAt
diff --git a/src/modules/chat/room.js b/src/modules/chat/room.js
index e0330ffa..a963734f 100644
--- a/src/modules/chat/room.js
+++ b/src/modules/chat/room.js
@@ -380,16 +380,23 @@ export default class Room {
// Badge Data
// ========================================================================
+ hasBadges() {
+ return !! this.badges
+ }
+
updateBadges(badges) {
- if ( ! badges )
+ if ( ! Array.isArray(badges) )
this.badges = badges;
else {
- const b = {};
- for(const data of badges) {
- const sid = data.setID,
- bs = b[sid] = b[sid] || {};
+ let b = null;
+ if ( badges.length ) {
+ b = {};
+ for(const data of badges) {
+ const sid = data.setID,
+ bs = b[sid] = b[sid] || {};
- bs[data.version] = data;
+ bs[data.version] = data;
+ }
}
this.badges = b;
diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx
index 120b58da..73255e88 100644
--- a/src/modules/chat/tokenizers.jsx
+++ b/src/modules/chat/tokenizers.jsx
@@ -44,7 +44,7 @@ export const Links = {
return '';
if ( target.dataset.isMail === 'true' )
- return [this.i18n.t('tooltip.email-link', 'E-Mail %{address}', {address: target.textContent})];
+ return [this.i18n.t('tooltip.email-link', 'E-Mail {address}', {address: target.textContent})];
return this.get_link_info(target.dataset.url).then(data => {
if ( ! data || (data.v || 1) > TOOLTIP_VERSION )
@@ -58,7 +58,7 @@ export const Links = {
content += (content.length ? ' ' : '') +
sanitize(this.i18n.t(
'tooltip.link-destination',
- 'Destination: %{url}',
+ 'Destination: {url}',
{url: data.urls[data.urls.length-1][1]}
));
@@ -66,7 +66,7 @@ export const Links = {
const reasons = Array.from(new Set(data.urls.map(x => x[2]).filter(x => x))).join(', ');
content = this.i18n.t(
'tooltip.link-unsafe',
- "Caution: This URL is on Google's Safe Browsing List for: %{reasons}",
+ "Caution: This URL is on Google's Safe Browsing List for: {reasons}",
{reasons: sanitize(reasons.toLowerCase())}
) + (content.length ? ` ${content}` : '');
}
@@ -96,7 +96,7 @@ export const Links = {
return content;
}).catch(error =>
- sanitize(this.i18n.t('tooltip.error', 'An error occurred. (%{error})', {error}))
+ sanitize(this.i18n.t('tooltip.error', 'An error occurred. ({error})', {error}))
);
},
@@ -736,7 +736,7 @@ export const CheerEmotes = {
data-prefix={prefix}
data-tier={tier}
/>),
- this.i18n.t('tooltip.bits', '%{count|number} Bits', amount),
+ this.i18n.t('tooltip.bits', '{count,number} Bits', amount),
];
if ( length > 1 ) {
@@ -755,7 +755,7 @@ export const CheerEmotes = {
if ( length > 12 ) {
out.push( );
- out.push(this.i18n.t('tooltip.bits.more', '(and %{count} more)', length-12));
+ out.push(this.i18n.t('tooltip.bits.more', '(and {count} more)', length-12));
}
}
@@ -1008,7 +1008,7 @@ export const AddonEmotes = {
source = this.i18n.t('emote.prime', 'Twitch Prime');
else
- source = this.i18n.t('tooltip.channel', 'Channel: %{source}', {source});
+ source = this.i18n.t('tooltip.channel', 'Channel: {source}', {source});
}
} else if ( provider === 'ffz' ) {
@@ -1025,7 +1025,7 @@ export const AddonEmotes = {
if ( emote.owner )
owner = this.i18n.t(
- 'emote.owner', 'By: %{owner}',
+ 'emote.owner', 'By: {owner}',
{owner: emote.owner.display_name});
if ( emote.urls[4] )
@@ -1052,7 +1052,7 @@ export const AddonEmotes = {
plain_name = true;
name = `:${emoji.names[0]}:${vcode ? `:${vcode.names[0]}:` : ''}`;
- source = this.i18n.t('tooltip.emoji', 'Emoji - %{category}', emoji);
+ source = this.i18n.t('tooltip.emoji', 'Emoji - {category}', emoji);
} else
return;
@@ -1069,7 +1069,7 @@ export const AddonEmotes = {
onLoad={tip.update}
/>) : preview),
- plain_name || (hide_source && ! owner) ? name : this.i18n.t('tooltip.emote', 'Emote: %{name}', {name}),
+ plain_name || (hide_source && ! owner) ? name : this.i18n.t('tooltip.emote', 'Emote: {name}', {name}),
! hide_source && source && this.context.get('tooltip.emote-sources') && (
{source}
diff --git a/src/modules/main_menu/components/action-editor.vue b/src/modules/main_menu/components/action-editor.vue
index f5ce8546..9e8079e4 100644
--- a/src/modules/main_menu/components/action-editor.vue
+++ b/src/modules/main_menu/components/action-editor.vue
@@ -9,7 +9,7 @@
{{ title }}
{{ description }}
- {{ t('setting.actions.visible', 'visible: %{list}', {list: visibility}) }}
+ {{ t('setting.actions.visible', 'visible: {list}', {list: visibility}) }}
@@ -281,7 +281,7 @@ export default {
const def = this.data.actions[this.display.action];
if ( ! def )
- return this.t('setting.actions.unknown', 'Unknown Action Type: %{action}', this.display);
+ return this.t('setting.actions.unknown', 'Unknown Action Type: {action}', this.display);
if ( def.title ) {
const data = this.getData(),
@@ -292,7 +292,7 @@ export default {
return this.t(i18n, out, data);
}
- return this.t('setting.actions.untitled', 'Action: %{action}', this.display);
+ return this.t('setting.actions.untitled', 'Action: {action}', this.display);
},
diff --git a/src/modules/main_menu/components/backup-restore.vue b/src/modules/main_menu/components/backup-restore.vue
index c24a9754..203a0f99 100644
--- a/src/modules/main_menu/components/backup-restore.vue
+++ b/src/modules/main_menu/components/backup-restore.vue
@@ -132,7 +132,7 @@ export default {
i++;
}
- this.message = this.t('setting.backup-restore.restored', '%{count} items have been restored. Please refresh this page.', {
+ this.message = this.t('setting.backup-restore.restored', '{count,number} items have been restored. Please refresh this page.', {
count: i
});
}
diff --git a/src/modules/main_menu/components/chat-reasons.vue b/src/modules/main_menu/components/chat-reasons.vue
index fff1e787..ea489079 100644
--- a/src/modules/main_menu/components/chat-reasons.vue
+++ b/src/modules/main_menu/components/chat-reasons.vue
@@ -64,8 +64,7 @@ export default {
data() {
return {
default_reason: {
- text: '',
- i18n: null
+ text: ''
}
}
},
@@ -101,6 +100,8 @@ export default {
if ( ! reason )
return;
+ delete reason.i18n;
+
const vals = Array.from(this.val);
vals.push({v: reason});
this.set(deep_copy(vals));
@@ -120,12 +121,9 @@ export default {
},
save(val, new_val) {
- if ( val.v && new_val ) {
- if ( new_val.i18n && new_val.text !== val.v.text )
- new_val.i18n = null;
- }
-
+ delete new_val.i18n;
val.v = new_val;
+
this.set(deep_copy(this.val));
}
}
diff --git a/src/modules/main_menu/components/experiments.vue b/src/modules/main_menu/components/experiments.vue
index 1e9c0230..5e66ca49 100644
--- a/src/modules/main_menu/components/experiments.vue
+++ b/src/modules/main_menu/components/experiments.vue
@@ -6,7 +6,7 @@
- {{ t('setting.experiments.unique-id', 'Unique ID: %{id}', {id: unique_id}) }}
+ {{ t('setting.experiments.unique-id', 'Unique ID: {id}', {id: unique_id}) }}
- {{ t('setting.experiments.entry', '%{value} (weight: %{weight})', i) }}
+ {{ t('setting.experiments.entry', '{value} (weight: {weight})', i) }}
diff --git a/src/modules/main_menu/components/faq-page.vue b/src/modules/main_menu/components/faq-page.vue
new file mode 100644
index 00000000..b4ccce06
--- /dev/null
+++ b/src/modules/main_menu/components/faq-page.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/main_menu/components/feedback-page.vue b/src/modules/main_menu/components/feedback-page.vue
index 3f8a4d41..b15ee672 100644
--- a/src/modules/main_menu/components/feedback-page.vue
+++ b/src/modules/main_menu/components/feedback-page.vue
@@ -1,31 +1,20 @@
-
Feedback
-
-
- You can provide feedback and bug reports by
-
- opening an issue at our GitHub repository .
-
- You can also
- tweet at us .
-
-
-
- When creating a GitHub issue, please check that someone else hasn't
- already created one for what you'd like to discuss or report.
-
-
-
- When creating an issue, please also upload the following logs and
- include a link in your report.
-
+
-
\ No newline at end of file
diff --git a/src/modules/main_menu/components/home-page.vue b/src/modules/main_menu/components/home-page.vue
index a2ddea3a..5db66630 100644
--- a/src/modules/main_menu/components/home-page.vue
+++ b/src/modules/main_menu/components/home-page.vue
@@ -9,45 +9,7 @@
- Welcome to the v4.0 Beta
-
-
- This is the initial, beta release of FrankerFaceZ v4.0 with support
- for the Twitch website rewrite.
-
- As you'll notice, this release is not complete.
- There are missing features. There are bugs. If you are a moderator,
- you will want to just keep opening a Legacy Chat Popout for now.
-
-
-
- FrankerFaceZ v4.0 is still under heavy development and there will
- be significant changes and improvements in the coming weeks. For
- now, here are some of the bigger issues:
-
-
-
- Settings from the old version are not being imported.
- Advanced input (better tab completion, history) isn't available.
-
-
- And the biggest features still under development:
-
-
- Chat Filtering (Highlighted Words, etc.)
- Room Status Indicators
- Custom Mod Cards
- Recent Highlights
- Portrait Mode
- Importing and exporting settings
- User Aliases
-
-
-
- For a possibly more up-to-date list of what I'm working on,
- please consult this Trello board .
-
-
+
@@ -90,7 +52,7 @@
@@ -99,11 +61,19 @@