lang en ja

Hakyllでファイルベースの多言語対応サイトを作る

Published Last Update author
2024/05/28 2024/05/28 Shumpei Tanaka

構想: ファイルベースで多言語

次のような構成で記事を書いて多言語サイトを作る

  • posts/2024-05-28-filename-en.md
  • posts/2024-05-28-filename-jp.md

具体例

このサイトは上記構成で作成している。 後日エンジン部分を分離し公開する予定。

Route の変更

まずは Route を変更する。変更のイメージを次に示す。

  • 元ファイルのパス
    • posts/2024-05-28-filename-en.md
    • posts/2024-05-28-filename-jp.md
  • 変換後 html のパス
    • en/2024-05-28-filename.html
    • jp/2024-05-28-filename.html

これを実現するには Route を書き換えてやればよい。

  • route 部分を抜粋
match (fromArticle "posts/*") $ do
    route langRoute
langRoute :: Routes
langRoute = customRoute ((`replaceExtension` ".html") . getLangPath)

langseperator :: String
langseperator = "-"

takeLang :: Identifier -> String
takeLang = last . splitAll langseperator . takeBaseName . toFilePath

dropLang :: Identifier -> String
dropLang = L.intercalate langseperator . init . splitAll langseperator . toFilePath

getLangPath :: Identifier -> String
getLangPath id = lang `combine` dropLang id
  where
    lang = takeLang id

行っている操作はシンプルで、次のような操作を行っている。

  • Identifier からファイルパスをもらう
  • 文字列操作で-en-jpの言語部分を抜き出す
  • 単純に入れ替えて/で繋ぎなおす

同じ記事の多言語バージョンへのリンクを埋め込む

この操作が肝になる。Route の変更は容易いがこの実装に苦労した。

次のような方法で実装している。

  • filename-lang.mdという Identifier から、filenameが一致するものを検索
    • filename*というPatternで検索する
  • 見つかったすべての-lang違いのものの情報をリストとして埋め込む
anothorLangFields :: String -> Snapshot -> Context String
anothorLangFields name snapshot = listFieldWith name baseCtx f
  where
    f item = loadAllSnapshots (fromGlob (mainname ++ "*")) snapshot
      where
        id = itemIdentifier item
        mainname = dropLang id
        lang = takeLang id

anothorLangFieldsを埋め込む実装は次の通り

  • snapshot を作り postCtx を使ってtemplateを適用する
match (fromArticle "posts/*") $ do
    route langRoute
    compile $ do
        pandocCompiler
            >>= saveSnapshot "posts-content"
            >>= loadAndApplyTemplate (idBase "templates/post.html") postCtx
  • postCtxbaseCtxanothorLangFields を繋げたもの
postCtx :: Context String
postCtx =
    baseCtx
        `mappend` anothorLangFields "anothorLangs" "posts-content"
  • baseCtx は基本的な情報を埋め込むコンテキスト
baseCtx :: Context String
baseCtx =
    dateField "date" "%Y/%m/%d"
        `mappend` dateFieldMeta "last-modified" "last-modified" "%Y/%m/%d"
        `mappend` readTimeField "readtime" "posts-content"
        `mappend` langField "lang"
        `mappend` sameLangField "home" "index"
        `mappend` defaultContext

これでコンテキストは次のような構造になる

  • date
  • last-modified
  • readtime
  • lang
  • home
  • (defaultContext)
  • anotherlangs
    • date
    • last-modified
    • readtime
    • lang
    • home
    • (defaultContext)

すなわち、templateで次のように呼び出すことが可能となる

  • ある記事のlang違いの情報を表示するtemplate
$for(anothorLangs)$
<tr>
    <td>
        <a href="$url$">$title$ ($lang$)</a>
    </td>
    <td>$date$</td>
    <td>$if(last-modified)$ $last-modified$ $else$ $date$ $endif$</td>
</tr>
$endfor$

Routeを変更したうえで md の内部リンクをする

これはより汎的な内容のため別ページに分離。

Hakyll で Route を変更した上で内部リンクを使う

post list を再編成する

この方式では、単にposts/*.mdを読み込むと-en.md-jp.mdがそれぞれ並べられてしまう。

posts/file-lang.mdのうちfileが一致するものは一つに絞りたい。この実装をする。

次のように読み込んだposts/*に対してfilterUniqsを適用し、fileがユニークなもののみ残す。

compile $ do
    posts <- recentFirst . filterUniqs =<< loadAllSnapshots "posts/*" "posts-content"

filterUniqsは、やってくる [Item String]に対して-lang部分を落としたファイル名が一致しないもののみ 後ろにつけてリストを再構築する関数として定義する。

filterUniqs :: [Item String] -> [Item String]
filterUniqs [] = []
filterUniqs (x : xs) = x : filterUniqs (filter ((/= f x) . f) xs)
  where
    f = dropLang . itemIdentifier

これで期待した操作を実現することができる。

Say thank you

役立ったら下のリンクから支援をもらえると嬉しい
paypal.me Badge paypal.me Badge