IntlPull
Technical
11 min read

Translation QA Checks: Automated Quality Assurance for Localization

Complete guide to translation quality assurance. Learn automated QA checks, validation types, quality scoring, CI integration, and building comprehensive QA pipelines.

IntlPull Team
IntlPull Team
Feb 12, 2026
On this page
Summary

Complete guide to translation quality assurance. Learn automated QA checks, validation types, quality scoring, CI integration, and building comprehensive QA pipelines.

Why Translation Quality Assurance Matters

Translation quality directly impacts user experience, brand perception, and product usability across markets. Poor translations create confusion, erode trust, and can render products unusable in target languages. A single mistranslated error message can prevent users from completing critical workflows. Inconsistent terminology across product, documentation, and support creates cognitive friction. Cultural insensitivity or inappropriate tone damages brand reputation. Technical errors like broken placeholders cause runtime crashes or display corrupted data.

Manual review alone cannot catch all quality issues at scale. When translating thousands of keys across dozens of languages, human reviewers miss formatting errors, placeholder mismatches, and consistency violations. Automated quality assurance checks act as a first line of defense, catching common issues before human review, allowing reviewers to focus on linguistic quality rather than technical correctness. This layered approach—automated QA followed by human review—delivers both speed and quality.

Types of QA Checks

Format Validation

JSON/YAML/XML Syntax: Verify translation files are well-formed before deployment to prevent runtime parsing errors.

JavaScript
1function validateJSONSyntax(filePath) {
2  try {
3    const content = fs.readFileSync(filePath, 'utf8');
4    JSON.parse(content);
5    return { valid: true };
6  } catch (error) {
7    return {
8      valid: false,
9      error: `JSON syntax error at line ${error.lineNumber}: ${error.message}`
10    };
11  }
12}

Character Encoding: Ensure files use UTF-8 encoding to prevent character corruption (é → é).

JavaScript
1function validateEncoding(filePath) {
2  const buffer = fs.readFileSync(filePath);
3  const detected = chardet.detect(buffer);
4
5  if (detected !== 'UTF-8') {
6    return {
7      valid: false,
8      error: `File uses ${detected} encoding, expected UTF-8`
9    };
10  }
11
12  return { valid: true };
13}

HTML Entity Validation: Check that HTML entities in source are preserved in translation.

JavaScript
1function validateHTMLEntities(source, target) {
2  const sourceEntities = source.match(/&[a-zA-Z]+;/g) || [];
3  const targetEntities = target.match(/&[a-zA-Z]+;/g) || [];
4
5  if (sourceEntities.length !== targetEntities.length) {
6    return {
7      valid: false,
8      error: `HTML entity mismatch. Source: ${sourceEntities.join(', ')}, Target: ${targetEntities.join(', ')}`
9    };
10  }
11
12  return { valid: true };
13}

Placeholder Matching

Variable Consistency: Ensure all placeholder variables in source appear in translation with correct formatting.

JavaScript
1function extractPlaceholders(text, format = 'icu') {
2  const patterns = {
3    icu: /\{(\w+)\}/g,              // {variable}
4    i18next: /\{\{(\w+)\}\}/g,    // {{variable}}
5    printf: /%s|%d|%f|%[0-9]+\$[sdf]/g, // %s, %1$s
6    react: /\{(\w+)\}/g             // {variable}
7  };
8
9  const matches = [];
10  const regex = patterns[format];
11  let match;
12
13  while ((match = regex.exec(text)) !== null) {
14    matches.push(match[1] || match[0]);
15  }
16
17  return matches.sort();
18}
19
20function validatePlaceholders(source, target, format = 'icu') {
21  const sourcePlaceholders = extractPlaceholders(source, format);
22  const targetPlaceholders = extractPlaceholders(target, format);
23
24  const sourceSet = new Set(sourcePlaceholders);
25  const targetSet = new Set(targetPlaceholders);
26
27  const missing = sourcePlaceholders.filter(p => !targetSet.has(p));
28  const extra = targetPlaceholders.filter(p => !sourceSet.has(p));
29
30  if (missing.length > 0 || extra.length > 0) {
31    return {
32      valid: false,
33      error: `Placeholder mismatch.${missing.length ? ` Missing: ${missing.join(', ')}.` : ''}${extra.length ? ` Extra: ${extra.join(', ')}.` : ''}`
34    };
35  }
36
37  return { valid: true };
38}

Example:

Source (EN): "Welcome {name}, you have {count} messages"
Translation (ES): "Bienvenido {name}, tienes {count} mensajes" ✓ Valid

Translation (ES - ERROR): "Bienvenido {nombre}, tienes {count} mensajes" ✗ Invalid
Error: Missing placeholder: {name}. Extra placeholder: {nombre}

Placeholder Order Flexibility: Some languages require different placeholder order than source language. Validate presence, not position.

Source (EN): "{firstName} {lastName}"
Translation (JP): "{lastName} {firstName}" ✓ Valid (Japanese surname-first convention)

Length Limits

UI Constraint Validation: Ensure translations fit within UI space constraints to prevent truncation or layout breaking.

JavaScript
1function validateLength(key, translation, limits) {
2  const limit = limits[key];
3
4  if (!limit) return { valid: true };
5
6  const length = translation.length;
7
8  if (length > limit.max) {
9    return {
10      valid: false,
11      severity: 'error',
12      error: `Translation exceeds max length. Current: ${length}, Max: ${limit.max}`
13    };
14  }
15
16  if (length > limit.warning) {
17    return {
18      valid: true,
19      severity: 'warning',
20      error: `Translation approaching max length. Current: ${length}, Warning: ${limit.warning}, Max: ${limit.max}`
21    };
22  }
23
24  return { valid: true };
25}
26
27// Length limits configuration
28const lengthLimits = {
29  'button.submit': { warning: 15, max: 20 },
30  'nav.dashboard': { warning: 12, max: 15 },
31  'modal.title': { warning: 40, max: 50 }
32};

Character Count vs Visual Width: For languages with wide characters (Chinese, Japanese), consider visual width in addition to character count.

JavaScript
1function visualWidth(text) {
2  // Approximate: CJK characters = 2 units, others = 1 unit
3  let width = 0;
4  for (const char of text) {
5    const code = char.charCodeAt(0);
6    // CJK Unicode ranges (simplified)
7    if ((code >= 0x4E00 && code <= 0x9FFF) || // Chinese
8        (code >= 0x3040 && code <= 0x309F) || // Hiragana
9        (code >= 0x30A0 && code <= 0x30FF)) { // Katakana
10      width += 2;
11    } else {
12      width += 1;
13    }
14  }
15  return width;
16}

Glossary Compliance

Approved Term Enforcement: Validate that translations use approved glossary terms, not prohibited alternatives.

JavaScript
1function validateGlossary(source, target, language, glossary) {
2  const violations = [];
3
4  glossary.forEach(entry => {
5    const sourceRegex = new RegExp(`\\b${entry.term}\\b`, 'gi');
6
7    // Check if source contains glossary term
8    if (sourceRegex.test(source)) {
9      const approvedTerm = entry.translations[language]?.approved;
10      const prohibitedTerms = entry.translations[language]?.prohibited || [];
11
12      if (!approvedTerm) return; // No glossary entry for this language
13
14      // Check if approved term is used
15      const approvedRegex = new RegExp(`\\b${approvedTerm}\\b`, 'gi');
16      if (!approvedRegex.test(target)) {
17        violations.push({
18          term: entry.term,
19          severity: 'error',
20          message: `Must use approved term "${approvedTerm}" for "${entry.term}"`
21        });
22      }
23
24      // Check if prohibited terms are used
25      prohibitedTerms.forEach(prohibited => {
26        const prohibitedRegex = new RegExp(`\\b${prohibited}\\b`, 'gi');
27        if (prohibitedRegex.test(target)) {
28          violations.push({
29            term: entry.term,
30            severity: 'error',
31            message: `Prohibited term "${prohibited}" used. Use "${approvedTerm}" instead.`
32          });
33        }
34      });
35    }
36  });
37
38  return violations.length > 0
39    ? { valid: false, violations }
40    : { valid: true };
41}
42
43// Example glossary
44const glossary = [
45  {
46    term: 'dashboard',
47    translations: {
48      es: { approved: 'panel de control', prohibited: ['tablero', 'escritorio'] },
49      fr: { approved: 'tableau de bord', prohibited: ['panneau de contrôle'] }
50    }
51  }
52];

Consistency Checks

Cross-Key Consistency: Ensure identical source text is translated consistently across all keys.

JavaScript
1function findInconsistencies(translations, language) {
2  const translationMap = new Map();
3  const inconsistencies = [];
4
5  // Build map of source → translations
6  translations.forEach(({ key, source, target }) => {
7    if (!translationMap.has(source)) {
8      translationMap.set(source, []);
9    }
10    translationMap.get(source).push({ key, target });
11  });
12
13  // Find inconsistencies
14  translationMap.forEach((targets, source) => {
15    const uniqueTranslations = new Set(targets.map(t => t.target));
16
17    if (uniqueTranslations.size > 1) {
18      inconsistencies.push({
19        source,
20        translations: targets,
21        severity: 'warning',
22        message: `Inconsistent translations for "${source}": ${Array.from(uniqueTranslations).join(' vs ')}`
23      });
24    }
25  });
26
27  return inconsistencies;
28}

Example:

Source: "Save changes"
Key: "profile.save" → Translation: "Guardar cambios" ✓
Key: "settings.save" → Translation: "Guardar cambios" ✓ Consistent

Key: "account.save" → Translation: "Guardar" ✗ Inconsistent
Warning: "Save changes" translated as "Guardar cambios" in 2 keys but "Guardar" in account.save

Style and Grammar

Capitalization: Verify capitalization style matches language conventions and project guidelines.

JavaScript
1function validateCapitalization(translation, rules) {
2  if (rules.style === 'sentence') {
3    // First letter capitalized, rest lowercase (except proper nouns)
4    if (!/^[A-Z][^A-Z]*/.test(translation)) {
5      return { valid: false, error: 'Should use sentence case' };
6    }
7  } else if (rules.style === 'title') {
8    // Title Case for English, may differ for other languages
9    // (Simplified check)
10    const words = translation.split(' ');
11    const capitalized = words.filter(w => /^[A-Z]/.test(w));
12    if (capitalized.length < words.length * 0.5) {
13      return { valid: false, error: 'Should use title case' };
14    }
15  }
16
17  return { valid: true };
18}

Punctuation: Ensure appropriate punctuation for language and context.

JavaScript
1function validatePunctuation(source, target, language) {
2  const issues = [];
3
4  // Check if source ends with punctuation
5  const sourceEndsWithPunctuation = /[.!?]$/.test(source);
6  const targetEndsWithPunctuation = /[.!?。!?]$/.test(target);
7
8  if (sourceEndsWithPunctuation && !targetEndsWithPunctuation) {
9    issues.push({ severity: 'warning', message: 'Source has ending punctuation, target does not' });
10  }
11
12  // Language-specific punctuation rules
13  if (language === 'fr') {
14    // French uses space before ?!:;
15    if (/[?!:;]/.test(target) && !/\s[?!:;]/.test(target)) {
16      issues.push({ severity: 'warning', message: 'French requires space before ?!:;' });
17    }
18  }
19
20  return issues.length > 0 ? { valid: false, issues } : { valid: true };
21}

Spelling: Integrate spell-checking for target languages.

JavaScript
1const nspell = require('nspell');
2const dictionaryEs = require('dictionary-es');
3
4async function spellCheck(text, language) {
5  const dictionary = await loadDictionary(language);
6  const spell = nspell(dictionary);
7
8  const words = text.match(/\b[\w']+\b/g) || [];
9  const misspelled = words.filter(word => !spell.correct(word));
10
11  if (misspelled.length > 0) {
12    return {
13      valid: false,
14      severity: 'warning',
15      message: `Possible spelling errors: ${misspelled.join(', ')}`
16    };
17  }
18
19  return { valid: true };
20}

Context and Meaning

Untranslated Text Detection: Flag translations that contain source language text (possible oversight).

JavaScript
1function detectUntranslatedText(source, target, sourceLang, targetLang) {
2  // Simple heuristic: if target contains significant source text
3  const sourceWords = new Set(source.toLowerCase().split(/\s+/));
4  const targetWords = target.toLowerCase().split(/\s+/);
5
6  const sourceWordsInTarget = targetWords.filter(word =>
7    sourceWords.has(word) && word.length > 3 // Ignore short words (to, in, etc.)
8  );
9
10  if (sourceWordsInTarget.length > sourceWords.size * 0.5) {
11    return {
12      valid: false,
13      severity: 'warning',
14      message: `Target may contain untranslated text: ${sourceWordsInTarget.join(', ')}`
15    };
16  }
17
18  return { valid: true };
19}

Over-Translation Detection: Warn if translation is significantly longer than source (may indicate over-explanation).

JavaScript
1function detectOverTranslation(source, target) {
2  const sourceLength = source.length;
3  const targetLength = target.length;
4
5  // Allow 50% length increase (languages naturally differ)
6  const threshold = sourceLength * 1.5;
7
8  if (targetLength > threshold) {
9    return {
10      valid: true,
11      severity: 'info',
12      message: `Translation is ${Math.round((targetLength / sourceLength - 1) * 100)}% longer than source. Verify accuracy.`
13    };
14  }
15
16  return { valid: true };
17}

Automated vs Manual QA

Automated QA (First Line of Defense)

What to Automate:

  • Format validation (syntax, encoding)
  • Placeholder matching
  • Length limit checks
  • Glossary compliance
  • Consistency across keys
  • Basic punctuation and capitalization

Strengths:

  • Instant feedback during translation
  • 100% coverage of all translations
  • Objective, deterministic checks
  • Scales to millions of keys
  • Prevents technical errors from reaching production

Limitations:

  • Cannot assess linguistic quality (naturalness, tone, cultural appropriateness)
  • Misses context-dependent issues
  • May generate false positives requiring human judgment

Manual QA (Quality Refinement)

What Requires Human Review:

  • Linguistic accuracy and fluency
  • Cultural appropriateness
  • Brand voice alignment
  • Context-specific correctness
  • Idiomatic expressions
  • Subtle meaning nuances

Strengths:

  • Catches issues automated checks miss
  • Ensures natural, culturally appropriate translations
  • Validates tone and brand voice
  • Provides qualitative feedback to improve future translations

Limitations:

  • Time-intensive and expensive
  • Coverage limited by reviewer capacity
  • Subjective (different reviewers may disagree)
  • Doesn't scale to all translations

Tier 1 - Automated QA: Run immediately when translator submits. Blocks submission if critical errors found.

Tier 2 - Senior Translator Review: Human linguist reviews translations that passed automated checks. Focuses on linguistic quality.

Tier 3 - Native Reviewer: For high-visibility content (marketing, legal), native speaker in target market reviews for cultural appropriateness.

Translation Submission
  ↓
Automated QA (Tier 1)
  ↓ [Pass]
Senior Translator Review (Tier 2)
  ↓ [Approve]
Deploy to Staging
  ↓
Native Reviewer Spot Check (Tier 3) [for critical content]
  ↓ [Approve]
Deploy to Production

QA Tools Comparison

Open-Source QA Tools

Checkmate (Standalone CLI)

  • Format validation, placeholder checks
  • Supports JSON, PO, XLIFF, YAML
  • Configurable rules via YAML
  • CI/CD integration via exit codes

i18n-tasks (Ruby)

  • Detects missing translations, unused keys
  • Consistency checking
  • Google Translate integration for suggestions
  • Best for Rails applications

translation-check (JavaScript)

  • Placeholder validation
  • Length limit checks
  • Custom rule plugins
  • Lightweight, zero dependencies

Commercial QA Platforms

Xbench (Desktop Application)

  • Extensive QA checks (100+ rules)
  • Supports CAT tool formats
  • Batch processing
  • Expensive license ($400-800/year)

Verifika (Desktop Application)

  • Comprehensive QA suite
  • TM and glossary checks
  • Report generation
  • $300-600/year

QA Distiller (Standalone)

  • Automated QA checks
  • Integration with SDL Trados
  • Corporate licensing
  • $500+/year

TMS-Integrated QA

IntlPull QA (Included)

  • Real-time QA during translation
  • Configurable rules per project
  • Automated blocking for critical errors
  • Free with platform

Phrase QA Checks

  • Format, placeholder, length validation
  • Included in platform subscription
  • Configurable severity levels

Crowdin QA

  • Basic validation checks
  • Available in paid plans
  • Limited customization

CI Integration

Pre-Merge QA Pipeline

GitHub Actions Workflow:

YAML
1name: Translation QA
2
3on:
4  pull_request:
5    paths:
6      - 'locales/**'
7
8jobs:
9  qa:
10    runs-on: ubuntu-latest
11    steps:
12      - uses: actions/checkout@v3
13
14      - name: Install dependencies
15        run: npm install
16
17      - name: Run QA checks
18        id: qa
19        run: |
20          npm run i18n:qa > qa-report.json
21          EXIT_CODE=$?
22          echo "exit_code=$EXIT_CODE" >> $GITHUB_OUTPUT
23          exit $EXIT_CODE
24
25      - name: Comment results on PR
26        if: always()
27        uses: actions/github-script@v6
28        with:
29          script: |
30            const fs = require('fs');
31            const report = JSON.parse(fs.readFileSync('qa-report.json', 'utf8'));
32
33            const errors = report.filter(r => r.severity === 'error');
34            const warnings = report.filter(r => r.severity === 'warning');
35
36            let body = '## Translation QA Report\n\n';
37
38            if (errors.length > 0) {
39              body += '### ❌ Errors\n';
40              errors.forEach(e => {
41                body += `- ${e.key} (${e.language}): ${e.message}\n`;
42              });
43            }
44
45            if (warnings.length > 0) {
46              body += '\n### ⚠️ Warnings\n';
47              warnings.forEach(w => {
48                body += `- ${w.key} (${w.language}): ${w.message}\n`;
49              });
50            }
51
52            if (errors.length === 0 && warnings.length === 0) {
53              body += '### ✅ All checks passed!';
54            }
55
56            github.rest.issues.createComment({
57              issue_number: context.issue.number,
58              owner: context.repo.owner,
59              repo: context.repo.repo,
60              body
61            });
62
63      - name: Fail if errors found
64        if: steps.qa.outputs.exit_code != '0'
65        run: exit 1

Quality Scoring

Assign quality scores based on QA results:

JavaScript
1function calculateQualityScore(qaResults) {
2  let score = 100;
3
4  qaResults.forEach(result => {
5    if (result.severity === 'error') {
6      score -= 10;
7    } else if (result.severity === 'warning') {
8      score -= 3;
9    } else if (result.severity === 'info') {
10      score -= 1;
11    }
12  });
13
14  return Math.max(0, score);
15}
16
17function getQualityGrade(score) {
18  if (score >= 95) return 'A';
19  if (score >= 85) return 'B';
20  if (score >= 70) return 'C';
21  if (score >= 50) return 'D';
22  return 'F';
23}

Track quality trends over time:

JavaScript
1const qualityMetrics = {
2  timestamp: new Date(),
3  language: 'es',
4  totalKeys: 2450,
5  checkedKeys: 2450,
6  errors: 12,
7  warnings: 45,
8  score: 87,
9  grade: 'B',
10  topIssues: [
11    { type: 'placeholder_mismatch', count: 8 },
12    { type: 'length_exceeded', count: 15 },
13    { type: 'glossary_violation', count: 4 }
14  ]
15};
16
17// Store in database, track trends

Reporting and Dashboards

QA Report Generation

JavaScript
1function generateQAReport(qaResults, language) {
2  const errors = qaResults.filter(r => r.severity === 'error');
3  const warnings = qaResults.filter(r => r.severity === 'warning');
4  const infos = qaResults.filter(r => r.severity === 'info');
5
6  const report = {
7    summary: {
8      totalChecks: qaResults.length,
9      errors: errors.length,
10      warnings: warnings.length,
11      infos: infos.length,
12      score: calculateQualityScore(qaResults),
13      grade: getQualityGrade(calculateQualityScore(qaResults))
14    },
15    errorsByType: groupBy(errors, 'type'),
16    warningsByType: groupBy(warnings, 'type'),
17    details: qaResults
18  };
19
20  return report;
21}
22
23function groupBy(array, key) {
24  return array.reduce((acc, item) => {
25    const group = item[key] || 'other';
26    acc[group] = (acc[group] || 0) + 1;
27    return acc;
28  }, {});
29}

HTML Report:

HTML
1<html>
2<head><title>Translation QA Report - Spanish</title></head>
3<body>
4  <h1>Translation QA Report</h1>
5  <h2>Summary</h2>
6  <p>Quality Score: <strong>87/100 (B)</strong></p>
7  <ul>
8    <li>Errors: 12</li>
9    <li>Warnings: 45</li>
10    <li>Info: 18</li>
11  </ul>
12
13  <h2>Errors by Type</h2>
14  <table>
15    <tr><th>Type</th><th>Count</th></tr>
16    <tr><td>Placeholder Mismatch</td><td>8</td></tr>
17    <tr><td>Glossary Violation</td><td>4</td></tr>
18  </table>
19
20  <h2>Details</h2>
21  <!-- Full error list -->
22</body>
23</html>

IntlPull's QA Features

IntlPull provides comprehensive built-in QA without requiring external tools or custom development.

Real-Time Validation

As translators type, IntlPull runs QA checks in real-time, highlighting issues immediately. Placeholder mismatches, length violations, and glossary errors surface instantly, allowing translators to fix before submission.

Configurable Rules

Configure QA rules per project: enable/disable checks, set severity levels (error, warning, info), customize length limits, and define glossary enforcement. Tailor QA to project requirements.

Blocking vs Non-Blocking

Mark critical checks as blocking (translator cannot submit until fixed) vs non-blocking warnings (reviewer can override). Balance quality control with translator flexibility.

Quality Reports

Access quality dashboards showing QA metrics by language, translator, and project. Identify recurring issues, track quality trends, and provide targeted feedback to improve translation quality over time.

CI/CD Integration

IntlPull's API exposes QA check results, enabling CI/CD pipelines to enforce quality gates. Block deployments if translation quality falls below threshold.

By embedding QA directly in the translation workflow, IntlPull ensures quality without adding friction or requiring separate tool management.

Frequently Asked Questions

How many QA checks should I enable?

Start with essential checks (placeholder validation, format validation, length limits) and gradually add more (glossary, consistency, spelling) as you refine processes. Too many checks create alert fatigue; focus on checks with high signal-to-noise ratio. Monitor false positive rates and adjust.

Should QA checks block translation submission?

Block submission for critical technical errors (placeholder mismatches, format errors) that would break functionality. Make linguistic checks (spelling, style) non-blocking warnings for human review. Balance automation with translator trust.

How do I reduce false positives in QA checks?

Allow translators to mark false positives and ignore specific warnings. Refine check logic based on feedback. For example, if length check triggers frequently but translations work fine in UI, increase limits. Regularly review QA rules and adjust based on real-world results.

Can QA checks handle all languages equally?

Some checks are language-agnostic (placeholder matching, format validation). Others require language-specific configuration (capitalization rules, punctuation conventions, spelling dictionaries). Invest in configuring QA properly for your key languages rather than one-size-fits-all approach.

How do I measure QA effectiveness?

Track defect escape rate: issues found in production that QA should have caught. Low escape rate indicates effective QA. Also measure false positive rate and translator feedback on QA usefulness. Effective QA catches real issues without annoying translators with noise.

Should I run QA checks on machine-translated content?

Yes, especially placeholder and format validation. MT often mangles placeholders or formatting. However, expect higher error rates on MT content compared to human translations. Use QA to identify MT content requiring post-editing rather than blocking all MT.

How often should I review and update QA rules?

Review quarterly, especially when launching new products, entering new markets, or receiving translator feedback about problematic rules. Major reviews annually to ensure rules still align with quality standards and product evolution. QA rules should evolve with your localization program.

Tags
qa
quality-assurance
translation-quality
automation
checks
validation
localization
IntlPull Team
IntlPull Team
Engineering

Building tools to help teams ship products globally. Follow us for more insights on localization and i18n.