構想: ファイルベースで多言語
次のような構成で記事を書いて多言語サイトを作る
- 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 部分を抜粋
"posts/*") $ do
match (fromArticle route langRoute
langRoute :: Routes
= customRoute ((`replaceExtension` ".html") . getLangPath)
langRoute
langseperator :: String
= "-"
langseperator
takeLang :: Identifier -> String
= last . splitAll langseperator . takeBaseName . toFilePath
takeLang
dropLang :: Identifier -> String
= L.intercalate langseperator . init . splitAll langseperator . toFilePath
dropLang
getLangPath :: Identifier -> String
id = lang `combine` dropLang id
getLangPath where
= takeLang id lang
行っている操作はシンプルで、次のような操作を行っている。
Identifier
からファイルパスをもらう- 文字列操作で
-en
、-jp
の言語部分を抜き出す - 単純に入れ替えて
/
で繋ぎなおす
同じ記事の多言語バージョンへのリンクを埋め込む
この操作が肝になる。Route
の変更は容易いがこの実装に苦労した。
次のような方法で実装している。
filename-lang.md
という Identifier から、filename
が一致するものを検索filename*
というPattern
で検索する
- 見つかったすべての
-lang
違いのものの情報をリストとして埋め込む
anothorLangFields :: String -> Snapshot -> Context String
= listFieldWith name baseCtx f
anothorLangFields name snapshot where
= loadAllSnapshots (fromGlob (mainname ++ "*")) snapshot
f item where
id = itemIdentifier item
= dropLang id
mainname = takeLang id lang
anothorLangFields
を埋め込む実装は次の通り
snapshot
を作りpostCtx
を使ってtemplate
を適用する
"posts/*") $ do
match (fromArticle
route langRoute$ do
compile
pandocCompiler>>= saveSnapshot "posts-content"
>>= loadAndApplyTemplate (idBase "templates/post.html") postCtx
postCtx
はbaseCtx
とanothorLangFields
を繋げたもの
postCtx :: Context String
=
postCtx
baseCtx`mappend` anothorLangFields "anothorLangs" "posts-content"
baseCtx
は基本的な情報を埋め込むコンテキスト
baseCtx :: Context String
=
baseCtx "date" "%Y/%m/%d"
dateField `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
がユニークなもののみ残す。
$ do
compile <- recentFirst . filterUniqs =<< loadAllSnapshots "posts/*" "posts-content" posts
filterUniqs
は、やってくる [Item String]
に対して-lang
部分を落としたファイル名が一致しないもののみ
後ろにつけてリストを再構築する関数として定義する。
filterUniqs :: [Item String] -> [Item String]
= []
filterUniqs [] : xs) = x : filterUniqs (filter ((/= f x) . f) xs)
filterUniqs (x where
= dropLang . itemIdentifier f
これで期待した操作を実現することができる。