構想: ファイルベースで多言語
次のような構成で記事を書いて多言語サイトを作る
- 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 langRoutelangRoute :: 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 idanothorLangFieldsを埋め込む実装は次の通り
snapshotを作りpostCtxを使ってtemplateを適用する
match (fromArticle "posts/*") $ do
route langRoute
compile $ do
pandocCompiler
>>= saveSnapshot "posts-content"
>>= loadAndApplyTemplate (idBase "templates/post.html") postCtxpostCtxはbaseCtxとanothorLangFieldsを繋げたもの
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これで期待した操作を実現することができる。