Files
heygermany-e2e-mrg511by/backends/i18n-generator/src/google-sheets.ts
e2e 36f1b746bd
Some checks failed
Main / Setup and Test (push) Has been cancelled
Main / Build Website (push) Has been cancelled
chore: heygermany customer project
2026-07-11 11:03:59 +02:00

227 lines
8.0 KiB
TypeScript

import { GoogleSpreadsheet } from 'google-spreadsheet'
import { JWT } from 'google-auth-library'
function setNestedProperty(obj: any, path: string, value: string) {
const keys = path.split('.')
let current = obj
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i]
if (!current[key]) {
current[key] = {}
}
current = current[key]
}
const lastKey = keys[keys.length - 1]
// Check if the key has an array index pattern like "requirements[0]"
const arrayMatch = lastKey.match(/^([a-z]+)\[(\d+)\]$/i)
if (arrayMatch) {
const arrayName = arrayMatch[1]
const index = parseInt(arrayMatch[2], 10)
// Initialize array if it doesn't exist
if (!Array.isArray(current[arrayName])) {
current[arrayName] = []
}
current[arrayName][index] = value
} else {
current[lastKey] = value
}
}
export class GoogleSheets {
#doc: GoogleSpreadsheet
constructor(creds: any, docId: string) {
const jwt = new JWT({
email: creds.client_email,
key: creds.private_key,
scopes: [
'https://www.googleapis.com/auth/spreadsheets',
'https://www.googleapis.com/auth/drive.file',
],
});
this.#doc = new GoogleSpreadsheet(docId, jwt)
}
async init() {
await this.#doc.loadInfo()
}
async getI18n(languages: string[]): Promise<Record<string, Record<string, string>>> {
const i18n: Record<string, Record<string, string>> = {}
for (const [sheetTitle, sheet] of Object.entries(this.#doc.sheetsByTitle)) {
if (sheetTitle.includes('result-')) {
continue
}
const rows = await sheet.getRows()
if (rows.length === 0) continue
// Get header names from the first row to find the correct column names
const headerValues = Object.keys(rows[0].toObject())
for (const row of rows) {
let root = row.get('Root')
const key = row.get('Token')
if (!key) {
continue
}
if (!root) {
root = sheetTitle
}
for (const lang of languages) {
i18n[lang] = i18n[lang] || {}
// Find the column that matches the language (either "locale" or "locale (Country)")
const columnName = headerValues.find((header: string) => {
const normalizedHeader = header.toLowerCase()
return normalizedHeader === lang || normalizedHeader.startsWith(`${lang} (`)
})
if (columnName && row.get(columnName)) {
i18n[lang][`${root}.${key}`.toLowerCase()] = row.get(columnName)
}
}
}
}
return i18n
}
async getResultI18n(languages: string[]): Promise<Record<string, Record<string, any>>> {
const i18n: Record<string, Record<string, any>> = {}
for (const [sheetTitle, sheet] of Object.entries(this.#doc.sheetsByTitle)) {
if (!sheetTitle.includes('result-')) {
continue
}
const title = sheetTitle.replace('result-', '')
const rows = await sheet.getRows()
if (rows.length === 0) continue
// Get header names from the first row to find the correct column names
const headerValues = Object.keys(rows[0].toObject())
for (const row of rows) {
const key = row.get('Token')
if (!key) {
continue
}
for (const lang of languages) {
i18n[lang] = i18n[lang] || {}
i18n[lang][title] = i18n[lang][title] || {}
// Find the column that matches the language (either "locale" or "locale (Country)")
const columnName = headerValues.find((header: string) => {
const normalizedHeader = header.toLowerCase()
return normalizedHeader === lang || normalizedHeader.startsWith(`${lang} (`)
})
if (columnName && row.get(columnName)) {
setNestedProperty(i18n[lang][title], key.toLowerCase(), row.get(columnName))
}
}
}
}
return i18n
}
async syncLocaleToSheet(localeData: Record<string, string>, languages: string[]): Promise<void> {
// Get all existing keys from all sheets
const existingKeys = new Set<string>()
const sheetsByName: Record<string, any> = {}
for (const [sheetTitle, sheet] of Object.entries(this.#doc.sheetsByTitle)) {
if (sheetTitle.includes('result-')) {
continue
}
sheetsByName[sheetTitle.toLowerCase()] = sheet
const rows = await sheet.getRows()
for (const row of rows) {
const root = row.get('Root')
const key = row.get('Token')
if (key) {
const fullKey = root ? `${root}.${key}`.toLowerCase() : `${sheetTitle}.${key}`.toLowerCase()
existingKeys.add(fullKey)
}
}
}
// Find missing keys
const missingKeys: Array<{ fullKey: string, sheetName: string, token: string, value: string }> = []
for (const [fullKey, value] of Object.entries(localeData)) {
const normalizedKey = fullKey.toLowerCase()
if (!existingKeys.has(normalizedKey)) {
// Determine sheet name from first word
const parts = normalizedKey.split('.')
let sheetName = parts[0]
let token = parts.slice(1).join('.')
// If no matching sheet exists, use 'common' sheet
if (!sheetsByName[sheetName]) {
sheetName = 'common'
token = normalizedKey
}
missingKeys.push({ fullKey: normalizedKey, sheetName, token, value })
}
}
if (missingKeys.length === 0) {
console.log('No missing keys found')
return
}
console.log(`Found ${missingKeys.length} missing keys, adding to sheets...`)
// Group missing keys by sheet
const keysBySheet: Record<string, Array<{ token: string, value: string }>> = {}
for (const { sheetName, token, value } of missingKeys) {
if (!keysBySheet[sheetName]) {
keysBySheet[sheetName] = []
}
keysBySheet[sheetName].push({ token, value })
}
// Add rows to appropriate sheets
for (const [sheetName, keys] of Object.entries(keysBySheet)) {
let sheet = sheetsByName[sheetName]
// If sheet doesn't exist
if (!sheet) {
console.log(`Creating new sheet: ${sheetName}`)
sheet = await this.#doc.addSheet({
title: sheetName,
headerValues: ['Root', 'Token', 'en', ...languages.filter(l => l !== 'en')]
})
sheetsByName[sheetName] = sheet
}
// Get the header row to find column positions
await sheet.loadHeaderRow()
const headers = sheet.headerValues
console.log(`Adding ${keys.length} keys to sheet: ${sheetName}`)
// Prepare rows to add
const rowsToAdd = keys.map(({ token, value }) => {
const row: Record<string, string> = {
'Root': '',
'Token': token,
'EN': value
}
return row
})
// Add all rows at once
await sheet.addRows(rowsToAdd)
}
console.log('Successfully synced missing keys to sheets')
}
}