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.

  1. Translation functions
    1. Terminology
      1. POT file (PO template)
      2. PO file
    2. Plugin functions
      1. _( text {; arg…} )
      2. _n( singular ; plural ; count {; arg…} )
    3. Loading and resetting translations
      1. ACF_Load_TranslationPO( pathOrTextOrContainer )
      2. ACF_Clear_Translation()
    4. Updating the POT from runtime “misses”
      1. ACF_UpdatePOT( pathToPot )
      2. When to call it
      3. Recommended path handling (store it once)
    5. End-to-end workflow
    6. Best practices
    7. Mini script: switch language (container-based)
  2. Editing in Poedit
  3. Using DDRparser 1.2.0 to generate and update the POT
    1. Typical workflow
    2. Recommended structure
    3. Tips & best practices
  4. 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.

Example

// EN source
_( "Overwrite file “%1”?" ; Packages::FileName )

// NB translation might reorder:
"«%1» vil bli overskrevet. Fortsette?"

Example (Show custom dialog)

Skjermbilde 2025-08-13 kl. 00.00.43

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:

Skjermbilde 2025-08-13 kl. 00.18.22

1.2.2. _n( singular ; plural ; count {; arg…} )

Purpose: Translate strings with singular/plural variants.

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.

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.


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.

What it does

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

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


1.5. End-to-end workflow

  1. Author: Put strings inline in calcs/layouts:

    _("Orders"); _("Overwrite “%1”?" ; FileName)
    _n("%1 item" ; "%1 items" ; n ; n)
    
  2. Extract: Run DDRparserTranslationFiles/messages.pot.

  3. Translate: Use Poedit (or any PO tool) to create/update xx.po.

  4. Load: In FileMaker, run:

    ACF_Clear_Translation
    ACF_Load_TranslationPO ( Languages::PO )   // from container
    
  5. Iterate: 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


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:

Skjermbilde 2025-08-13 kl. 00.04.12

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:

Nothing “gets lost in translation”. 🙂

3.1. Typical workflow

  1. Run DDRparser (generate text + update POT)

    DDRparser \
     -i "/path/to/Summary.xml" \
     -f "/path/to/ProjectRoot/Parsed" \
     -t
    

    What 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

    • -f sets the target folder; SolutionName is auto-derived from the parsed DDR.

    • -t is additive: it scans the produced text and adds new msgids to messages.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.

  2. Translate Open messages.pot in Poedit (or your TMS) → create/update nb.po, ja.po, de.po, … (Poedit: Catalog → Update from POT… preserves existing translations and adds the new ones.)

  3. 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.
    
  4. 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.pot
    

    Then “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

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.