UNPKG

@akson/cortex-shopify-translations

Version:

Unified Shopify translations management client with product extraction, translation sync, and CLI tools

269 lines (268 loc) 8.78 kB
// src/client/config.ts var getShopifyConfig = () => { const shop = process.env.SHOPIFY_STORE_DOMAIN; const accessToken = process.env.SHOPIFY_ACCESS_TOKEN || process.env.SHOPIFY_ADMIN_API_TOKEN; if (!shop || !accessToken) { throw new Error("Missing required Shopify configuration. Please set SHOPIFY_STORE_DOMAIN and SHOPIFY_ACCESS_TOKEN environment variables."); } const sourceLanguage = process.env.SHOPIFY_SOURCE_LANGUAGE || "fr"; const targetLanguagesEnv = process.env.SHOPIFY_TARGET_LANGUAGES || "de,it,en"; const targetLanguages = targetLanguagesEnv.split(",").map((lang) => lang.trim()).filter(Boolean); return { shop: shop.replace(/^https?:\/\//, "").replace(/\/$/, ""), // Just remove protocol if present accessToken, apiVersion: process.env.SHOPIFY_API_VERSION || "2024-10", // Use latest stable API version rateLimitDelay: parseInt(process.env.SHOPIFY_RATE_LIMIT_DELAY || "500", 10), // 500ms default sourceLanguage, targetLanguages }; }; // src/client/graphql-client.ts var ShopifyGraphQLClient = class { // Conservative estimate constructor() { this.lastCallTime = 0; this.remainingCost = 2e3; this.config = getShopifyConfig(); } get sourceLanguage() { return this.config.sourceLanguage; } get targetLanguages() { return this.config.targetLanguages; } async rateLimitDelay() { const now = Date.now(); const timeSinceLastCall = now - this.lastCallTime; if (timeSinceLastCall < this.config.rateLimitDelay) { const delayTime = this.config.rateLimitDelay - timeSinceLastCall; await new Promise((resolve) => setTimeout(resolve, delayTime)); } this.lastCallTime = Date.now(); } get endpoint() { const shop = this.config.shop.includes(".myshopify.com") ? this.config.shop : `${this.config.shop}.myshopify.com`; return `https://${shop}/admin/api/${this.config.apiVersion}/graphql.json`; } get headers() { return { "X-Shopify-Access-Token": this.config.accessToken, "Content-Type": "application/json" }; } async query(query, variables = {}) { await this.rateLimitDelay(); const response = await fetch(this.endpoint, { method: "POST", headers: this.headers, body: JSON.stringify({ query, variables }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`GraphQL HTTP Error (${response.status}): ${errorText}`); } const result = await response.json(); if (result.extensions?.cost) { this.remainingCost = result.extensions.cost.throttleStatus.currentlyAvailable; } if (result.errors && result.errors.length > 0) { const errorMessages = result.errors.map((err) => err.message).join("; "); throw new Error(`GraphQL Error: ${errorMessages}`); } return result; } /** * Discover all available translatable resource types */ async discoverTranslatableResourceTypes() { const query = ` query IntrospectTranslatableResourceType { __type(name: "TranslatableResourceType") { name enumValues { name description } } } `; const response = await this.query(query); if (response.data?.__type?.enumValues) { return response.data.__type.enumValues.map((value) => value.name); } return [ "PRODUCT", "COLLECTION", "ARTICLE", "BLOG", "PAGE", "ONLINE_STORE_THEME", "SHOP", "SHOP_POLICY" ]; } /** * Get all translatable resources of a specific type with pagination */ async getTranslatableResources(resourceType, first = 50, after) { const query = ` query GetTranslatableResources($resourceType: TranslatableResourceType!, $first: Int!, $after: String) { translatableResources(resourceType: $resourceType, first: $first, after: $after) { edges { node { resourceId translatableContent { key value digest locale } } cursor } pageInfo { hasNextPage endCursor hasPreviousPage startCursor } } } `; const response = await this.query( query, { resourceType, first, after } ); if (!response.data?.translatableResources) { throw new Error("Failed to fetch translatable resources"); } const resourcesWithTranslations = await this.fetchTranslationsForResources( response.data.translatableResources.edges ); return { ...response.data.translatableResources, edges: resourcesWithTranslations }; } /** * Get all translatable resources for all types */ async getAllTranslatableResources() { const resourceTypes = await this.discoverTranslatableResourceTypes(); const results = {}; for (const resourceType of resourceTypes) { console.log(`\u{1F50D} Fetching ${resourceType} resources...`); const resources = []; let hasNextPage = true; let cursor; while (hasNextPage) { const connection = await this.getTranslatableResources(resourceType, 50, cursor); resources.push(...connection.edges.map((edge) => edge.node)); hasNextPage = connection.pageInfo.hasNextPage; cursor = connection.pageInfo.endCursor; console.log(` \u{1F4C4} Fetched ${connection.edges.length} resources (Total: ${resources.length})`); } results[resourceType] = resources; } return results; } /** * Register translations for multiple resources */ async registerTranslations(translations) { const mutation = ` mutation RegisterTranslations($resourceId: ID!, $translations: [TranslationInput!]!) { translationsRegister(resourceId: $resourceId, translations: $translations) { translations { key value locale } userErrors { field message } } } `; for (const resource of translations) { const translationInputs = resource.translations.map((translation) => ({ key: translation.key, value: translation.value, locale: translation.locale, translatableContentDigest: translation.translatableContentDigest })); const response = await this.query(mutation, { resourceId: resource.resourceId, translations: translationInputs }); if (response.data?.translationsRegister?.userErrors?.length > 0) { const errors = response.data.translationsRegister.userErrors; throw new Error(`Translation registration failed: ${errors.map((e) => e.message).join("; ")}`); } } return true; } /** * Fetch translations for a batch of resources efficiently */ async fetchTranslationsForResources(edges) { const supportedLocales = this.targetLanguages; const resourcesWithTranslations = []; for (const edge of edges) { const resourceWithTranslations = { ...edge }; const allTranslations = []; for (const locale of supportedLocales) { try { const translationsQuery = ` query GetTranslations($resourceId: ID!, $locale: String!) { translatableResource(resourceId: $resourceId) { translations(locale: $locale) { key value locale outdated } } } `; await this.rateLimitDelay(); const response = await this.query(translationsQuery, { resourceId: edge.node.resourceId, locale }); if (response.data?.translatableResource?.translations) { allTranslations.push(...response.data.translatableResource.translations); } } catch (error) { console.warn(`Failed to fetch ${locale} translations for resource ${edge.node.resourceId}: ${error instanceof Error ? error.message : String(error)}`); } } resourceWithTranslations.node.translations = allTranslations; resourcesWithTranslations.push(resourceWithTranslations); } return resourcesWithTranslations; } /** * Get current query cost status */ getRemainingCost() { return this.remainingCost; } /** * Check if we need to pause due to rate limiting */ needsRateLimit() { return this.remainingCost < 100; } }; var createGraphQLClient = () => new ShopifyGraphQLClient(); export { ShopifyGraphQLClient, createGraphQLClient, getShopifyConfig }; //# sourceMappingURL=index.js.map