During one release sprint, developers added more than a dozen UI strings to the development branch. The archive job completed successfully, yet testers found English fallbacks and blank buttons after switching languages. The compiler was not at fault—the pipeline had never treated localization integrity as a check that could fail the build. For a long-running cloud Mac, the most effective approach is not to wait for someone to click through every screen, but to validate .xcstrings directly before every build.
Define exactly what the gate should block
A String Catalog is a JSON file, but being parseable does not mean it is ready to ship. A practical gate should check at least three types of problems: a required target locale is missing, the translation state is not translated, or the final string is empty. Plural forms, device-specific variants, and similar content can also appear under variations, so reading only the first-level stringUnit entries will miss errors.
Keep the acceptance criteria in the repository rather than in temporary settings on the build node. If the project’s source language is English and it currently requires Simplified Chinese, Japanese, and French, pass those target locales to the script as arguments.
| Check | Failure condition | Action |
|---|---|---|
| Target locale | No matching key exists in localizations |
Block the build |
| Translation state | Any leaf node is not translated |
Return the exact key and locale |
| String content | value is empty or contains only whitespace |
Block the build |
| Excluded from translation | shouldTranslate is false |
Skip explicitly |
The gate only determines whether the localization resources in the catalog are complete. It does not replace manual acceptance testing for UI truncation, dynamic arguments, or semantic accuracy.
Build a validator with no external dependencies
Save the script as Scripts/check_xcstrings.py. It relies only on the Python runtime included with macOS. If the team uses a fixed standalone Python path, specify it explicitly in the build configuration to avoid differences between the interactive shell and the CI PATH.
#!/usr/bin/env python3
import argparse
import json
import pathlib
import sys
def units(node):
found = []
if isinstance(node, dict):
unit = node.get("stringUnit")
if isinstance(unit, dict):
found.append(unit)
for key, value in node.items():
if key != "stringUnit":
found.extend(units(value))
elif isinstance(node, list):
for value in node:
found.extend(units(value))
return found
parser = argparse.ArgumentParser()
parser.add_argument("root")
parser.add_argument("--locale", action="append", required=True)
args = parser.parse_args()
failures = []
files = sorted(pathlib.Path(args.root).rglob("*.xcstrings"))
if not files:
failures.append("no .xcstrings files found")
for path in files:
with path.open(encoding="utf-8") as handle:
catalog = json.load(handle)
for key, entry in catalog.get("strings", {}).items():
if entry.get("shouldTranslate") is False:
continue
localizations = entry.get("localizations", {})
for locale in args.locale:
localized = localizations.get(locale)
if localized is None:
failures.append(f"{path}:{key}:{locale}:missing locale")
continue
leaves = units(localized)
if not leaves:
failures.append(f"{path}:{key}:{locale}:missing stringUnit")
continue
for index, unit in enumerate(leaves):
state = unit.get("state")
value = unit.get("value", "")
if state != "translated":
failures.append(
f"{path}:{key}:{locale}:{index}:state={state}"
)
if not value.strip():
failures.append(
f"{path}:{key}:{locale}:{index}:empty value"
)
for failure in failures:
print(failure, file=sys.stderr)
sys.exit(1 if failures else 0)
Run it from the repository root:
python3 Scripts/check_xcstrings.py . \
--locale zh-Hans \
--locale ja \
--locale fr
When the script exits with a nonzero status, any common task orchestrator can stop the remaining steps. Its output includes the file, string key, locale, and leaf-node index, so the person fixing the issue does not need to search through the full build log first.
Add the gate to the cloud Mac workflow before compilation
Place the check after dependency resolution but before xcodebuild build or archive. Preparing dependencies confirms that the repository contents have been written to disk completely, while running the check early avoids compiling, testing, and archiving a commit that is already known to be invalid.
set -euo pipefail
mkdir -p build/reports
python3 Scripts/check_xcstrings.py Sources \
--locale zh-Hans \
--locale ja \
--locale fr \
2> build/reports/localization-errors.txt
xcodebuild \
-project App.xcodeproj \
-scheme App \
-configuration Release \
-destination 'generic/platform=iOS' \
build
set -o pipefail is important. If the output is later piped through tee, omitting this setting may cause the pipeline to see only the final command’s successful status and discard the validator failure. Recreate the report directory at the start of the job as well, so files left by a previous run on the cloud Mac are not mistaken for current results.
Add a manual review layer with exported localizations
Parsing .xcstrings directly works well for an automated gate, but before release you should also export the localization packages and verify that the development language, comments, and context are complete. Run the following commands for the target locales:
rm -rf build/xcloc
mkdir -p build/xcloc
xcodebuild -exportLocalizations \
-project App.xcodeproj \
-localizationPath build/xcloc \
-exportLanguage zh-Hans
xcodebuild -exportLocalizations \
-project App.xcodeproj \
-localizationPath build/xcloc \
-exportLanguage ja
If an export fails, do not immediately retry and overwrite the evidence. Preserve the complete standard error output first, then verify the scheme, project path, and target locale identifier. The locale code must match the corresponding Catalog key exactly. For example, zh-Hans and zh-Hant are separate targets and cannot be replaced with an ambiguous prefix.
The exported .xcloc packages are suitable for spot checks by the localization owner, but build artifacts should not be committed to the main branch. The repository should retain the source .xcstrings files, validation script, and target-locale list, while the export directory remains an artifact of a single job.
Resolve common false positives and complete acceptance testing
The first category of false positives comes from technical strings that do not require translation. Do not make the script arbitrarily ignore key-name prefixes. Instead, set shouldTranslate: false explicitly in the Catalog so the rule stays with the resource itself. The second category occurs when only the one branch of a plural is translated and other is omitted. Recursively reading every stringUnit identifies the exact missing branch.
A third issue occurs when a developer adds a locale but forgets to update the CI arguments. Keep a single source of truth for the target-locale list, such as an array in the project script or a pipeline variable, rather than maintaining separate copies in multiple jobs. The fourth category involves spaces, line breaks, or placeholders being mistaken for valid translations. The current script rejects whitespace-only values. For format arguments such as %@ and %d, you can also compare the placeholder sets in the source and target strings, but account for escaping and positional arguments before making that comparison blocking.
Complete acceptance testing in this order before merging:
- Deliberately remove one target locale and confirm that the job fails.
- Replace one translation with a space and confirm that the report identifies the exact key.
- Create an unfinished plural branch and confirm that the recursive check finds it.
- Restore the resources and rerun the job, confirming that an old report does not affect the result.
- Finally, run a Release configuration build and verify that the gate and the production job use the same checked-out content.
The result is not a one-off translation scan, but an engineering constraint that can be reproduced reliably on a cloud Mac: incomplete resources fail early, and compilation, testing, and archiving begin only after those resources are fixed.
Frequently asked questions
Where should the String Catalog check run in a CI pipeline?
Run it after dependencies are prepared but before compilation and archiving. A localization failure then stops the job before the most expensive build stages begin.
Is checking for the translated state enough?
No. The validator must also confirm that every target locale exists, reject empty values, and recursively inspect each stringUnit nested under plural or other variants.
Run your next build on a cloud Mac.
Compare three Apple Silicon configurations and choose the region that best fits your current workflow across five overseas nodes.