chore: heygermany customer project
This commit is contained in:
226
backends/i18n-generator/src/google-sheets.ts
Normal file
226
backends/i18n-generator/src/google-sheets.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
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')
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user