// ==UserScript==
// @name Append Tag Searching Tub
// @namespace http://loda.jp/script/
// @id niconico-adds-search-tab-347021
// @version 3.0.1
// @description 『niconico』の検索窓にタグ検索タブを追加 / Adds "tag" search tab above search box in "niconico"
// @match http://www.nicovideo.jp/*
// @match http://seiga.nicovideo.jp/search/*
// @match http://live.nicovideo.jp/*
// @match http://watch.live.nicovideo.jp/*
// @match http://com.nicovideo.jp/*
// @match http://blog.nicovideo.jp/en_info/*
// @match http://tw.blog.nicovideo.jp/*
// @match http://info.nicovideo.jp/psvita/en/*
// @grant GM_xmlhttpRequest
// @domain www.nicovideo.jp
// @domain seiga.nicovideo.jp
// @domain live.nicovideo.jp
// @domain watch.live.nicovideo.jp
// @domain com.nicovideo.jp
// @domain blog.nicovideo.jp
// @domain tw.blog.nicovideo.jp
// @domain info.nicovideo.jp
// @run-at document-start
// @icon data:image/vnd.microsoft.icon;base64,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
// @author 100の人 https://userscripts.org/users/347021
// @license Creative Commons Attribution 3.0 Unported License
// ==/UserScript==
(function () {
'use strict';
polyfill();
// L10N
setLocalizedTexts({
'en': {
'静画を検索': 'Search Image',
'静画': 'Images',
'生放送を検索': 'Search Live Program',
'生放送': 'Live',
'タグ': 'Tags',
'マンガ': 'Comics',
},
'zh': {
'静画を検索': '搜尋靜畫',
'静画': '靜畫',
'生放送を検索': '搜尋生放送',
'生放送': '生放送',
'タグ': '標籤',
'マンガ': '漫畫',
},
});
/**
* 検索窓の最大幅
* @constant {string}
*/
var MAX_SEARCH_BOX_WIDTH = '268px';
var host = window.location.host, pathname = window.location.pathname, pageType,
targetParentIdFirefox, isTargetParentFirefox, isTargetFirefox;
// 検索ページの種類を取得
switch (host) {
case 'www.nicovideo.jp':
if (window.location.pathname === '/') {
pageType = 'top';
} else if (/^\/(?:search\/|mylist_search(?:\/|$))/.test(pathname)) {
pageType = 'video';
} else if (/^\/(?:(?:tag|related_tag|watch|mylist)\/|(?:recent|newarrival|hotlist|video_top|openlist|playlist|recommendations)(?:\/|$))/.test(pathname)) {
pageType = 'tag';
}
break;
case 'seiga.nicovideo.jp':
pageType = 'image';
break;
case 'live.nicovideo.jp':
case 'watch.live.nicovideo.jp':
if (pathname.startsWith('/search')) {
pageType = 'live';
}
break;
case 'info.nicovideo.jp':
if (pathname.startsWith('/psvita/en/')) {
// 英語版PS Vita紹介ページ
startScript(prepare,
function (parent) { return parent.localName === 'body'; },
function (target) { return target.id === 'header'; },
function () { return document.getElementById('header'); },
{
isTargetParent: function (parent) { return parent.localName === 'html'; },
isTarget: function (target) { return target.localName === 'body'; },
});
}
return;
}
// 上部メニューが追加されるまで待機
switch (host) {
case 'seiga.nicovideo.jp':
// 静画
isTargetFirefox = function (target) { return target.id === 'wrapper'; };
break;
case 'live.nicovideo.jp':
// 生放送
targetParentIdFirefox = 'body_header';
break;
case 'blog.nicovideo.jp':
// 英語版ニコニコインフォ
targetParentIdFirefox = 'container-inner';
break;
case 'tw.blog.nicovideo.jp':
// 台湾版ニコニコインフォ
targetParentIdFirefox = 'header';
break;
case 'info.nicovideo.jp':
break;
}
if (!isTargetParentFirefox) {
if (targetParentIdFirefox) {
isTargetParentFirefox = function (parent) { return parent.id === targetParentIdFirefox; };
} else {
isTargetParentFirefox = function (parent) { return parent.localName === 'body'; };
}
}
startScript(prepare,
function (parent) { return parent.classList.contains('siteHeaderGlovalNavigation'); },
function (target) { return target.id === 'siteHeaderLeftMenu'; },
function () { return document.getElementById('siteHeaderLeftMenu'); },
{
isTargetParent: isTargetParentFirefox,
isTarget: isTargetFirefox || function (target) { return target.id === 'siteHeader'; },
});
function prepare () {
var parentId, parentIdFirefox, targetId, targetIdFirefox, isTargetParent, isTargetParentFirefox,
textVideo, harajuku, itemLive, item;
// ニコニコ生放送ではlang属性値が常にja-JPのため、ニコニコ動画へのリンク文字によって、ページの言語を判定する
textVideo = document.querySelector('[href^="http://www.nicovideo.jp/video_top"]').textContent;
if (textVideo.contains('Video')) {
setlang('en');
} else if (textVideo.contains('動畫')) {
setlang('zh');
}
if (!document.querySelector(pageType === 'image' ? '#siteHeader [href="/?header"], #siteHeader [href="/"]' : '#siteHeader [href^="http://seiga.nicovideo.jp/"], #globalNav [href^="http://seiga.nicovideo.jp/"]')) {
// ヘッダに静画へのリンクが無ければ
// 生放送へのリンクを取得
itemLive = document.querySelector('#siteHeader [href^="http://live.nicovideo.jp/"], #globalNav [href^="http://live.nicovideo.jp/"]').parentNode;
// 生放送リンクの複製
item = itemLive.cloneNode(true);
// リンク文字を変更
(item.getElementsByTagName('span')[0] || item.getElementsByTagName('a')[0]).textContent = _('静画');
// アドレスを変更
item.getElementsByTagName('a')[0].host = 'seiga.nicovideo.jp';
// ヘッダに静画へのリンクを追加
itemLive.parentNode.insertBefore(item, itemLive);
}
// スクリプトを起動
if (!pageType) {
return;
}
harajuku = document.doctype.publicId;
switch (pageType) {
case 'video':
if (harajuku) {
// マイリスト検索、キーワード検索
parentId = 'form_search';
targetId = 'search_united_form';
parentIdFirefox = 'PAGEMAIN';
targetIdFirefox = 'PAGEBODY';
} else {
// GINZAバージョンのキーワード検索
startScript(main,
function (parent) { return parent.classList.contains('formSearch'); },
function (target) { return target.id === 'search_united_form'; },
function () { return document.getElementById('search_united_form'); },
{
isTargetParent: function (parent) { return parent.localName === 'body'; },
isTarget: function (target) { return target.localName === 'section'; },
});
return;
}
break;
case 'top':
// トップページ
main = mainTop;
parentId = 'searchFormInner';
targetId = 'searchForm';
isTargetParentFirefox = function (parent) {
return parent.id === 'main_container' || parent.localName === 'body';
};
targetIdFirefox = 'searchFormWrap';
break;
case 'image':
// 静画
parentId = 'usearch_form';
targetId = 'usearch_form_input';
parentIdFirefox = 'wrapper';
targetIdFirefox = 'main';
break;
case 'live':
// 生放送
isTargetParentFirefox = isTargetParent = function (target) {
return target.classList.contains('container');
};
targetIdFirefox = targetId = 'form_frm_btm';
break;
case 'tag':
if (harajuku) {
// タグ検索等
main = mainTag;
parentId = 'search_tab';
targetId = 'target_m';
parentIdFirefox = 'PAGEMAIN';
targetIdFirefox = 'PAGEBODY';
} else {
// GINZAバージョンのタグ検索
startScript(mainTag,
function (parent) { return parent.classList.contains('videoSearchOption'); },
function (target) { return target.classList.contains('optMylist'); },
function () { return document.getElementsByClassName('optMylist')[0]; },
{
isTargetParent: function (parent) { return parent.localName === 'body'; },
isTarget: function (target) { return target.localName === 'header'; },
});
return;
}
}
startScript(main,
isTargetParent || function (parent) { return parent.id === parentId; },
function (target) { return target.id === targetId; },
function () { return document.getElementById(targetId); },
{
isTargetParent: isTargetParentFirefox || function (parent) { return parent.id === parentIdFirefox; },
isTarget: function (target) { return target.id === targetIdFirefox; },
});
}
// タグ検索
function mainTag () {
var mylistTab, tabList, styleSheet, cssRules, script;
// スタイルの設定
styleSheet = document.head.appendChild(document.createElement('style')).sheet;
cssRules = styleSheet.cssRules;
[
'#PAGEHEADER > div {'
+ 'display: flex;'
+ '}',
'#head_search {'
+ 'max-width: ' + MAX_SEARCH_BOX_WIDTH + ';'
+ 'flex-grow: 1;'
+ '}',
'#search_input {'
+ 'width: 100%;'
+ 'display: flex;'
+ '}',
'#search_input .typeText {'
+ 'flex-grow: 1;'
+ '}',
'#head_ads {'
+ 'margin-right: -26px;'
+ '}',
'#search_input #bar_search {'
+ '-moz-box-sizing: border-box;'
+ 'box-sizing: border-box;'
+ 'width: 100% !important;'
+ '}',
// GINZAバージョン
'.siteHeader > .inner {'
+ 'display: flex;'
+ '}',
'.videoSearch {'
+ 'max-width: ' + MAX_SEARCH_BOX_WIDTH + ';'
+ 'flex-grow: 1;'
+ 'padding-left: 4px;'
+ 'padding-right: 4px;'
+ '}',
'.videoSearch form {'
+ 'display: flex;'
+ '}',
'.videoSearch form .inputText {'
+ 'flex-grow: 1;'
+ '}',
].forEach(function (rule) {
styleSheet.insertRule(rule, cssRules.length);
});
// タブリストの取得
mylistTab = document.querySelector('#target_m, .optMylist');
tabList = mylistTab.parentNode;
// タブの複製・追加
[
{
type: 'image',
title: _('静画を検索'),
uri: 'http://seiga.nicovideo.jp/search',
text: _('静画'),
},
{
type: 'live',
title: _('生放送を検索'),
uri: 'http://live.nicovideo.jp/search',
text: _('生放送'),
},
].forEach(function (option) {
var tab = mylistTab.cloneNode(true);
if (mylistTab.classList.contains('optMylist')) {
// GINZAバージョン
tab.classList.remove('optMylist');
tab.classList.add('opt' + option.type[0].toUpperCase() + option.type.slice(1));
tab.dataset.type = option.type;
tab.getElementsByTagName('a')[0].textContent = option.text;
} else {
// 原宿バージョン
tab.id = 'target_' + option.type[0];
tab.title = option.title;
tab.setAttribute('onclick', tab.getAttribute('onclick').replace(/'.+?'/, '\'' + option.uri + '\''));
tab.textContent = option.text;
}
tabList.appendChild(tab);
});
if (mylistTab.classList.contains('optMylist')) {
// GINZAバージョン
script = document.createElement('script');
script.text = '(' + (function () {
eval('Nico.Navigation.HeaderSearch.Controller.search = ' + Nico.Navigation.HeaderSearch.Controller.search.toString().replace(/(switch.+?{.+?)(})/, '$1; break;'
+ 'case "image":'
+ 'd = "http://seiga.nicovideo.jp/search/" + e; break;'
+ 'case "live":'
+ 'd = "http://live.nicovideo.jp/search/" + e; break;'
+ '$2'));
}).toString() + ')();';
document.head.appendChild(script);
}
}
// トップページ
function mainTop() {
var styleSheet, cssRules, refItem, item, anchor;
fixPrototypeJavaScriptFramework();
// スタイルの設定
styleSheet = document.head.appendChild(document.createElement('style')).sheet;
cssRules = styleSheet.cssRules;
[
'#searchFormInner {'
+ 'width: auto;'
+ 'margin-left: 136px;'
+ '}',
].forEach(function (rule) {
styleSheet.insertRule(rule, cssRules.length);
});
// マイリスト検索ボタンの取得
refItem = document.getElementsByClassName('sMylist')[0].parentNode;
// マイリスト検索ボタンの複製
item = refItem.cloneNode(true);
// ボタン名を変更
anchor = item.getElementsByTagName('a')[0];
anchor.textContent = _('タグ');
// クラス名を変更
anchor.className = 'sVideo';
// アドレスを変更
anchor.href = 'http://www.nicovideo.jp/tag/';
// タグ検索ボタンを追加
refItem.parentNode.insertBefore(item, refItem);
if (!document.getElementsByClassName('sSeiga')[0]) {
// 静画検索ボタンが存在しなければ
// 生放送検索の取得
refItem = document.getElementsByClassName('sLive')[0].parentNode;
// 生放送検索の複製
item = refItem.cloneNode(true);
// ボタン名を変更
anchor = item.getElementsByTagName('a')[0];
anchor.textContent = _('静画');
// クラス名を変更
anchor.className = 'sSeiga';
// アドレスを変更
anchor.href = 'http://seiga.nicovideo.jp/search/';
// 静画検索を追加
refItem.parentNode.insertBefore(item, refItem);
startScript(function () {
var list, item, anchor;
// メニューの生放送リンクの取得
list = document.querySelector('.service_main .live').parentNode.parentNode;
// 生放送リンクの複製
item = list.cloneNode(true);
// リンク文字を変更
anchor = item.getElementsByTagName('a')[0];
anchor.title = anchor.textContent = _('静画');
// クラス名を変更
anchor.classList.remove('live');
anchor.classList.add('seiga');
// アドレスを変更
item.getElementsByTagName('a')[0].href = 'http://seiga.nicovideo.jp/';
// メニューに静画へのリンクを追加
list.parentNode.insertBefore(item, list);
// サブメニューの複製
item = document.getElementsByClassName('service_sub')[0].cloneNode(true);
// 2つ目以降の要素を削除
Array.prototype.forEach.call(item.querySelectorAll('li:first-child ~ li'), function (item) {
item.parentNode.removeChild(item);
});
// リンク文字を変更
anchor = item.getElementsByTagName('a')[0];
anchor.title = anchor.textContent = _('マンガ');
// アドレスを変更
item.getElementsByTagName('a')[0].href = 'http://seiga.nicovideo.jp/manga/';
// メニューに静画のサブメニューへのリンクを追加
list.parentNode.insertBefore(item, list);
},
function (parent) { return parent.id === 'sideNav'; },
function (target) { return target.id === 'trendyTags'; },
function () { return document.querySelector('#menuService [href="http://live.nicovideo.jp/timetable/"]'); },
{
isTarget: function (target) { return target.id === 'NewServiceList'; },
});
}
}
// キーワード検索、マイリスト検索、静画検索、生放送検索
function main() {
var inactiveTab, mylistTab, tagTab, tabNameNode, searchCount, anchor, searchWords = '', searchWordsPattern;
// マイリスト検索タブの取得
mylistTab = document.querySelector('.tab_table td:nth-of-type(2), #search_frm_a a:nth-of-type(2), .seachFormA a:nth-of-type(2)');
// マイリスト検索タブの複製
tagTab = mylistTab.cloneNode(true);
// タブ名を変更
anchor = tagTab.tagName.toLowerCase() === 'a' ? tagTab : tagTab.getElementsByTagName('a')[0];
tabNameNode = anchor.getElementsByTagName('div');
tabNameNode = (tabNameNode.length > 0 ? tabNameNode[0].firstChild : anchor.firstChild);
tabNameNode.data = _('タグ') + (pageType === 'live' ? '(' : ' ( ');
// クラス名を変更・動画件数をリセット
searchCount = tagTab.querySelector('strong, span');
if (pageType === 'image') {
searchCount.classList.remove('search_value_em');
searchCount.classList.add('search_value');
} else if (pageType === 'live') {
searchCount.classList.remove('Redtxt');
} else{
searchCount.style.removeProperty('color');
}
searchCount.textContent = '-';
if (searchCount.id) {
// 生放送
searchCount.id = 'search_count_tag';
}
// 検索語句を取得
searchWordsPattern = /(?:\/(?:search|tag|mylist_search)\/|[?&]keyword=)([^?&#]+)/g;
if (searchWords = window.location.href.match(searchWordsPattern)) {
searchWords = searchWordsPattern.exec(searchWords[pageType === 'live' ? searchWords.length - 1 : 0])[1];
}
// タグが付いた動画件数を取得・表示
if (searchWords) {
GM_xmlhttpRequest({
method: 'GET',
url: 'http://www.nicovideo.jp/tag/' + searchWords,
onload: function (response) {
var responseDocument, total, trimmedThousandsSep;
responseDocument = new DOMParser().parseFromString(response.responseText, 'text/html');
if (!responseDocument) {
// Blink
// Issue 265379: DOMParser + text/html does not work <http://code.google.com/p/chromium/issues/detail?id=265379>
responseDocument = document.implementation.createHTMLDocument();
responseDocument.documentElement.innerHTML = response.responseText;
}
total = responseDocument.title.contains('(原宿)')
// 原宿バージョン
? /[,0-9]+/.exec(responseDocument.getElementsByClassName('searchTagTotal')[0].textContent)[0]
// GINZAバージョン
: responseDocument.querySelector('.tagCaption .dataValue .num').textContent;
trimmedThousandsSep = total.replace(/,/g, '');
if (trimmedThousandsSep >= 100) {
if (pageType === 'image') {
searchCount.classList.remove('search_value');
searchCount.classList.add('search_value_em');
} else if (pageType === 'live') {
searchCount.classList.add('Redtxt');
} else {
searchCount.style.color = '#CC0000';
}
}
searchCount.textContent = pageType === 'live' ? trimmedThousandsSep : (pageType === 'image' ? total : ' ' + total + ' ');
}
});
}
// 非アクティブタブを取得
inactiveTab = document.querySelector('.tab_0, .tab1');
// クラス名を変更
anchor.className = inactiveTab.className;
// アドレスを変更
anchor.href = 'http://www.nicovideo.jp/tag/' + searchWords + inactiveTab.search;
// タグ検索タブを追加
mylistTab.parentNode.insertBefore(tagTab, mylistTab);
if (inactiveTab.classList.contains('tab1')) {
// GINZAバージョン
mylistTab.parentNode.insertBefore(tagTab.previousSibling.cloneNode(true), mylistTab);
}
}
/**
* 挿入された節の親節が、目印となる節の親節か否かを返すコールバック関数
* @callback isTargetParent
* @param {(Document|Element)} parent
* @returns {boolean}
*/
/**
* 挿入された節が、目印となる節か否かを返すコールバック関数
* @callback isTarget
* @param {(DocumentType|Element)} target
* @returns {boolean}
*/
/**
* 目印となる節が文書に存在するか否かを返すコールバック関数
* @callback existsTarget
* @returns {boolean}
*/
/**
* 目印となる節が挿入された直後に関数を実行する
* @param {Function} main - 実行する関数
* @param {isTargetParent} isTargetParent
* @param {isTarget} isTarget
* @param {existsTarget} existsTarget
* @param {Object} [callbacksForFirefox] - DOMContentLoaded前のタイミングで1回だけスクリプトを起動させる場合に設定
* @param {isTargetParent} [callbacksForFirefox.isTargetParent] - FirefoxにおけるisTargetParent
* @param {isTarget} [callbacksForFirefox.isTarget] - FirefoxにおけるisTarget
* @version 2013-09-23
*/
function startScript(main, isTargetParent, isTarget, existsTarget, callbacksForFirefox) {
var observer, flag;
// FirefoxのDOMContentLoaded前のMutationObserverは、要素をまとめて挿入したと見なすため、isTargetParent、isTargetを変更
if (callbacksForFirefox && window.navigator.userAgent.contains(' Firefox/')) {
if (callbacksForFirefox.isTargetParent) {
isTargetParent = callbacksForFirefox.isTargetParent;
}
if (callbacksForFirefox.isTarget) {
isTarget = callbacksForFirefox.isTarget;
}
}
// 指定した節が既に存在していれば、即実行
startMain();
if (flag) {
return;
}
observer = new MutationObserver(mutationCallback);
observer.observe(document, {
childList: true,
subtree: true,
});
if (callbacksForFirefox) {
// DOMContentLoadedまでにスクリプトを実行できなかった場合、監視を停止(指定した節が存在するか確認し、存在すれば実行)
document.addEventListener('DOMContentLoaded', function stopScript(event) {
event.target.removeEventListener('DOMContentLoaded', stopScript);
if (observer) {
observer.disconnect();
}
startMain();
flag = true;
});
}
/**
* 目印となる節が挿入されたら、監視を停止し、{@link checkExistingTarget}を実行する
* @param {MutationRecord[]} mutations - a list of MutationRecord objects
* @param {MutationObserver} observer - the constructed MutationObserver object
*/
function mutationCallback(mutations, observer) {
var mutation, target, nodeType, addedNodes, addedNode, i, j, l, l2;
for (i = 0, l = mutations.length; i < l; i++) {
mutation = mutations[i];
target = mutation.target;
nodeType = target.nodeType;
if ((nodeType === Node.ELEMENT_NODE || nodeType === Node.DOCUMENT_NODE) && isTargetParent(target)) {
// 子が追加された節が要素節か文書節で、かつそのノードについてisTargetParentが真を返せば
addedNodes = Array.prototype.slice.call(mutation.addedNodes);
for (j = 0, l2 = addedNodes.length; j < l2; j++) {
addedNode = addedNodes[j];
nodeType = addedNode.nodeType;
if ((nodeType === Node.ELEMENT_NODE || nodeType === Node.DOCUMENT_TYPE_NODE) && isTarget(addedNode)) {
// 追加された子が要素節か文書型節で、かつそのノードについてisTargetが真を返せば
observer.disconnect();
checkExistingTarget(0);
return;
}
}
}
}
}
/**
* {@link startMain}を実行し、スクリプトが開始されていなければ再度実行
* @param {number} count - {@link startMain}を実行した回数
*/
function checkExistingTarget(count) {
var LIMIT = 500, INTERVAL = 10;
startMain();
if (!flag && count < LIMIT) {
window.setTimeout(checkExistingTarget, INTERVAL, count + 1);
}
}
/**
* 指定した節が存在するか確認し、存在すれば監視を停止しスクリプトを実行
*/
function startMain() {
if (!flag && existsTarget()) {
flag = true;
main();
}
}
}
/**
* prototype汚染が行われる Prototype JavaScript Framework (prototype.js) 1.5.1.1 のバグを修正(Tampermonkey用)
*/
function fixPrototypeJavaScriptFramework() {
[
[document, 'getElementsByClassName'],
].forEach(function (objectProperty) {
delete objectProperty[0][objectProperty[1]];
});
}
/**
* 国際化・地域化関数の読み込み、ECMAScript仕様のPolyfill
*/
function polyfill() {
// i18n
(function () {
/**
* 翻訳対象文字列 (msgid) の言語
* @constant {string}
*/
var ORIGINAL_LOCALE = 'ja';
/**
* クライアントの言語の翻訳リソースが存在しないとき、どの言語に翻訳するか
* @constant {string}
*/
var DEFAULT_LOCALE = 'en';
/**
* 以下のような形式の翻訳リソース
* {
* 'IETF言語タグ': {
* '翻訳前 (msgid)': '翻訳後 (msgstr)',
* ……
* },
* ……
* }
* @typedef {Object} LocalizedTexts
*/
/**
* クライアントの言語。{@link setlang}から変更される
* @type {string}
* @access private
*/
var langtag = 'ja';
/**
* クライアントの言語のlanguage部分。{@link setlang}から変更される
* @type {string}
* @access private
*/
var language = 'ja';
/**
* 翻訳リソース。{@link setLocalizedTexts}から変更される
* @type {LocalizedTexts}
* @access private
*/
var multilingualLocalizedTexts = {};
multilingualLocalizedTexts[ORIGINAL_LOCALE] = {};
/**
* テキストをクライアントの言語に変換する
* @param {string} message - 翻訳前
* @returns {string} 翻訳後
*/
window._ = window.gettext = function (message) {
// クライアントの言語の翻訳リソースが存在すれば、それを返す
return langtag in multilingualLocalizedTexts && multilingualLocalizedTexts[langtag][message]
// 地域下位タグを取り除いた言語タグの翻訳リソースが存在すれば、それを返す
|| language in multilingualLocalizedTexts && multilingualLocalizedTexts[language][message]
// デフォルト言語の翻訳リソースが存在すれば、それを返す
|| DEFAULT_LOCALE in multilingualLocalizedTexts && multilingualLocalizedTexts[DEFAULT_LOCALE][message]
// そのまま返す
|| message;
};
/**
* {@link gettext}から参照されるクライアントの言語を設定する
* @param {string} lang - IETF言語タグ(「language」と「language-REGION」にのみ対応)
*/
window.setlang = function (lang) {
lang = lang.split('-', 2);
language = lang[0].toLowerCase();
langtag = language + (lang[1] ? '-' + lang[1].toUpperCase() : '');
};
/**
* {@link gettext}から参照される翻訳リソースを追加する
* @param {LocalizedTexts} localizedTexts
*/
window.setLocalizedTexts = function (localizedTexts) {
var localizedText, lang, language, langtag, msgid;
for (lang in localizedTexts) {
localizedText = localizedTexts[lang];
lang = lang.split('-');
language = lang[0].toLowerCase();
langtag = language + (lang[1] ? '-' + lang[1].toUpperCase() : '');
if (langtag in multilingualLocalizedTexts) {
// すでに該当言語の翻訳リソースが存在すれば、統合する(同じmsgidがあれば上書き)
for (msgid in localizedText) {
multilingualLocalizedTexts[langtag][msgid] = localizedText[msgid];
}
} else {
multilingualLocalizedTexts[langtag] = localizedText;
}
if (language !== langtag) {
// 言語タグに地域下位タグが含まれていれば
// 地域下位タグを取り除いた言語タグも翻訳リソースとして追加する
if (language in multilingualLocalizedTexts) {
// すでに該当言語の翻訳リソースが存在すれば、統合する(同じmsgidがあれば無視)
for (msgid in localizedText) {
if (!(msgid in multilingualLocalizedTexts[language])) {
multilingualLocalizedTexts[language][msgid] = localizedText[msgid];
}
}
} else {
multilingualLocalizedTexts[language] = localizedText;
}
}
// msgidの言語の翻訳リソースを生成
for (msgid in localizedText) {
multilingualLocalizedTexts[ORIGINAL_LOCALE][msgid] = msgid;
}
}
};
})();
// Polyfill for Blink
if (!String.prototype.hasOwnProperty('startsWith')) {
/**
* Determines whether a string begins with the characters of another string, returning true or false as appropriate.
* @param {string} searchString - The characters to be searched for at the start of this string.
* @param {number} [position=0] - The position in this string at which to begin searching for searchString.
* @returns {boolean}
* @see {@link http://people.mozilla.org/~jorendorff/es6-draft.html#sec-string.prototype.startswith 21.1.3.18 String.prototype.startsWith (searchString [, position ] )}
* @see {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith String.startsWith - JavaScript | MDN}
* @version polyfill-2013-11-05
* @name String.prototype.startsWith
*/
Object.defineProperty(String.prototype, 'startsWith', {
writable: true,
enumerable: false,
configurable: true,
value: function (searchString) {
var position = arguments[1];
return this.indexOf(searchString, position) === Math.max(Math.floor(position) || 0, 0);
},
});
}
if (!String.prototype.hasOwnProperty('contains')) {
/**
* Determines whether one string may be found within another string, returning true or false as appropriate.
* @param {string} searchString - A string to be searched for within this string.
* @param {number} [position=0] - The position in this string at which to begin searching for searchString.
* @returns {boolean}
* @see {@link http://people.mozilla.org/~jorendorff/es6-draft.html#sec-string.prototype.contains 21.1.3.6 String.prototype.contains (searchString, position = 0 )}
* @see {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/contains String.contains - JavaScript | MDN}
* @version polyfill-2013-11-05
* @name String.prototype.contains
*/
Object.defineProperty(String.prototype, 'contains', {
writable: true,
enumerable: false,
configurable: true,
value: function (searchString) {
return this.indexOf(searchString, arguments[1]) !== -1;
},
});
}
}
})();