OSDN Git Service

#27617 言語名へのリンクを行わない設定を追加し、Wikitravelの設定に言語名を追加,
[wptscs/wpts.git] / Wptscs / Logics / MediaWikiTranslator.cs
index a05f010..9cf9997 100644 (file)
@@ -3,7 +3,7 @@
 //      Wikipedia用の翻訳支援処理実装クラスソース</summary>
 //
 // <copyright file="MediaWikiTranslator.cs" company="honeplusのメモ帳">
-//      Copyright (C) 2010 Honeplus. All rights reserved.</copyright>
+//      Copyright (C) 2012 Honeplus. All rights reserved.</copyright>
 // <author>
 //      Honeplus</author>
 // ================================================================================================
@@ -13,18 +13,62 @@ namespace Honememo.Wptscs.Logics
     using System;
     using System.Collections.Generic;
     using System.IO;
+    using System.Linq;
     using System.Net;
     using System.Text;
     using System.Windows.Forms;
+    using Honememo.Models;
+    using Honememo.Parsers;
     using Honememo.Utilities;
     using Honememo.Wptscs.Models;
+    using Honememo.Wptscs.Parsers;
     using Honememo.Wptscs.Properties;
+    using Honememo.Wptscs.Utilities;
+    using Honememo.Wptscs.Websites;
 
     /// <summary>
-    /// Wikipedia用の翻訳支援処理実装クラスです。
+    /// MediaWiki用の翻訳支援処理実装クラスです。
     /// </summary>
     public class MediaWikiTranslator : Translator
     {
+        #region private変数
+
+        /// <summary>
+        /// <see cref="Translator.ItemTable"/>用ロックオブジェクト。
+        /// </summary>
+        private LockObject itemTableLock = new LockObject();
+
+        #endregion
+
+        #region コンストラクタ
+
+        /// <summary>
+        /// MediaWikiでの翻訳支援処理を行うトランスレータを作成。
+        /// </summary>
+        /// <remarks>
+        /// 別途プロパティに必要なパラメータを設定する必要あり。
+        /// 通常は<see cref="Translator.Create"/>にて設定ファイルから作成する。
+        /// </remarks>
+        public MediaWikiTranslator()
+        {
+            // このクラス用のロガーと、デフォルトの確認処理としてメッセージダイアログ版を設定
+            this.Logger = new MediaWikiLogger();
+            this.IsContinueAtInterwikiExisted = this.IsContinueAtInterwikiExistedWithDialog;
+        }
+
+        #endregion
+
+        #region デリゲート
+
+        /// <summary>
+        /// 対象記事に言語間リンクが存在する場合の確認処理を表すデリゲート。
+        /// </summary>
+        /// <param name="interwiki">言語間リンク先記事。</param>
+        /// <returns>処理を続行する場合<c>true</c>。</returns>
+        public delegate bool IsContinueAtInterwikiExistedDelegate(string interwiki);
+
+        #endregion
+
         #region プロパティ
 
         /// <summary>
@@ -58,61 +102,15 @@ namespace Honememo.Wptscs.Logics
                 base.To = value;
             }
         }
-        
-        #endregion
-
-        #region 整理予定の静的メソッド
-
-        /// <summary>
-        /// コメント区間のチェック。
-        /// </summary>
-        /// <param name="comment">解析したコメント。</param>
-        /// <param name="text">解析するテキスト。</param>
-        /// <param name="index">解析開始インデックス。</param>
-        /// <returns>コメント区間の場合、終了位置のインデックスを返す。それ以外は-1。</returns>
-        public static int ChkComment(out string comment, string text, int index)
-        {
-            // 入力値確認
-            if (String.IsNullOrEmpty(text))
-            {
-                comment = String.Empty;
-                return -1;
-            }
-
-            // 改良版メソッドをコール
-            if (!LazyXmlParser.TryParseComment(text.Substring(index), out comment))
-            {
-                comment = String.Empty;
-                return -1;
-            }
-
-            return index + comment.Length - 1;
-        }
 
         /// <summary>
-        /// nowiki区間のチェック
+        /// 対象記事に言語間リンクが存在する場合の確認処理
         /// </summary>
-        /// <param name="nowiki">解析したnowikiブロック。</param>
-        /// <param name="text">解析するテキスト。</param>
-        /// <param name="index">解析開始インデックス。</param>
-        /// <returns>nowiki区間の場合、終了位置のインデックスを返す。それ以外は-1。</returns>
-        public static int ChkNowiki(out string nowiki, string text, int index)
+        /// <remarks>確認を行わない場合<c>null</c>。</remarks>
+        public IsContinueAtInterwikiExistedDelegate IsContinueAtInterwikiExisted
         {
-            // 入力値確認
-            if (String.IsNullOrEmpty(text))
-            {
-                nowiki = String.Empty;
-                return -1;
-            }
-
-            // 改良版メソッドをコール
-            if (!MediaWikiPage.TryParseNowiki(text.Substring(index), out nowiki))
-            {
-                nowiki = String.Empty;
-                return -1;
-            }
-
-            return index + nowiki.Length - 1;
+            get;
+            set;
         }
 
         #endregion
@@ -124,81 +122,61 @@ namespace Honememo.Wptscs.Logics
         /// ※継承クラスでは、この関数に処理を実装すること
         /// </summary>
         /// <param name="name">記事名。</param>
-        /// <returns><c>true</c> 処理成功。</returns>
-        protected override bool RunBody(string name)
+        /// <exception cref="ApplicationException">処理が中断された場合。中断の理由は<see cref="Translator.Logger"/>に出力される。</exception>
+        protected override void RunBody(string name)
         {
-            System.Diagnostics.Debug.WriteLine("\nMediaWikiTranslator.runBody > " + name);
-
             // 対象記事を取得
-            MediaWikiPage article = this.ChkTargetArticle(name);
+            MediaWikiPage article = this.GetTargetPage(name);
             if (article == null)
             {
-                return false;
+                throw new ApplicationException("article is not found");
             }
 
             // 対象記事に言語間リンクが存在する場合、処理を継続するか確認
-            string interWiki = article.GetInterWiki(this.To.Language.Code);
-            if (interWiki != String.Empty)
+            // ※ 言語間リンク取得中は、処理状態を解析中に変更
+            MediaWikiLink interlanguage;
+            using (var sm = this.StatusManager.Switch(Resources.StatusParsing))
             {
-                if (MessageBox.Show(
-                        String.Format(Resources.QuestionMessage_ArticleExist, interWiki),
-                        Resources.QuestionTitle,
-                        MessageBoxButtons.YesNo,
-                        MessageBoxIcon.Question)
-                   == System.Windows.Forms.DialogResult.No)
-                {
-                    this.LogLine(ENTER + String.Format(Resources.QuestionMessage_ArticleExist, interWiki));
-                    return false;
-                }
-                else
-                {
-                    this.LogLine(Resources.RightArrow + " " + String.Format(Resources.LogMessage_ArticleExistInterWiki, interWiki));
-                }
+                interlanguage = article.GetInterlanguage(this.To.Language.Code);
             }
 
-            // 冒頭部を作成
-            this.Text += "'''xxx'''";
-            string bracket = this.To.Language.Bracket;
-            if (bracket.Contains("{0}"))
+            if (interlanguage != null)
             {
-                string originalName = String.Empty;
-                string langTitle = this.GetFullName(this.From, this.To.Language.Code);
-                if (langTitle != String.Empty)
+                // 確認処理の最中は処理時間をカウントしない(ダイアログ等を想定するため)
+                this.Stopwatch.Stop();
+                if (this.IsContinueAtInterwikiExisted != null && !this.IsContinueAtInterwikiExisted(interlanguage.Title))
                 {
-                    originalName = "[[" + langTitle + "]]: ";
+                    throw new ApplicationException("user canceled");
                 }
 
-                this.Text += String.Format(bracket, originalName + "'''" + name + "'''");
+                this.Stopwatch.Start();
+                this.Logger.AddResponse(Resources.LogMessageTargetArticleHadInterWiki, interlanguage.Title);
             }
 
-            this.Text += "\n\n";
-
-            // 言語間リンク・定型句の変換
-            this.LogLine(ENTER + Resources.RightArrow + " " + String.Format(Resources.LogMessage_CheckAndReplaceStart, interWiki));
-            this.Text += this.ReplaceText(article.Text, article.Title);
+            // 冒頭部を作成
+            this.Text += this.CreateOpening(article.Title);
 
-            // ユーザーからの中止要求をチェック
-            if (CancellationPending)
+            // 言語間リンク・定型句の変換、実行中は処理状態を解析中に設定
+            this.Logger.AddSeparator();
+            this.Logger.AddResponse(Resources.LogMessageStartParseAndReplace);
+            using (var sm = this.StatusManager.Switch(Resources.StatusParsing))
             {
-                return false;
+                IElement element;
+                using (MediaWikiParser parser = new MediaWikiParser(this.From))
+                {
+                    element = parser.Parse(article.Text);
+                }
+
+                this.Text += this.ReplaceElement(element, article).ToString();
             }
 
-            // 新しい言語間リンクと、コメントを追記
-            this.Text += "\n\n[[" + this.From.Language.Code + ":" + name + "]]\n";
-            this.Text += String.Format(
-                Resources.ArticleFooter,
-                FormUtils.ApplicationName(),
-                this.From.Language.Code,
-                name,
-                article.Timestamp.HasValue ? article.Timestamp.Value.ToString("U") : String.Empty) + "\n";
+            // 記事の末尾に新しい言語間リンクと、コメントを追記
+            this.Text += this.CreateEnding(article);
 
-            // ダウンロードされるテキストがLFなので、ここで全てCRLFに変換
-            // ※ダウンロード時にCRLFにするような仕組みが見つかれば、そちらを使う
+            // ダウンロードされるテキストがLFなので、最後にクライアント環境に合わせた改行コードに変換
+            // ※ダウンロード時に変換するような仕組みが見つかれば、そちらを使う
             //   その場合、上のように\nをべたに吐いている部分を修正する
-            this.Text = this.Text.Replace("\n", ENTER);
-
-            System.Diagnostics.Debug.WriteLine("MediaWikiTranslator.runBody > Success!");
-            return true;
+            this.Text = this.Text.Replace("\n", Environment.NewLine);
         }
 
         #endregion
@@ -206,853 +184,873 @@ namespace Honememo.Wptscs.Logics
         #region 他のクラスの処理をこのクラスにあわせて拡張したメソッド
 
         /// <summary>
-        /// ログメッセージを出力しつつページを取得
+        /// ログ出力によるエラー処理を含んだページ取得処理
         /// </summary>
         /// <param name="title">ページタイトル。</param>
-        /// <param name="notFoundMsg">取得できない場合に出力するメッセージ。</param>
-        /// <returns>取得したページ。ページが存在しない場合は <c>null</c> を返す。</returns>
-        /// <remarks>通信エラーなど例外が発生した場合は、別途エラーログを出力する。</remarks>
-        protected new MediaWikiPage GetPage(string title, string notFoundMsg)
+        /// <param name="page">取得したページ。ページが存在しない場合は <c>null</c> を返す。</param>
+        /// <returns>処理が成功した(404も含む)場合<c>true</c>、失敗した(通信エラーなど)の場合<c>false</c>。</returns>
+        /// <exception cref="ApplicationException"><see cref="Translator.CancellationPending"/>が<c>true</c>の場合。</exception>
+        /// <remarks>
+        /// 本メソッドは、大きく3パターンの動作を行う。
+        /// <list type="number">
+        /// <item><description>正常にページが取得できた → <c>true</c>でページを設定、ログ出力無し</description></item>
+        /// <item><description>404など想定内の例外でページが取得できなかった → <c>true</c>でページ無し、ログ出力無し</description></item>
+        /// <item><description>想定外の例外でページが取得できなかった → <c>false</c>でページ無し、ログ出力有り
+        ///                    or <c>ApplicationException</c>で処理中断(アプリケーション設定のIgnoreErrorによる)。</description></item>
+        /// </list>
+        /// また、実行中は処理状態をサーバー接続中に更新する。
+        /// 実行前後には終了要求のチェックも行う。
+        /// </remarks>
+        protected bool TryGetPage(string title, out MediaWikiPage page)
         {
-            // 親クラスのメソッドを戻り値の型だけ変更
-            return base.GetPage(title, notFoundMsg) as MediaWikiPage;
+            // &amp; &nbsp; 等の特殊文字をデコードして、親クラスのメソッドを呼び出し
+            Page p;
+            bool success = base.TryGetPage(WebUtility.HtmlDecode(title), out p);
+            page = p as MediaWikiPage;
+            return success;
         }
 
         #endregion
 
-        #region å\90\84å\87¦ç\90\86ã\81®メソッド
-        
+        #region å\86\92é ­ï¼\8fæ\9c«å°¾ã\83\96ã\83­ã\83\83ã\82¯ã\81®ç\94\9fæ\88\90メソッド
+
         /// <summary>
-        /// 翻訳支援対象のページを取得
+        /// 変換後記事冒頭用の「'''日本語記事名'''([[英語|英]]: '''{{Lang|en|英語記事名}}''')」みたいなのを作成する
         /// </summary>
-        /// <param name="title">ページ名。</param>
-        /// <returns>å\8f\96å¾\97ã\81\97ã\81\9fã\83\9aã\83¼ã\82¸ã\80\82å\8f\96å¾\97失æ\95\97æ\99\82ã\81¯<c>null</c>。</returns>
-        protected MediaWikiPage ChkTargetArticle(string title)
+        /// <param name="title">翻訳支援対象の記事名。</param>
+        /// <returns>å\86\92é ­é\83¨ã\81®ã\83\86ã\82­ã\82¹ã\83\88。</returns>
+        protected virtual string CreateOpening(string title)
         {
-            // 指定された記事の生データをWikipediaから取得
-            this.LogLine(String.Format(Resources.LogMessage_GetArticle, this.From.Location, title));
-            MediaWikiPage page = this.GetPage(title, Resources.RightArrow + " " + Resources.LogMessage_ArticleNothing);
+            string langPart = String.Empty;
+            IElement langLink = this.GetLanguageLink();
+            if (langLink != null)
+            {
+                langPart = langLink.ToString() + ": ";
+            }
 
-            // リダイレクトかをチェックし、リダイレクトであれば、その先の記事を取得
-            if (page != null && page.IsRedirect())
+            string langBody = this.To.FormatLang(this.From.Language.Code, title);
+            if (String.IsNullOrEmpty(langBody))
             {
-                this.LogLine(Resources.RightArrow + " " + Resources.LogMessage_Redirect + " [[" + page.Redirect.Title + "]]");
-                page = this.GetPage(page.Redirect.Title, Resources.RightArrow + " " + Resources.LogMessage_ArticleNothing);
+                langBody = title;
             }
 
-            return page;
+            StringBuilder b = new StringBuilder("'''xxx'''");
+            b.Append(this.To.Language.FormatBracket(langPart + "'''" + langBody + "'''"));
+            b.Append("\n\n");
+            return b.ToString();
         }
 
         /// <summary>
-        /// ログメッセージを出力しつつ、指定された記事の指定された言語コードへの言語間リンクを返す
+        /// 変換後記事末尾用の新しい言語間リンクとコメントを作成する
         /// </summary>
-        /// <param name="title">記事名。</param>
-        /// <param name="code">言語コード。</param>
-        /// <returns>言語間リンク先の記事名。見つからない場合は空。ページ自体が存在しない場合は<c>null</c>。</returns>
-        protected string GetInterWiki(string title, string code)
+        /// <param name="page">翻訳支援対象の記事。</param>
+        /// <returns>末尾部のテキスト。</returns>
+        protected virtual string CreateEnding(MediaWikiPage page)
         {
-            MediaWikiPage page = this.GetPage(title, Resources.LogMessage_LinkArticleNothing);
+            MediaWikiLink link = new MediaWikiLink();
+            link.Title = page.Title;
+            link.Interwiki = this.From.Language.Code;
+            return "\n\n" + link.ToString() + "\n" + String.Format(
+                Resources.ArticleFooter,
+                FormUtils.ApplicationName(),
+                this.From.Language.Code,
+                page.Title,
+                page.Timestamp.HasValue ? page.Timestamp.Value.ToString("U") : String.Empty) + "\n";
+        }
 
-            // リダイレクトかをチェックし、リダイレクトであれば、その先の記事を取得
-            if (page != null && page.IsRedirect())
+        #endregion
+
+        #region 要素の変換メソッド
+
+        /// <summary>
+        /// 渡されたページ要素の変換を行う。
+        /// </summary>
+        /// <param name="element">ページ要素。</param>
+        /// <param name="parent">ページ要素を取得した変換元記事。</param>
+        /// <returns>変換後のページ要素。</returns>
+        protected virtual IElement ReplaceElement(IElement element, MediaWikiPage parent)
+        {
+            // ユーザーからの中止要求をチェック
+            this.ThrowExceptionIfCanceled();
+
+            // 要素の型に応じて、必要な置き換えを行う
+            if (element is MediaWikiTemplate)
             {
-                this.Log += Resources.LogMessage_Redirect + " [[" + page.Redirect.Title + "]] " + Resources.RightArrow + " ";
-                page = this.GetPage(page.Redirect.Title, Resources.LogMessage_LinkArticleNothing);
+                // テンプレート
+                return this.ReplaceTemplate((MediaWikiTemplate)element, parent);
             }
-
-            // 記事があればその言語間リンクを取得
-            string interWiki = null;
-            if (page != null)
+            else if (element is MediaWikiLink)
             {
-                interWiki = page.GetInterWiki(this.To.Language.Code);
-                if (!String.IsNullOrEmpty(interWiki))
-                {
-                    Log += "[[" + interWiki + "]]";
-                }
-                else
-                {
-                    Log += Resources.LogMessage_InterWikiNothing;
-                }
+                // 内部リンク
+                return this.ReplaceLink((MediaWikiLink)element, parent);
+            }
+            else if (element is MediaWikiHeading)
+            {
+                // 見出し
+                return this.ReplaceHeading((MediaWikiHeading)element, parent);
+            }
+            else if (element is MediaWikiVariable)
+            {
+                // 変数
+                return this.ReplaceVariable((MediaWikiVariable)element, parent);
+            }
+            else if (element is ListElement)
+            {
+                // 値を格納する要素
+                return this.ReplaceListElement((ListElement)element, parent);
             }
 
-            return interWiki;
+            // それ以外は、特に何もせず元の値を返す
+            return element;
         }
 
         /// <summary>
-        /// ログメッセージを出力しつつ、指定された記事の指定された言語コードへの言語間リンクを返す
+        /// 内部リンクを解析し、変換先言語の記事へのリンクに変換する
         /// </summary>
-        /// <param name="title">記事名。</param>
-        /// <param name="code">言語コード。</param>
-        /// <returns>言語間リンク先の記事名。見つからない場合は空。ページ自体が存在しない場合は<c>null</c>。</returns>
-        /// <remarks>対訳表が指定されている場合、その内容を使用する。また取得結果を対訳表に追加する。</remarks>
-        protected string GetInterWikiUseTable(string title, string code)
+        /// <param name="link">変換元リンク。</param>
+        /// <param name="parent">ページ要素を取得した変換元記事。</param>
+        /// <returns>変換済みリンク。</returns>
+        protected virtual IElement ReplaceLink(MediaWikiLink link, MediaWikiPage parent)
         {
-            if (this.ItemTable == null)
+            // 記事名が存在しないor自記事内の別セクションへのリンクの場合、記事名絡みの処理を飛ばす
+            if (!this.IsSectionLink(link, parent.Title))
             {
-                // 対訳表が指定されていない場合は、普通に記事を取得
-                return this.GetInterWiki(title, code);
-            }
+                // 記事名の種類に応じて処理を実施
+                MediaWikiPage article = new MediaWikiPage(this.From, link.Title);
 
-            string interWiki = null;
-            lock (this.ItemTable)
-            {
-                TranslationDictionary.Item item;
-                if (this.ItemTable.TryGetValue(title, out item))
+                bool child = false;
+                if (link.IsSubpage())
                 {
-                    // 対訳表に存在する場合はその値を使用
-                    // リダイレクトがあれば、そのメッセージも表示
-                    if (!String.IsNullOrWhiteSpace(item.Alias))
+                    // サブページ(子)の場合だけ後で記事名を復元するので記録
+                    child = link.Title.StartsWith("/");
+                    
+                    // ページ名を完全な形に補完
+                    string title = parent.Normalize(link);
+                    if (parent.Title.StartsWith(title))
                     {
-                        this.Log += Resources.LogMessage_Redirect + " [[" + item.Alias + "]] " + Resources.RightArrow + " ";
-                    }
+                        // サブページ(親)の場合、変換してもしょうがないのでセクションだけチェックして終了
+                        if (!String.IsNullOrEmpty(link.Section))
+                        {
+                            link.Section = this.ReplaceLinkSection(link.Section);
+                            link.ParsedString = null;
+                        }
 
-                    if (!String.IsNullOrEmpty(item.Word))
-                    {
-                        interWiki = item.Word;
-                        Log += "[[" + interWiki + "]]";
-                    }
-                    else
-                    {
-                        interWiki = String.Empty;
-                        Log += Resources.LogMessage_InterWikiNothing;
+                        return link;
                     }
 
-                    Log += Resources.LogMessageTranslation;
-                    return interWiki;
+                    link.Title = title;
                 }
-
-                // 対訳表に存在しない場合は、普通に取得し表に記録
-                // ※ nullも存在しないことの記録として格納
-                item = new TranslationDictionary.Item { Timestamp = DateTime.UtcNow };
-                MediaWikiPage page = this.GetPage(title, Resources.LogMessage_LinkArticleNothing);
-
-                // リダイレクトかをチェックし、リダイレクトであれば、その先の記事を取得
-                if (page != null && page.IsRedirect())
+                else if (!String.IsNullOrEmpty(link.Interwiki))
                 {
-                    item.Alias = page.Redirect.Title;
-                    this.Log += Resources.LogMessage_Redirect + " [[" + page.Redirect.Title + "]] " + Resources.RightArrow + " ";
-                    page = this.GetPage(page.Redirect.Title, Resources.LogMessage_LinkArticleNothing);
+                    // 言語間リンク・姉妹プロジェクトへのリンクの場合、変換対象外とする
+                    // ただし、先頭が : でない、翻訳先言語への言語間リンクだけは削除
+                    return this.ReplaceLinkInterwiki(link);
+                }
+                else if (article.IsFile())
+                {
+                    // 画像の場合、名前空間を翻訳先言語の書式に変換、パラメータ部を再帰的に処理
+                    return this.ReplaceLinkFile(link, parent);
+                }
+                else if (article.IsCategory() && !link.IsColon)
+                {
+                    // カテゴリで記事へのリンクでない([[:Category:xxx]]みたいなリンクでない)場合、
+                    // カテゴリ用の変換を実施
+                    return this.ReplaceLinkCategory(link);
                 }
 
-                // 記事があればその言語間リンクを取得
-                if (page != null)
+                // 専用処理の無い内部リンクの場合、言語間リンクによる置き換えを行う
+                string interWiki = this.GetInterlanguage(link);
+                if (interWiki == null)
                 {
-                    interWiki = page.GetInterWiki(this.To.Language.Code);
-                    if (!String.IsNullOrEmpty(interWiki))
-                    {
-                        Log += "[[" + interWiki + "]]";
-                    }
-                    else
+                    // 記事自体が存在しない(赤リンク)場合、リンクはそのまま
+                }
+                else if (interWiki == String.Empty)
+                {
+                    // 言語間リンクが存在しない場合、可能なら{{仮リンク}}に置き換え
+                    if (!String.IsNullOrEmpty(this.To.LinkInterwikiFormat))
                     {
-                        Log += Resources.LogMessage_InterWikiNothing;
+                        return this.ReplaceLinkLinkInterwiki(link);
                     }
 
-                    item.Word = interWiki;
-                    this.ItemTable[title] = item;
+                    // 設定が無ければ [[:en:xxx]] みたいな形式に置換
+                    link.Title = this.From.Language.Code + ':' + link.Title;
+                    link.IsColon = true;
+                }
+                else if (child)
+                {
+                    // 言語間リンクが存在してサブページ(子)の場合、親ページ部分を消す
+                    // TODO: 兄弟や叔父のパターンも対処したい(ややこしいので現状未対応)
+                    link.Title = StringUtils.Substring(interWiki, interWiki.IndexOf('/'));
+                }
+                else
+                {
+                    // 普通に言語間リンクが存在する場合、記事名を置き換え
+                    link.Title = interWiki;
+                }
+
+                if (link.PipeTexts.Count == 0 && interWiki != null)
+                {
+                    // 表示名が存在しない場合、元の名前を表示名に設定
+                    // 元の名前にはあればセクションも含む
+                    link.PipeTexts.Add(
+                        new TextElement(new MediaWikiLink { Title = article.Title, Section = link.Section }
+                            .GetLinkString()));
                 }
             }
 
-            return interWiki;
+            // セクション部分([[#関連項目]]とか)を変換
+            if (!String.IsNullOrEmpty(link.Section))
+            {
+                link.Section = this.ReplaceLinkSection(link.Section);
+            }
+
+            link.ParsedString = null;
+            return link;
         }
-        
+
         /// <summary>
-        /// 指定された記事を取得し、言語間リンクを確認、返す
+        /// テンプレートを解析し、変換先言語の記事へのテンプレートに変換する
         /// </summary>
-        /// <param name="title">記事名。</param>
-        /// <param name="template"><c>true</c> テンプレート。</param>
-        /// <returns>言語間リンク先の記事、存在しない場合 <c>null</c>。</returns>
-        protected string GetInterWiki(string title, bool template)
+        /// <param name="template">変換元テンプレート。</param>
+        /// <param name="parent">ページ要素を取得した変換元記事。</param>
+        /// <returns>変換済みテンプレート。</returns>
+        protected virtual IElement ReplaceTemplate(MediaWikiTemplate template, MediaWikiPage parent)
         {
-            // 指定された記事の生データをWikipediaから取得
-            // ※記事自体が存在しない場合、NULLを返す
-            if (!template)
+            // システム変数({{PAGENAME}}とか)の場合は対象外
+            if (this.From.IsMagicWord(template.Title))
+            {
+                return template;
+            }
+
+            // テンプレートは通常名前空間が省略されているので補完する
+            string filledTitle = this.FillTemplateName(template, parent);
+
+            // リンクを辿り、対象記事の言語間リンクを取得
+            string interWiki = this.GetInterlanguage(new MediaWikiTemplate(filledTitle));
+            if (interWiki == null)
+            {
+                // 記事自体が存在しない(赤リンク)場合、リンクはそのまま
+                return template;
+            }
+            else if (interWiki == String.Empty)
             {
-                Log += "[[" + title + "]] " + Resources.RightArrow + " ";
+                // 言語間リンクが存在しない場合、[[:en:Template:xxx]]みたいな普通のリンクに置換
+                // おまけで、元のテンプレートの状態をコメントでつける
+                ListElement list = new ListElement();
+                MediaWikiLink link = new MediaWikiLink();
+                link.IsColon = true;
+                link.Title = this.From.Language.Code + ':' + filledTitle;
+                list.Add(link);
+                XmlCommentElement comment = new XmlCommentElement();
+                comment.Raw = ' ' + template.ToString() + ' ';
+                list.Add(comment);
+                return list;
             }
             else
             {
-                Log += "{{" + title + "}} " + Resources.RightArrow + " ";
+                // 言語間リンクが存在する場合、そちらを指すように置換
+                // : より前の部分を削除して出力(: が無いときは-1+1で0から)
+                template.Title = interWiki.Substring(interWiki.IndexOf(':') + 1);
+
+                // | の後に内部リンクやテンプレートが書かれている場合があるので、再帰的に処理する
+                template.PipeTexts = this.ReplaceElements(template.PipeTexts, parent);
+                template.ParsedString = null;
+                return template;
             }
+        }
+
+        /// <summary>
+        /// 指定された見出しに対して、対訳表による変換を行う。
+        /// </summary>
+        /// <param name="heading">見出し。</param>
+        /// <param name="parent">ページ要素を取得した変換元記事。</param>
+        /// <returns>変換後の見出し。</returns>
+        protected virtual IElement ReplaceHeading(MediaWikiHeading heading, MediaWikiPage parent)
+        {
+            // 変換元ログ出力
+            this.Logger.AddSource(heading);
 
-            // リダイレクトかをチェックし、リダイレクトであれば、その先の記事を取得
-            string interWiki = this.GetInterWikiUseTable(title, this.To.Language.Code);
+            // 定型句変換
+            StringBuilder oldText = new StringBuilder();
+            foreach (IElement e in heading)
+            {
+                oldText.Append(e.ToString());
+            }
 
-            // 改行が出力されていない場合(正常時)、改行
-            if (!Log.EndsWith(ENTER))
+            string newText = this.GetHeading(oldText.ToString().Trim());
+            if (newText != null)
             {
-                Log += ENTER;
+                // 対訳表による変換が行えた場合、変換先をログ出力し処理終了
+                heading.Clear();
+                heading.ParsedString = null;
+                heading.Add(new XmlTextElement(newText));
+                this.Logger.AddDestination(heading);
+                return heading;
             }
 
-            return interWiki;
+            // 対訳表に存在しない場合、内部要素を通常の変換で再帰的に処理
+            return this.ReplaceListElement(heading, parent);
         }
 
         /// <summary>
-        /// 指定された記事を取得し、言語間リンクを確認、返す(テンプレート以外)
+        /// 変数要素を再帰的に解析し、変換先言語の記事への要素に変換する
         /// </summary>
-        /// <param name="name">記事名。</param>
-        /// <returns>言語間リンク先の記事、存在しない場合 <c>null</c>。</returns>
-        protected string GetInterWiki(string name)
+        /// <param name="variable">変換元変数要素。</param>
+        /// <param name="parent">ページ要素を取得した変換元記事。</param>
+        /// <returns>変換済み変数要素。</returns>
+        protected virtual IElement ReplaceVariable(MediaWikiVariable variable, MediaWikiPage parent)
         {
-            return this.GetInterWiki(name, false);
+            // 変数、これ自体は処理しないが、再帰的に探索
+            string old = variable.Value.ToString();
+            variable.Value = this.ReplaceElement(variable.Value, parent);
+            if (variable.Value.ToString() != old)
+            {
+                // 内部要素が変化した(置き換えが行われた)場合、変換前のテキストを破棄
+                variable.ParsedString = null;
+            }
+
+            return variable;
         }
 
         /// <summary>
-        /// 渡されたテキストを解析し、言語間リンク・見出し等の変換を行う
+        /// 要素を再帰的に解析し、変換先言語の記事への要素に変換する
         /// </summary>
-        /// <param name="text">記事テキスト。</param>
-        /// <param name="parent">元記事タイトル。</param>
-        /// <param name="headingEnable">見出しのチェックを行うか?</param>
-        /// <returns>変換後の記事テキスト。</returns>
-        protected string ReplaceText(string text, string parent, bool headingEnable)
+        /// <param name="listElement">変換元要素。</param>
+        /// <param name="parent">ページ要素を取得した変換元記事。</param>
+        /// <returns>変換済み要素。</returns>
+        protected virtual IElement ReplaceListElement(ListElement listElement, MediaWikiPage parent)
         {
-            // 指定された記事の言語間リンク・見出しを探索し、翻訳先言語での名称に変換し、それに置換した文字列を返す
-            StringBuilder b = new StringBuilder();
-            bool enterFlag = true;
-            MediaWikiPage wikiAP = new MediaWikiPage(this.From, "dummy", null);
-            for (int i = 0; i < text.Length; i++)
+            // 値を格納する要素、これ自体は処理しないが、再帰的に探索
+            for (int i = 0; i < listElement.Count; i++)
             {
-                // ユーザーからの中止要求をチェック
-                if (CancellationPending == true)
+                string old = listElement[i].ToString();
+                listElement[i] = this.ReplaceElement(listElement[i], parent);
+                if (listElement[i].ToString() != old)
                 {
-                    break;
+                    // 内部要素が変化した(置き換えが行われた)場合、変換前のテキストを破棄
+                    listElement.ParsedString = null;
                 }
-
-                char c = text[i];
-
-                // 見出しも処理対象の場合
-                if (headingEnable)
-                {
-                    // 改行の場合、次のループで見出し行チェックを行う
-                    if (c == '\n')
-                    {
-                        enterFlag = true;
-                        b.Append(c);
-                        continue;
-                    }
-
-                    // 行の始めでは、その行が見出しの行かのチェックを行う
-                    if (enterFlag)
-                    {
-                        string newTitleLine;
-                        int index2 = this.ChkTitleLine(out newTitleLine, text, i);
-                        if (index2 != -1)
-                        {
-                            // 行の終わりまでインデックスを移動
-                            i = index2;
-
-                            // 置き換えられた見出し行を出力
-                            b.Append(newTitleLine);
-                            continue;
-                        }
-                        else
-                        {
-                            enterFlag = false;
-                        }
-                    }
-                }
-
-                // コメント(<!--)のチェック
-                string comment;
-                int index = MediaWikiTranslator.ChkComment(out comment, text, i);
-                if (index != -1)
-                {
-                    i = index;
-                    b.Append(comment);
-                    if (comment.Contains("\n") == true)
-                    {
-                        enterFlag = true;
-                    }
-
-                    continue;
-                }
-
-                // nowikiのチェック
-                string nowiki;
-                index = MediaWikiTranslator.ChkNowiki(out nowiki, text, i);
-                if (index != -1)
-                {
-                    i = index;
-                    b.Append(nowiki);
-                    continue;
-                }
-
-                // 変数({{{1}}}とか)のチェック
-                string variable;
-                string value;
-                index = wikiAP.ChkVariable(out variable, out value, text, i);
-                if (index != -1)
-                {
-                    i = index;
-
-                    // 変数の | 以降に値が記述されている場合、それに対して再帰的に処理を行う
-                    int valueIndex = variable.IndexOf('|');
-                    if (valueIndex != -1 && !String.IsNullOrEmpty(value))
-                    {
-                        variable = variable.Substring(0, valueIndex + 1) + this.ReplaceText(value, parent) + "}}}";
-                    }
-
-                    b.Append(variable);
-                    continue;
-                }
-
-                // 内部リンク・テンプレートのチェック&変換、言語間リンクを取得し出力する
-                string subtext;
-                index = this.ReplaceLink(out subtext, text, i, parent);
-                if (index != -1)
-                {
-                    i = index;
-                    b.Append(subtext);
-                    continue;
-                }
-
-                // 通常はそのままコピー
-                b.Append(text[i]);
             }
 
-            return b.ToString();
+            return listElement;
         }
 
+        #endregion
+
+        #region 対訳表アクセスメソッド
+
         /// <summary>
-        /// 渡されたテキストを解析し、言語間リンク・見出し等の変換を行う。
+        /// 対訳表に指定された記事名の情報が登録されているか?
         /// </summary>
-        /// <param name="text">記事テキスト。</param>
-        /// <param name="parent">元記事タイトル。</param>
-        /// <returns>変換後の記事テキスト。</returns>
-        protected string ReplaceText(string text, string parent)
+        /// <param name="title">記事名。</param>
+        /// <returns>指定した記事の情報が登録されている場合<c>true</c>。</returns>
+        /// <remarks>複数スレッドからのアクセスに対応する。また項目の対訳表が無い場合も動作する。</remarks>
+        protected bool ContainsAtItemTable(string title)
         {
-            return this.ReplaceText(text, parent, true);
-        }
+            if (this.ItemTable == null)
+            {
+                return false;
+            }
 
+            // 以下マルチスレッドで使われることも想定して対訳表へのアクセス時はロック
+            // ※ 対訳表へのアクセス時は記事名をデコードしておく
+            string decodedTitle = WebUtility.HtmlDecode(title);
+            lock (this.itemTableLock.GetObject(decodedTitle.ToLower()))
+            {
+                // 対訳表へのキーとしてはHTMLデコードした記事名を使用する
+                return this.ItemTable.ContainsKey(decodedTitle);
+            }
+        }
+        
         /// <summary>
-        /// リンクの解析・置換を行う
+        /// 指定されたコードでの見出しに相当する、別の言語での見出しを取得
         /// </summary>
-        /// <param name="link">解析したリンク。</param>
-        /// <param name="text">解析するテキスト。</param>
-        /// <param name="index">解析開始インデックス。</param>
-        /// <param name="parent">元記事タイトル。</param>
-        /// <returns>リンクの場合、終了位置のインデックスを返す。それ以外は-1。</returns>
-        protected int ReplaceLink(out string link, string text, int index, string parent)
+        /// <param name="heading">翻訳元言語での見出し。</param>
+        /// <returns>翻訳先言語での見出し。値が存在しない場合は<c>null</c>。</returns>
+        /// <remarks>見出しの対訳表が無い場合も動作する。</remarks>
+        protected string GetHeading(string heading)
         {
-            // 出力値初期化
-            int lastIndex = -1;
-            link = String.Empty;
-            MediaWikiPage.Link l;
-
-            // 内部リンク・テンプレートの確認と解析
-            MediaWikiPage wikiAP = new MediaWikiPage(this.From, "dummy", null);
-            lastIndex = wikiAP.ChkLinkText(out l, text, index);
-            if (lastIndex != -1)
+            if (this.HeadingTable == null)
             {
-                // 記事名に変数が使われている場合があるので、そのチェックと展開
-                int subindex = l.Title.IndexOf("{{{");
-                if (subindex != -1)
-                {
-                    string variable;
-                    string value;
-                    int lastIndex2 = wikiAP.ChkVariable(out variable, out value, l.Title, subindex);
-                    if (lastIndex2 != -1 && !String.IsNullOrEmpty(value))
-                    {
-                        // 変数の | 以降に値が記述されている場合、それに置き換える
-                        string newArticle = l.Title.Substring(0, subindex) + value;
-                        if (lastIndex2 + 1 < l.Title.Length)
-                        {
-                            newArticle += l.Title.Substring(lastIndex2 + 1);
-                        }
-
-                        l.Title = newArticle;
-                    }
-                    else
-                    {
-                        // 値が設定されていない場合、処理してもしょうがないので、除外
-                        System.Diagnostics.Debug.WriteLine("MediaWikiTranslator.replaceLink > 対象外 : " + l.OriginalText);
-                        return -1;
-                    }
-                }
-
-                string newText = null;
-
-                // 内部リンクの場合
-                if (text[index] == '[')
-                {
-                    // 内部リンクの変換後文字列を取得
-                    newText = this.ReplaceInnerLink(l, parent);
-                }
-                else if (text[index] == '{')
-                {
-                    // テンプレートの場合
-                    // テンプレートの変換後文字列を取得
-                    newText = this.ReplaceTemplate(l, parent);
-                }
-                else
-                {
-                    // 上記以外の場合は、対象外
-                    System.Diagnostics.Debug.WriteLine("MediaWikiTranslator.replaceLink > プログラムミス : " + l.OriginalText);
-                }
-
-                // 変換後文字列がNULL以外
-                if (newText != null)
-                {
-                    link = newText;
-                }
-                else
-                {
-                    lastIndex = -1;
-                }
+                return null;
             }
 
-            return lastIndex;
+            return this.HeadingTable.GetWord(heading);
         }
 
+        #endregion
+
+        #region 言語間リンク取得メソッド
+        
         /// <summary>
-        /// 内部リンクの文字列を変換する
+        /// ロガーに取得結果を出力しつつ、指定された要素の記事の翻訳先言語への言語間リンクを返す
         /// </summary>
-        /// <param name="link">変換元リンク文字列。</param>
-        /// <param name="parent">元記事タイトル。</param>
-        /// <returns>変換済みリンク文字列。</returns>
-        protected string ReplaceInnerLink(MediaWikiPage.Link link, string parent)
+        /// <param name="element">内部リンク要素。</param>
+        /// <returns>言語間リンク先の記事名。見つからない場合は空。ページ自体が存在しない場合は<c>null</c>。</returns>
+        /// <remarks>取得処理では対訳表を使用する。また新たな取得結果は対訳表に追加する。</remarks>
+        protected string GetInterlanguage(MediaWikiLink element)
         {
-            // 変数初期設定
-            StringBuilder b = new StringBuilder("[[");
-            string comment = String.Empty;
-            MediaWikiPage.Link l = link;
-
-            // 記事内を指している場合([[#関連項目]]だけとか)以外
-            if (!String.IsNullOrEmpty(l.Title) &&
-               !(l.Title == parent && String.IsNullOrEmpty(l.Code) && !String.IsNullOrEmpty(l.Section)))
+            // 翻訳元をロガーに出力
+            this.Logger.AddSource(element);
+            string title = element.Title;
+            TranslationDictionary.Item item;
+            if (this.ItemTable == null)
             {
-                // 変換の対象外とするリンクかをチェック
-                MediaWikiPage article = new MediaWikiPage(this.From, l.Title);
+                // 対訳表が指定されていない場合は、使わずに言語間リンクを探索して終了
+                return this.GetInterlanguageWithCreateCache(title, out item);
+            }
 
-                // サブページの場合、記事名を補填
-                if (l.IsSubpage)
-                {
-                    l.Title = parent + l.Title;
-                }
-                else if (!String.IsNullOrEmpty(l.Code))
+            // 対訳表を使用して言語間リンクを探索。
+            // 以下マルチスレッドで使われることも想定して対訳表へのアクセス時はロック(記事名単位)。
+            // また、対訳表へのアクセス時は記事名をデコードしておく。
+            string decodedTitle = WebUtility.HtmlDecode(title);
+            lock (this.itemTableLock.GetObject(decodedTitle.ToLower()))
+            {
+                if (this.ItemTable.TryGetValue(decodedTitle, out item))
                 {
-                    // 言語間リンク・姉妹プロジェクトへのリンクは対象外
-                    // 先頭が : でない、翻訳先言語への言語間リンクの場合
-                    if (!l.IsColon && l.Code == this.To.Language.Code)
+                    // 存在する場合はその値を使用
+                    if (!String.IsNullOrWhiteSpace(item.Alias))
                     {
-                        // 削除する。正常終了で、置換後文字列なしを返す
-                        System.Diagnostics.Debug.WriteLine("MediaWikiTranslator.replaceInnerLink > " + l.OriginalText + " を削除");
-                        return String.Empty;
+                        // リダイレクトがあれば、そのメッセージも表示
+                        this.Logger.AddAlias(new MediaWikiLink(item.Alias));
                     }
 
-                    // それ以外は対象外
-                    System.Diagnostics.Debug.WriteLine("MediaWikiTranslator.replaceInnerLink > 対象外 : " + l.OriginalText);
-                    return null;
-                }
-                else if (article.IsFile())
-                {
-                    // 画像も対象外だが、名前空間だけ翻訳先言語の書式に変換
-                    return this.ReplaceFileLink(l);
-                }
-
-                // リンクを辿り、対象記事の言語間リンクを取得
-                string interWiki = this.GetInterWiki(l.Title);
-
-                // 記事自体が存在しない(赤リンク)場合、リンクはそのまま
-                if (interWiki == null)
-                {
-                    b.Append(l.Title);
-                }
-                else if (interWiki == String.Empty)
-                {
-                    // 言語間リンクが存在しない場合、[[:en:xxx]]みたいな形式に置換
-                    b.Append(":");
-                    b.Append(this.From.Language.Code);
-                    b.Append(":");
-                    b.Append(l.Title);
-                }
-                else
-                {
-                    // 言語間リンクが存在する場合、そちらを指すように置換
-                    // 前の文字列を復元
-                    if (l.IsSubpage)
-                    {
-                        int index = interWiki.IndexOf('/');
-                        if (index == -1)
-                        {
-                            index = 0;
-                        }
-
-                        b.Append(interWiki.Substring(index));
-                    }
-                    else if (l.IsColon)
+                    if (!String.IsNullOrEmpty(item.Word))
                     {
-                        b.Append(":");
-                        b.Append(interWiki);
+                        this.Logger.AddDestination(new MediaWikiLink(item.Word), true);
+                        return item.Word;
                     }
                     else
                     {
-                        b.Append(interWiki);
+                        this.Logger.AddDestination(new TextElement(Resources.LogMessageInterWikiNotFound), true);
+                        return String.Empty;
                     }
                 }
 
-                // カテゴリーの場合は、コメントで元の文字列を追加する
-                if (article.IsCategory() && !l.IsColon)
+                // 対訳表に存在しない場合は、普通に取得し表に記録
+                // ※ こちらは内部でデコードしているためデコードした記事名を渡してはならない
+                string interlanguage = this.GetInterlanguageWithCreateCache(title, out item);
+                if (interlanguage != null)
                 {
-                    comment = "<!-- " + l.OriginalText + " -->";
-
-                    // カテゴリーで[[:en:xxx]]みたいな形式にした場合、| 以降は不要なので削除
-                    if (interWiki == String.Empty)
-                    {
-                        l.PipeTexts = new List<string>();
-                    }
-                }
-                else if (l.PipeTexts.Count == 0 && interWiki != null)
-                {
-                    // 表示名が存在しない場合、元の名前を表示名に設定
-                    l.PipeTexts.Add(article.Title);
+                    // ページ自体が存在しない場合を除き、結果を対訳表に登録
+                    // ※ キャッシュとしては登録すべきかもしれないが、一応"対訳表"であるので
+                    this.ItemTable[decodedTitle] = item;
                 }
+
+                return interlanguage;
             }
+        }
 
-            // 見出し([[#関連項目]]とか)を出力
-            if (!String.IsNullOrEmpty(l.Section))
+        /// <summary>
+        /// ロガーに取得結果を出力しつつ、指定された記事の翻訳先言語への言語間リンクを返す。
+        /// キャッシュ用の処理結果情報も出力する。
+        /// </summary>
+        /// <param name="title">記事名。</param>
+        /// <param name="item">キャッシュ用の処理結果情報。</param>
+        /// <returns>言語間リンク先の記事名。見つからない場合は空。ページ自体が存在しない場合は<c>null</c>。</returns>
+        private string GetInterlanguageWithCreateCache(string title, out TranslationDictionary.Item item)
+        {
+            // 記事名から記事を探索
+            item = new TranslationDictionary.Item { Timestamp = DateTime.UtcNow };
+            MediaWikiPage page = this.GetDestinationPage(title);
+            if (page != null && page.IsRedirect())
             {
-                // 見出しは、定型句変換を通す
-                string heading = this.GetHeading(l.Section);
-                b.Append("#");
-                b.Append(heading != null ? heading : l.Section);
+                // リダイレクトの場合、リダイレクトである旨出力し、その先の記事を取得
+                this.Logger.AddAlias(new MediaWikiLink(page.Redirect.Title));
+                item.Alias = page.Redirect.Title;
+                page = this.GetDestinationPage(page.Redirect.Title);
             }
 
-            // 表示名を出力
-            foreach (string text in l.PipeTexts)
+            if (page == null)
             {
-                b.Append("|");
-                if (!String.IsNullOrEmpty(text))
-                {
-                    // 画像の場合、| の後に内部リンクやテンプレートが書かれている場合があるが、
-                    // 画像は処理対象外でありその中のリンクは個別に再度処理されるため、ここでは特に何もしない
-                    b.Append(text);
-                }
+                // ページ自体が存在しない場合はnull
+                return null;
             }
 
-            // リンクを閉じる
-            b.Append("]]");
-
-            // コメントを付加
-            if (comment != String.Empty)
+            // 記事があればその言語間リンクを取得
+            MediaWikiLink interlanguage = page.GetInterlanguage(this.To.Language.Code);
+            if (interlanguage != null)
             {
-                b.Append(comment);
+                item.Word = interlanguage.Title;
+                this.Logger.AddDestination(interlanguage);
+            }
+            else
+            {
+                // 見つからない場合は空
+                item.Word = String.Empty;
+                this.Logger.AddDestination(new TextElement(Resources.LogMessageInterWikiNotFound));
             }
 
-            System.Diagnostics.Debug.WriteLine("MediaWikiTranslator.replaceInnerLink > " + l.OriginalText);
-            return b.ToString();
+            return item.Word;
         }
 
         /// <summary>
-        /// テンプレートの文字列を変換する。
+        /// 変換先の記事を取得する。
         /// </summary>
-        /// <param name="link">変換元テンプレート文字列。</param>
-        /// <param name="parent">元記事タイトル。</param>
-        /// <returns>変換済みテンプレート文字列。</returns>
-        protected string ReplaceTemplate(MediaWikiPage.Link link, string parent)
+        /// <param name="title">ページタイトル。</param>
+        /// <returns>取得したページ。ページが存在しない場合は <c>null</c> を返す。</returns>
+        /// <remarks>記事が無い場合、通信エラーなど例外が発生した場合は、エラーログを出力する。</remarks>
+        private MediaWikiPage GetDestinationPage(string title)
         {
-            // 変数初期設定
-            MediaWikiPage.Link l = link;
-
-            // テンプレートは記事名が必須
-            if (String.IsNullOrEmpty(l.Title))
+            MediaWikiPage page;
+            if (this.TryGetPage(title, out page) && page == null)
             {
-                System.Diagnostics.Debug.WriteLine("MediaWikiTranslator.replaceTemplate > 対象外 : " + l.OriginalText);
-                return null;
+                // 記事が存在しない場合だけ、変換先に「記事無し」を出力
+                // ※ エラー時のログはTryGetPageが自動的に出力
+                this.Logger.AddDestination(new TextElement(Resources.LogMessageLinkArticleNotFound));
             }
 
-            // システム変数の場合は対象外
-            if (this.From.IsMagicWord(l.Title))
-            {
-                System.Diagnostics.Debug.WriteLine("MediaWikiTranslator.replaceTemplate > システム変数 : " + l.OriginalText);
-                return null;
-            }
+            return page;
+        }
 
-            // テンプレート名前空間か、普通の記事かを判定
-            if (!l.IsColon && !l.IsSubpage)
-            {
-                string prefix = null;
-                IList<string> prefixes = this.From.Namespaces[this.From.TemplateNamespace];
-                if (prefixes != null && prefixes.Count > 0)
-                {
-                    prefix = prefixes[0];
-                }
+        #endregion
 
-                if (!String.IsNullOrEmpty(prefix) && !l.Title.StartsWith(prefix + ":"))
-                {
-                    // 頭にTemplate:を付けた記事名でアクセスし、テンプレートが存在するかをチェック
-                    string title = prefix + ":" + l.Title;
-                    MediaWikiPage page = null;
-                    try
-                    {
-                        page = this.From.GetPage(title) as MediaWikiPage;
-                    }
-                    catch (WebException e)
-                    {
-                        if (e.Status == WebExceptionStatus.ProtocolError
-                            && (e.Response as HttpWebResponse).StatusCode != HttpStatusCode.NotFound)
-                        {
-                            // 記事が取得できない場合も、404でない場合は存在するとして処理
-                            this.LogLine(String.Format(Resources.LogMessage_TemplateUnknown, l.Title, prefix, e.Message));
-                            l.Title = title;
-                        }
-                    }
-                    catch (Exception e)
-                    {
-                        System.Diagnostics.Debug.WriteLine("MediaWikiTranslator.ReplaceTemplate > " + e.Message);
-                    }
+        #region 要素の変換関連その他メソッド
 
-                    if (page != null)
-                    {
-                        // 記事が存在する場合、テンプレートをつけた名前を使用
-                        l.Title = title;
-                    }
-                }
-            }
-            else if (l.IsSubpage)
+        /// <summary>
+        /// 同記事内の別のセクションを指すリンク([[#関連項目]]とか[[自記事#関連項目]]とか)か?
+        /// </summary>
+        /// <param name="link">判定する内部リンク。</param>
+        /// <param name="parent">内部リンクがあった記事。</param>
+        /// <returns>セクション部分のみ変換済みリンク。</returns>
+        private bool IsSectionLink(MediaWikiLink link, string parent)
+        {
+            // 記事名が指定されていない、または記事名が自分の記事名で
+            // 言語コード等も特に無く、かつセクションが指定されている場合
+            // (記事名もセクションも指定されていない・・・というケースもありえるが、
+            //   その場合他に指定できるものも思いつかないので通す)
+            return String.IsNullOrEmpty(link.Title)
+                || (link.Title == parent && String.IsNullOrEmpty(link.Interwiki) && !String.IsNullOrEmpty(link.Section));
+        }
+
+        /// <summary>
+        /// 内部リンクのセクション部分([[#関連項目]]とか)の定型句変換を行う。
+        /// </summary>
+        /// <param name="section">セクション文字列。</param>
+        /// <returns>セクション部分のみ変換済みリンク。</returns>
+        private string ReplaceLinkSection(string section)
+        {
+            // セクションが指定されている場合、定型句変換を通す
+            string heading = this.GetHeading(section);
+            return heading != null ? heading : section;
+        }
+
+        /// <summary>
+        /// 言語間リンク指定の内部リンクを解析し、不要であれば削除する。
+        /// </summary>
+        /// <param name="link">変換元言語間リンク。</param>
+        /// <returns>変換済み言語間リンク。</returns>
+        private IElement ReplaceLinkInterwiki(MediaWikiLink link)
+        {
+            // 言語間リンク・姉妹プロジェクトへのリンクの場合、変換対象外とする
+            // ただし、先頭が : でない、翻訳先言語への言語間リンクだけは削除
+            if (!link.IsColon && link.Interwiki == this.To.Language.Code)
             {
-                // サブページの場合、記事名を補填
-                l.Title = parent + l.Title;
+                return new TextElement();
             }
 
-            // リンクを辿り、対象記事の言語間リンクを取得
-            string interWiki = this.GetInterWiki(l.Title, true);
+            return link;
+        }
 
-            // 記事自体が存在しない(赤リンク)場合、リンクはそのまま
-            StringBuilder b = new StringBuilder();
+        /// <summary>
+        /// カテゴリ指定の内部リンクを解析し、変換先言語のカテゴリへのリンクに変換する。
+        /// </summary>
+        /// <param name="link">変換元カテゴリ。</param>
+        /// <returns>変換済みカテゴリ。</returns>
+        private IElement ReplaceLinkCategory(MediaWikiLink link)
+        {
+            // リンクを辿り、対象記事の言語間リンクを取得
+            string interWiki = this.GetInterlanguage(link);
             if (interWiki == null)
             {
-                b.Append(l.OriginalText);
+                // 記事自体が存在しない(赤リンク)場合、リンクはそのまま
+                return link;
             }
             else if (interWiki == String.Empty)
             {
-                // 言語間リンクが存在しない場合、[[:en:Template:xxx]]みたいな普通のリンクに置換
-                // おまけで、元のテンプレートの状態をコメントでつける
-                b.Append("[[:");
-                b.Append(this.From.Language.Code);
-                b.Append(":");
-                b.Append(l.Title);
-                b.Append("]]<!-- ");
-                b.Append(l.OriginalText);
-                b.Append(" -->");
+                // 言語間リンクが存在しない場合、コメントで元の文字列を保存した後
+                // [[:en:xxx]]みたいな形式に置換。また | 以降は削除する
+                XmlCommentElement comment = new XmlCommentElement();
+                comment.Raw = ' ' + link.ToString() + ' ';
+
+                link.Title = this.From.Language.Code + ':' + link.Title;
+                link.IsColon = true;
+                link.PipeTexts.Clear();
+                link.ParsedString = null;
+
+                ListElement list = new ListElement();
+                list.Add(link);
+                list.Add(comment);
+                return list;
             }
             else
             {
-                // 言語間リンクが存在する場合、そちらを指すように置換
-                b.Append("{{");
-
-                // 前の文字列を復元
-                if (l.IsColon)
-                {
-                    b.Append(":");
-                }
-
-                if (l.IsMsgnw)
-                {
-                    b.Append(MediaWikiPage.Msgnw);
-                }
-
-                // : より前の部分を削除して出力(: が無いときは-1+1で0から)
-                b.Append(interWiki.Substring(interWiki.IndexOf(':') + 1));
-
-                // 改行を復元
-                if (l.Enter)
-                {
-                    b.Append("\n");
-                }
+                // 普通に言語間リンクが存在する場合、記事名を置き換え
+                link.Title = interWiki;
+                link.ParsedString = null;
+                return link;
+            }
+        }
 
-                // | の後を付加
-                foreach (string text in l.PipeTexts)
-                {
-                    b.Append("|");
-                    if (!String.IsNullOrEmpty(text))
-                    {
-                        // | の後に内部リンクやテンプレートが書かれている場合があるので、再帰的に処理する
-                        b.Append(this.ReplaceText(text, parent));
-                    }
-                }
+        /// <summary>
+        /// ファイル指定の内部リンクを解析し、変換先言語で参照可能なファイルへのリンクに変換する。
+        /// </summary>
+        /// <param name="link">変換元リンク。</param>
+        /// <param name="parent">ページ要素を取得した変換元記事。</param>
+        /// <returns>変換済みリンク。</returns>
+        private IElement ReplaceLinkFile(MediaWikiLink link, MediaWikiPage parent)
+        {
+            // 名前空間を翻訳先言語の書式に変換、またパラメータ部を再帰的に処理
+            link.Title = this.ReplaceLinkNamespace(link.Title, this.To.FileNamespace);
+            link.PipeTexts = this.ReplaceElements(link.PipeTexts, parent);
+            link.ParsedString = null;
+            return link;
+        }
 
-                // リンクを閉じる
-                b.Append("}}");
+        /// <summary>
+        /// 記事名のうち名前空間部分の変換先言語への変換を行う。
+        /// </summary>
+        /// <param name="title">変換元記事名。</param>
+        /// <param name="id">名前空間のID。</param>
+        /// <returns>変換済み記事名。</returns>
+        private string ReplaceLinkNamespace(string title, int id)
+        {
+            // 名前空間だけ翻訳先言語の書式に変換
+            IgnoreCaseSet names;
+            if (!this.To.Namespaces.TryGetValue(id, out names))
+            {
+                // 翻訳先言語に相当する名前空間が無い場合、何もしない
+                return title;
             }
 
-            System.Diagnostics.Debug.WriteLine("MediaWikiTranslator.replaceTemplate > " + l.OriginalText);
-            return b.ToString();
+            // 記事名の名前空間部分を置き換えて返す
+            return names.FirstOrDefault() + title.Substring(title.IndexOf(':'));
         }
 
         /// <summary>
-        /// 指定されたインデックスの位置に存在する見出し(==関連項目==みたいなの)を解析し、可能であれば変換して返す
+        /// 内部リンクを他言語版への{{仮リンク}}等に変換する。
         /// </summary>
-        /// <param name="heading">変換後の見出し。</param>
-        /// <param name="text">解析するテキスト。</param>
-        /// <param name="index">解析開始インデックス。</param>
-        /// <returns>見出しの場合、見出し終了位置のインデックスを返す。それ以外は-1。</returns>
-        protected virtual int ChkTitleLine(out string heading, string text, int index)
+        /// <param name="link">変換元言語間リンク。</param>
+        /// <returns>変換済み言語間リンク。</returns>
+        private IElement ReplaceLinkLinkInterwiki(MediaWikiLink link)
         {
-            // 初期化
-            // ※見出しではない、構文がおかしいなどの場合、-1を返す
-            int lastIndex = -1;
-            
-            // 構文を解析して、1行の文字列と、=の個数を取得
-            // ※構文はWikipediaのプレビューで色々試して確認、足りなかったり間違ってたりするかも・・・
-            // ※Wikipediaでは <!--test-.=<!--test-.=関連項目<!--test-.==<!--test-. みたいなのでも
-            //   正常に認識するので、できるだけ対応する
-            // ※変換が正常に行われた場合、コメントは削除される
-            bool startFlag = true;
-            int startSignCounter = 0;
-            string nonCommentLine = String.Empty;
-            StringBuilder b = new StringBuilder();
-            for (lastIndex = index; lastIndex < text.Length; lastIndex++)
+            // 仮リンクにはセクションの指定が可能なので、存在する場合付加する
+            // ※ 渡されたlinkをそのまま使わないのは、余計なゴミが含まれる可能性があるため
+            MediaWikiLink title = new MediaWikiLink { Title = link.Title, Section = link.Section };
+            string langTitle = title.GetLinkString();
+            if (!String.IsNullOrEmpty(title.Section))
             {
-                char c = text[lastIndex];
+                // 変換先言語版のセクションは、セクションの変換を通したものにする
+                title.Section = this.ReplaceLinkSection(title.Section);
+            }
 
-                // 改行まで
-                if (c == '\n')
-                {
-                    break;
-                }
+            // 表示名は、設定されていればその値を、なければ変換元言語の記事名を使用
+            string label = langTitle;
+            if (link.PipeTexts.Count > 0)
+            {
+                label = link.PipeTexts.Last().ToString();
+            }
 
-                // コメントは無視する
-                string comment;
-                int subindex = MediaWikiTranslator.ChkComment(out comment, text, lastIndex);
-                if (subindex != -1)
-                {
-                    b.Append(comment);
-                    lastIndex = subindex;
-                    continue;
-                }
-                else if (startFlag)
-                {
-                    // 先頭部の場合、=の数を数える
-                    if (c == '=')
-                    {
-                        ++startSignCounter;
-                    }
-                    else
-                    {
-                        startFlag = false;
-                    }
-                }
+            // 書式化した文字列を返す
+            // ※ {{仮リンク}}を想定しているが、やろうと思えば何でもできるのでテキストで処理
+            return new TextElement(this.To.FormatLinkInterwiki(title.GetLinkString(), this.From.Language.Code, langTitle, label));
+        }
 
-                nonCommentLine += c;
-                b.Append(c);
+        /// <summary>
+        /// 渡された要素リストに対して<see cref="ReplaceElement"/>による変換を行う。
+        /// </summary>
+        /// <param name="elements">変換元要素リスト。</param>
+        /// <param name="parent">ページ要素を取得した変換元記事。</param>
+        /// <returns>変換済み要素リスト。</returns>
+        private IList<IElement> ReplaceElements(IList<IElement> elements, MediaWikiPage parent)
+        {
+            if (elements == null)
+            {
+                return null;
             }
 
-            heading = b.ToString();
+            IList<IElement> result = new List<IElement>();
+            foreach (IElement e in elements)
+            {
+                result.Add(this.ReplaceElement(e, parent));
+            }
 
-            // 改行文字、または文章の最後+1になっているはずなので、1文字戻す
-            --lastIndex;
+            return result;
+        }
 
-            // = で始まる行ではない場合、処理対象外
-            if (startSignCounter < 1)
+        /// <summary>
+        /// テンプレート名に必要に応じて名前空間を補完する。
+        /// </summary>
+        /// <param name="template">テンプレート。</param>
+        /// <param name="parent">ページ要素を取得した変換元記事。</param>
+        /// <returns>補完済みのテンプレート名。</returns>
+        private string FillTemplateName(MediaWikiTemplate template, MediaWikiPage parent)
+        {
+            // プレフィックスが付いた記事名を作成
+            string filledTitle = parent.Normalize(template);
+            if (filledTitle == template.Title || template.IsSubpage())
             {
-                heading = String.Empty;
-                return -1;
+                // 補完が不要な場合、またはサブページだった場合、ここで終了
+                return filledTitle;
             }
 
-            // 終わりの = の数を確認
-            // ※↓の処理だと中身の無い行(====とか)は弾かれてしまうが、どうせ処理できないので許容する
-            int endSignCounter = 0;
-            for (int i = nonCommentLine.Length - 1; i >= startSignCounter; i--)
+            // プレフィックスが付いた記事名が実際に存在するかを確認
+            // ※ 不要かもしれないが、マジックワードの漏れ等の誤検出を減らしたいので
+            if (this.ContainsAtItemTable(filledTitle))
             {
-                if (nonCommentLine[i] == '=')
-                {
-                    ++endSignCounter;
-                }
-                else
-                {
-                    break;
-                }
+                // 対訳表に記事名が確認されている場合、既知の名前として確定
+                return filledTitle;
             }
 
-            // = で終わる行ではない場合、処理対象外
-            if (endSignCounter < 1)
+            // 実際に頭にプレフィックスを付けた記事名でアクセスし、存在するかをチェック
+            // TODO: GetInterWikiの方とあわせ、テンプレートでは2度GetPageが呼ばれている。可能であれば共通化する
+            MediaWikiPage page = null;
+            try
             {
-                heading = String.Empty;
-                return -1;
+                // 記事が存在する場合、プレフィックスをつけた名前を使用
+                page = this.From.GetPage(WebUtility.HtmlDecode(filledTitle)) as MediaWikiPage;
+                return filledTitle;
             }
-
-            // 始まりと終わり、=の少ないほうにあわせる(==test===とか用の処理)
-            int signCounter = startSignCounter;
-            if (startSignCounter > endSignCounter)
+            catch (FileNotFoundException)
             {
-                signCounter = endSignCounter;
+                // 記事が存在しない場合、元のページ名を使用
+                return template.Title;
             }
-
-            // 定型句変換
-            string oldText = nonCommentLine.Substring(signCounter, nonCommentLine.Length - (signCounter * 2)).Trim();
-            string newText = this.GetHeading(oldText);
-            if (newText != null)
+            catch (Exception e)
             {
-                string sign = "=";
-                for (int i = 1; i < signCounter; i++)
+                // 想定外の例外が発生した場合
+                if (!Settings.Default.IgnoreError)
                 {
-                    sign += "=";
+                    // エラーを無視しない場合、ここで翻訳支援処理を中断する
+                    this.Logger.AddError(e);
+                    throw new ApplicationException(e.Message, e);
                 }
 
-                string newHeading = sign + newText + sign;
-                this.LogLine(ENTER + heading + " " + Resources.RightArrow + " " + newHeading);
-                heading = newHeading;
+                // 続行する場合は、とりあえずプレフィックスをつけた名前で処理
+                this.Logger.AddResponse(Resources.LogMessageTemplateNameUnidentified, template.Title, filledTitle, e.Message);
+                return filledTitle;
             }
-            else
-            {
-                this.LogLine(ENTER + heading);
-            }
-
-            return lastIndex;
         }
 
-        /// <summary>
-        /// 指定されたコードでの見出しに相当する、別の言語での見出しを取得。
-        /// </summary>
-        /// <param name="heading">翻訳元言語での見出し。</param>
-        /// <returns>翻訳先言語での見出し。値が存在しない場合は<c>null</c>。</returns>
-        protected string GetHeading(string heading)
-        {
-            return this.HeadingTable.GetWord(heading);
-        }
+        #endregion
+
+        #region その他内部処理用メソッド
 
         /// <summary>
-        /// 指定した言語での言語名称を ページ名|略称 の形式で取得。
+        /// 翻訳支援対象のページを取得。
         /// </summary>
-        /// <param name="site">サイト。</param>
-        /// <param name="code">言語のコード。</param>
-        /// <returns>ページ名|略称形式の言語名称。</returns>
-        protected string GetFullName(Website site, string code)
+        /// <param name="title">翻訳支援対象の記事名。</param>
+        /// <returns>取得したページ。取得失敗時は<c>null</c>。</returns>
+        /// <remarks>
+        /// ここで取得した記事のURIを、以後の翻訳支援処理で使用するRefererとして登録
+        /// (ここで処理しているのは、リダイレクトの場合のリダイレクト先への
+        /// アクセス時にもRefererを入れたかったから。
+        /// リダイレクトの場合は、最終的には転送先ページのURIとなる)。
+        /// </remarks>
+        private MediaWikiPage GetTargetPage(string title)
         {
-            if (site.Language.Names.ContainsKey(code))
+            // 指定された記事をWikipediaから取得、リダイレクトの場合その先まで探索
+            // ※ この処理ではキャッシュは使用しない。
+            // ※ 万が一相互にリダイレクトしていると無限ループとなるが、特に判定はしない。
+            //    ユーザーが画面上から止めることを期待。
+            this.Logger.AddMessage(Resources.LogMessageGetTargetArticle, this.From.Location, title);
+            MediaWikiPage page;
+            for (string s = title; this.TryGetPage(s, out page); s = page.Redirect.Title)
             {
-                Language.LanguageName name = site.Language.Names[code];
-                if (!String.IsNullOrEmpty(name.ShortName))
+                if (page == null)
                 {
-                    return name.Name + "|" + name.ShortName;
+                    // 記事が存在しない場合、メッセージを出力して終了
+                    this.Logger.AddResponse(Resources.LogMessageTargetArticleNotFound);
+                    break;
                 }
-                else
+
+                // 取得した記事のURIを以後のアクセスで用いるRefererとして登録
+                this.From.WebProxy.Referer = page.Uri.ToString();
+                this.To.WebProxy.Referer = page.Uri.ToString();
+
+                if (!page.IsRedirect())
                 {
-                    return name.Name;
+                    // リダイレクト以外ならこれで終了
+                    break;
                 }
+
+                // リダイレクトであれば、さらにその先の記事を取得
+                this.Logger.AddResponse(Resources.LogMessageRedirect
+                    + " " + new MediaWikiLink(page.Redirect.Title).ToString());
             }
 
-            return String.Empty;
+            return page;
         }
 
         /// <summary>
-        /// 画像などのファイルへの内部リンクの置き換えを行う
+        /// 指定した言語での言語名称を [[言語名称|略称]]の内部リンクで取得
         /// </summary>
-        /// <param name="link">内部リンク。</param>
-        /// <returns>置き換え後のリンク文字列、置き換えを行わない場合<c>null</c>。</returns>
-        private string ReplaceFileLink(MediaWikiPage.Link link)
+        /// <returns>
+        /// [[言語名称|略称]]の内部リンク。登録されていない場合<c>null</c>。
+        /// サーバーにそうした記事が存在しない場合、リンクではなく言語名称or略称の文字列を返す。
+        /// </returns>
+        private IElement GetLanguageLink()
         {
-            // 名前空間だけ翻訳先言語の書式に変換
-            IList<string> names;
-            if (!this.To.Namespaces.TryGetValue(this.To.FileNamespace, out names))
+            // 言語情報を取得
+            Language.LanguageName name;
+            if (!this.From.Language.Names.TryGetValue(this.To.Language.Code, out name))
             {
-                // 翻訳先言語に相当する名前空間が無い場合、何もしない
                 return null;
             }
 
-            // 記事名の名前空間部分を置き換えて返す
-            link.Title = names[0] + link.Title.Substring(link.Title.IndexOf(':'));
-            return link.Text;
+            // 略称を取得
+            IElement shortName = null;
+            if (!String.IsNullOrEmpty(name.ShortName))
+            {
+                shortName = new TextElement(name.ShortName);
+            }
+
+            if (this.To.HasLanguagePage)
+            {
+                // サーバーにこの言語の記事が存在することが期待される場合、
+                // 内部リンクとして返す
+                MediaWikiLink link = new MediaWikiLink(name.Name);
+                if (shortName != null)
+                {
+                    link.PipeTexts.Add(shortName);
+                }
+
+                return link;
+            }
+            else if (shortName != null)
+            {
+                // 存在しない場合、まずあれば略称を返す
+                return shortName;
+            }
+            else
+            {
+                // 無ければ言語名を返す
+                return new TextElement(name.Name);
+            }
+        }
+
+        /// <summary>
+        /// 対象記事に言語間リンクが存在する場合にメッセージダイアログでユーザーに確認する処理。
+        /// </summary>
+        /// <param name="interwiki">言語間リンク先記事。</param>
+        /// <returns>処理を続行する場合<c>true</c>。</returns>
+        private bool IsContinueAtInterwikiExistedWithDialog(string interwiki)
+        {
+            // 確認ダイアログを表示
+            if (MessageBox.Show(
+                        String.Format(Resources.QuestionMessageArticleExisted, interwiki),
+                        Resources.QuestionTitle,
+                        MessageBoxButtons.YesNo,
+                        MessageBoxIcon.Question)
+                   == DialogResult.No)
+            {
+                // 中断の場合、同じメッセージをログにも表示
+                this.Logger.AddSeparator();
+                this.Logger.AddMessage(Resources.QuestionMessageArticleExisted, interwiki);
+                return false;
+            }
+
+            return true;
         }
 
         #endregion