Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/i18n-meta.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: i18n-meta

on:
push:
branches:
- main
paths:
- 'i18n/locales/en.json'

permissions:
contents: write

jobs:
update:
name: 🌐 Update en.meta.json
runs-on: ubuntu-24.04-arm

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 2

- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: lts/*

- uses: pnpm/action-setup@1e1c8eafbd745f64b1ef30a7d7ed7965034c486c # 1e1c8eafbd745f64b1ef30a7d7ed7965034c486c
name: 🟧 Install pnpm
with:
cache: true

- name: 📦 Install dependencies
run: pnpm install

- name: 🌐 Update i18n metadata
run: pnpm i18n:meta:update-en-meta

- name: ⬆︎ Commit and Push changes
run: |
git config --global user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add i18n/locales/en.meta.json
if ! git diff --cached --quiet; then
git commit -m 'chore(i18n): update `en.meta.json` [skip ci]'
git push
else
echo "No changes in en.meta.json, skipping commit."
fi
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"i18n:report": "node scripts/find-invalid-translations.ts",
"i18n:report:fix": "node scripts/remove-unused-translations.ts",
"i18n:schema": "node scripts/generate-i18n-schema.ts",
"i18n:meta:update-en-meta": "node scripts/i18n-meta/cli.ts update-en-meta",
"knip": "knip",
"knip:fix": "knip --fix",
"lint": "oxlint && oxfmt --check",
Expand Down Expand Up @@ -130,6 +131,7 @@
"@storybook/addon-a11y": "catalog:storybook",
"@storybook/addon-docs": "catalog:storybook",
"@storybook/addon-themes": "catalog:storybook",
"@types/dot-object": "2.1.6",
"@types/node": "24.10.13",
"@types/sanitize-html": "2.16.0",
"@types/semver": "7.7.1",
Expand All @@ -140,6 +142,7 @@
"chromatic": "15.1.0",
"defu": "6.1.4",
"devalue": "5.6.3",
"dot-object": "2.1.5",
"eslint-plugin-regexp": "3.0.0",
"fast-check": "4.5.3",
"h3": "1.15.5",
Expand Down
11 changes: 11 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions scripts/i18n-meta/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { parseArgs } from 'node:util'
import { updateEnMetaJson } from './update-en-meta-json.ts'

function showHelp() {
const scriptName = process.env.npm_lifecycle_event || 'node scripts/i18n-meta/cli.ts'
console.log(`Usage: pnpm run ${scriptName} update-en-meta`)
}

function main() {
const { positionals } = parseArgs({ allowPositionals: true })

if (positionals[0] !== 'update-en-meta') {
showHelp()
return
}

updateEnMetaJson()
}

main()
16 changes: 16 additions & 0 deletions scripts/i18n-meta/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export type EnJson = {
[key: string]: string | EnJson
}

export type MetaEntry = {
text: string
commit: string
}

export type EnMetaJson = {
$meta?: {
last_updated_commit: string
updated_at: string
}
[key: string]: string | MetaEntry | EnMetaJson
}
64 changes: 64 additions & 0 deletions scripts/i18n-meta/update-en-meta-json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
import dot from 'dot-object'
import {
checkTranslationChanges,
createUpdatedEnMetaJson,
getCurrentCommitHash,
getNewEnJson,
getOldEnMetaJson,
} from './utils.ts'
import type { EnJson, EnMetaJson, MetaEntry } from './types.d.ts'

const enJsonPath = resolve('i18n/locales/en.json')
const enMetaJsonPath = resolve('i18n/locales/en.meta.json')

/**
* Update a metadata JSON file for English translations.
*/
export function updateEnMetaJson() {
const newEnJson = getNewEnJson(enJsonPath)
const oldEnMetaJson = getOldEnMetaJson(enMetaJsonPath)

const currentCommitHash = getCurrentCommitHash()
if (!currentCommitHash) {
console.error('❌ Commit hash missing. Skipping update to protect existing metadata.')
process.exit(1)
}
const enMetaJson = makeEnMetaJson(oldEnMetaJson, newEnJson, currentCommitHash)

const hasChanges = checkTranslationChanges(oldEnMetaJson, enMetaJson)
if (!hasChanges) {
console.info('ℹ️ No translation changes – en.meta.json left untouched')
return
}

const updatedEnMetaJson = createUpdatedEnMetaJson(currentCommitHash, enMetaJson)

writeFileSync(enMetaJsonPath, JSON.stringify(updatedEnMetaJson, null, 2) + '\n', 'utf-8')
console.log(`✅ Updated en.meta.json – last_updated_commit: ${currentCommitHash}`)
}

export function makeEnMetaJson(
oldMetaEnJson: EnMetaJson,
newEnJson: EnJson,
latestCommitHash: string,
): EnMetaJson {
const newFlat = dot.dot(newEnJson) as Record<string, string>
const oldMetaFlat = dot.dot(oldMetaEnJson) as Record<string, string>
const metaFlat: Record<string, MetaEntry> = {}

for (const key in newFlat) {
const newText = newFlat[key]

const lastSeenText = oldMetaFlat[`${key}.text`]
const lastCommit = oldMetaFlat[`${key}.commit`]

if (newText === lastSeenText) {
metaFlat[key] = { text: newText, commit: lastCommit ?? latestCommitHash }
} else {
metaFlat[key] = { text: newText, commit: latestCommitHash }
}
}
return dot.object(metaFlat) as EnMetaJson
}
55 changes: 55 additions & 0 deletions scripts/i18n-meta/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { execSync } from 'node:child_process'
import * as fs from 'node:fs'
import type { EnJson, EnMetaJson } from './types.d.ts'

export function getCurrentCommitHash() {
return git('rev-parse HEAD')
}

export function getNewEnJson(enJsonPath: string): EnJson {
if (fs.existsSync(enJsonPath)) {
return readJson<EnJson>(enJsonPath)
}
return {} as EnJson
}

export function getOldEnMetaJson(enMetaJsonPath: string): EnMetaJson {
if (fs.existsSync(enMetaJsonPath)) {
return readJson<EnMetaJson>(enMetaJsonPath)
}
return {} as EnMetaJson
}

export function checkTranslationChanges(oldMeta: EnMetaJson, newMeta: EnMetaJson): boolean {
const oldObj = omitMeta(oldMeta)
const newObj = omitMeta(newMeta)
return JSON.stringify(oldObj) !== JSON.stringify(newObj)
}

export function createUpdatedEnMetaJson(commitHash: string, content: EnMetaJson): EnMetaJson {
return {
$meta: {
last_updated_commit: commitHash,
updated_at: new Date().toISOString(),
},
...omitMeta(content),
}
}

function git(command: string) {
try {
return execSync(`git ${command}`, { encoding: 'utf-8' }).trim()
} catch {
console.error(`🚨 Git command failed: git ${command}`)
return null
}
}

function readJson<T>(filePath: string): T {
return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as T
}

function omitMeta<T extends object>(obj: T): Omit<T, '$meta'> {
const { $meta: _, ...rest } = obj as T & { $meta?: unknown }
return rest
}
16 changes: 16 additions & 0 deletions scripts/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "nodenext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"declaration": true,
"types": ["node"],
"declarationMap": true
},
"include": ["**/*.ts"],
"exclude": ["node_modules", "dist"]
}
Loading
Loading