@dsi18n/airtable-sync 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts","../src/core/node.ts","../src/core/fetch.ts","../src/core/normalize.ts","../src/core/validate.ts","../src/core/run-build.ts","../src/load-project-env.ts","../src/index.ts","../src/cli.ts"],"names":["mkdirNode","writeFileNode","resolve","DEFAULT_RATE_LIMIT_DELAY_MS","sleep","extractPlaceholders","dirname","path","loadEnv"],"mappings":"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","file":"cli.js","sourcesContent":["import type { BuildConfig } from \"./core/run-build.js\";\n\nexport function parseBooleanEnv(value: string | undefined): boolean {\n return value === \"1\" || value === \"true\";\n}\n\nexport function parseNumberEnv(value: string | undefined): number | undefined {\n if (!value) {\n return undefined;\n }\n\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\n/** Build config from process environment (CLI / CI). */\nexport function loadBuildConfigFromEnv(): BuildConfig {\n return {\n airtableApiKey: process.env.AIRTABLE_API_KEY,\n baseId: process.env.AIRTABLE_BASE_ID,\n table: process.env.AIRTABLE_TABLE,\n tablesCsv: process.env.I18N_TABLES,\n outDir: process.env.I18N_OUT_DIR,\n version: process.env.I18N_VERSION,\n keyField: process.env.I18N_KEY_FIELD,\n defaultLocale: process.env.I18N_DEFAULT_LOCALE,\n localesCsv: process.env.I18N_LOCALES,\n typesOutFile: process.env.I18N_TYPES_OUT_FILE,\n debug: parseBooleanEnv(process.env.I18N_DEBUG),\n rateLimitDelayMs: parseNumberEnv(process.env.I18N_RATE_LIMIT_DELAY_MS),\n };\n}\n","// @ts-nocheck\nimport { mkdir as mkdirNode, writeFile as writeFileNode } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nexport async function mkdir(pathValue: string): Promise<void> {\n await mkdirNode(pathValue, { recursive: true });\n}\n\nexport async function writeUtf8File(\n pathValue: string,\n content: string,\n): Promise<void> {\n await writeFileNode(pathValue, content, \"utf8\");\n}\n\nexport function joinPath(...parts: string[]): string {\n return path.join(...parts);\n}\n\nexport function dirname(pathValue: string): string {\n return path.dirname(pathValue);\n}\n","// @ts-nocheck\n\ntype AirtableFieldValue = string | number | boolean | null | undefined;\n\nexport type AirtableRecord = {\n id: string;\n fields: Record<string, AirtableFieldValue>;\n};\n\ntype AirtableResponse = {\n records: AirtableRecord[];\n offset?: string;\n};\n\nexport type FetchFn = typeof fetch;\n\nexport type FetchParams = {\n apiKey?: string;\n baseId: string;\n table: string;\n fetchFn?: FetchFn;\n debug?: boolean;\n rateLimitDelayMs?: number;\n};\n\ntype AirtableMetaResponse = {\n tables: Array<{\n id: string;\n name: string;\n }>;\n};\n\nconst RETRY_ATTEMPTS = 3;\nconst RETRY_BASE_DELAY_MS = 300;\nconst DEFAULT_RATE_LIMIT_DELAY_MS = 200;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\nfunction isRetryableStatus(status: number): boolean {\n return status === 429 || status >= 500;\n}\n\nasync function fetchWithRetry(\n url: URL,\n init: RequestInit,\n fetchFn: FetchFn,\n debug?: boolean,\n): Promise<Response> {\n let lastError: Error | null = null;\n\n for (let attempt = 0; attempt < RETRY_ATTEMPTS; attempt += 1) {\n try {\n if (debug) {\n console.log(`[fetch] attempt=${attempt + 1} url=${url.toString()}`);\n }\n const response = await fetchFn(url, init);\n if (!isRetryableStatus(response.status)) {\n return response;\n }\n\n if (debug) {\n console.warn(\n `[fetch] retryable_status=${response.status} attempt=${attempt + 1} url=${url.toString()}`,\n );\n }\n const body = await response.text();\n lastError = new Error(\n `Retryable Airtable response (${response.status}): ${response.statusText}\\n${body}`,\n );\n } catch (error: unknown) {\n if (debug) {\n console.warn(\n `[fetch] network_error attempt=${attempt + 1} url=${url.toString()}`,\n );\n }\n lastError =\n error instanceof Error ? error : new Error(\"Unknown fetch error\");\n }\n\n if (attempt < RETRY_ATTEMPTS - 1) {\n await sleep(RETRY_BASE_DELAY_MS * (attempt + 1));\n }\n }\n\n throw lastError ?? new Error(\"Fetch failed after retries\");\n}\n\nasync function fetchAirtablePage(params: {\n apiKey?: string;\n baseId: string;\n table: string;\n offset?: string;\n fetchFn?: FetchFn;\n debug?: boolean;\n}): Promise<AirtableResponse> {\n const encodedTable = encodeURIComponent(params.table);\n const url = new URL(\n `https://api.airtable.com/v0/${params.baseId}/${encodedTable}`,\n );\n\n if (params.offset) {\n url.searchParams.set(\"offset\", params.offset);\n }\n\n const headers: Record<string, string> = {};\n if (params.apiKey) {\n headers.Authorization = `Bearer ${params.apiKey}`;\n }\n\n const fetchFn = params.fetchFn ?? fetch;\n if (params.debug) {\n const offsetValue = params.offset ?? \"none\";\n console.log(`[fetch] table=${params.table} offset=${offsetValue}`);\n }\n const response = await fetchWithRetry(url, { headers }, fetchFn, params.debug);\n if (!response.ok) {\n const body = await response.text();\n throw new Error(\n `Airtable request failed (${response.status}): ${response.statusText}\\n${body}`,\n );\n }\n\n return (await response.json()) as AirtableResponse;\n}\n\nexport async function fetchAllRecords(\n params: FetchParams,\n): Promise<AirtableRecord[]> {\n const records: AirtableRecord[] = [];\n let offset: string | undefined;\n const rateLimitDelayMs = params.rateLimitDelayMs ?? DEFAULT_RATE_LIMIT_DELAY_MS;\n\n do {\n const page = await fetchAirtablePage({\n apiKey: params.apiKey,\n baseId: params.baseId,\n table: params.table,\n offset,\n fetchFn: params.fetchFn,\n debug: params.debug,\n });\n records.push(...page.records);\n offset = page.offset;\n if (offset) {\n await sleep(rateLimitDelayMs);\n }\n } while (offset);\n\n return records;\n}\n\nexport async function listBaseTables(params: {\n apiKey: string;\n baseId: string;\n fetchFn?: FetchFn;\n debug?: boolean;\n}): Promise<string[]> {\n // Airtable metadata endpoint requires extra scopes in many workspaces.\n // Keep this isolated so callers can fallback to explicit table lists.\n const url = new URL(`https://api.airtable.com/v0/meta/bases/${params.baseId}/tables`);\n const fetchFn = params.fetchFn ?? fetch;\n const response = await fetchWithRetry(\n url,\n {\n headers: {\n Authorization: `Bearer ${params.apiKey}`,\n },\n },\n fetchFn,\n params.debug,\n );\n\n if (!response.ok) {\n const body = await response.text();\n throw new Error(\n `Airtable metadata request failed (${response.status}): ${response.statusText}\\n${body}`,\n );\n }\n\n const json = (await response.json()) as AirtableMetaResponse;\n return json.tables.map((table) => table.name);\n}\n","export type NormalizedEntry = {\n key: string;\n translations: Partial<Record<string, string>>;\n};\n\ntype AirtableFieldValue = string | number | boolean | null | undefined;\n\nexport type AirtableRecord = {\n id: string;\n fields: Record<string, AirtableFieldValue>;\n};\n\nexport function parseLocalesFromEnv(value: string | undefined): string[] {\n if (!value) {\n return [];\n }\n\n return value\n .split(\",\")\n .map((locale) => locale.trim())\n .filter(Boolean);\n}\n\nexport function inferLocalesFromRecords(\n records: AirtableRecord[],\n keyField: string,\n): string[] {\n const locales = new Set<string>();\n\n for (const record of records) {\n const fields = Object.keys(record.fields);\n for (const fieldName of fields) {\n if (fieldName === keyField) {\n continue;\n }\n\n const value = record.fields[fieldName];\n if (typeof value === \"string\") {\n locales.add(fieldName);\n }\n }\n }\n\n return [...locales].sort();\n}\n\nexport function normalizeRecords(\n records: AirtableRecord[],\n locales: string[],\n keyField: string,\n options?: {\n debug?: boolean;\n },\n): NormalizedEntry[] {\n const normalizedEntries: Array<NormalizedEntry | null> = records\n .map((record) => {\n const keyValue = record.fields[keyField];\n if (typeof keyValue !== \"string\" || keyValue.trim().length === 0) {\n if (options?.debug) {\n console.warn(`Skipping record ${record.id}: invalid key`);\n }\n return null;\n }\n\n const key = keyValue.trim();\n const translations: Partial<Record<string, string>> = Object.fromEntries(\n locales\n .map((locale) => {\n const value = record.fields[locale];\n if (typeof value !== \"string\") {\n return null;\n }\n\n const trimmed = value.trim();\n return trimmed.length > 0 ? [locale, trimmed] : null;\n })\n .filter((item): item is [string, string] => item !== null),\n );\n\n return { key, translations };\n });\n\n return normalizedEntries.filter(\n (entry): entry is NormalizedEntry => entry !== null,\n );\n}\n","import type { NormalizedEntry } from \"./normalize.js\";\n\nexport type ValidationIssue =\n | {\n type: \"missing_translation\";\n key: string;\n locale: string;\n }\n | {\n type: \"missing_placeholder\";\n key: string;\n locale: string;\n baseLocale: string;\n placeholder: string;\n }\n | {\n type: \"extra_placeholder\";\n key: string;\n locale: string;\n baseLocale: string;\n placeholder: string;\n };\n\ntype PlaceholderIssue = Extract<\n ValidationIssue,\n { type: \"missing_placeholder\" | \"extra_placeholder\" }\n>;\n\nfunction extractPlaceholders(text: string): string[] {\n const matches = text.match(/\\{([a-zA-Z0-9_]+)\\}/g) ?? [];\n const cleaned = matches.map((token) => token.replace(/[{}]/g, \"\"));\n return [...new Set(cleaned)];\n}\n\nexport function detectDuplicateKeys(entries: NormalizedEntry[]): string[] {\n const seen = new Set<string>();\n const duplicates = new Set<string>();\n\n for (const entry of entries) {\n if (seen.has(entry.key)) {\n duplicates.add(entry.key);\n continue;\n }\n\n seen.add(entry.key);\n }\n\n return [...duplicates].sort();\n}\n\nexport function validateMissingTranslations(\n entries: NormalizedEntry[],\n locales: string[],\n): ValidationIssue[] {\n return entries\n .flatMap((entry) =>\n locales.map((locale) => ({\n key: entry.key,\n locale,\n value: entry.translations[locale],\n })),\n )\n .filter((item) => item.value == null || item.value === \"\")\n .map((item) => ({\n type: \"missing_translation\" as const,\n key: item.key,\n locale: item.locale,\n }));\n}\n\nexport function validatePlaceholdersConsistency(\n entries: NormalizedEntry[],\n locales: string[],\n baseLocale: string,\n): ValidationIssue[] {\n const otherLocales = locales.filter((locale) => locale !== baseLocale);\n\n return entries.flatMap((entry) => {\n const baseTranslation = entry.translations[baseLocale];\n if (!baseTranslation) {\n return [];\n }\n const basePlaceholders = extractPlaceholders(baseTranslation);\n\n return otherLocales\n .map((locale) => {\n const translation = entry.translations[locale];\n if (!translation) {\n return null;\n }\n const placeholders = extractPlaceholders(translation);\n const missingIssues = basePlaceholders\n .filter((placeholder) => !placeholders.includes(placeholder))\n .map((placeholder) => ({\n type: \"missing_placeholder\" as const,\n key: entry.key,\n locale,\n baseLocale,\n placeholder,\n }));\n const extraIssues = placeholders\n .filter((placeholder) => !basePlaceholders.includes(placeholder))\n .map((placeholder) => ({\n type: \"extra_placeholder\" as const,\n key: entry.key,\n locale,\n baseLocale,\n placeholder,\n }));\n\n return [...missingIssues, ...extraIssues];\n })\n .flat()\n .filter((item): item is PlaceholderIssue => item !== null);\n });\n}\n\nexport function validateDefaultLocale(\n locales: string[],\n defaultLocale: string,\n): void {\n if (!locales.includes(defaultLocale)) {\n throw new Error(\n `Default locale \"${defaultLocale}\" is not present in locales: ${locales.join(\", \")}`,\n );\n }\n}\n","import { dirname, joinPath, mkdir, writeUtf8File } from \"./node.js\";\nimport {\n fetchAllRecords,\n listBaseTables,\n type AirtableRecord,\n type FetchFn,\n} from \"./fetch.js\";\nimport {\n inferLocalesFromRecords,\n normalizeRecords,\n parseLocalesFromEnv,\n type NormalizedEntry,\n} from \"./normalize.js\";\nimport {\n detectDuplicateKeys,\n type ValidationIssue,\n validateDefaultLocale,\n validateMissingTranslations,\n validatePlaceholdersConsistency,\n} from \"./validate.js\";\nimport type {\n LocaleDictionaryFile,\n ManifestFile,\n} from \"@dsi18n/schema/contracts\";\n\n/** Relative to each `<locale>.json` inside the default `packages/locales/` layout. */\nconst DICTIONARY_SCHEMA_REF = \"../schema/dictionary.schema.json\";\nconst MANIFEST_SCHEMA_REF = \"../schema/manifest.schema.json\";\n\nexport type BuildConfig = {\n airtableApiKey?: string;\n baseId?: string;\n table?: string;\n tablesCsv?: string;\n outDir?: string;\n version?: string;\n keyField?: string;\n defaultLocale?: string;\n localesCsv?: string;\n typesOutFile?: string;\n fetchFn?: FetchFn;\n debug?: boolean;\n rateLimitDelayMs?: number;\n};\n\nconst DEFAULT_OUT_DIR = \"packages/locales\";\nconst DEFAULT_VERSION = \"0.1.0\";\nconst DEFAULT_KEY_FIELD = \"key\";\nconst DEFAULT_LOCALE = \"en\";\nconst DEFAULT_TYPES_OUT_FILE =\n \"packages/i18n-core/src/generated/translation-keys.ts\";\nconst DEFAULT_RATE_LIMIT_DELAY_MS = 200;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\nfunction formatValidationIssue(issue: ValidationIssue): string {\n if (issue.type === \"missing_translation\") {\n return `Missing ${issue.locale} for key: ${issue.key}`;\n }\n\n if (issue.type === \"missing_placeholder\") {\n return `Missing placeholder \"${issue.placeholder}\" for key \"${issue.key}\" (${issue.locale}) using base locale \"${issue.baseLocale}\"`;\n }\n\n return `Extra placeholder \"${issue.placeholder}\" for key \"${issue.key}\" (${issue.locale}) using base locale \"${issue.baseLocale}\"`;\n}\n\nfunction parseTablesFromEnv(value: string | undefined): string[] {\n if (!value) {\n return [];\n }\n\n return value\n .split(\",\")\n .map((table) => table.trim())\n .filter(Boolean);\n}\n\nfunction extractPlaceholders(text: string): string[] {\n const matches = text.match(/\\{([a-zA-Z0-9_]+)\\}/g) ?? [];\n const cleaned = matches.map((token) => token.replace(/[{}]/g, \"\"));\n return [...new Set(cleaned)];\n}\n\nfunction buildDictionaries(\n entries: NormalizedEntry[],\n locales: string[],\n version: string,\n): Record<string, LocaleDictionaryFile> {\n const dictionaries = Object.fromEntries(\n locales.map((locale) => [\n locale,\n {\n locale,\n version,\n entries: {},\n },\n ]),\n ) as Record<string, LocaleDictionaryFile>;\n\n entries.forEach((entry) => {\n locales.forEach((locale) => {\n const value = entry.translations[locale];\n if (!value) {\n return;\n }\n\n dictionaries[locale].entries[entry.key] = {\n value,\n placeholders: extractPlaceholders(value),\n };\n });\n });\n\n return dictionaries;\n}\n\nasync function writeOutputs(params: {\n outDir: string;\n dictionaries: Record<string, LocaleDictionaryFile>;\n manifest: ManifestFile;\n}): Promise<void> {\n await mkdir(params.outDir);\n\n const dictionaryEntries = Object.entries(params.dictionaries);\n await Promise.all(\n dictionaryEntries.map(async ([locale, dictionary]) => {\n const filePath = joinPath(params.outDir, `${locale}.json`);\n const payload: LocaleDictionaryFile = {\n $schema: DICTIONARY_SCHEMA_REF,\n locale: dictionary.locale,\n version: dictionary.version,\n entries: dictionary.entries,\n };\n await writeUtf8File(filePath, `${JSON.stringify(payload, null, 2)}\\n`);\n }),\n );\n\n const manifestPath = joinPath(params.outDir, \"manifest.json\");\n const manifestPayload: ManifestFile = {\n ...params.manifest,\n $schema: MANIFEST_SCHEMA_REF,\n };\n await writeUtf8File(\n manifestPath,\n `${JSON.stringify(manifestPayload, null, 2)}\\n`,\n );\n}\n\nfunction buildTypeDefinitions(keys: string[]): string {\n const sortedKeys = [...keys].sort();\n\n const keyUnion =\n sortedKeys.length > 0\n ? sortedKeys.map((key) => ` | \"${key}\"`).join(\"\\n\")\n : ' | \"__no_translation_keys__\"';\n\n return [\n \"// AUTO-GENERATED FILE. DO NOT EDIT.\",\n \"// Generated by packages/airtable-sync/src/core/run-build.ts\",\n \"\",\n \"export type TranslationKey =\",\n keyUnion + \";\",\n \"\",\n \"export const translationKeys = [\",\n ...sortedKeys.map((key) => ` \"${key}\",`),\n \"] as const;\",\n \"\",\n ].join(\"\\n\");\n}\n\nasync function writeTypeDefinitions(\n typeOutputPath: string,\n entries: NormalizedEntry[],\n): Promise<void> {\n const keys = entries.map((entry) => entry.key);\n const typeContent = buildTypeDefinitions(keys);\n\n await mkdir(dirname(typeOutputPath));\n await writeUtf8File(typeOutputPath, typeContent);\n}\n\nexport async function runBuild(config: BuildConfig): Promise<void> {\n const baseId = config.baseId;\n const airtableApiKey = config.airtableApiKey;\n\n if (!baseId) {\n throw new Error(\"Missing required config: baseId\");\n }\n\n if (!airtableApiKey) {\n throw new Error(\"Missing airtableApiKey\");\n }\n\n const outDir = config.outDir ?? DEFAULT_OUT_DIR;\n const version = config.version ?? DEFAULT_VERSION;\n const keyField = config.keyField ?? DEFAULT_KEY_FIELD;\n const defaultLocale = config.defaultLocale ?? DEFAULT_LOCALE;\n const typesOutFile = config.typesOutFile ?? DEFAULT_TYPES_OUT_FILE;\n const rateLimitDelayMs =\n config.rateLimitDelayMs ?? DEFAULT_RATE_LIMIT_DELAY_MS;\n\n const tablesFromCsv = parseTablesFromEnv(config.tablesCsv);\n const selectedTables =\n config.table && config.table.trim().length > 0\n ? [config.table.trim()]\n : tablesFromCsv;\n const tables =\n selectedTables.length > 0\n ? selectedTables\n : await (async () => {\n if (!config.airtableApiKey) {\n throw new Error(\n \"AIRTABLE_TABLE or I18N_TABLES is required unless AIRTABLE_API_KEY can access Airtable metadata.\",\n );\n }\n\n // If this metadata call fails due to restricted scopes, pass\n // AIRTABLE_TABLE or I18N_TABLES to skip metadata discovery.\n return listBaseTables({\n apiKey: airtableApiKey,\n baseId,\n fetchFn: config.fetchFn,\n debug: config.debug,\n });\n })();\n\n if (tables.length === 0) {\n throw new Error(\"No tables found to read from Airtable base.\");\n }\n\n const records = await tables.reduce<Promise<AirtableRecord[]>>(\n async (\n accPromise: Promise<AirtableRecord[]>,\n table: string,\n index: number,\n ) => {\n const acc = await accPromise;\n if (index > 0) {\n await sleep(rateLimitDelayMs);\n }\n\n const tableRecords = await fetchAllRecords({\n apiKey: airtableApiKey,\n baseId,\n table,\n fetchFn: config.fetchFn,\n debug: config.debug,\n rateLimitDelayMs,\n });\n\n return [...acc, ...tableRecords];\n },\n Promise.resolve([]),\n );\n const configuredLocales = parseLocalesFromEnv(config.localesCsv);\n const inferredLocales = inferLocalesFromRecords(records, keyField);\n const locales =\n configuredLocales.length > 0 ? configuredLocales : inferredLocales;\n\n if (locales.length === 0) {\n throw new Error(\n \"No locales found. Set I18N_LOCALES or provide language columns in Airtable.\",\n );\n }\n\n const normalized = normalizeRecords(records, locales, keyField, {\n debug: config.debug,\n });\n const duplicateKeys = detectDuplicateKeys(normalized);\n if (duplicateKeys.length > 0) {\n throw new Error(\n `Duplicate translation keys found: ${duplicateKeys.join(\", \")}`,\n );\n }\n\n const missingTranslations = validateMissingTranslations(normalized, locales);\n if (missingTranslations.length > 0) {\n console.warn(\"Missing translations:\");\n missingTranslations.forEach((issue: ValidationIssue) =>\n console.warn(` - ${formatValidationIssue(issue)}`),\n );\n }\n\n validateDefaultLocale(locales, defaultLocale);\n\n const placeholderMismatches = validatePlaceholdersConsistency(\n normalized,\n locales,\n defaultLocale,\n );\n if (placeholderMismatches.length > 0) {\n console.warn(\"Placeholder consistency issues:\");\n placeholderMismatches.forEach((issue: ValidationIssue) =>\n console.warn(` - ${formatValidationIssue(issue)}`),\n );\n }\n\n const dictionaries = buildDictionaries(normalized, locales, version);\n\n const manifest: ManifestFile = {\n version,\n defaultLocale,\n fallbackChain: [defaultLocale],\n locales,\n generatedAt: new Date().toISOString(),\n source: {\n type: \"airtable\",\n baseId,\n tables,\n },\n };\n\n await writeOutputs({\n outDir,\n dictionaries,\n manifest,\n });\n await writeTypeDefinitions(typesOutFile, normalized);\n\n console.log(\n `Generated locales: ${locales.join(\", \")} | records: ${normalized.length} | out: ${outDir} | types: ${typesOutFile}`,\n );\n}\n","import { existsSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { config as loadEnv } from \"dotenv\";\n\n/**\n * Loads `.env` from `startDir` and each parent directory (up to 6 levels).\n * Root `.env` is applied first; nested `.env` files override those keys.\n *\n * @param startDir - Directory to start from (default: `process.cwd()`).\n */\nexport function loadProjectEnv(startDir = process.cwd()): void {\n const chain: string[] = [];\n let dir = resolve(startDir);\n for (let i = 0; i < 6; i += 1) {\n chain.unshift(dir);\n const parent = dirname(dir);\n if (parent === dir) {\n break;\n }\n dir = parent;\n }\n for (const folder of chain) {\n const path = join(folder, \".env\");\n if (existsSync(path)) {\n loadEnv({ path });\n }\n }\n}\n","import { loadBuildConfigFromEnv } from \"./config.js\";\nimport { runBuild } from \"./core/run-build.js\";\n\nexport type { BuildConfig } from \"./core/run-build.js\";\nexport { runBuild } from \"./core/run-build.js\";\nexport {\n loadBuildConfigFromEnv,\n parseBooleanEnv,\n parseNumberEnv,\n} from \"./config.js\";\nexport { loadProjectEnv } from \"./load-project-env.js\";\n\nexport async function main(): Promise<void> {\n await runBuild(loadBuildConfigFromEnv());\n}\n","#!/usr/bin/env node\nimport { main } from \"./index.js\";\nimport { loadProjectEnv } from \"./load-project-env.js\";\n\nloadProjectEnv();\n\nmain().catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n console.error(message);\n process.exit(1);\n});\n"]}
@@ -0,0 +1,36 @@
1
+ type FetchFn = typeof fetch;
2
+
3
+ type BuildConfig = {
4
+ airtableApiKey?: string;
5
+ baseId?: string;
6
+ table?: string;
7
+ tablesCsv?: string;
8
+ outDir?: string;
9
+ version?: string;
10
+ keyField?: string;
11
+ defaultLocale?: string;
12
+ localesCsv?: string;
13
+ typesOutFile?: string;
14
+ fetchFn?: FetchFn;
15
+ debug?: boolean;
16
+ rateLimitDelayMs?: number;
17
+ };
18
+ declare function runBuild(config: BuildConfig): Promise<void>;
19
+
20
+ declare function parseBooleanEnv(value: string | undefined): boolean;
21
+ declare function parseNumberEnv(value: string | undefined): number | undefined;
22
+ /** Build config from process environment (CLI / CI). */
23
+ declare function loadBuildConfigFromEnv(): BuildConfig;
24
+
25
+ /**
26
+ * Loads `.env` from `startDir` and each parent directory (up to 6 levels).
27
+ * Root `.env` is applied first; nested `.env` files override those keys.
28
+ *
29
+ * @param startDir - Directory to start from (default: `process.cwd()`).
30
+ */
31
+ declare function loadProjectEnv(startDir?: string): void;
32
+
33
+ declare function main(): Promise<void>;
34
+
35
+ export { loadBuildConfigFromEnv, loadProjectEnv, main, parseBooleanEnv, parseNumberEnv, runBuild };
36
+ export type { BuildConfig };
package/dist/index.js ADDED
@@ -0,0 +1,530 @@
1
+ import { mkdir as mkdir$1, writeFile } from 'fs/promises';
2
+ import path, { resolve, dirname as dirname$1, join } from 'path';
3
+ import { existsSync } from 'fs';
4
+ import { config } from 'dotenv';
5
+
6
+ // src/config.ts
7
+ function parseBooleanEnv(value) {
8
+ return value === "1" || value === "true";
9
+ }
10
+ function parseNumberEnv(value) {
11
+ if (!value) {
12
+ return void 0;
13
+ }
14
+ const parsed = Number(value);
15
+ return Number.isFinite(parsed) ? parsed : void 0;
16
+ }
17
+ function loadBuildConfigFromEnv() {
18
+ return {
19
+ airtableApiKey: process.env.AIRTABLE_API_KEY,
20
+ baseId: process.env.AIRTABLE_BASE_ID,
21
+ table: process.env.AIRTABLE_TABLE,
22
+ tablesCsv: process.env.I18N_TABLES,
23
+ outDir: process.env.I18N_OUT_DIR,
24
+ version: process.env.I18N_VERSION,
25
+ keyField: process.env.I18N_KEY_FIELD,
26
+ defaultLocale: process.env.I18N_DEFAULT_LOCALE,
27
+ localesCsv: process.env.I18N_LOCALES,
28
+ typesOutFile: process.env.I18N_TYPES_OUT_FILE,
29
+ debug: parseBooleanEnv(process.env.I18N_DEBUG),
30
+ rateLimitDelayMs: parseNumberEnv(process.env.I18N_RATE_LIMIT_DELAY_MS)
31
+ };
32
+ }
33
+ async function mkdir(pathValue) {
34
+ await mkdir$1(pathValue, { recursive: true });
35
+ }
36
+ async function writeUtf8File(pathValue, content) {
37
+ await writeFile(pathValue, content, "utf8");
38
+ }
39
+ function joinPath(...parts) {
40
+ return path.join(...parts);
41
+ }
42
+ function dirname(pathValue) {
43
+ return path.dirname(pathValue);
44
+ }
45
+
46
+ // src/core/fetch.ts
47
+ var RETRY_ATTEMPTS = 3;
48
+ var RETRY_BASE_DELAY_MS = 300;
49
+ var DEFAULT_RATE_LIMIT_DELAY_MS = 200;
50
+ function sleep(ms) {
51
+ return new Promise((resolve2) => {
52
+ setTimeout(resolve2, ms);
53
+ });
54
+ }
55
+ function isRetryableStatus(status) {
56
+ return status === 429 || status >= 500;
57
+ }
58
+ async function fetchWithRetry(url, init, fetchFn, debug) {
59
+ let lastError = null;
60
+ for (let attempt = 0; attempt < RETRY_ATTEMPTS; attempt += 1) {
61
+ try {
62
+ if (debug) {
63
+ console.log(`[fetch] attempt=${attempt + 1} url=${url.toString()}`);
64
+ }
65
+ const response = await fetchFn(url, init);
66
+ if (!isRetryableStatus(response.status)) {
67
+ return response;
68
+ }
69
+ if (debug) {
70
+ console.warn(
71
+ `[fetch] retryable_status=${response.status} attempt=${attempt + 1} url=${url.toString()}`
72
+ );
73
+ }
74
+ const body = await response.text();
75
+ lastError = new Error(
76
+ `Retryable Airtable response (${response.status}): ${response.statusText}
77
+ ${body}`
78
+ );
79
+ } catch (error) {
80
+ if (debug) {
81
+ console.warn(
82
+ `[fetch] network_error attempt=${attempt + 1} url=${url.toString()}`
83
+ );
84
+ }
85
+ lastError = error instanceof Error ? error : new Error("Unknown fetch error");
86
+ }
87
+ if (attempt < RETRY_ATTEMPTS - 1) {
88
+ await sleep(RETRY_BASE_DELAY_MS * (attempt + 1));
89
+ }
90
+ }
91
+ throw lastError ?? new Error("Fetch failed after retries");
92
+ }
93
+ async function fetchAirtablePage(params) {
94
+ const encodedTable = encodeURIComponent(params.table);
95
+ const url = new URL(
96
+ `https://api.airtable.com/v0/${params.baseId}/${encodedTable}`
97
+ );
98
+ if (params.offset) {
99
+ url.searchParams.set("offset", params.offset);
100
+ }
101
+ const headers = {};
102
+ if (params.apiKey) {
103
+ headers.Authorization = `Bearer ${params.apiKey}`;
104
+ }
105
+ const fetchFn = params.fetchFn ?? fetch;
106
+ if (params.debug) {
107
+ const offsetValue = params.offset ?? "none";
108
+ console.log(`[fetch] table=${params.table} offset=${offsetValue}`);
109
+ }
110
+ const response = await fetchWithRetry(url, { headers }, fetchFn, params.debug);
111
+ if (!response.ok) {
112
+ const body = await response.text();
113
+ throw new Error(
114
+ `Airtable request failed (${response.status}): ${response.statusText}
115
+ ${body}`
116
+ );
117
+ }
118
+ return await response.json();
119
+ }
120
+ async function fetchAllRecords(params) {
121
+ const records = [];
122
+ let offset;
123
+ const rateLimitDelayMs = params.rateLimitDelayMs ?? DEFAULT_RATE_LIMIT_DELAY_MS;
124
+ do {
125
+ const page = await fetchAirtablePage({
126
+ apiKey: params.apiKey,
127
+ baseId: params.baseId,
128
+ table: params.table,
129
+ offset,
130
+ fetchFn: params.fetchFn,
131
+ debug: params.debug
132
+ });
133
+ records.push(...page.records);
134
+ offset = page.offset;
135
+ if (offset) {
136
+ await sleep(rateLimitDelayMs);
137
+ }
138
+ } while (offset);
139
+ return records;
140
+ }
141
+ async function listBaseTables(params) {
142
+ const url = new URL(`https://api.airtable.com/v0/meta/bases/${params.baseId}/tables`);
143
+ const fetchFn = params.fetchFn ?? fetch;
144
+ const response = await fetchWithRetry(
145
+ url,
146
+ {
147
+ headers: {
148
+ Authorization: `Bearer ${params.apiKey}`
149
+ }
150
+ },
151
+ fetchFn,
152
+ params.debug
153
+ );
154
+ if (!response.ok) {
155
+ const body = await response.text();
156
+ throw new Error(
157
+ `Airtable metadata request failed (${response.status}): ${response.statusText}
158
+ ${body}`
159
+ );
160
+ }
161
+ const json = await response.json();
162
+ return json.tables.map((table) => table.name);
163
+ }
164
+
165
+ // src/core/normalize.ts
166
+ function parseLocalesFromEnv(value) {
167
+ if (!value) {
168
+ return [];
169
+ }
170
+ return value.split(",").map((locale) => locale.trim()).filter(Boolean);
171
+ }
172
+ function inferLocalesFromRecords(records, keyField) {
173
+ const locales = /* @__PURE__ */ new Set();
174
+ for (const record of records) {
175
+ const fields = Object.keys(record.fields);
176
+ for (const fieldName of fields) {
177
+ if (fieldName === keyField) {
178
+ continue;
179
+ }
180
+ const value = record.fields[fieldName];
181
+ if (typeof value === "string") {
182
+ locales.add(fieldName);
183
+ }
184
+ }
185
+ }
186
+ return [...locales].sort();
187
+ }
188
+ function normalizeRecords(records, locales, keyField, options) {
189
+ const normalizedEntries = records.map((record) => {
190
+ const keyValue = record.fields[keyField];
191
+ if (typeof keyValue !== "string" || keyValue.trim().length === 0) {
192
+ if (options?.debug) {
193
+ console.warn(`Skipping record ${record.id}: invalid key`);
194
+ }
195
+ return null;
196
+ }
197
+ const key = keyValue.trim();
198
+ const translations = Object.fromEntries(
199
+ locales.map((locale) => {
200
+ const value = record.fields[locale];
201
+ if (typeof value !== "string") {
202
+ return null;
203
+ }
204
+ const trimmed = value.trim();
205
+ return trimmed.length > 0 ? [locale, trimmed] : null;
206
+ }).filter((item) => item !== null)
207
+ );
208
+ return { key, translations };
209
+ });
210
+ return normalizedEntries.filter(
211
+ (entry) => entry !== null
212
+ );
213
+ }
214
+
215
+ // src/core/validate.ts
216
+ function extractPlaceholders(text) {
217
+ const matches = text.match(/\{([a-zA-Z0-9_]+)\}/g) ?? [];
218
+ const cleaned = matches.map((token) => token.replace(/[{}]/g, ""));
219
+ return [...new Set(cleaned)];
220
+ }
221
+ function detectDuplicateKeys(entries) {
222
+ const seen = /* @__PURE__ */ new Set();
223
+ const duplicates = /* @__PURE__ */ new Set();
224
+ for (const entry of entries) {
225
+ if (seen.has(entry.key)) {
226
+ duplicates.add(entry.key);
227
+ continue;
228
+ }
229
+ seen.add(entry.key);
230
+ }
231
+ return [...duplicates].sort();
232
+ }
233
+ function validateMissingTranslations(entries, locales) {
234
+ return entries.flatMap(
235
+ (entry) => locales.map((locale) => ({
236
+ key: entry.key,
237
+ locale,
238
+ value: entry.translations[locale]
239
+ }))
240
+ ).filter((item) => item.value == null || item.value === "").map((item) => ({
241
+ type: "missing_translation",
242
+ key: item.key,
243
+ locale: item.locale
244
+ }));
245
+ }
246
+ function validatePlaceholdersConsistency(entries, locales, baseLocale) {
247
+ const otherLocales = locales.filter((locale) => locale !== baseLocale);
248
+ return entries.flatMap((entry) => {
249
+ const baseTranslation = entry.translations[baseLocale];
250
+ if (!baseTranslation) {
251
+ return [];
252
+ }
253
+ const basePlaceholders = extractPlaceholders(baseTranslation);
254
+ return otherLocales.map((locale) => {
255
+ const translation = entry.translations[locale];
256
+ if (!translation) {
257
+ return null;
258
+ }
259
+ const placeholders = extractPlaceholders(translation);
260
+ const missingIssues = basePlaceholders.filter((placeholder) => !placeholders.includes(placeholder)).map((placeholder) => ({
261
+ type: "missing_placeholder",
262
+ key: entry.key,
263
+ locale,
264
+ baseLocale,
265
+ placeholder
266
+ }));
267
+ const extraIssues = placeholders.filter((placeholder) => !basePlaceholders.includes(placeholder)).map((placeholder) => ({
268
+ type: "extra_placeholder",
269
+ key: entry.key,
270
+ locale,
271
+ baseLocale,
272
+ placeholder
273
+ }));
274
+ return [...missingIssues, ...extraIssues];
275
+ }).flat().filter((item) => item !== null);
276
+ });
277
+ }
278
+ function validateDefaultLocale(locales, defaultLocale) {
279
+ if (!locales.includes(defaultLocale)) {
280
+ throw new Error(
281
+ `Default locale "${defaultLocale}" is not present in locales: ${locales.join(", ")}`
282
+ );
283
+ }
284
+ }
285
+
286
+ // src/core/run-build.ts
287
+ var DICTIONARY_SCHEMA_REF = "../schema/dictionary.schema.json";
288
+ var MANIFEST_SCHEMA_REF = "../schema/manifest.schema.json";
289
+ var DEFAULT_OUT_DIR = "packages/locales";
290
+ var DEFAULT_VERSION = "0.1.0";
291
+ var DEFAULT_KEY_FIELD = "key";
292
+ var DEFAULT_LOCALE = "en";
293
+ var DEFAULT_TYPES_OUT_FILE = "packages/i18n-core/src/generated/translation-keys.ts";
294
+ var DEFAULT_RATE_LIMIT_DELAY_MS2 = 200;
295
+ function sleep2(ms) {
296
+ return new Promise((resolve2) => {
297
+ setTimeout(resolve2, ms);
298
+ });
299
+ }
300
+ function formatValidationIssue(issue) {
301
+ if (issue.type === "missing_translation") {
302
+ return `Missing ${issue.locale} for key: ${issue.key}`;
303
+ }
304
+ if (issue.type === "missing_placeholder") {
305
+ return `Missing placeholder "${issue.placeholder}" for key "${issue.key}" (${issue.locale}) using base locale "${issue.baseLocale}"`;
306
+ }
307
+ return `Extra placeholder "${issue.placeholder}" for key "${issue.key}" (${issue.locale}) using base locale "${issue.baseLocale}"`;
308
+ }
309
+ function parseTablesFromEnv(value) {
310
+ if (!value) {
311
+ return [];
312
+ }
313
+ return value.split(",").map((table) => table.trim()).filter(Boolean);
314
+ }
315
+ function extractPlaceholders2(text) {
316
+ const matches = text.match(/\{([a-zA-Z0-9_]+)\}/g) ?? [];
317
+ const cleaned = matches.map((token) => token.replace(/[{}]/g, ""));
318
+ return [...new Set(cleaned)];
319
+ }
320
+ function buildDictionaries(entries, locales, version) {
321
+ const dictionaries = Object.fromEntries(
322
+ locales.map((locale) => [
323
+ locale,
324
+ {
325
+ locale,
326
+ version,
327
+ entries: {}
328
+ }
329
+ ])
330
+ );
331
+ entries.forEach((entry) => {
332
+ locales.forEach((locale) => {
333
+ const value = entry.translations[locale];
334
+ if (!value) {
335
+ return;
336
+ }
337
+ dictionaries[locale].entries[entry.key] = {
338
+ value,
339
+ placeholders: extractPlaceholders2(value)
340
+ };
341
+ });
342
+ });
343
+ return dictionaries;
344
+ }
345
+ async function writeOutputs(params) {
346
+ await mkdir(params.outDir);
347
+ const dictionaryEntries = Object.entries(params.dictionaries);
348
+ await Promise.all(
349
+ dictionaryEntries.map(async ([locale, dictionary]) => {
350
+ const filePath = joinPath(params.outDir, `${locale}.json`);
351
+ const payload = {
352
+ $schema: DICTIONARY_SCHEMA_REF,
353
+ locale: dictionary.locale,
354
+ version: dictionary.version,
355
+ entries: dictionary.entries
356
+ };
357
+ await writeUtf8File(filePath, `${JSON.stringify(payload, null, 2)}
358
+ `);
359
+ })
360
+ );
361
+ const manifestPath = joinPath(params.outDir, "manifest.json");
362
+ const manifestPayload = {
363
+ ...params.manifest,
364
+ $schema: MANIFEST_SCHEMA_REF
365
+ };
366
+ await writeUtf8File(
367
+ manifestPath,
368
+ `${JSON.stringify(manifestPayload, null, 2)}
369
+ `
370
+ );
371
+ }
372
+ function buildTypeDefinitions(keys) {
373
+ const sortedKeys = [...keys].sort();
374
+ const keyUnion = sortedKeys.length > 0 ? sortedKeys.map((key) => ` | "${key}"`).join("\n") : ' | "__no_translation_keys__"';
375
+ return [
376
+ "// AUTO-GENERATED FILE. DO NOT EDIT.",
377
+ "// Generated by packages/airtable-sync/src/core/run-build.ts",
378
+ "",
379
+ "export type TranslationKey =",
380
+ keyUnion + ";",
381
+ "",
382
+ "export const translationKeys = [",
383
+ ...sortedKeys.map((key) => ` "${key}",`),
384
+ "] as const;",
385
+ ""
386
+ ].join("\n");
387
+ }
388
+ async function writeTypeDefinitions(typeOutputPath, entries) {
389
+ const keys = entries.map((entry) => entry.key);
390
+ const typeContent = buildTypeDefinitions(keys);
391
+ await mkdir(dirname(typeOutputPath));
392
+ await writeUtf8File(typeOutputPath, typeContent);
393
+ }
394
+ async function runBuild(config) {
395
+ const baseId = config.baseId;
396
+ const airtableApiKey = config.airtableApiKey;
397
+ if (!baseId) {
398
+ throw new Error("Missing required config: baseId");
399
+ }
400
+ if (!airtableApiKey) {
401
+ throw new Error("Missing airtableApiKey");
402
+ }
403
+ const outDir = config.outDir ?? DEFAULT_OUT_DIR;
404
+ const version = config.version ?? DEFAULT_VERSION;
405
+ const keyField = config.keyField ?? DEFAULT_KEY_FIELD;
406
+ const defaultLocale = config.defaultLocale ?? DEFAULT_LOCALE;
407
+ const typesOutFile = config.typesOutFile ?? DEFAULT_TYPES_OUT_FILE;
408
+ const rateLimitDelayMs = config.rateLimitDelayMs ?? DEFAULT_RATE_LIMIT_DELAY_MS2;
409
+ const tablesFromCsv = parseTablesFromEnv(config.tablesCsv);
410
+ const selectedTables = config.table && config.table.trim().length > 0 ? [config.table.trim()] : tablesFromCsv;
411
+ const tables = selectedTables.length > 0 ? selectedTables : await (async () => {
412
+ if (!config.airtableApiKey) {
413
+ throw new Error(
414
+ "AIRTABLE_TABLE or I18N_TABLES is required unless AIRTABLE_API_KEY can access Airtable metadata."
415
+ );
416
+ }
417
+ return listBaseTables({
418
+ apiKey: airtableApiKey,
419
+ baseId,
420
+ fetchFn: config.fetchFn,
421
+ debug: config.debug
422
+ });
423
+ })();
424
+ if (tables.length === 0) {
425
+ throw new Error("No tables found to read from Airtable base.");
426
+ }
427
+ const records = await tables.reduce(
428
+ async (accPromise, table, index) => {
429
+ const acc = await accPromise;
430
+ if (index > 0) {
431
+ await sleep2(rateLimitDelayMs);
432
+ }
433
+ const tableRecords = await fetchAllRecords({
434
+ apiKey: airtableApiKey,
435
+ baseId,
436
+ table,
437
+ fetchFn: config.fetchFn,
438
+ debug: config.debug,
439
+ rateLimitDelayMs
440
+ });
441
+ return [...acc, ...tableRecords];
442
+ },
443
+ Promise.resolve([])
444
+ );
445
+ const configuredLocales = parseLocalesFromEnv(config.localesCsv);
446
+ const inferredLocales = inferLocalesFromRecords(records, keyField);
447
+ const locales = configuredLocales.length > 0 ? configuredLocales : inferredLocales;
448
+ if (locales.length === 0) {
449
+ throw new Error(
450
+ "No locales found. Set I18N_LOCALES or provide language columns in Airtable."
451
+ );
452
+ }
453
+ const normalized = normalizeRecords(records, locales, keyField, {
454
+ debug: config.debug
455
+ });
456
+ const duplicateKeys = detectDuplicateKeys(normalized);
457
+ if (duplicateKeys.length > 0) {
458
+ throw new Error(
459
+ `Duplicate translation keys found: ${duplicateKeys.join(", ")}`
460
+ );
461
+ }
462
+ const missingTranslations = validateMissingTranslations(normalized, locales);
463
+ if (missingTranslations.length > 0) {
464
+ console.warn("Missing translations:");
465
+ missingTranslations.forEach(
466
+ (issue) => console.warn(` - ${formatValidationIssue(issue)}`)
467
+ );
468
+ }
469
+ validateDefaultLocale(locales, defaultLocale);
470
+ const placeholderMismatches = validatePlaceholdersConsistency(
471
+ normalized,
472
+ locales,
473
+ defaultLocale
474
+ );
475
+ if (placeholderMismatches.length > 0) {
476
+ console.warn("Placeholder consistency issues:");
477
+ placeholderMismatches.forEach(
478
+ (issue) => console.warn(` - ${formatValidationIssue(issue)}`)
479
+ );
480
+ }
481
+ const dictionaries = buildDictionaries(normalized, locales, version);
482
+ const manifest = {
483
+ version,
484
+ defaultLocale,
485
+ fallbackChain: [defaultLocale],
486
+ locales,
487
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
488
+ source: {
489
+ type: "airtable",
490
+ baseId,
491
+ tables
492
+ }
493
+ };
494
+ await writeOutputs({
495
+ outDir,
496
+ dictionaries,
497
+ manifest
498
+ });
499
+ await writeTypeDefinitions(typesOutFile, normalized);
500
+ console.log(
501
+ `Generated locales: ${locales.join(", ")} | records: ${normalized.length} | out: ${outDir} | types: ${typesOutFile}`
502
+ );
503
+ }
504
+ function loadProjectEnv(startDir = process.cwd()) {
505
+ const chain = [];
506
+ let dir = resolve(startDir);
507
+ for (let i = 0; i < 6; i += 1) {
508
+ chain.unshift(dir);
509
+ const parent = dirname$1(dir);
510
+ if (parent === dir) {
511
+ break;
512
+ }
513
+ dir = parent;
514
+ }
515
+ for (const folder of chain) {
516
+ const path2 = join(folder, ".env");
517
+ if (existsSync(path2)) {
518
+ config({ path: path2 });
519
+ }
520
+ }
521
+ }
522
+
523
+ // src/index.ts
524
+ async function main() {
525
+ await runBuild(loadBuildConfigFromEnv());
526
+ }
527
+
528
+ export { loadBuildConfigFromEnv, loadProjectEnv, main, parseBooleanEnv, parseNumberEnv, runBuild };
529
+ //# sourceMappingURL=index.js.map
530
+ //# sourceMappingURL=index.js.map