Integrations
iOS & Android Integration
langctl pulls platform-specific files for iOS (Localizable.strings) and Android (strings.xml) with automatic parameter conversion.
iOS Integration
Pull for iOS
# once per repo: writes langctl.json
langctl init --project my-app --format ios --output "Resources/{lang}.lproj/Localizable.strings"
# then, whenever translations change
langctl pull File Structure
MyApp/
Resources/
en.lproj/
Localizable.strings
es.lproj/
Localizable.strings
fr.lproj/
Localizable.strings
Using Translations in Swift
// Basic usage
let welcome = NSLocalizedString("home.welcome", comment: "Welcome message")
// With parameters
let greeting = String(
format: NSLocalizedString("home.greeting", comment: ""),
username
)
// SwiftUI
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text(LocalizedStringKey("home.welcome"))
Text(LocalizedStringKey("home.subtitle"))
}
}
} langctl converts {{param}} to positional iOS format specifiers:
| langctl | iOS Output |
|---|---|
{{username}} | %1$@ |
{{username}}, {{count}} | %1$@, %2$@ |
{{name}} … {{name}} | %1$@ … %1$@ |
A placeholder used twice keeps the same position, and a literal % is escaped where needed.
Android Integration
Pull for Android
# once per repo: writes langctl.json
langctl init --project my-app --format android --output "app/src/main/res/{android}/strings.xml"
# then, whenever translations change
langctl pull {android} expands to the resource folder for each language: values for the project’s default language, values-es, values-pt-rBR, and so on.
File Structure
app/
src/
main/
res/
values/
strings.xml (English - default)
values-es/
strings.xml (Spanish)
values-fr/
strings.xml (French)
Using Translations in Kotlin
// Basic usage
val welcome = getString(R.string.home_welcome)
// Set in TextView
binding.textView.text = getString(R.string.home_welcome)
// With parameters
val greeting = getString(R.string.home_greeting, username)
binding.textView.text = getString(R.string.home_greeting, username, count)
// In Compose
@Composable
fun HomeScreen() {
Column {
Text(stringResource(R.string.home_welcome))
Text(stringResource(R.string.home_subtitle))
}
} langctl converts {{param}} to Android format and turns key names into valid resource names (home.welcome → home_welcome):
| langctl | Android Output |
|---|---|
{{username}} | %1$s |
{{username}}, {{count}} | %1$s, %2$s |
{{name}} … {{name}} | %1$s … %1$s |
Note
Android uses positional parameters (%1$s, %2$s) so translators can reorder them. Apostrophes are escaped as Android requires. If two keys map to the same resource name (e.g. home.title and home_title), the pull fails and names both keys instead of silently dropping one.
CI/CD
Commit the pulled files and check them on every pull request with langctl pull --check (exit code 7 when they’re out of date) — see CI/CD Integration. If you’d rather pull at build time:
name: iOS Build
on: [push]
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- name: Pull Translations
env:
LANGCTL_API_KEY: ${{ secrets.LANGCTL_API_KEY }}
run: npx langctl@0 pull # uses langctl.json
- name: Build iOS App
run: xcodebuild -scheme MyApp -configuration Release name: Android Build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Pull Translations
env:
LANGCTL_API_KEY: ${{ secrets.LANGCTL_API_KEY }}
run: npx langctl@0 pull # uses langctl.json
- name: Build Android App
run: ./gradlew assembleRelease lane :fetch_translations do
# Fastlane runs lanes from ./fastlane; langctl.json is found in parent directories
sh "npx langctl@0 pull"
end
lane :release do
fetch_translations
build_app(scheme: "MyApp")
upload_to_app_store
end lane :fetch_translations do
# Fastlane runs lanes from ./fastlane; langctl.json is found in parent directories
sh "npx langctl@0 pull"
end
lane :release do
fetch_translations
gradle(task: "assembleRelease")
upload_to_play_store
end Best Practices
iOS Best Practices
-
Commit Localizable.strings:
- Commit the pulled
.lprojfolders with your code - Run
langctl pull --checkin CI so they never drift - Output is sorted with no timestamps, so diffs show only real changes
- Commit the pulled
-
Localized String Keys:
- Use descriptive keys:
home.welcomenotstring_001 - Group by feature:
auth.login.title - Add comments for context
- Use descriptive keys:
-
Test RTL Languages:
- Pull Arabic/Hebrew (
langctl pull -l ar,he) - Test layout in Xcode
- Use Auto Layout constraints
- Pull Arabic/Hebrew (
Android Best Practices
-
Resource Qualifiers:
- Use correct directory names:
values-es,values-ar - Follow Android conventions
- Test with different configurations
- Use correct directory names:
-
Pluralization:
- langctl writes plain
<string>entries; it doesn’t generate<plurals>resources - Keep quantity strings in a separate, hand-maintained resource file, or use one key per case
- langctl writes plain
-
String Formatting:
- Avoid hardcoded strings
- Use string resources everywhere
- Format numbers/dates properly
Tip
Always test translations on actual devices in different languages to catch layout and formatting issues.
Troubleshooting
iOS Issues
Strings not appearing:
- Clean build folder (Cmd+Shift+K)
- Check .lproj folder names match language codes
- Verify Localizable.strings is in Copy Bundle Resources
Parameters not working:
- Check String.format() syntax
- Verify the argument order matches the positions (
%1$@,%2$@) - Use String(format:) correctly
Android Issues
Resources not found:
- Check values-XX folder naming
- Ensure strings.xml is valid XML
- Clean and rebuild project
Parameters not formatting:
- Placeholders are written as strings (
%1$s); pass formatted numbers as strings - Check positional indicators (%1$s, %2$s)
- Look up resources by the converted name (
R.string.home_welcomeforhome.welcome)
Next Steps
- Cross-Platform - Flutter & React Native
- CI/CD Integration - Automate workflows
- File Formats - Format specifications
- CLI Export & Import - Command-line tools