diff --git a/CHANGELOG.md b/CHANGELOG.md index f370a8bd6..46a84eb6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +## [7.2.6] - 2025-11-05 +### Changed +- Enhance the translation utilities by introducing a new state marker for translatable message strings, updating the parsing and formatting logic to handle this marker and ensuring that already translated messages are bypassed further processing + ## [7.2.5] - 2025-10-28 ### Changed - Added a new metrics tooling layer using @vtex/diagnostics-nodejs to replace the legacy MetricsAccumulator system diff --git a/docs/examples/README.md b/docs/examples/README.md new file mode 100644 index 000000000..6b816b989 --- /dev/null +++ b/docs/examples/README.md @@ -0,0 +1,220 @@ +# Integração do Client VBase no Middleware do Next.js + +Este guia explica como integrar o client VBase do `@vtex/api` no middleware do Next.js. + +## 📋 Pré-requisitos + +1. **Projeto Next.js** (versão 12.2 ou superior para suporte ao middleware) + +2. **Instalação de dependências:** + ```bash + npm install @vtex/api uuid + # ou + yarn add @vtex/api uuid + ``` + +3. **Variáveis de ambiente** (crie um arquivo `.env.local`): + ```env + # Obrigatório + VTEX_ACCOUNT=seu-account-name + VTEX_AUTH_TOKEN=seu-token-de-autenticacao + VTEX_APP_ID=vendor.app-name@major-version + + # Opcionais + VTEX_WORKSPACE=master + VTEX_REGION=aws-us-east-1 + ``` + + > **Importante:** O `VTEX_APP_ID` é obrigatório para o client VBase funcionar. Ele deve seguir o formato `vendor.app-name@major-version` (ex: `vtex.my-app@1.x`). + +## 🚀 Como usar + +### Passo 1: Copiar as funções helper + +Copie as funções `createVBaseContext` e `createVBaseClient` do arquivo de exemplo para seu projeto, ou crie um arquivo separado (ex: `lib/vtex/vbase-helpers.ts`). + +### Passo 2: Criar o middleware + +Crie um arquivo `middleware.ts` na raiz do seu projeto Next.js: + +```typescript +import { NextRequest, NextResponse } from 'next/server' +import { createVBaseContext, createVBaseClient } from './lib/vtex/vbase-helpers' + +export async function middleware(request: NextRequest) { + // Extrair account e workspace + const account = + request.headers.get('x-vtex-account') || + process.env.VTEX_ACCOUNT || '' + + const workspace = + request.headers.get('x-vtex-workspace') || + process.env.VTEX_WORKSPACE || 'master' + + if (!account) { + return NextResponse.next() + } + + try { + // Criar contexto e client VBase + const context = createVBaseContext(request, { + account, + workspace, + authToken: request.headers.get('x-vtex-credential') || process.env.VTEX_AUTH_TOKEN, + }) + + const vbase = createVBaseClient(context) + + // Usar o client VBase + const bucket = 'meu-bucket' + const filePath = 'arquivo.json' + const data = await vbase.getJSON(bucket, filePath, true) + + // Modificar resposta com dados do VBase + const response = NextResponse.next() + if (data) { + response.headers.set('x-vbase-data', JSON.stringify(data)) + } + + return response + } catch (error) { + console.error('Erro no middleware VBase:', error) + return NextResponse.next() + } +} + +export const config = { + matcher: [ + '/((?!api|_next/static|_next/image|favicon.ico).*)', + ], +} +``` + +## 📝 Exemplos de uso + +### 1. Buscar configurações do VBase + +```typescript +const config = await vbase.getJSON<{ theme: string; features: string[] }>( + 'app-config', + 'settings.json', + true // retorna null se não encontrar +) +``` + +### 2. Listar arquivos de um bucket + +```typescript +const files = await vbase.listFiles('app-assets', { + prefix: 'images/', + limit: 100 +}) +``` + +### 3. Salvar dados no VBase + +```typescript +await vbase.saveJSON('app-config', 'settings.json', { + theme: 'dark', + features: ['feature1', 'feature2'] +}) +``` + +### 4. Buscar arquivo binário + +```typescript +const buffer = await vbase.getFile('app-assets', 'image.png') +``` + +## 🔍 Extraindo informações da requisição + +### Opção 1: Headers HTTP + +```typescript +const account = request.headers.get('x-vtex-account') +const workspace = request.headers.get('x-vtex-workspace') +const authToken = request.headers.get('x-vtex-credential') +``` + +### Opção 2: Query parameters + +```typescript +const account = request.nextUrl.searchParams.get('account') +const workspace = request.nextUrl.searchParams.get('workspace') +``` + +### Opção 3: Do hostname + +```typescript +const host = request.headers.get('host') || '' +const hostParts = host.split('.') +const account = hostParts[0] // ex: myaccount.vtexcommercestable.com.br +``` + +### Opção 4: Variáveis de ambiente + +```typescript +const account = process.env.VTEX_ACCOUNT +const workspace = process.env.VTEX_WORKSPACE || 'master' +``` + +## ⚠️ Considerações importantes + +### Performance + +- O middleware é executado em **cada requisição** que corresponder ao matcher +- Evite fazer muitas chamadas ao VBase no middleware +- Considere implementar cache para dados que não mudam frequentemente + +### Tratamento de erros + +- Sempre trate erros do VBase para não quebrar o site +- Em caso de erro, geralmente é melhor continuar com `NextResponse.next()` do que retornar um erro 500 + +### Autenticação + +- O `VTEX_AUTH_TOKEN` é necessário para fazer chamadas autenticadas ao VBase +- Você pode obtê-lo através do VTEX IO CLI ou da plataforma VTEX + +## 🔧 Troubleshooting + +### Erro: "VTEX_APP_ID não está configurado" + +Configure a variável de ambiente `VTEX_APP_ID` com o formato `vendor.app-name@major-version`. + +### Erro: "Invalid path to access VBase" + +Verifique se: +1. O `VTEX_APP_ID` está configurado corretamente +2. O formato está correto: `vendor.app-name@major-version` + +### Erro de autenticação (401) + +Verifique se: +1. O `VTEX_AUTH_TOKEN` está configurado corretamente +2. O token tem permissões para acessar o VBase +3. O account e workspace estão corretos + +### Tracer não inicializa + +O Tracer pode falhar em ambientes que não são VTEX IO. O código de exemplo inclui um fallback para usar um MockTracer. Se necessário, instale: + +```bash +npm install @tiagonapoli/opentracing-alternate-mock +``` + +## 📚 Recursos adicionais + +- [Documentação do Next.js Middleware](https://nextjs.org/docs/advanced-features/middleware) +- [Documentação do @vtex/api](https://github.com/vtex/node-vtex-api) +- [Documentação do VBase](https://developers.vtex.com/docs/guides/vbase-overview) + +## 🆘 Suporte + +Se você encontrar problemas, verifique: +1. As variáveis de ambiente estão configuradas corretamente +2. As versões do Next.js e @vtex/api são compatíveis +3. O account e workspace estão acessíveis +4. O token de autenticação está válido + + diff --git a/docs/examples/vbase-nextjs-middleware.ts b/docs/examples/vbase-nextjs-middleware.ts new file mode 100644 index 000000000..31fc1ff5c --- /dev/null +++ b/docs/examples/vbase-nextjs-middleware.ts @@ -0,0 +1,378 @@ +/** + * Exemplo de integração do client VBase no middleware do Next.js + * + * Este arquivo mostra como criar e usar o client VBase no middleware do Next.js + * + * PRÉ-REQUISITOS: + * 1. Instalar dependências: + * npm install @vtex/api uuid + * # ou + * yarn add @vtex/api uuid + * + * 2. Configurar variáveis de ambiente no .env.local: + * VTEX_ACCOUNT=seu-account + * VTEX_WORKSPACE=master (ou outro workspace) + * VTEX_AUTH_TOKEN=seu-token-de-autenticacao + * VTEX_APP_ID=vendor.app-name@major-version (necessário para VBase funcionar) + * VTEX_REGION=aws-us-east-1 (opcional) + */ + +import { VBase } from '@vtex/api/lib/clients/infra/VBase' +import { Logger } from '@vtex/api/lib/service/logger/logger' +import { TracerSingleton } from '@vtex/api/lib/service/tracing/TracerSingleton' +import { IOContext } from '@vtex/api/lib/service/worker/runtime/typings' +import { UserLandTracer } from '@vtex/api/lib/tracing/UserLandTracer' +import { NextRequest, NextResponse } from 'next/server' +import { v4 as uuidv4 } from 'uuid' + +/** + * Cria um IOContext mínimo para uso no middleware do Next.js + * + * @param request - Requisição do Next.js + * @param options - Opções adicionais (account, workspace, region) + * @returns IOContext criado + */ +export function createVBaseContext( + request: NextRequest, + options: { + account: string + workspace: string + region?: string + authToken?: string + } +): IOContext { + const requestId = request.headers.get('x-request-id') || uuidv4() + const operationId = request.headers.get('x-operation-id') || uuidv4() + + // Verifica se VTEX_APP_ID está configurado (necessário para VBase) + if (!process.env.VTEX_APP_ID) { + throw new Error( + 'VTEX_APP_ID não está configurado. Configure a variável de ambiente VTEX_APP_ID com o formato: vendor.app-name@major-version' + ) + } + + // Cria um logger básico + const logger = new Logger({ + account: options.account, + workspace: options.workspace, + requestId, + operationId, + production: options.workspace === 'master', + }) + + // Cria um tracer básico + // Nota: TracerSingleton requer algumas variáveis de ambiente do VTEX IO + // Em ambientes Next.js, pode ser necessário criar um tracer mock ou simplificado + let userlandTracer: UserLandTracer + try { + const tracer = TracerSingleton.getTracer() + userlandTracer = new UserLandTracer(tracer) + } catch (error) { + // Fallback: cria um tracer básico sem dependências do ambiente VTEX IO + // Você pode precisar criar uma implementação mock do tracer para desenvolvimento + console.warn('Falha ao inicializar tracer:', error) + const MockTracer = require('@tiagonapoli/opentracing-alternate-mock').MockTracer + const mockTracer = new MockTracer() + userlandTracer = new UserLandTracer(mockTracer) + } + + // Retorna o contexto mínimo necessário + return { + account: options.account, + workspace: options.workspace, + region: options.region || process.env.VTEX_REGION || 'aws-us-east-1', + authToken: options.authToken || process.env.VTEX_AUTH_TOKEN || '', + platform: options.account.startsWith('gc-') ? 'gocommerce' : 'vtex', + production: options.workspace === 'master', + product: process.env.VTEX_PRODUCT || '', + userAgent: process.env.VTEX_APP_ID || '', + requestId, + operationId, + logger, + tracer: userlandTracer, + route: { + id: request.nextUrl.pathname, + params: {}, + type: 'public', + }, + } +} + +/** + * Cria uma instância do client VBase + * + * @param context - IOContext criado + * @returns Instância do VBase client + */ +export function createVBaseClient(context: IOContext): VBase { + return new VBase(context) +} + +/** + * ============================================================================ + * EXEMPLOS DE USO NO MIDDLEWARE DO NEXT.JS + * ============================================================================ + * + * Copie e cole o código abaixo em um arquivo middleware.ts na raiz do seu projeto Next.js + */ + +// ============================================================================ +// EXEMPLO 1: Buscar dados do VBase e injetar na requisição +// ============================================================================ +export async function middlewareExample1(request: NextRequest) { + // Extrair account e workspace + const account = + request.headers.get('x-vtex-account') || + request.nextUrl.searchParams.get('account') || + process.env.VTEX_ACCOUNT || + '' + + const workspace = + request.headers.get('x-vtex-workspace') || + request.nextUrl.searchParams.get('workspace') || + process.env.VTEX_WORKSPACE || + 'master' + + if (!account) { + return NextResponse.next() // Continua sem fazer nada se não houver account + } + + try { + // Criar contexto e client + const context = createVBaseContext(request, { + account, + workspace, + authToken: request.headers.get('x-vtex-credential') || process.env.VTEX_AUTH_TOKEN, + }) + const vbase = createVBaseClient(context) + + // Buscar configurações do VBase + const bucket = 'app-config' + const filePath = 'settings.json' + const config = await vbase.getJSON<{ featureFlags: string[] }>(bucket, filePath, true) + + if (config) { + // Adicionar dados do VBase como headers customizados + const response = NextResponse.next() + response.headers.set('x-app-config', JSON.stringify(config)) + return response + } + + return NextResponse.next() + } catch (error) { + console.error('Erro ao buscar do VBase:', error) + return NextResponse.next() + } +} + +// ============================================================================ +// EXEMPLO 2: Buscar conteúdo específico baseado na rota +// ============================================================================ +export async function middlewareExample2(request: NextRequest) { + const account = request.headers.get('x-vtex-account') || process.env.VTEX_ACCOUNT || '' + const workspace = request.headers.get('x-vtex-workspace') || process.env.VTEX_WORKSPACE || 'master' + + if (!account) { + return NextResponse.next() + } + + try { + const context = createVBaseContext(request, { account, workspace }) + const vbase = createVBaseClient(context) + + const pathname = request.nextUrl.pathname + + // Exemplo: buscar configuração de localização baseada na rota + if (pathname.startsWith('/produto/')) { + const locale = request.headers.get('x-vtex-locale') || 'pt-BR' + const bucket = 'translations' + const translations = await vbase.getJSON>(bucket, `translations-${locale}.json`, true) + + if (translations) { + const response = NextResponse.next() + response.headers.set('x-translations', JSON.stringify(translations)) + return response + } + } + + return NextResponse.next() + } catch (error) { + console.error('Erro no middleware:', error) + return NextResponse.next() + } +} + +// ============================================================================ +// EXEMPLO 3: Cache de dados do VBase com revalidação +// ============================================================================ +const vbaseCache = new Map() +const CACHE_TTL = 60000 // 1 minuto + +export async function middlewareExample3(request: NextRequest) { + const account = request.headers.get('x-vtex-account') || process.env.VTEX_ACCOUNT || '' + const workspace = request.headers.get('x-vtex-workspace') || process.env.VTEX_WORKSPACE || 'master' + + if (!account) { + return NextResponse.next() + } + + try { + const cacheKey = `${account}-${workspace}-config` + const cached = vbaseCache.get(cacheKey) + + // Retornar do cache se ainda válido + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { + const response = NextResponse.next() + response.headers.set('x-cached-config', 'true') + response.headers.set('x-config', JSON.stringify(cached.data)) + return response + } + + // Buscar do VBase + const context = createVBaseContext(request, { account, workspace }) + const vbase = createVBaseClient(context) + + const config = await vbase.getJSON('app-config', 'settings.json', true) + + if (config) { + // Atualizar cache + vbaseCache.set(cacheKey, { data: config, timestamp: Date.now() }) + + const response = NextResponse.next() + response.headers.set('x-config', JSON.stringify(config)) + return response + } + + return NextResponse.next() + } catch (error) { + console.error('Erro no middleware:', error) + return NextResponse.next() + } +} + +// ============================================================================ +// EXEMPLO 4: Buscar e listar arquivos de um bucket +// ============================================================================ +export async function middlewareExample4(request: NextRequest) { + const account = request.headers.get('x-vtex-account') || process.env.VTEX_ACCOUNT || '' + const workspace = request.headers.get('x-vtex-workspace') || process.env.VTEX_WORKSPACE || 'master' + + if (!account) { + return NextResponse.next() + } + + try { + const context = createVBaseContext(request, { account, workspace }) + const vbase = createVBaseClient(context) + + // Listar arquivos de um bucket + const bucket = 'app-assets' + const files = await vbase.listFiles(bucket) + + // Processar arquivos encontrados + if (files?.data) { + const response = NextResponse.next() + response.headers.set('x-bucket-files-count', files.data.length.toString()) + response.headers.set('x-bucket-files', JSON.stringify(files.data.map((f) => f.path))) + return response + } + + return NextResponse.next() + } catch (error) { + console.error('Erro ao listar arquivos:', error) + return NextResponse.next() + } +} + +// ============================================================================ +// EXEMPLO COMPLETO: Implementação prática para usar no middleware.ts +// ============================================================================ +export async function middleware(request: NextRequest) { + // Extrair informações da requisição + const account = + request.headers.get('x-vtex-account') || + request.nextUrl.searchParams.get('account') || + process.env.VTEX_ACCOUNT || + '' + + const workspace = + request.headers.get('x-vtex-workspace') || + request.nextUrl.searchParams.get('workspace') || + process.env.VTEX_WORKSPACE || + 'master' + + // Opção alternativa: extrair do host (ex: myaccount.vtexcommercestable.com.br) + // const host = request.headers.get('host') || '' + // const hostParts = host.split('.') + // const accountFromHost = hostParts.length > 0 ? hostParts[0] : '' + + if (!account) { + // Se não houver account, continua normalmente (não é obrigatório em todos os casos) + return NextResponse.next() + } + + try { + // Criar contexto VTEX + const context = createVBaseContext(request, { + account, + workspace, + authToken: request.headers.get('x-vtex-credential') || process.env.VTEX_AUTH_TOKEN, + }) + + // Criar client VBase + const vbase = createVBaseClient(context) + + // Exemplo prático: buscar configuração da aplicação + const bucket = 'app-config' // Nome do seu bucket + const filePath = 'settings.json' // Caminho do arquivo + + const config = await vbase.getJSON>(bucket, filePath, true) + + // Criar resposta e adicionar headers com os dados do VBase + const response = NextResponse.next() + + if (config) { + response.headers.set('x-vbase-config', JSON.stringify(config)) + + // Exemplo: usar os dados para modificar a requisição + // Você pode adicionar mais lógica aqui baseada nos dados do VBase + } + + return response + } catch (error: any) { + // Log do erro para debug + console.error('Erro no middleware VBase:', { + message: error?.message, + stack: error?.stack, + account, + workspace, + }) + + // Em caso de erro, você pode: + // 1. Retornar erro 500: return NextResponse.json({ error: 'Internal error' }, { status: 500 }) + // 2. Redirecionar: return NextResponse.redirect(new URL('/error', request.url)) + // 3. Continuar normalmente (recomendado para não quebrar o site): + return NextResponse.next() + } +} + +// ============================================================================ +// CONFIGURAÇÃO DO MIDDLEWARE +// ============================================================================ +// Configure quais rotas devem passar pelo middleware +export const config = { + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - api (API routes) + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + */ + '/((?!api|_next/static|_next/image|favicon.ico).*)', + + // Ou seja mais específico: + // '/produto/:path*', + // '/categoria/:path*', + ], +} diff --git a/package.json b/package.json index ef8ec9df6..c44969bb6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vtex/api", - "version": "7.2.5", + "version": "7.2.6", "description": "VTEX I/O API client", "main": "lib/index.js", "typings": "lib/index.d.ts", diff --git a/src/service/worker/runtime/graphql/utils/translations.ts b/src/service/worker/runtime/graphql/utils/translations.ts index 419f02ee1..1a3c5f875 100644 --- a/src/service/worker/runtime/graphql/utils/translations.ts +++ b/src/service/worker/runtime/graphql/utils/translations.ts @@ -4,12 +4,14 @@ import { MessagesLoaderV2 } from '../schema/messagesLoaderV2' export const CONTEXT_REGEX = /\(\(\((?(.)*)\)\)\)/ export const FROM_REGEX = /\<\<\<(?(.)*)\>\>\>/ -export const CONTENT_REGEX = /\(\(\((?(.)*)\)\)\)|\<\<\<(?(.)*)\>\>\>/g +export const STATE_REGEX = /\[\[\[(?(.)*)\]\]\]/ +export const CONTENT_REGEX = /\(\(\((?(.)*)\)\)\)|\<\<\<(?(.)*)\>\>\>|\[\[\[(?(.)*)\]\]\]/g export interface TranslatableMessageV2 { from?: string content: string context?: string + state?: 'original' | 'translated' } export type TranslationDirectiveType = 'translatableV2' | 'translateTo' @@ -17,50 +19,53 @@ export type TranslationDirectiveType = 'translatableV2' | 'translateTo' export const parseTranslatableStringV2 = (rawMessage: string): TranslatableMessageV2 => { const context = rawMessage.match(CONTEXT_REGEX)?.groups?.context const from = rawMessage.match(FROM_REGEX)?.groups?.from + const state = rawMessage.match(STATE_REGEX)?.groups?.state const content = rawMessage.replace(CONTENT_REGEX, '') return { content: content?.trim(), context: context?.trim(), from: from?.trim(), + state: (state?.trim() === 'translated' ? 'translated' : 'original') as 'translated' | 'original', } } -export const formatTranslatableStringV2 = ({ from, content, context }: TranslatableMessageV2): string => - `${content} ${context ? `(((${context})))` : ''} ${from ? `<<<${from}>>>` : ''}` +export const formatTranslatableStringV2 = ({ from, content, context, state }: TranslatableMessageV2): string => + `${content} ${context ? `(((${context})))` : ''} ${from ? `<<<${from}>>>` : ''} ${state ? `[[[${state}]]]` : ''}` -export const handleSingleString = ( - ctx: IOContext, - loader: MessagesLoaderV2, - behavior: Behavior, - directiveName: TranslationDirectiveType -) => async (rawMessage: string | null) => { - // Messages only know how to process non empty strings. - if (rawMessage == null) { - return rawMessage - } +export const handleSingleString = + (ctx: IOContext, loader: MessagesLoaderV2, behavior: Behavior, directiveName: TranslationDirectiveType) => + async (rawMessage: string | null) => { + // Messages only know how to process non empty strings. + if (rawMessage == null) { + return rawMessage + } - const { content, context, from: maybeFrom } = parseTranslatableStringV2(rawMessage) - const { binding, tenant } = ctx + const { content, context, from: maybeFrom, state } = parseTranslatableStringV2(rawMessage) + const { binding, tenant } = ctx - if (content == null) { - throw new Error( - `@${directiveName} directive needs a content to translate, but received ${JSON.stringify(rawMessage)}` - ) - } + if (content == null) { + throw new Error( + `@${directiveName} directive needs a content to translate, but received ${JSON.stringify(rawMessage)}` + ) + } - const from = maybeFrom || binding?.locale || tenant?.locale + if (state === 'translated') { + return content + } - if (from == null) { - throw new Error( - `@${directiveName} directive needs a source language to translate from. You can do this by either setting ${'`ctx.vtex.tenant`'} variable, call this app with the header ${'`x-vtex-tenant`'} or format the string with the ${'`formatTranslatableStringV2`'} function with the ${'`from`'} option set` - ) - } + const from = maybeFrom || binding?.locale || tenant?.locale - return loader.load({ - behavior, - content, - context, - from, - }) -} + if (from == null) { + throw new Error( + `@${directiveName} directive needs a source language to translate from. You can do this by either setting ${'`ctx.vtex.tenant`'} variable, call this app with the header ${'`x-vtex-tenant`'} or format the string with the ${'`formatTranslatableStringV2`'} function with the ${'`from`'} option set` + ) + } + + return loader.load({ + behavior, + content, + context, + from, + }) + }