FoLabelMaker is a .NET 10 command-line tool for D365 Finance and Operations label maintenance.
It scans FO metadata and X++ source, finds hard-coded user-facing text, creates a safe change plan, updates label files, applies replacements, generates JSON and HTML reports, and can translate labels through OpenAI.
FoLabelMaker.CoreAll business logic.FoLabelMaker.CliThin command-line wrapper.
The built executable is:
FoLabelMaker.exe
Typical build output location:
FoLabelMaker.Cli\bin\Debug\net10.0\FoLabelMaker.exe
The tool is intentionally split into separate responsibilities:
scanInspect metadata and report findings.planBuild a safe plan for label replacements and label-file additions.applyApply a previously created plan.translateTranslate label files.improveSuggest wording and formatting improvements.
Translation is separate from scan, plan, and apply.
scandoes not change metadata.plandoes not change metadata.applyonly changes files from an explicit plan file.- modified metadata and label files get
.bakbackups. applywrites a changed-files manifest.
dotnet build FOLabelMaker.slnxThe CLI accepts single-dash long options.
Examples:
FoLabelMaker scan -model MyModel
FoLabelMaker plan -model MyModel
FoLabelMaker apply -plan mymodel
FoLabelMaker translate -model MyModel -target-lang no
FoLabelMaker improve -model MyModelPositional model names are also supported for commands that operate on a model.
Examples:
FoLabelMaker scan MyModel
FoLabelMaker plan MyModel
FoLabelMaker translate MyModel -target-lang noThat path is treated as the target working root.
It can be:
- a repository root
- a metadata root
- an exact model path
The tool assumes the current directory is the working root.
So if you are already standing in the target repo root, this works:
FoLabelMaker plan MyModelIf the working root contains multiple models, pass a model name.
The tool supports:
- exact model matching
- case-insensitive matching
- case-insensitive contains matching when the result is unambiguous
The tool intentionally avoids technical duplicate metadata trees such as:
XppMetadata
Relative output paths are written to the target working root.
Whenever a JSON report is written, a companion HTML report is also written.
Examples:
mymodel-scan.json
mymodel-scan-report.html
mymodel-plan.json
mymodel-plan-report.html
The HTML report contains:
- a readable report title
- created date/time
- summary cards
- detailed tables
- validation details
If -output is omitted, default names are based on the resolved model name.
Examples:
-
FoLabelMaker scan -model MyModelwrites:mymodel-scan.jsonmymodel-scan-report.html
-
FoLabelMaker plan -model MyModelwrites:mymodel-plan.jsonmymodel-plan-report.html
-
FoLabelMaker improve -model MyModelwrites:mymodel-improvements.jsonmymodel-improvements-report.html
If no model can be resolved, fallback names use report.
Scans metadata and reports:
- detected user-facing text candidates
- ignored candidates with reasons
- missing text proposals
- improvement suggestions
- validation errors
Examples:
FoLabelMaker scan -model MyModel
FoLabelMaker scan MyModel
FoLabelMaker scan -metadata-root "C:\Dev\MyRepo" -model MyModelCreates a JSON plan of:
- metadata replacements
- X++ string replacements
- label-file additions
The plan also includes:
- missing text proposals
- ignored candidates
- validation errors
Examples:
FoLabelMaker plan -model MyModel
FoLabelMaker plan MyModel
FoLabelMaker plan -metadata-root "C:\Dev\MyRepo" -model MyModel -output custom-plan.jsonplan does not translate labels.
Applies a previously generated plan.
Examples:
FoLabelMaker apply -plan mymodel-plan.json
FoLabelMaker apply -plan mymodelIf -plan is given without an extension and without a path, the tool assumes the default plan file name:
<value>-plan.json
So this:
FoLabelMaker apply -plan mymodelresolves to:
mymodel-plan.json
Apply behavior:
- updates metadata files from the plan
- updates base-language label files
- creates
.bakbackups - writes
fo-labelmaker-apply-manifest.json
apply does not perform translation.
Creates or updates translated label files using OpenAI.
Examples:
FoLabelMaker translate -model MyModel -target-lang no
FoLabelMaker translate MyModel -target-lang no
FoLabelMaker translate -metadata-root "C:\Dev\MyRepo" -model MyModel -base-lang en -target-lang svTranslate behavior:
- reads the base-language label file as source
- only sends label text and minimal context
- preserves placeholders like
%1and{0} - validates placeholder and line-break preservation
- writes translated label files using FO label-file structure
This is the only command that accepts translation-specific options like:
-target-lang-target-language- overwrite translation settings
Produces text-improvement suggestions without changing metadata.
Examples:
FoLabelMaker improve -model MyModel
FoLabelMaker improve MyModelFoLabelMaker scan MyModel
FoLabelMaker plan MyModelFoLabelMaker apply -plan mymodelFoLabelMaker plan MyModel
FoLabelMaker apply -plan mymodel
FoLabelMaker translate MyModel -target-lang noThe tool tries to reuse the existing D365 FO label-file structure.
Typical structure:
AxLabelFile\MyLabels_en-US.xml
AxLabelFile\LabelResources\en-US\MyLabels.en-US.label.txt
In that case it should:
- add to the existing label text file
- use the existing label file ID in references
- continue the existing label ID sequence
The tool creates a model-local FO label-file structure named after the model.
Example pattern:
AxLabelFile\MyModel_en-US.xml
AxLabelFile\LabelResources\en-US\MyModel.en-US.label.txt
For translations the same structure is used:
AxLabelFile\MyModel_nb-NO.xml
AxLabelFile\LabelResources\nb-NO\MyModel.nb-NO.label.txt
The CLI loads appsettings.json at startup.
Lookup order:
- current working directory
- application base directory
Example:
{
"LabelMaker": {
"MetadataRootPath": "C:\\Dev\\MyRepo",
"ModelName": "MyModel",
"LabelPrefix": "@MY",
"BaseLanguage": "en-US",
"TargetLanguages": ["nb-NO"],
"ReuseSimilarLabels": false,
"OverwriteTranslations": false
},
"OpenAi": {
"ApiKey": "your-real-key-here",
"Model": "gpt-5-mini",
"ApiKeyEnvironmentVariable": "OPENAI_API_KEY",
"BaseUrl": "https://api.openai.com/v1/chat/completions",
"CacheFilePath": ".fo-labelmaker-ai-cache.json"
}
}Precedence:
- command-line option
appsettings.json- built-in defaults
OpenAI authentication precedence:
OpenAi.ApiKey- environment variable named by
OpenAi.ApiKeyEnvironmentVariable
The CLI supports common language shortcuts and normalizes them.
Examples:
en->en-USen-us->en-USno->nb-NOnb->nb-NOsv->sv-SEda->da-DKth->th-TH
Examples:
FoLabelMaker translate MyModel -target-lang no
FoLabelMaker translate MyModel -base-lang en -target-lang svExamples normally treated as user-facing:
- labels
- captions
- help text
- menu item text
- button text
- error/warning text
- form/report text
Examples normally ignored:
- existing label references such as
@MyFile:MyLabel123 - URLs
- file paths
- GUIDs
- SQL fragments
- placeholders-only strings like
%1 - file extensions like
.pfx - technical identifiers in code
The reports distinguish between:
The exact text already exists in a model label file.
The same new text appears multiple times in the current scan or plan, so those occurrences share one label ID.
The tool reports missing labels and captions for important FO elements.
Examples of generated proposals:
- form captions
- field labels
- EDT labels
- menu item labels
- report labels
These proposals are reported, not silently applied.
During translate, the tool prints progress such as:
- preparing translation plan
- resolved model
- base language
- target languages
- number of translation requests
- cache hits
- OpenAI request start
- OpenAI response status
- persistence of translated label files
This is intended to make long-running translation steps visible instead of appearing stalled.
0success1usage or argument problem2validation or runtime failure
-plan is only an option for the apply command.
Wrong:
FoLabelMaker scan -model MyModel -planRight:
FoLabelMaker plan MyModel
FoLabelMaker apply -plan mymodelPossible reasons:
- the model was already labelized by a previous apply run
- the remaining strings are classified as technical
- the wrong path or wrong model was used
This comes from OpenAI throttling or quota limits.
Current behavior:
- translation stops on the API failure
- no retry/backoff is implemented yet
Default names are based on the resolved model name.
Examples:
FoLabelMaker scan MyModel->mymodel-scan.jsonFoLabelMaker plan MyModel->mymodel-plan.json
If no model can be resolved, the fallback prefix is report.
- no automatic retry/backoff for OpenAI
429 - missing-text proposals are not yet automatically converted into applyable changes
- translation is intentionally separate from scan, plan, and apply
FoLabelMaker scan MyModel
FoLabelMaker plan MyModel
FoLabelMaker apply -plan mymodel
FoLabelMaker translate MyModel -target-lang no
FoLabelMaker improve MyModel