chore: heygermany customer project
Some checks failed
Main / Setup and Test (push) Has been cancelled
Main / Build Website (push) Has been cancelled

This commit is contained in:
e2e
2026-07-11 09:36:03 +02:00
commit 1e78e21c36
398 changed files with 38345 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
import fs from 'fs/promises'
import path from 'path'
interface JsonObject {
[key: string]: any
}
/**
* Recursively flattens a JSON object into dot-notation paths with array indices
* Example: { a: { b: ["x", "y"] } } => ["a.b[0]", "a.b[1]"]
*/
function flattenKeys(obj: JsonObject, prefix = ''): string[] {
const keys: string[] = []
for (const [key, value] of Object.entries(obj)) {
const newPrefix = prefix ? `${prefix}.${key}` : key
if (Array.isArray(value)) {
// For arrays, create keys with indices
value.forEach((_, index) => {
keys.push(`${newPrefix}[${index}]`)
})
} else if (typeof value === 'object' && value !== null) {
// Recursively process nested objects
keys.push(...flattenKeys(value, newPrefix))
} else {
// Leaf node (string/number/boolean/null)
keys.push(newPrefix)
}
}
return keys
}
/**
* Reads all JSON files from a directory and returns flattened keys
*/
async function getKeysFromDirectory(dirPath: string): Promise<Set<string>> {
const allKeys = new Set<string>()
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name)
if (entry.isDirectory()) {
// Recursively process subdirectories
const subKeys = await getKeysFromDirectory(fullPath)
subKeys.forEach(key => allKeys.add(key))
} else if (entry.isFile() && entry.name.endsWith('.json')) {
// Process JSON file
const content = await fs.readFile(fullPath, 'utf-8')
const json = JSON.parse(content)
const keys = flattenKeys(json)
keys.forEach(key => allKeys.add(key))
}
}
} catch (error) {
console.error(`Error reading directory ${dirPath}:`, error)
}
return allKeys
}
export async function generateLanguageTypes(rootDir: string) {
const localesDir = path.join(rootDir, 'packages/functions/locales')
// Get all unique translation keys
const allKeys = await getKeysFromDirectory(localesDir)
// Sort keys for consistent output
const sortedKeys = Array.from(allKeys).sort()
// Generate TypeScript type definition
const typeDefinition = `/**
* Auto-generated translation token types
* @generated - DO NOT EDIT MANUALLY
*/
export type TranslationTokens =
${sortedKeys.map(key => ` | '${key}'`).join('\n')}
/**
* Alias for backwards compatibility
*/
export type Tokens = TranslationTokens
`
// Write to file
const outputPath = path.join(rootDir, 'packages/functions/src/engines/language.types.gen.d.ts')
await fs.writeFile(outputPath, typeDefinition, 'utf-8')
console.log(`✅ Generated language types with ${sortedKeys.length} tokens`)
console.log(`📝 Output: ${outputPath}`)
}

View 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')
}
}

View File

@@ -0,0 +1,85 @@
import { GoogleSheets } from './google-sheets'
import { generateLanguageTypes } from './generate-language-types'
import { identOf } from './ident'
import fs from 'fs/promises'
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const rootDir = join(__dirname, '../../..');
// Flatten the sheet's dotted i18n map to inlang's snake_case token catalog.
// `website.hero.title` → `website_hero_title`. Identical transform to the app
// scaffold's identOf, so generator output ↔ runtime lookup stay aligned.
const toInlangCatalog = (flat: Record<string, string>): Record<string, string> => {
const out: Record<string, string> = {}
for (const [key, value] of Object.entries(flat)) {
out[identOf(key)] = value
}
return out
}
export const main = async () => {
let creds: any
if (process.env.GOOGLE_SHEETS_SERVICE_ACCOUNT) {
creds = JSON.parse(process.env.GOOGLE_SHEETS_SERVICE_ACCOUNT)
} else {
throw new Error('GOOGLE_SHEETS_SERVICE_ACCOUNT must be set')
}
const googleSheets = new GoogleSheets({
client_email: creds.client_email,
private_key: creds.private_key,
}, '18Ct1kIbxJQXr5LQ8vDGRQkKUmWliieewkDaMLct91XE')
let success = false
for (let i = 0; i < 3; i++) {
try {
await googleSheets.init()
const languages = ['en', 'de', 'uk', 'fil', 'tr', 'bs', 'sq', 'vi', 'es', 'hi', 'ar']
// Read existing en.json locale file and sync missing keys to sheet
try {
const enLocaleData = await fs.readFile(`${rootDir}/apps/website/locales/en.json`, 'utf-8')
const enLocale = JSON.parse(enLocaleData)
console.log('Syncing locale keys to Google Sheets...')
await googleSheets.syncLocaleToSheet(enLocale, languages)
} catch (error) {
console.log('No existing locales/en.json found or error syncing:', error)
}
const i18n = await googleSheets.getI18n(languages)
for (const lang of languages) {
await fs.mkdir(`${rootDir}/apps/website/locales/`, { recursive: true })
await fs.writeFile(`${rootDir}/apps/website/locales/${lang.toLowerCase()}.json`, JSON.stringify(i18n[lang], null, 2))
console.log(`Wrote ${Object.keys(i18n[lang]).length} keys for ${lang}`)
}
const resultI18n = await googleSheets.getResultI18n(languages)
for (const lang of Object.keys(resultI18n)) {
for (const sheet of Object.keys(resultI18n[lang])) {
await fs.mkdir(`${rootDir}/packages/functions/locales/${lang.toLowerCase()}`, { recursive: true })
await fs.writeFile(`${rootDir}/packages/functions/locales/${lang.toLowerCase()}/${sheet}.json`, JSON.stringify(resultI18n[lang][sheet], null, 2))
console.log(`Wrote ${Object.keys(resultI18n[lang][sheet]).length} keys for ${lang}/${sheet}`)
}
}
// Generate TypeScript type definitions
await generateLanguageTypes(rootDir)
success = true
break
} catch (e) {
await new Promise(res => setTimeout(res, 5000))
console.error('Error fetching or writing i18n data:', e)
}
}
if (!success) {
console.error('Failed to generate i18n files after 3 attempts')
process.exit(1)
}
}
main()