
1. Translation functions
The ACF plugin (v1.7.6.0) + DDRparser (v1.2.0) provide a simple, professional workflow for translating FileMaker solutions using standard POT/PO files.
- Translation functions
- Editing in Poedit
- Using DDRparser 1.2.0 to generate and update the POT
- Case: Producing foreign invoices with translated product names.
1.1. Terminology ↑
1.1.1. POT file (PO template) ↑
A POT file is a template containing only source strings (no translations). DDRparser extracts _("…") / _n("…") calls and writes them to messages.pot. Tools then merge new strings into existing POTs so nothing already collected is lost.
Keep your POT under version control; other PO files are updated from it.
1.1.2. PO file ↑
A PO file contains translations for one language. It has the same format as POT, but msgstr is filled in. Tools like Poedit (or hosted TMSes) can “Update from POT” to add new strings without overwriting existing translations.
1.2. Plugin functions ↑
The functions exist both as FileMaker calculation functions and in the ACF language; names and behavior are identical.
1.2.1. _( text {; arg…} ) ↑
Purpose: Translate a single string using the currently loaded language.
- Parameters:
text— the source string (msgid). Optionalarg…— values for numbered placeholders%1,%2, … (order matters). - Returns: The translated string. If missing, returns the original
textand records the “miss” internally (you can merge misses into the POT later). - Notes: Lookups are fast (hash-map). FileMaker calcs using plugin functions are unstored.
Example
// EN source
_( "Overwrite file “%1”?" ; Packages::FileName )
// NB translation might reorder:
"«%1» vil bli overskrevet. Fortsette?"
Example (Show custom dialog)

The PO file for the Norwegian text contains:
msgid "You should have \".acf\" as extension to the ACF package"
msgstr "ACF-pakken bør ha «.acf» som filendelse"
Then the dialogue shows the Norwegian text like this:

1.2.2. _n( singular ; plural ; count {; arg…} ) ↑
Purpose: Translate strings with singular/plural variants.
- Parameters:
singular,plural,count, optional placeholders. - Returns: Chooses
singularwhencount = 1, otherwiseplural, then applies placeholders.
Example
_n( "%1 item" ; "%1 items" ; Items::Count ; Items::Count )
1.3. Loading and resetting translations ↑
1.3.1. ACF_Load_TranslationPO( pathOrTextOrContainer ) ↑
Purpose: Load/merge a .po file into memory so _()/_n() can translate.
Parameter 1 (required):
- Text path to a
.pofile or - Raw PO text or
- Container holding the PO (prefer TEXT part; falls back to FILE part).
- Text path to a
Returns:
"OK"on success, otherwise an error string.
Typical usage
// Load from a container field in Languages table.
ACF_Clear_Translation
ACF_Load_TranslationPO ( Languages::PO )
// Or from disk (client or server-accessible path).
ACF_Clear_Translation
ACF_Load_TranslationPO ( Get ( DocumentsPath ) & "i18n/en.po" )
Encoding: UTF-8 (BOM handled). Hosted files: If evaluated on Server, ensure the path is reachable by the Server. Session scope: Loads into the calling client’s process; call again after switching user/language.
1.3.2. ACF_Clear_Translation() ↑
Purpose: Clear the in-memory translation catalog and the internal “missing strings” set.
- Parameters: none
- Returns:
"OK" - When to use: Before loading a different language, on logout/login, or to reset state during development.
1.4. Updating the POT from runtime “misses” ↑
1.4.1. ACF_UpdatePOT( pathToPot ) ↑
Purpose: Append any missing strings encountered at runtime (calls to _() / _n() that had no translation) to your POT file. This complements DDRparser by capturing strings you exercised in the UI but hadn’t yet extracted.
- Parameter:
pathToPot— POSIX/absolute path to themessages.potyou want to update (e.g.,/Users/me/Project/TranslationFiles/messages.pot). - Returns:
"OK"on success, otherwise an error string.
What it does
- Loads the existing POT if present.
- Adds all unique runtime-missing
msgids(no duplicates). - Does not remove existing entries; it’s additive.
- Writes the POT back to disk.
- Clears the internal “missing” set so the same keys aren’t re-added next time.
Keep the POT under version control. After
ACF_UpdatePOT, commit changes and let translators update from POT in Poedit.
1.4.2. When to call it ↑
- After a dev/test pass, where you navigated the app and saw base-language strings.
- Right before handing off to translators, to ensure POT completeness.
1.4.3. Recommended path handling (store it once) ↑
Keep a global field (e.g., Settings::gPOTPath) with the POT’s POSIX path so updates are one click.
Set once
Set Field [ Settings::gPOTPath ;
ACFU_SelectFileOnly( ""; _("Select POT file from the translation project (messages.pot)") ) ]
Update button/script
If [ not IsEmpty ( Settings::gPOTPath ) ]
ACF_UpdatePOT ( Settings::gPOTPath )
Show Custom Dialog [ "POT updated" ; "New strings (if any) were merged into the template." ]
Else
Show Custom Dialog [ "Missing path" ; "Set Settings::gPOTPath first." ]
End If
Notes
- Ensure the path is writable by the client (or Server, if evaluated there).
- Call
ACF_UpdatePOTbeforeACF_Clear_Translationif you want to capture the session’s misses.
1.5. End-to-end workflow ↑
Author: Put strings inline in calcs/layouts:
_("Orders"); _("Overwrite “%1”?" ; FileName) _n("%1 item" ; "%1 items" ; n ; n)Extract: Run DDRparser →
TranslationFiles/messages.pot.Translate: Use Poedit (or any PO tool) to create/update
xx.po.Load: In FileMaker, run:
ACF_Clear_Translation ACF_Load_TranslationPO ( Languages::PO ) // from containerIterate: Missing strings show in base language. Optionally call
ACF_UpdatePOT ( pathTo/messages.pot )to merge runtime misses into the POT, then update POs in Poedit.
1.6. Best practices ↑
- Use placeholders (
%1,%2) instead of concatenation — translators can reorder naturally. - Keep msgids stable: avoid trailing spaces/typos; fix early (Poedit can fuzzy-match during updates).
Layout text (FM 20+): Use Layout Calculation:
<<ƒ:_("My orders")>>Value lists: plugin calcs are unstored (not indexable). If you need a value list from translated text, use a stored “shadow” field refreshed by script (Replace Field Contents) or build a per-session UI list.
1.7. Mini script: switch language (container-based) ↑
# Assumes Languages table with Name + PO container
# Script: SwitchLanguage
ACF_Clear_Translation
ACF_Load_TranslationPO ( Languages::PO )
# Optionally refresh any stored “_forVL” fields here, refresh window, etc.
2. Editing in Poedit ↑
The Poedit application is free, but you can add some good additions that help you in the translation work for a low subscription fee. Here is how it looks like:

It can be downloaded from https://poedit.net/download
3. Using DDRparser 1.2.0 to generate and update the POT ↑
DDRparser is a free command-line utility we have made. The command line switch -t tells DDRparser to update the POT by scanning the produced text files. In addition, DDRparser’s default job is to generate a searchable file tree (plain-text files for each script, layout, custom function, etc.), which makes reviews and diffs easy.
We strongly recommend keeping the POT at: (The DDR parser already places this file correctly)
…/<YourOutput>/TranslationFiles/messages.pot
In other words, beside the solution folders (not inside them). This way, both sources can write to the same file:
- DDRparser
-t(extracted strings from code) ACF_UpdatePOT(runtime misses from users/testers)
Nothing “gets lost in translation”. 🙂
3.1. Typical workflow ↑
Run DDRparser (generate text + update POT)
DDRparser \ -i "/path/to/Summary.xml" \ -f "/path/to/ProjectRoot/Parsed" \ -tWhat this does
- Creates/updates the readable export here (solution name is taken from the DDR):
ProjectRoot/Parsed/<SolutionName>/… - Creates/updates the POT here (shared across solutions in the same project):
ProjectRoot/Parsed/TranslationFiles/messages.pot
Notes
-fsets the target folder;SolutionNameis auto-derived from the parsed DDR.-tis additive: it scans the produced text and adds new msgids tomessages.pot(no deletions).Keep a global with the POSIX path to the POT (e.g.,
Settings::gPOTPath = /path/to/ProjectRoot/Parsed/TranslationFiles/messages.pot) so a button can call:ACF_UpdatePOT ( Settings::gPOTPath )to merge runtime “misses” into the same template.
- Creates/updates the readable export here (solution name is taken from the DDR):
Translate Open
messages.potin Poedit (or your TMS) → create/updatenb.po,ja.po,de.po, … (Poedit: Catalog → Update from POT… preserves existing translations and adds the new ones.)Load in FileMaker In your app/script:
ACF_Clear_Translation ACF_Load_TranslationPO ( Languages::PO ) // from a container. # or ACF_Load_TranslationPO ( Settings::gPOPath ) // from disk.Catch any misses during testing If you see base-language strings while clicking around, append them to the POT:
ACF_UpdatePOT ( Settings::gPOTPath ) // points to …/TranslationFiles/messages.potThen “Update from POT” again in Poedit and fill the new entries.
3.2. Recommended structure ↑
ProjectRoot/
Parsed/
SolutionName/ # DDRparser text output (safe to re-generate)
TranslationFiles/
messages.pot # canonical template
nb.po
de.po
ja.po
en.po
…
Keep messages.pot and your *.po files under version control. Re-running DDRparser with -t is additive—it won’t delete existing entries; it only adds new msgids it finds.
3.3. Tips & best practices ↑
- Use placeholders
%1,%2, … in source strings; translators can reorder naturally. - Keep msgids stable (avoid typos/trailing spaces) for clean diffs and fewer fuzzy matches.
- Layout text (FM 20+): use Layout Calculation →
<<ƒ:_("My orders")>>. - Value lists: if you need translated, indexable lists, populate a stored “shadow” field via
Replace Field Contentson language change. Store the POT path once: a global like
Settings::gPOTPath(POSIX path) makes the “Update POT” button trivial:If [ not IsEmpty ( Settings::gPOTPath ) ] ACF_UpdatePOT ( Settings::gPOTPath ) End IfOptional references: if you enabled reference capture in DDRparser, you’ll see
#:lines in the POT (great context for translators).
With this loop—extract → translate → load → update POT from misses—your strings stay discoverable, your translators stay happy, and your app stays multilingual without schema gymnastics.
In short, missed hits turn dynamic text into a guided capture pass: anything DDRparser can’t infer at parse time (because it’s built from data or calcs) is collected the first time it’s shown at runtime. Keep your msgids stable by using numbered placeholders so only the data varies, then run a light sweep (e.g., loop products/categories) to exercise those screens. Finish with ACF_UpdatePOT to merge the unique misses into Parsed/TranslationFiles/messages.pot—store that POT’s POSIX path once in a global (e.g., Settings::gPOTPath) for a one-click update. Translators update the .po, you reload with ACF_Load_TranslationPO, and you’re done. Remember: misses are per session and cleared by ACF_Clear_Translation, so call ACF_UpdatePOT before clearing; it’s additive and safe to run repeatedly (no overwrites, no duplicates).
4. Case: Producing foreign invoices with translated product names. ↑
Generating translation for product names.
Let's say you have a product table with 5000 products. You just need to generate "missed hits" for the product names. Either looping the products or using some other means to collect the strings:
Show All Records
Go to Record/Request/Page [First]
loop
Set Variable [$name; _(Products::name)]
Go to Record/Request/Page [next; exit after last]
end loop
After the loop is finished, we have missed hits for all unique product names.
Use the ACF_UpdatePOT as described above to add the names to the POT file.
Using an AI service to do the actual translation, inspect the result, edit and save to the language PO file. Load it into the container, and reload.
Now, you can use a layout calculation on the invoice layout to display the translated product name:
<<ƒ:_(InvoiceLines::ProductName)>>
Or, you can add a calculated field, unstored, calculation like above, then include this field on the invoice.
If you later add products to the table, and you see invoices with some items not translated. Update the POT, translate, reload and print the invoice again.
