add translate
This commit is contained in:
@@ -114,6 +114,7 @@ async function listRecordsFromApi(did: string, collection: string, pdsUrl: strin
|
||||
title: r.value.title as string || 'Untitled',
|
||||
content: r.value.content as string || '',
|
||||
createdAt: r.value.createdAt as string || new Date().toISOString(),
|
||||
translations: r.value.translations as BlogPost['translations'] || undefined,
|
||||
})).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
}
|
||||
|
||||
@@ -394,22 +395,68 @@ function generateTabsHtml(activeTab: 'blog' | 'browser', handle: string): string
|
||||
`
|
||||
}
|
||||
|
||||
function generateLangSelectorHtml(): string {
|
||||
const langIcon = `<svg viewBox="0 0 640 640" width="20" height="20" fill="currentColor"><path d="M192 64C209.7 64 224 78.3 224 96L224 128L352 128C369.7 128 384 142.3 384 160C384 177.7 369.7 192 352 192L342.4 192L334 215.1C317.6 260.3 292.9 301.6 261.8 337.1C276 345.9 290.8 353.7 306.2 360.6L356.6 383L418.8 243C423.9 231.4 435.4 224 448 224C460.6 224 472.1 231.4 477.2 243L605.2 531C612.4 547.2 605.1 566.1 589 573.2C572.9 580.3 553.9 573.1 546.8 557L526.8 512L369.3 512L349.3 557C342.1 573.2 323.2 580.4 307.1 573.2C291 566 283.7 547.1 290.9 531L330.7 441.5L280.3 419.1C257.3 408.9 235.3 396.7 214.5 382.7C193.2 399.9 169.9 414.9 145 427.4L110.3 444.6C94.5 452.5 75.3 446.1 67.4 430.3C59.5 414.5 65.9 395.3 81.7 387.4L116.2 370.1C132.5 361.9 148 352.4 162.6 341.8C148.8 329.1 135.8 315.4 123.7 300.9L113.6 288.7C102.3 275.1 104.1 254.9 117.7 243.6C131.3 232.3 151.5 234.1 162.8 247.7L173 259.9C184.5 273.8 197.1 286.7 210.4 298.6C237.9 268.2 259.6 232.5 273.9 193.2L274.4 192L64.1 192C46.3 192 32 177.7 32 160C32 142.3 46.3 128 64 128L160 128L160 96C160 78.3 174.3 64 192 64zM448 334.8L397.7 448L498.3 448L448 334.8z"/></svg>`
|
||||
|
||||
return `
|
||||
<div class="lang-selector" id="lang-selector">
|
||||
<button type="button" class="lang-btn" id="lang-btn" title="Language">
|
||||
${langIcon}
|
||||
</button>
|
||||
<div class="lang-dropdown" id="lang-dropdown">
|
||||
<div class="lang-option" data-lang="ja">
|
||||
<span class="lang-name">日本語</span>
|
||||
<span class="lang-check">✓</span>
|
||||
</div>
|
||||
<div class="lang-option selected" data-lang="en">
|
||||
<span class="lang-name">English</span>
|
||||
<span class="lang-check">✓</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function generatePostListHtml(posts: BlogPost[]): string {
|
||||
if (posts.length === 0) {
|
||||
return '<p class="no-posts">No posts yet</p>'
|
||||
}
|
||||
|
||||
// Build translations data for titles
|
||||
const titleTranslations: Record<string, { original: string; translated: string }> = {}
|
||||
posts.forEach(post => {
|
||||
const rkey = post.uri.split('/').pop()
|
||||
if (rkey && post.translations?.en?.title) {
|
||||
titleTranslations[rkey] = {
|
||||
original: post.title,
|
||||
translated: post.translations.en.title
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const hasTranslations = Object.keys(titleTranslations).length > 0
|
||||
const translationScript = hasTranslations
|
||||
? `<script id="title-translations" type="application/json">${JSON.stringify(titleTranslations)}</script>`
|
||||
: ''
|
||||
|
||||
const items = posts.map(post => {
|
||||
const rkey = post.uri.split('/').pop()
|
||||
// Default to English title if available
|
||||
const displayTitle = post.translations?.en?.title || post.title
|
||||
return `
|
||||
<li class="post-item">
|
||||
<a href="/post/${rkey}/" class="post-link">
|
||||
<span class="post-title">${escapeHtml(post.title)}</span>
|
||||
<span class="post-title" data-rkey="${rkey}">${escapeHtml(displayTitle)}</span>
|
||||
<span class="post-date">${formatDate(post.createdAt)}</span>
|
||||
</a>
|
||||
</li>
|
||||
`
|
||||
}).join('')
|
||||
return `<ul class="post-list">${items}</ul>`
|
||||
return `
|
||||
<div class="content-header">${generateLangSelectorHtml()}</div>
|
||||
${translationScript}
|
||||
<ul class="post-list">${items}</ul>
|
||||
`
|
||||
}
|
||||
|
||||
// Map network to app URL for discussion links
|
||||
@@ -423,7 +470,15 @@ function getAppUrl(network: string): string {
|
||||
function generatePostDetailHtml(post: BlogPost, handle: string, collection: string, network: string, siteUrl?: string): string {
|
||||
const rkey = post.uri.split('/').pop() || ''
|
||||
const jsonUrl = `/at/${handle}/${collection}/${rkey}/`
|
||||
const content = marked.parse(post.content) as string
|
||||
const originalContent = marked.parse(post.content) as string
|
||||
|
||||
// Check for English translation
|
||||
const hasTranslation = post.translations?.en?.content
|
||||
const translatedContent = hasTranslation ? marked.parse(post.translations!.en.content) as string : ''
|
||||
|
||||
// Default to English if translation exists
|
||||
const displayContent = hasTranslation ? translatedContent : originalContent
|
||||
|
||||
// Use siteUrl from config, or construct from handle
|
||||
const baseSiteUrl = siteUrl || `https://${handle}`
|
||||
const postUrl = `${baseSiteUrl}/post/${rkey}/`
|
||||
@@ -439,16 +494,32 @@ function generatePostDetailHtml(post: BlogPost, handle: string, collection: stri
|
||||
const searchQuery = basePath + rkeyPrefix
|
||||
const searchUrl = `${appUrl}/search?q=${encodeURIComponent(searchQuery)}`
|
||||
|
||||
// Store translation data in script tag for JS switching
|
||||
const hasTranslatedTitle = !!post.translations?.en?.title
|
||||
const translationScript = hasTranslation
|
||||
? `<script id="translation-data" type="application/json">${JSON.stringify({
|
||||
original: originalContent,
|
||||
translated: translatedContent,
|
||||
originalTitle: post.title,
|
||||
translatedTitle: post.translations?.en?.title || post.title
|
||||
})}</script>`
|
||||
: ''
|
||||
|
||||
// Default to English title if available
|
||||
const displayTitle = post.translations?.en?.title || post.title
|
||||
|
||||
return `
|
||||
<div class="content-header">${generateLangSelectorHtml()}</div>
|
||||
${translationScript}
|
||||
<article class="post-detail">
|
||||
<header class="post-header">
|
||||
<h1 class="post-title">${escapeHtml(post.title)}</h1>
|
||||
<h1 class="post-title" id="post-detail-title">${escapeHtml(displayTitle)}</h1>
|
||||
<div class="post-meta">
|
||||
<time class="post-date">${formatDate(post.createdAt)}</time>
|
||||
<a href="${jsonUrl}" class="json-btn">json</a>
|
||||
</div>
|
||||
</header>
|
||||
<div class="post-content">${content}</div>
|
||||
<div class="post-content">${displayContent}</div>
|
||||
</article>
|
||||
<div class="discussion-section">
|
||||
<a href="${searchUrl}" target="_blank" rel="noopener" class="discuss-link">
|
||||
@@ -645,16 +716,32 @@ async function generate() {
|
||||
const localPosts = localDid ? loadPostsFromFiles(localDid, config.collection) : []
|
||||
console.log(`Found ${localPosts.length} posts from local`)
|
||||
|
||||
// Merge: API is the source of truth
|
||||
// - If post exists in API: always use API (has latest edits)
|
||||
// - If post exists in local only: keep if not deleted (for posts beyond API limit)
|
||||
// Merge: API is the source of truth for content, but local has translations
|
||||
// - If post exists in both: use API data but merge translations from local
|
||||
// - If post exists in API only: use API
|
||||
// - If post exists in local only: keep (for posts beyond API limit)
|
||||
const localPostMap = new Map<string, BlogPost>()
|
||||
for (const post of localPosts) {
|
||||
const rkey = post.uri.split('/').pop()
|
||||
if (rkey) localPostMap.set(rkey, post)
|
||||
}
|
||||
|
||||
const apiRkeys = new Set(apiPosts.map(p => p.uri.split('/').pop()))
|
||||
|
||||
// Merge API posts with local translations
|
||||
const mergedApiPosts = apiPosts.map(apiPost => {
|
||||
const rkey = apiPost.uri.split('/').pop()
|
||||
const localPost = rkey ? localPostMap.get(rkey) : undefined
|
||||
if (localPost?.translations && !apiPost.translations) {
|
||||
return { ...apiPost, translations: localPost.translations }
|
||||
}
|
||||
return apiPost
|
||||
})
|
||||
|
||||
// Local posts that don't exist in API (older posts beyond 100 limit)
|
||||
// Note: these might be deleted posts, so we keep them cautiously
|
||||
const oldLocalPosts = localPosts.filter(p => !apiRkeys.has(p.uri.split('/').pop()))
|
||||
|
||||
posts = [...apiPosts, ...oldLocalPosts].sort((a, b) =>
|
||||
posts = [...mergedApiPosts, ...oldLocalPosts].sort((a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user