Getting AI translation right for mobile apps isn't just about feeding your resource files into ChatGPT and hoping for the best. After showing you all the ways AI can destroy your strings.xml and .strings files, it's time for the practical solution: how to actually use AI translation safely without breaking your builds or corrupting your localization files.
This isn't about replacing professional translation workflows entirely — it's about doing AI translation correctly when you choose to use it, with proper safeguards and validation at every step.
The first rule of safe AI translation is never send your entire resource file to an AI model. You need to extract translatable content while preserving the structural elements that AI models love to mangle.
Here's what proper extraction looks like for each platform:
Android strings.xml Extraction
import xml.etree.ElementTree as ET
import re
def extract_android_strings(xml_file_path):
"""Extract translatable strings while preserving structure"""
tree = ET.parse(xml_file_path)
root = tree.getroot()
translatable_content = {}
for string_elem in root.findall('.//string'):
# Skip non-translatable strings
if string_elem.get('translatable') == 'false':
continue
key = string_elem.get('name')
text = string_elem.text or ''
# Preserve placeholders and formatting
translatable_content[key] = {
'text': text,
'placeholders': re.findall(r'%\d+\$[sd]|%[sd]|{[^}]+}', text),
'formatting': re.findall(r'<[^>]+>', text)
}
return translatable_content
def create_translation_input(content_dict, source_lang, target_lang):
"""Create clean input for AI translation"""
translation_map = {}
for key, data in content_dict.items():
# Replace placeholders with safe tokens
safe_text = data['text']
placeholder_map = {}
for i, placeholder in enumerate(data['placeholders']):
token = f"PLACEHOLDER_{i}"
placeholder_map[token] = placeholder
safe_text = safe_text.replace(placeholder, token)
translation_map[key] = {
'original': safe_text,
'placeholder_map': placeholder_map,
'formatting': data['formatting']
}
return translation_map
import re
def extract_ios_strings(strings_file_path):
"""Extract translatable content from .strings files"""
with open(strings_file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Match .strings format: "key" = "value";
pattern = r'"([^"]+)"\s*=\s*"([^"]*(?:\\.[^"]*)*)"\s*;'
matches = re.findall(pattern, content)
translatable_content = {}
for key, value in matches:
# Preserve iOS-specific placeholders
placeholders = re.findall(r'%[@sdfdioxX]|%\d+\$[@sdfdioxX]', value)
translatable_content[key] = {
'text': value,
'placeholders': placeholders
}
return translatable_content
import json
def extract_rn_strings(json_file_path):
"""Extract translatable content from React Native i18n JSON"""
with open(json_file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
def extract_recursive(obj, prefix=''):
translatable = {}
for key, value in obj.items():
current_key = f"{prefix}.{key}" if prefix else key
if isinstance(value, str):
# Extract React i18next placeholders
placeholders = re.findall(r'{{[^}]+}}|\$t\([^)]+\)', value)
translatable[current_key] = {
'text': value,
'placeholders': placeholders
}
elif isinstance(value, dict):
translatable.update(extract_recursive(value, current_key))
return translatable
return extract_recursive(data)
Safe Prompting Techniques That Actually Work
The key to safe AI translation is being extremely explicit about what the model should and shouldn't touch. Generic prompts lead to corrupted files — specific instructions prevent disasters.
The Safe Translation Prompt Template
def create_safe_translation_prompt(content_dict, source_lang, target_lang):
"""Generate explicit translation instructions"""
prompt = f"""
TRANSLATION TASK: Translate mobile app strings from {source_lang} to {target_lang}
CRITICAL RULES:
1. NEVER modify text that starts with "PLACEHOLDER_" - these are protected tokens
2. NEVER change XML tags, HTML tags, or formatting markup
3. NEVER add or remove quotation marks, semicolons, or brackets
4. NEVER translate technical terms like variable names or API endpoints
5. Preserve all whitespace and line breaks exactly as shown
INPUT FORMAT: Each line shows KEY: TEXT_TO_TRANSLATE
TRANSLATION RULES FOR {target_lang}:
- Maintain formal/informal tone appropriate for mobile apps
- Use platform-standard terminology (e.g., "Settings" not "Preferences")
- Keep button text concise (mobile screen constraints)
EXAMPLE:
KEY: welcome_message
ORIGINAL: "Welcome to our app, PLACEHOLDER_0!"
TRANSLATION: "Bienvenue dans notre application, PLACEHOLDER_0!"
NOW TRANSLATE THESE STRINGS:
"""
for key, data in content_dict.items():
prompt += f"KEY: {key}\n"
prompt += f"ORIGINAL: \"{data['original']}\"\n"
prompt += f"TRANSLATION: \n\n"
return prompt
def parse_translation_response(response_text, original_dict):
"""Parse AI response back into structured format"""
translations = {}
# Extract translations using pattern matching
pattern = r'KEY:\s*([^\n]+)\s*\n.*?TRANSLATION:\s*"([^"]*(?:\\.[^"]*)*)"'
matches = re.findall(pattern, response_text, re.DOTALL)
for key, translation in matches:
key = key.strip()
if key in original_dict:
# Restore placeholders
restored_translation = translation
for token, placeholder in original_dict[key]['placeholder_map'].items():
restored_translation = restored_translation.replace(token, placeholder)
translations[key] = restored_translation
return translations
Post-processing Validation: Catch Errors Before They Break Production
Validation is where most teams fail with AI translation. You need automated checks that catch structural corruption, missing placeholders, and format violations.
Comprehensive Validation Scripts
class TranslationValidator:
def __init__(self, platform_type):
self.platform = platform_type
self.errors = []
self.warnings = []
def validate_android_xml(self, original_content, translated_content):
"""Validate Android strings.xml translations"""
for key in original_content:
if key not in translated_content:
self.errors.append(f"Missing translation for key: {key}")
continue
original = original_content[key]['text']
translation = translated_content[key]
# Check placeholder integrity
orig_placeholders = set(original_content[key]['placeholders'])
trans_placeholders = set(re.findall(r'%\d+\$[sd]|%[sd]|{[^}]+}', translation))
if orig_placeholders != trans_placeholders:
self.errors.append(f"Placeholder mismatch in {key}: {orig_placeholders} vs {trans_placeholders}")
# Check for XML corruption
if '<string' in translation or 'name=' in translation:
self.errors.append(f"XML structure found in translation content: {key}")
# Check for untranslated placeholders tokens
if 'PLACEHOLDER_' in translation:
self.errors.append(f"Untranslated placeholder token in {key}: {translation}")
# Platform-specific validations
if re.search(r'%\d+\$[sd]', original) and not re.search(r'%\d+\$[sd]', translation):
self.warnings.append(f"Positional parameter format may be lost in {key}")
def validate_ios_strings(self, original_content, translated_content):
"""Validate iOS .strings translations"""
for key in original_content:
if key not in translated_content:
self.errors.append(f"Missing translation for key: {key}")
continue
original = original_content[key]['text']
translation = translated_content[key]
# Check iOS-specific placeholders
orig_placeholders = set(re.findall(r'%[@sdfdioxX]|%\d+\$[@sdfdioxX]', original))
trans_placeholders = set(re.findall(r'%[@sdfdioxX]|%\d+\$[@sdfdioxX]', translation))
if orig_placeholders != trans_placeholders:
self.errors.append(f"iOS placeholder mismatch in {key}")
# Check for .strings format corruption
if '" = "' in translation or '";' in translation:
self.errors.append(f".strings format found in translation content: {key}")
def validate_react_native_json(self, original_content, translated_content):
"""Validate React Native JSON translations"""
for key in original_content:
if key not in translated_content:
self.errors.append(f"Missing translation for key: {key}")
continue
original = original_content[key]['text']
translation = translated_content[key]
# Check React i18next placeholders
orig_placeholders = set(re.findall(r'{{[^}]+}}|\$t\([^)]+\)', original))
trans_placeholders = set(re.findall(r'{{[^}]+}}|\$t\([^)]+\)', translation))
if orig_placeholders != trans_placeholders:
self.errors.append(f"React Native placeholder mismatch in {key}")
# Check for JSON corruption
if translation.startswith('{') or translation.endswith('}'):
self.warnings.append(f"Possible JSON structure in translation: {key}")
def get_validation_report(self):
"""Return comprehensive validation results"""
return {
'valid': len(self.errors) == 0,
'errors': self.errors,
'warnings': self.warnings,
'error_count': len(self.errors),
'warning_count': len(self.warnings)
}
Each platform has quirks that will bite you if you're not careful. Here's what actually matters in production:
Android XML Specifics
Android's string resources have several gotchas that AI models consistently mess up:
def reconstruct_android_xml(original_tree, translations):
"""Safely rebuild Android strings.xml with translations"""
root = original_tree.getroot()
for string_elem in root.findall('.//string'):
key = string_elem.get('name')
if key in translations and string_elem.get('translatable') != 'false':
# Preserve CDATA if present
if string_elem.text and string_elem.text.strip().startswith('<![CDATA['):
string_elem.text = f"<![CDATA[{translations[key]}]]>"
else:
string_elem.text = translations[key]
# Preserve all attributes
# AI often strips formatted="false" or other important attrs
return ET.tostring(root, encoding='unicode')
iOS .strings Edge Cases
iOS has some specific formatting requirements that break easily:
def reconstruct_ios_strings(original_file_path, translations):
"""Rebuild .strings file preserving comments and formatting"""
with open(original_file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Preserve comments and empty lines
lines = content.split('\n')
result_lines = []
for line in lines:
# Check if it's a translation line
match = re.match(r'^"([^"]+)"\s*=\s*"([^"]*)"\s*;\s*$', line.strip())
if match and match.group(1) in translations:
key = match.group(1)
# Preserve original formatting/spacing
spacing = re.match(r'^(\s*)', line).group(1)
result_lines.append(f'{spacing}"{key}" = "{translations[key]}";')
else:
# Preserve comments, empty lines, etc.
result_lines.append(line)
return '\n'.join(result_lines)
React Native JSON Reconstruction
JSON requires careful handling to preserve nested structure:
def reconstruct_rn_json(original_data, translations):
"""Rebuild React Native JSON preserving structure"""
def update_recursive(obj, prefix=''):
for key, value in obj.items():
current_key = f"{prefix}.{key}" if prefix else key
if isinstance(value, str) and current_key in translations:
obj[key] = translations[current_key]
elif isinstance(value, dict):
update_recursive(value, current_key)
# Work on a copy to avoid mutating original
result = json.loads(json.dumps(original_data))
update_recursive(result)
return result
CI/CD Integration That Actually Works
The final piece is integrating this safely into your development workflow. You need validation that runs automatically and fails fast when something goes wrong.
GitHub Actions Workflow
name: AI Translation Validation
on:
pull_request:
paths:
- 'android/app/src/main/res/values*/**'
- 'ios/App/App/Supporting Files/*.lproj/**'
- 'src/i18n/*.json'
jobs:
validate-translations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: |
pip install xml-parser requests
- name: Validate Android strings
run: |
python scripts/validate_translations.py \
--platform android \
--source android/app/src/main/res/values/strings.xml \
--target android/app/src/main/res/values-*/strings.xml
- name: Validate iOS strings
run: |
python scripts/validate_translations.py \
--platform ios \
--source ios/App/App/Supporting\ Files/en.lproj/Localizable.strings \
--target ios/App/App/Supporting\ Files/*.lproj/Localizable.strings
- name: Validate React Native JSON
run: |
python scripts/validate_translations.py \
--platform react-native \
--source src/i18n/en.json \
--target src/i18n/*.json
- name: Comment validation results
if: failure()
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Translation validation failed. Check the workflow logs for details.'
})
Automated Translation Script
#!/usr/bin/env python3
"""
Safe AI translation script for mobile resource files
Usage: python translate_resources.py --platform android --source en --target fr
"""
import argparse
import sys
from pathlib import Path
def main():
parser = argparse.ArgumentParser(description='Safely translate mobile resources with AI')
parser.add_argument('--platform', choices=['android', 'ios', 'react-native'], required=True)
parser.add_argument('--source', required=True, help='Source language code')
parser.add_argument('--target', required=True, help='Target language code')
parser.add_argument('--validate-only', action='store_true', help='Only validate existing translations')
args = parser.parse_args()
# Initialize platform-specific handlers
if args.platform == 'android':
from android_translator import AndroidTranslator
translator = AndroidTranslator()
elif args.platform == 'ios':
from ios_translator import IOSTranslator
translator = IOSTranslator()
elif args.platform == 'react-native':
from rn_translator import ReactNativeTranslator
translator = ReactNativeTranslator()
if args.validate_only:
validator = TranslationValidator(args.platform)
result = validator.validate_existing_translations(args.source, args.target)
if not result['valid']:
print(f"Validation failed with {result['error_count']} errors:")
for error in result['errors']:
print(f" - {error}")
sys.exit(1)
else:
print("All translations valid!")
sys.exit(0)
# Run full translation pipeline
try:
translator.translate_resources(args.source, args.target)
print(f"Successfully translated {args.platform} resources from {args.source} to {args.target}")
except Exception as e:
print(f"Translation failed: {e}")
sys.exit(1)
if __name__ == '__main__':
main()
Putting It All Together: Production-Ready Workflow
Here's how this all works in a real development workflow. When you need to add a new language or update existing translations:
- Extract: Run the extraction script to pull translatable content from your source files
- Translate: Use the safe prompting template with explicit instructions for your AI model
- Validate: Run comprehensive validation to catch any structural issues or corrupted output
- Reconstruct: Rebuild your resource files preserving all original formatting and structure
- Test: Run your automated tests to ensure the app still builds and functions correctly
This approach gives you the speed benefits of AI translation while maintaining the reliability you need for production apps. The key insight is that AI translation isn't about replacing your entire localization workflow — it's about doing the translation step safely when you choose to use AI.
For teams managing more complex localization needs, including the broader workflow challenges around string management across platforms and preventing localization technical debt, AI translation is just one piece of a larger puzzle. But when you need that piece, doing it right matters.
The scripts and techniques here prevent the common corruption patterns that break builds and frustrate teams. Use them as a foundation, adapt them to your specific needs, and always validate before pushing to production.