Skip to content

Instantly share code, notes, and snippets.

@acro5piano
Created May 25, 2025 04:26
Show Gist options
  • Save acro5piano/59d2d71489aa118472d3de6251da6f69 to your computer and use it in GitHub Desktop.
Save acro5piano/59d2d71489aa118472d3de6251da6f69 to your computer and use it in GitHub Desktop.
simple i18n based on yaml
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
import { dirname, join } from 'path'
import { glob } from 'glob'
import yaml from 'js-yaml'
interface TranslationEntry {
key: string
files: string[]
}
// Extract t`...` template strings from file content
function extractTranslations(
content: string,
filePath: string,
): TranslationEntry[] {
const translations: TranslationEntry[] = []
// Regex to match t`...` template strings
// This handles single-line and multi-line template strings
const regex = /\bt`([^`]+)`/g
let match
while ((match = regex.exec(content)) !== null) {
const key = match[1]
translations.push({
key,
files: [filePath],
})
}
return translations
}
// Merge translations from multiple files
function mergeTranslations(
allTranslations: TranslationEntry[],
): Map<string, string[]> {
const merged = new Map<string, string[]>()
for (const entry of allTranslations) {
const existing = merged.get(entry.key) || []
merged.set(entry.key, [...existing, ...entry.files])
}
return merged
}
// Load existing translations from YAML
function loadExistingTranslations(locale: string): Record<string, string> {
const filePath = join('src', 'i18n', `${locale}.yaml`)
if (!existsSync(filePath)) {
return {}
}
const content = readFileSync(filePath, 'utf-8')
try {
const data = yaml.load(content) as Record<string, string>
return data || {}
} catch (error) {
console.error(`Error parsing ${filePath}:`, error)
return {}
}
}
// Generate translation YAML file
function generateTranslationFile(
translations: Map<string, string[]>,
existing: Record<string, string>,
): string {
const translationObject: Record<string, string> = {}
// Do not sort keys (newer keys come to the last)
const keys = Array.from(translations.keys())
for (const key of keys) {
translationObject[key] = existing[key] || ''
}
const rawYaml = yaml.dump(translationObject, {
lineWidth: -1, // Disable line wrapping
quotingType: '"', // Use double quotes
forceQuotes: false, // Only quote when necessary
sortKeys: false, // Keep our sorted order
})
return rawYaml.replace(/""/, '|\n') // Replace empty string into newline for readability
}
async function main() {
console.log('Extracting i18n strings...')
// Find all .ts, .tsx, and .astro files under src/
const files = await glob('src/**/*.{ts,tsx,astro}', {
ignore: ['src/i18n/*.ts', 'src/i18n/*.yaml'], // Ignore translation files
})
console.log(`Found ${files.length} files to process`)
const allTranslations: TranslationEntry[] = []
// Extract translations from each file
for (const file of files) {
const content = readFileSync(file, 'utf-8')
const translations = extractTranslations(content, file)
if (translations.length > 0) {
console.log(`Found ${translations.length} translations in ${file}`)
allTranslations.push(...translations)
}
}
// Merge translations
const mergedTranslations = mergeTranslations(allTranslations)
console.log(`Total unique translations: ${mergedTranslations.size}`)
// Generate translation files for each locale
const locales = ['ja'] // Add more locales as needed
for (const locale of locales) {
const existing = loadExistingTranslations(locale)
const content = generateTranslationFile(mergedTranslations, existing)
const outputPath = join('src', 'i18n', `${locale}.yaml`)
// Ensure directory exists
const dir = dirname(outputPath)
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true })
}
writeFileSync(outputPath, content)
console.log(`Generated ${outputPath}`)
// Report missing translations
const missingCount = Array.from(mergedTranslations.keys()).filter(
(key) => !existing[key] || existing[key] === '',
).length
if (missingCount > 0) {
console.log(`⚠️ ${missingCount} missing translations in ${locale}`)
}
}
console.log('Done!')
}
// Run the script
main().catch(console.error)
To get started, open the src/pages directory in your project.: |
開始するには、 src/pages フォルダを編集してください
What's New in Astro 5.0?: |
Astro 5.0 での変更点
From content layers to server islands, click to learn more about the new features and improvements in Astro 5.0: |
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment