Webpage translation JSSDK

更新时间:
复制 MD 格式

Description

This page translation plugin, powered by a large translation model, adds one-click multilingual transformation to your website.

How it works

image

Integrate the JSSDK

Step 1: Add the script

<html>
  <head></head>
  <body>
    <!-- Insert the translation script before the closing </body> tag -->
    <script src="https://g.alicdn.com/translate-js-sdk/translate-js-sdk-stable/2.0.6/light.js"></script>
  </body>
</html>

Step 2: Configure the JSSDK

interface ITokenResponseData {
  code: 200;
  data: {
    url: string,
    host: string,
    method: "POST",
    headers: {
      "host": string,
      "x-acs-action": "BatchTranslateForHtml",
      "x-acs-version": string,
      "x-acs-date": string;
      "x-acs-signature-nonce": string;
      "content-type": "application/json",
      "x-acs-content-sha256": string;
      Authorization: string;
    };
    body: string;
  };
}
interface ITokenRequestData {
  sourceLanguage: string;
  targetLanguage: string;
  streaming: false,
  text: {[index: number]: string}
  scene: 'mt-turbo',
  fallbackTimeoutMs: number;
}
interface ISetupConfig {
  getToken: (data: ITokenRequestData) => Promise<ITokenResponseData>;
}
// Initialize only once.
__AliTranslate.setup({
  getToken: async (data) => {
    const res = await fetch('YOUR_GET_TOKEN_URL', { method: 'post', body: JSON.stringify(data) });
    return (await res.json()).data;
  }
});

Step 3: Call page translation

Display translation only

interface IPageTranslate {
  srcLanguage?: string; // The source language. The default value is 'auto'.
  tgtLanguage?: string; // The target language. The default value is 'en'.
  lazyload?: boolean; // Specifies whether to translate only the visible area. The default value is false.
  lazyOffset?: number; // The offset for extending the visible area.
  target?: HTMLElement; // The target area to translate. The default value is body.
  except?: string; // The areas to exclude from translation. Default value is an empty string.
}
const instance = __AliTranslate.pageTranslate({
  lazyload: true,
  lazyOffset: 500
});

Side-by-side translation

interface IParagraphTranslate {
  srcLanguage?: string; // The source language. The default value is 'auto'.
  tgtLanguage?: string; // The target language. The default value is 'en'.
  target?: HTMLElement; // The target area to translate. The default value is body.
  dynamic?: boolean; // Specifies whether to enable dynamic translation. The default value is false.
}
const instance = __AliTranslate.paragraphTranslate({
  srcLanguage: 'zh',
  tgtLanguage: 'en'
});

Step 4: Cancel translation

// Cancels the translation and restores the original page content.
instance.destroy();

PageTranslate parameters

Parameter

Description

Type

Default

srcLanguage

The source language.

String

auto (automatically detected from page content)

tgtLanguage

The target language.

String

en

lazyload

Specifies whether to translate only the visible area.

Boolean

false

lazyOffset

The pixel offset for extending the visible area. This parameter only applies when lazyload is true.

Number

-1 (No offset is applied.)

target

The target area to translate.

HTMLElement | HTMLElement[]

body

except

A CSS selector for areas to exclude from translation.

Selector(String)

'' (empty string)

ParagraphTranslate parameters

Parameter

Description

Type

Default

srcLanguage

The source language.

String

auto (automatically detected from page content)

tgtLanguage

The target language.

String

en

target

The target area to translate.

HTMLElement | HTMLElement[]

body

dynamic

If true, the page is automatically re-translated when its content changes. If false, the page is translated only once.

Boolean

false

lazyload

Specifies whether to translate only the visible area.

Boolean

false

lazyOffset

The pixel offset for extending the visible area. This parameter only applies when lazyload is true.

Number

300

Implement a token service

Sign requests using the V3 request body and signature mechanism of Alibaba Cloud.

// Generate a signature by using the calculateSignature method.
import * as crypto from 'crypto';
export interface SignatureRequest {
  httpMethod: string;
  canonicalUri: string;
  host: string;
  xAcsAction: string;
  xAcsVersion: string;
  headers: Record<string, string>;
  body: any;
  queryParam: Record<string, any>;
}
export interface BatchTranslateRequest {
  scene: string;
  sourceLanguage: string;
  streaming: boolean;
  targetLanguage: string;
  text: { [key: string]: string };
}
export class Request implements SignatureRequest {
  httpMethod: string;
  canonicalUri: string;
  host: string;
  xAcsAction: string;
  xAcsVersion: string;
  headers: Record<string, string>;
  body: any;
  queryParam: Record<string, any>;
  constructor(httpMethod: string, canonicalUri: string, host: string, xAcsAction: string, xAcsVersion: string) {
    this.httpMethod = httpMethod;
    this.canonicalUri = canonicalUri || '/';
    this.host = host;
    this.xAcsAction = xAcsAction;
    this.xAcsVersion = xAcsVersion;
    this.headers = {};
    this.body = null;
    this.queryParam = {};
    this.initHeader();
  }
  private initHeader() {
    const date = new Date();
    this.headers = {
      'host': this.host,
      'x-acs-action': this.xAcsAction,
      'x-acs-version': this.xAcsVersion,
      'x-acs-date': date.toISOString().replace(/\..+/, 'Z'),
      'x-acs-signature-nonce': crypto.randomBytes(16).toString('hex')
    }
  }
}
const ALGORITHM = 'ACS3-HMAC-SHA256';
const accessKeyId = process.env.ACCESS_KEY_ID; // Your Access Key ID.
const accessKeySecret = process.env.ACCESS_KEY_SECRET; // Your Access Key Secret.
const workspaceId = process.env.WORKSPACE_ID; // Your workspace ID from Model Studio.
const encoder = new TextEncoder();
const httpMethod = 'POST';
const canonicalUri = '/anytrans/translate/batchForHtml';
const xAcsAction = 'BatchTranslateForHtml';
const xAcsVersion = '2025-07-07';
const host = 'anytrans.cn-beijing.aliyuncs.com';
export interface SignedRequest {
  url: string;
  method: string;
  host: string;
  headers: Record<string, string>;
  body?: any; // Can be a Uint8Array or a Base64 string.
}
export function getAuthorization(signRequest: SignatureRequest): SignedRequest {
  try {
    const newQueryParam: Record<string, any> = {};
    processObject(newQueryParam, "", signRequest.queryParam);
    signRequest.queryParam = newQueryParam;
    // Step 1: Build the canonical request string.
    const canonicalQueryString = Object.entries(signRequest.queryParam)
      .sort(([a], [b]) => a.localeCompare(b))
      .map(([key, value]) => `${percentCode(key)}=${percentCode(value)}`)
      .join('&');
    // The request payload. For requests with an empty body, such as GET requests, the RequestPayload is an empty string.
    const requestPayload = signRequest.body || encoder.encode('');
    const hashedRequestPayload = sha256Hex(requestPayload);
    signRequest.headers['x-acs-content-sha256'] = hashedRequestPayload;
    // Convert all keys to lowercase.
    signRequest.headers = Object.fromEntries(
      Object.entries(signRequest.headers).map(([key, value]) => [key.toLowerCase(), value])
    );
    const sortedKeys = Object.keys(signRequest.headers)
      .filter(key => key.startsWith('x-acs-') || key === 'host' || key === 'content-type')
      .sort();
    // A list of signed headers. The header names are in lowercase, sorted alphabetically, and separated by semicolons (;).
    const signedHeaders = sortedKeys.join(";")
    // Construct the canonical headers. The canonical headers are concatenated in ascending order of their lowercase header names.
    const canonicalHeaders = sortedKeys.map(key => `${key}:${signRequest.headers[key]}`).join('\n') + '\n';
    const canonicalRequest = [
      signRequest.httpMethod,
      signRequest.canonicalUri,
      canonicalQueryString,
      canonicalHeaders,
      signedHeaders,
      hashedRequestPayload
    ].join('\n');
    console.log('canonicalRequest=========>\n', canonicalRequest);
    // Step 2: Build the string to sign.
    const hashedCanonicalRequest = sha256Hex(encoder.encode(canonicalRequest));
    const stringToSign = `${ALGORITHM}\n${hashedCanonicalRequest}`;
    console.log('stringToSign=========>', stringToSign);
    // Step 3: Calculate the signature.
    const signature = hmac256(accessKeySecret!, stringToSign);
    console.log('signature=========>', signature);
    // Step 4: Build the Authorization header.
    const authorization = `${ALGORITHM} Credential=${accessKeyId},SignedHeaders=${signedHeaders},Signature=${signature}`;
    console.log('authorization=========>', authorization);
    signRequest.headers['Authorization'] = authorization;
    // Build the complete URL.
    let url = `https://${signRequest.host}${signRequest.canonicalUri}`;
    if (signRequest.queryParam && Object.keys(signRequest.queryParam).length > 0) {
      const query = new URLSearchParams(signRequest.queryParam);
      url += '?' + query.toString();
    }
    return {
      url,
      host: signRequest.host,
      method: signRequest.httpMethod.toUpperCase(),
      headers: signRequest.headers,
      body: signRequest.body
    };
  } catch (error) {
    console.error('Failed to get authorization');
    console.error(error);
    throw error;
  }
}
function percentCode(str: string): string {
  return encodeURIComponent(str)
    .replace(/\+/g, '%20')
    .replace(/\*/g, '%2A')
    .replace(/~/g, '%7E');
}
function hmac256(key: string, data: string): string {
  const hmac = crypto.createHmac('sha256', key);
  hmac.update(data, 'utf8');
  return hmac.digest('hex').toLowerCase();
}
function sha256Hex(bytes: any): string {
  const hash = crypto.createHash('sha256');
  const digest = hash.update(bytes).digest('hex');
  return digest.toLowerCase();
}
function processObject(map: Record<string, any>, key: string, value: any): void {
  // If the value is null, no further processing is required.
  if (value === null) {
    return;
  }
  if (key === null) {
    key = "";
  }
  // If the value is an array, iterate over each element and process it recursively.
  if (Array.isArray(value)) {
    value.forEach((item, index) => {
      processObject(map, `${key}.${index + 1}`, item);
    });
  } else if (typeof value === 'object' && value !== null) {
    // If the value is an object, iterate over each key-value pair and process it recursively.
    Object.entries(value).forEach(([subKey, subValue]) => {
      processObject(map, `${key}.${subKey}`, subValue);
    });
  } else {
    // For keys that start with a period (.), remove the leading period to maintain key continuity.
    if (key.startsWith('.')) {
      key = key.slice(1);
    }
    map[key] = String(value);
  }
}
export function formDataToString(formData: Record<string, any>): string {
  const tmp: Record<string, any> = {};
  processObject(tmp, "", formData);
  let queryString = '';
  for (let [key, value] of Object.entries(tmp)) {
    if (queryString !== '') {
      queryString += '&';
    }
    queryString += encodeURIComponent(key) + '=' + encodeURIComponent(value);
  }
  return queryString;
}
// Method for calculating signatures with fixed variables.
export interface FixedSignatureResult {
  httpMethod: string;
  canonicalUri: string;
  host: string;
  xAcsAction: string;
  xAcsVersion: string;
  signature: string;
  date: string;
  nonce: string;
  workspaceId: string;
  authorization: string;
}
export function calculateSignature(req: BatchTranslateRequest): SignedRequest {
  // Create a request instance.
  const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
  // Set query parameters.
  signRequest.queryParam = {
    RegionId: 'cn-beijing',
  };
  // Create a Uint8Array for the body.
  const bodyUint8Array = encoder.encode(JSON.stringify({
    ...req,
    workspaceId,
  }));
  // The request body used for signature calculation.
  signRequest.body = bodyUint8Array;
  signRequest.headers['content-type'] = 'application/json';
  // Get the signature.
  const result = getAuthorization(signRequest);
  // Convert the body to a Base64 string for transmission.
  const bodyBase64 = Buffer.from(bodyUint8Array).toString('base64');
  return {
    ...result,
    body: bodyBase64 // Return a Base64 string instead of a Uint8Array.
  };
} 
// Generate a signature by using the calculateSignature method.
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.io.UnsupportedEncodingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
/**
 * Signature request interface.
 */
interface SignatureRequest {
    String getHttpMethod();
    String getCanonicalUri();
    String getHost();
    String getXAcsAction();
    String getXAcsVersion();
    Map<String, String> getHeaders();
    byte[] getBody();
    Map<String, Object> getQueryParam();
    void setHeaders(Map<String, String> headers);
    void setBody(byte[] body);
    void setQueryParam(Map<String, Object> queryParam);
}
/**
 * Data class for batch translation requests.
 */
class BatchTranslateRequest {
    private String scene;
    private String sourceLanguage;
    private boolean streaming;
    private String targetLanguage;
    private Map<String, String> text;
    // Constructor
    public BatchTranslateRequest() {}
    public BatchTranslateRequest(String scene, 
                               String sourceLanguage, boolean streaming, 
                               String targetLanguage, Map<String, String> text) {
        this.scene = scene;
        this.sourceLanguage = sourceLanguage;
        this.streaming = streaming;
        this.targetLanguage = targetLanguage;
        this.text = text;
    }
    // Getters and Setters
    public String getScene() { return scene; }
    public void setScene(String scene) { this.scene = scene; }
    public String getSourceLanguage() { return sourceLanguage; }
    public void setSourceLanguage(String sourceLanguage) { this.sourceLanguage = sourceLanguage; }
    public boolean isStreaming() { return streaming; }
    public void setStreaming(boolean streaming) { this.streaming = streaming; }
    public String getTargetLanguage() { return targetLanguage; }
    public void setTargetLanguage(String targetLanguage) { this.targetLanguage = targetLanguage; }
    public Map<String, String> getText() { return text; }
    public void setText(Map<String, String> text) { this.text = text; }
}
/**
 * Request implementation class.
 */
class Request implements SignatureRequest {
    private String httpMethod;
    private String canonicalUri;
    private String host;
    private String xAcsAction;
    private String xAcsVersion;
    private Map<String, String> headers;
    private byte[] body;
    private Map<String, Object> queryParam;
    public Request(String httpMethod, String canonicalUri, String host, 
                  String xAcsAction, String xAcsVersion) {
        this.httpMethod = httpMethod;
        this.canonicalUri = canonicalUri != null ? canonicalUri : "/";
        this.host = host;
        this.xAcsAction = xAcsAction;
        this.xAcsVersion = xAcsVersion;
        this.headers = new HashMap<>();
        this.body = null;
        this.queryParam = new HashMap<>();
        initHeader();
    }
    private void initHeader() {
        Instant now = Instant.now();
        String date = DateTimeFormatter.ISO_INSTANT.format(now);
        String nonce = generateRandomHex(16);
        this.headers.put("host", this.host);
        this.headers.put("x-acs-action", this.xAcsAction);
        this.headers.put("x-acs-version", this.xAcsVersion);
        this.headers.put("x-acs-date", date);
        this.headers.put("x-acs-signature-nonce", nonce);
    }
    private String generateRandomHex(int length) {
        SecureRandom random = new SecureRandom();
        byte[] bytes = new byte[length];
        random.nextBytes(bytes);
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
    // Getters
    @Override
    public String getHttpMethod() { return httpMethod; }
    @Override
    public String getCanonicalUri() { return canonicalUri; }
    @Override
    public String getHost() { return host; }
    @Override
    public String getXAcsAction() { return xAcsAction; }
    @Override
    public String getXAcsVersion() { return xAcsVersion; }
    @Override
    public Map<String, String> getHeaders() { return headers; }
    @Override
    public byte[] getBody() { return body; }
    @Override
    public Map<String, Object> getQueryParam() { return queryParam; }
    // Setters
    @Override
    public void setHeaders(Map<String, String> headers) { this.headers = headers; }
    @Override
    public void setBody(byte[] body) { this.body = body; }
    @Override
    public void setQueryParam(Map<String, Object> queryParam) { this.queryParam = queryParam; }
}
/**
 * Signed request result.
 */
class SignedRequest {
    private String url;
    private String method;
    private String host;
    private Map<String, String> headers;
    private String body; // Base64 string.
    public SignedRequest(String url, String method, String host, 
                        Map<String, String> headers, String body) {
        this.url = url;
        this.method = method;
        this.host = host;
        this.headers = headers;
        this.body = body;
    }
    // Getters
    public String getUrl() { return url; }
    public String getMethod() { return method; }
    public String getHost() { return host; }
    public Map<String, String> getHeaders() { return headers; }
    public String getBody() { return body; }
}
/**
 * Interface for fixed signature results.
 */
interface FixedSignatureResult {
    String getHttpMethod();
    String getCanonicalUri();
    String getHost();
    String getXAcsAction();
    String getXAcsVersion();
    String getSignature();
    String getDate();
    String getNonce();
    String getWorkspaceId();
    String getAuthorization();
}
/**
 * Main class for signature calculation.
 */
public class NodeSignature {
    private static final String ALGORITHM = "ACS3-HMAC-SHA256";
    private static final String ACCESS_KEY_ID = System.getenv("ACCESS_KEY_ID"); // Your Access Key ID.
    private static final String ACCESS_KEY_SECRET = System.getenv("ACCESS_KEY_SECRET"); // Your Access Key Secret.
    private static final String WORKSPACE_ID = System.getenv("WORKSPACE_ID"); // Your workspace ID from Model Studio.
    private static final String HTTP_METHOD = "POST";
    private static final String CANONICAL_URI = "/anytrans/translate/batchForHtml";
    private static final String X_ACS_ACTION = "BatchTranslateForHtml";
    private static final String X_ACS_VERSION = "2025-07-07";
    private static final String HOST = "anytrans.cn-beijing.aliyuncs.com";
    private static final ObjectMapper objectMapper = new ObjectMapper();
    /**
     * Get authorization information.
     */
    public static SignedRequest getAuthorization(SignatureRequest signRequest) {
        try {
            Map<String, Object> newQueryParam = new HashMap<>();
            processObject(newQueryParam, "", signRequest.getQueryParam());
            signRequest.setQueryParam(newQueryParam);
            // Step 1: Build the canonical request string.
            String canonicalQueryString = signRequest.getQueryParam().entrySet().stream()
                .sorted(Map.Entry.comparingByKey())
                .map(entry -> percentCode(entry.getKey()) + "=" + percentCode(String.valueOf(entry.getValue())))
                .collect(Collectors.joining("&"));
            // The request payload. For requests with an empty body, such as GET requests, the RequestPayload is an empty string.
            byte[] requestPayload = signRequest.getBody() != null ? signRequest.getBody() : new byte[0];
            String hashedRequestPayload = sha256Hex(requestPayload);
            signRequest.getHeaders().put("x-acs-content-sha256", hashedRequestPayload);
            // Convert all keys to lowercase.
            Map<String, String> lowerCaseHeaders = signRequest.getHeaders().entrySet().stream()
                .collect(Collectors.toMap(
                    entry -> entry.getKey().toLowerCase(),
                    Map.Entry::getValue
                ));
            signRequest.setHeaders(lowerCaseHeaders);
            List<String> sortedKeys = lowerCaseHeaders.keySet().stream()
                .filter(key -> key.startsWith("x-acs-") || key.equals("host") || key.equals("content-type"))
                .sorted()
                .collect(Collectors.toList());
            // A list of signed headers. The header names are in lowercase, sorted alphabetically, and separated by semicolons (;).
            String signedHeaders = String.join(";", sortedKeys);
            // Construct the canonical headers. The canonical headers are concatenated in ascending order of their lowercase header names.
            String canonicalHeaders = sortedKeys.stream()
                .map(key -> key + ":" + lowerCaseHeaders.get(key))
                .collect(Collectors.joining("\n")) + "\n";
            String canonicalRequest = String.join("\n", Arrays.asList(
                signRequest.getHttpMethod(),
                signRequest.getCanonicalUri(),
                canonicalQueryString,
                canonicalHeaders,
                signedHeaders,
                hashedRequestPayload
            ));
            System.out.println("canonicalRequest=========>\n" + canonicalRequest);
            // Step 2: Build the string to sign.
            String hashedCanonicalRequest = sha256Hex(canonicalRequest.getBytes(StandardCharsets.UTF_8));
            String stringToSign = ALGORITHM + "\n" + hashedCanonicalRequest;
            System.out.println("stringToSign=========>" + stringToSign);
            // Step 3: Calculate the signature.
            String signature = hmac256(ACCESS_KEY_SECRET, stringToSign);
            System.out.println("signature=========>" + signature);
            // Step 4: Build the Authorization header.
            String authorization = String.format("%s Credential=%s,SignedHeaders=%s,Signature=%s",
                ALGORITHM, ACCESS_KEY_ID, signedHeaders, signature);
            System.out.println("authorization=========>" + authorization);
            lowerCaseHeaders.put("authorization", authorization);
            // Build the complete URL.
            String url = "https://" + signRequest.getHost() + signRequest.getCanonicalUri();
            if (signRequest.getQueryParam() != null && !signRequest.getQueryParam().isEmpty()) {
                String query = signRequest.getQueryParam().entrySet().stream()
                    .map(entry -> {
                        try {
                            return URLEncoder.encode(entry.getKey(), "UTF-8") + "=" + 
                                   URLEncoder.encode(String.valueOf(entry.getValue()), "UTF-8");
                        } catch (UnsupportedEncodingException e) {
                            throw new RuntimeException(e);
                        }
                    })
                    .collect(Collectors.joining("&"));
                url += "?" + query;
            }
            String bodyString = signRequest.getBody() != null ? 
                Base64.getEncoder().encodeToString(signRequest.getBody()) : null;
            return new SignedRequest(url, signRequest.getHttpMethod().toUpperCase(), 
                                   signRequest.getHost(), lowerCaseHeaders, bodyString);
        } catch (Exception error) {
            System.err.println("Failed to get authorization");
            error.printStackTrace();
            throw new RuntimeException(error);
        }
    }
    /**
     * URL encoding.
     */
    private static String percentCode(String str) {
        try {
            return URLEncoder.encode(str, "UTF-8")
                .replace("+", "%20")
                .replace("*", "%2A")
                .replace("~", "%7E");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * HMAC-SHA256 encryption.
     */
    private static String hmac256(String key, String data) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            mac.init(secretKeySpec);
            byte[] hmacBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
            return bytesToHex(hmacBytes).toLowerCase();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * SHA-256 hash.
     */
    private static String sha256Hex(byte[] bytes) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hashBytes = digest.digest(bytes);
            return bytesToHex(hashBytes).toLowerCase();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * Converts a byte array to a hexadecimal string.
     */
    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
    /**
     * Processes an object to flatten nested objects.
     */
    private static void processObject(Map<String, Object> map, String key, Object value) {
        // If the value is null, no further processing is required.
        if (value == null) {
            return;
        }
        if (key == null) {
            key = "";
        }
        // If the value is a list, iterate over each element and process it recursively.
        if (value instanceof List) {
            List<?> list = (List<?>) value;
            for (int i = 0; i < list.size(); i++) {
                processObject(map, key + "." + (i + 1), list.get(i));
            }
        } else if (value instanceof Object[] || value.getClass().isArray()) {
            Object[] array = (Object[]) value;
            for (int i = 0; i < array.length; i++) {
                processObject(map, key + "." + (i + 1), array[i]);
            }
        } else if (value instanceof Map) {
            // If the value is an object, iterate over each key-value pair and process it recursively.
            Map<?, ?> objectMap = (Map<?, ?>) value;
            for (Map.Entry<?, ?> entry : objectMap.entrySet()) {
                processObject(map, key + "." + entry.getKey(), entry.getValue());
            }
        } else {
            // For keys that start with a period (.), remove the leading period to maintain key continuity.
            if (key.startsWith(".")) {
                key = key.substring(1);
            }
            map.put(key, String.valueOf(value));
        }
    }
    /**
     * Converts form data to a string.
     */
    public static String formDataToString(Map<String, Object> formData) {
        Map<String, Object> tmp = new HashMap<>();
        processObject(tmp, "", formData);
        return tmp.entrySet().stream()
            .map(entry -> {
                try {
                    return URLEncoder.encode(entry.getKey(), "UTF-8") + "=" + 
                           URLEncoder.encode(String.valueOf(entry.getValue()), "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    throw new RuntimeException(e);
                }
            })
            .collect(Collectors.joining("&"));
    }
    /**
     * Method for calculating signatures with fixed variables.
     */
    public static SignedRequest calculateSignature(BatchTranslateRequest req) {
        try {
            // Create a request instance.
            Request signRequest = new Request(HTTP_METHOD, CANONICAL_URI, HOST, X_ACS_ACTION, X_ACS_VERSION);
            // Set query parameters.
            Map<String, Object> queryParam = new HashMap<>();
            queryParam.put("RegionId", "cn-beijing");
            signRequest.setQueryParam(queryParam);
            // Create request body data.
            Map<String, Object> bodyData = new HashMap<>();
            bodyData.put("scene", req.getScene());
            bodyData.put("sourceLanguage", req.getSourceLanguage());
            bodyData.put("streaming", req.isStreaming());
            bodyData.put("targetLanguage", req.getTargetLanguage());
            bodyData.put("text", req.getText());
            bodyData.put("workspaceId", WORKSPACE_ID);
            // Create a byte array for the body.
            String jsonString = objectMapper.writeValueAsString(bodyData);
            byte[] bodyBytes = jsonString.getBytes(StandardCharsets.UTF_8);
            // The request body used for signature calculation.
            signRequest.setBody(bodyBytes);
            signRequest.getHeaders().put("content-type", "application/json");
            // Get the signature.
            SignedRequest result = getAuthorization(signRequest);
            return result;
        } catch (JsonProcessingException e) {
            throw new RuntimeException("JSON serialization failed", e);
        }
    }
}

FAQ

1. How to obtain an AccessKey pair?

Obtain an AccessKey pair from the AccessKey Management section of the Alibaba Cloud console.

2. How to find the workspace ID?

Log in to the Alibaba Cloud account that is associated with your AccessKey pair. In the Model Studio console, find the workspace details in the lower-left corner.

The Workspace ID in the pop-up window is the required workspaceId. Click the copy icon to the right of the field to copy the ID.

3. API permission error

This error indicates that Tongyi Multimodal Translation is not activated. To resolve this, use your Alibaba Cloud account to activate the service in the Tongyi Multimodal Translation console.

Change log

March 2026

Version

Release date

Description

2.0.6

March 5, 2026

Added the lazyload and lazyOffset parameters for side-by-side translation.