Skip to content

Commit 2c8daee

Browse files
committed
feat(i18n): add en.meta.json generation script
1 parent 04bd490 commit 2c8daee

9 files changed

Lines changed: 265 additions & 0 deletions

File tree

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"i18n:report": "node scripts/find-invalid-translations.ts",
2121
"i18n:report:fix": "node scripts/remove-unused-translations.ts",
2222
"i18n:schema": "node scripts/generate-i18n-schema.ts",
23+
"i18n:meta:update-en-meta": "node scripts/i18n-meta/cli.ts update-en-meta",
2324
"knip": "knip",
2425
"knip:fix": "knip --fix",
2526
"lint": "oxlint && oxfmt --check",
@@ -136,6 +137,7 @@
136137
"chromatic": "15.1.0",
137138
"defu": "6.1.4",
138139
"devalue": "5.6.3",
140+
"dot-object": "2.1.5",
139141
"eslint-plugin-regexp": "3.0.0",
140142
"fast-check": "4.5.3",
141143
"h3": "1.15.5",

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/i18n-meta/cli.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { parseArgs } from 'node:util'
2+
import { updateEnMetaJson } from './update-en-meta-json.ts'
3+
4+
function showHelp() {
5+
const scriptName = process.env.npm_lifecycle_event || 'node scripts/i18n-meta/cli.ts'
6+
console.log(`Usage: pnpm run ${scriptName} update-en-meta`)
7+
}
8+
9+
function main() {
10+
const { positionals } = parseArgs({ allowPositionals: true })
11+
12+
if (positionals[0] !== 'update-en-meta') {
13+
showHelp()
14+
return
15+
}
16+
17+
updateEnMetaJson()
18+
}
19+
20+
main()

scripts/i18n-meta/git-utils.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { execSync } from 'node:child_process'
2+
import { relative } from 'node:path'
3+
4+
export function git(command: string): string {
5+
return execSync(command, { encoding: 'utf-8' }).trim()
6+
}
7+
8+
export function getCurrentCommitHash(): string {
9+
return git('git rev-parse HEAD')
10+
}
11+
12+
export function getPreviousEnJson(fileName: string): string {
13+
const relativePath = relative(process.cwd(), fileName)
14+
return git(`git show HEAD^:${relativePath}`)
15+
}

scripts/i18n-meta/types.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export type EnJson = {
2+
[key: string]: string | EnJson
3+
}
4+
5+
export type MetaEntry = {
6+
text: string
7+
commit: string
8+
}
9+
10+
export type EnMetaJson = {
11+
[key: string]: string | MetaEntry | EnMetaJson
12+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
2+
import { resolve } from 'node:path'
3+
import dot from 'dot-object'
4+
import { getCurrentCommitHash, getPreviousEnJson } from './git-utils.ts'
5+
import type { EnJson, EnMetaJson, MetaEntry } from './types.d.ts'
6+
7+
const enJsonPath = resolve('i18n/locales/en.json')
8+
const enMetaJsonPath = resolve('i18n/locales/en.meta.json')
9+
10+
/**
11+
* Update a metadata JSON file for English translations.
12+
*/
13+
export function updateEnMetaJson() {
14+
const newEnJsonRaw = readFileSync(enJsonPath, 'utf-8')
15+
const oldEnJsonRaw = getPreviousEnJson(enJsonPath)
16+
17+
let oldEnMetaJsonRaw = '{}'
18+
if (existsSync(enMetaJsonPath)) {
19+
oldEnMetaJsonRaw = readFileSync(enMetaJsonPath, 'utf-8')
20+
}
21+
22+
const latestCommitHash = getCurrentCommitHash()
23+
const enMetaJson = makeEnMetaJson(
24+
JSON.parse(newEnJsonRaw),
25+
JSON.parse(oldEnJsonRaw),
26+
JSON.parse(oldEnMetaJsonRaw),
27+
latestCommitHash,
28+
)
29+
30+
writeFileSync(enMetaJsonPath, JSON.stringify(enMetaJson, null, 2) + '\n', 'utf-8')
31+
console.log(`📃 Generated ${enMetaJsonPath}`)
32+
}
33+
34+
/**
35+
* Creates a metadata JSON object by comparing current and previous versions.
36+
*/
37+
export function makeEnMetaJson(
38+
newEnJson: EnJson,
39+
oldEnJson: EnJson,
40+
oldMetaEnJson: EnMetaJson,
41+
latestCommitHash: string,
42+
): EnMetaJson {
43+
const newFlat = dot.dot(newEnJson) as Record<string, string>
44+
const oldFlat = dot.dot(oldEnJson) as Record<string, string>
45+
const oldMetaFlat = dot.dot(oldMetaEnJson) as Record<string, string>
46+
const metaFlat: Record<string, MetaEntry> = {}
47+
48+
for (const key in newFlat) {
49+
if (Object.prototype.hasOwnProperty.call(newFlat, key)) {
50+
const newText = newFlat[key]
51+
const oldText = oldFlat[key]
52+
53+
if (oldText === newText) {
54+
const oldCommit = oldMetaFlat[`${key}.commit`]
55+
metaFlat[key] = { text: newText, commit: oldCommit ?? latestCommitHash }
56+
} else {
57+
metaFlat[key] = { text: newText, commit: latestCommitHash }
58+
}
59+
}
60+
}
61+
return dot.object(metaFlat) as EnMetaJson
62+
}

scripts/tsconfig.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "nodenext",
5+
"strict": true,
6+
"esModuleInterop": true,
7+
"skipLibCheck": true,
8+
"noEmit": true,
9+
"allowImportingTsExtensions": true,
10+
"declaration": true,
11+
"types": ["node"],
12+
"declarationMap": true
13+
},
14+
"include": ["**/*.ts"],
15+
"exclude": ["node_modules", "dist"]
16+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { makeEnMetaJson } from '#scripts/i18n-meta/update-en-meta'
3+
4+
describe('Create en.meta.json', () => {
5+
it('should handle an empty en.json file', () => {
6+
const oldEnJson = {}
7+
const newEnJson = {}
8+
const oldEnMetaJson = {}
9+
10+
const enMetaJson = makeEnMetaJson(newEnJson, oldEnJson, oldEnMetaJson, 'sha-new-12345')
11+
expect(enMetaJson).toEqual({})
12+
})
13+
14+
it('should generate en.meta.json correctly for an initial en.json', () => {
15+
const oldEnJson = {}
16+
const newEnJson = {
17+
tagline: 'npmx - a fast, modern browser for the npm registry',
18+
}
19+
const oldEnMetaJson = {}
20+
21+
const enMetaJson = makeEnMetaJson(newEnJson, oldEnJson, oldEnMetaJson, 'sha-new-12345')
22+
expect(enMetaJson).toEqual({
23+
tagline: {
24+
text: 'npmx - a fast, modern browser for the npm registry',
25+
commit: 'sha-new-12345',
26+
},
27+
})
28+
})
29+
30+
it('should update existing keys and add new keys with the latest commit hash', () => {
31+
const oldEnJson = {
32+
name: 'npmx',
33+
tagline: 'npmx - a better browser for the npm registry',
34+
}
35+
const newEnJson = {
36+
name: 'npmx',
37+
tagline: 'npmx - a fast, modern browser for the npm registry',
38+
description: 'Search, browse, and explore packages with a modern interface.',
39+
}
40+
const oldEnMetaJson = {
41+
name: {
42+
tagline: 'npmx',
43+
commit: 'sha-old-12345',
44+
},
45+
tagline: {
46+
tagline: 'npmx - a better browser for the npm registry',
47+
commit: 'sha-old-12345',
48+
},
49+
}
50+
51+
const enMetaJson = makeEnMetaJson(newEnJson, oldEnJson, oldEnMetaJson, 'sha-new-12345')
52+
expect(enMetaJson).toEqual({
53+
name: {
54+
text: 'npmx',
55+
commit: 'sha-old-12345',
56+
},
57+
tagline: {
58+
text: 'npmx - a fast, modern browser for the npm registry',
59+
commit: 'sha-new-12345',
60+
},
61+
description: {
62+
text: 'Search, browse, and explore packages with a modern interface.',
63+
commit: 'sha-new-12345',
64+
},
65+
})
66+
})
67+
68+
it('should remove keys that are no longer in en.json', () => {
69+
const oldEnJson = {
70+
tagline: 'npmx - a fast, modern browser for the npm registry',
71+
toBeRemoved: 'This will be gone',
72+
}
73+
const newEnJson = {
74+
tagline: 'npmx - a fast, modern browser for the npm registry',
75+
}
76+
const oldEnMetaJson = {
77+
tagline: {
78+
text: 'npmx - a fast, modern browser for the npm registry',
79+
commit: 'sha-old-12345',
80+
},
81+
toBeRemoved: { text: 'This will be gone', commit: 'sha-old-12345' },
82+
}
83+
84+
const enMetaJson = makeEnMetaJson(newEnJson, oldEnJson, oldEnMetaJson, 'sha-new-12345')
85+
expect(enMetaJson).toEqual({
86+
tagline: {
87+
text: 'npmx - a fast, modern browser for the npm registry',
88+
commit: 'sha-old-12345',
89+
},
90+
})
91+
})
92+
93+
it('should handle complex nested structures', () => {
94+
const oldEnJson = {
95+
a: {
96+
b: 'value-b',
97+
},
98+
c: 'value-c',
99+
}
100+
const newEnJson = {
101+
a: {
102+
b: 'updated-value',
103+
},
104+
c: 'value-c',
105+
d: 'added-value',
106+
}
107+
108+
const oldEnMetaJson = {
109+
a: {
110+
b: {
111+
text: 'value-b',
112+
commit: 'sha-old-12345',
113+
},
114+
},
115+
c: {
116+
text: 'value-c',
117+
commit: 'sha-old-12345',
118+
},
119+
d: {
120+
text: 'added-value',
121+
commit: 'sha-new-12345',
122+
},
123+
}
124+
125+
const enMetaJson = makeEnMetaJson(newEnJson, oldEnJson, oldEnMetaJson, 'sha-new-12345')
126+
expect(enMetaJson).toEqual({
127+
a: {
128+
b: { text: 'updated-value', commit: 'sha-new-12345' },
129+
},
130+
c: { text: 'value-c', commit: 'sha-old-12345' },
131+
d: { text: 'added-value', commit: 'sha-new-12345' },
132+
})
133+
})
134+
})

vitest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export default defineConfig({
1313
alias: {
1414
'#shared': `${rootDir}/shared`,
1515
'#server': `${rootDir}/server`,
16+
'#scripts': `${rootDir}/scripts`,
1617
},
1718
},
1819
test: {

0 commit comments

Comments
 (0)