@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
JavaScript
// 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