Translating Objects in the Thinkwise Software Factory
Every translatable thing in a model — a table, a column, a domain element, a task, a report
parameter, a menu, a validation message, and roughly 150 other concepts — gets exactly one
transl_object row, keyed by (model_id, branch_id, type_of_object, transl_object_id). Each
transl_object then has one transl_object_transl child row per configured application
language, keyed by the same four fields plus appl_lang_id, holding the actual translated text.
Everything in this skill was confirmed live against a real connected model (INSIGHTS, branch
MAIN) via sf_mcp — not guessed from documentation.
Apply this whenever an MCP connector with Software Factory access is used to read, write, generate,
or review translations. Follow the connector's standard discovery→act flow; never guess entity/task/
property names. transl_object, transl_object_transl, and branch_appl_lang live in a
manage_translation-style domain (confirmed sf/manage_translation live); the master appl_lang
list (the global catalogue of IETF language tags, independent of any model) lives in a
manage_datamodel-style domain instead — try these directly first, and only escalate to
search_capabilities/get_available_domains on an entity_set_not_found/domain_not_found-style
rejection rather than re-discovering a domain that already resolved earlier this session. This skill
is connector-agnostic: it names entities, tasks, and fields, not any one connector's literal tool
names.
Golden rule — several moves here need the user's word, not the assistant's judgment
The mechanics of reading and writing translation rows are simple API calls. A handful of moments in
that flow are design or process decisions that belong to the user, not something to resolve
silently by picking whatever seems safest — the thinkwise_software_factory_mcp_base
"Confirm-before-mutate" convention applied at this skill's own grain:
- Hand-creating a
transl_object. The platform generates these automatically in almost every case; hand-inserting one is a last resort and requires confirming with the user first — the object, thetype_of_objectyou intend to use, and why the automatic paths didn't apply (see "Before anything else" below). - Deciding
help_textcoverage scope. Filling inhelp_textfor one language is an implicit promise to cover every configuredbranch_appl_langor knowingly leave the rest without it — ask the user which before writing anyhelp_text(see "Help text" below). - Disapproving a translation.
task_mark_transl_object_transl_disapprovedhas a mandatoryfeedbackstring — there is no way to disapprove without a reason, so that reason has to come from the user (or genuinely reflect their stated concern), not be invented to satisfy the parameter.
Only skip asking when the user has already stated the answer unambiguously.
Before anything else: find the existing object — don't create one
Nearly every translatable object already has an auto-generated transl_object row the moment
it's created in the model — even if nobody has ever translated it, that row exists, holding
[bracketed]-placeholder text (see "Detecting untranslated objects" below). If you've been
asked to translate something, a row for it almost certainly already exists. The job is almost
always to find that row and overwrite its placeholder transl_object_transl text — not to add a
new transl_object. The platform generates translation objects; you very rarely should.
Mandatory sequence, in order:
- Identify the object's kind and id (a table, a column, a menu item, a role, …).
- Look up the candidate
type_of_objectvalue(s) in the lookup table below. If that kind is flagged as having a trap (its real translation lives under a differenttype_of_objectthan its own name suggests), check the trap target first, not the literal name. - Query
transl_objectfiltered to just the baretransl_object_id(notype_of_objectfilter) to see every type it's actually registered under, and cross-check the results against the lookup table — don't trust the first hit or assume "no row at the obvious type" means "no row anywhere." - If a row exists, that's the one to edit. Fetch its
transl_object_translfor the target language(s). If the field(s) show the[bracketed]placeholder, overwrite that text — that's the actual translation work. If it already holds real, non-bracketed text, it's already translated; leave it alone unless explicitly asked to change existing text. - Only if step 3 finds nothing at all, across every plausible type for that kind, is the row
genuinely missing. Reach for the platform's own generation path first — the object's own
generate_transl_object-style creation parameter, or the matchingtask_generate_transl_objectsvariant (see "Generating translation objects" below). That's still the platform creating the row, not you hand-authoring one. - Hand-inserting a
transl_objectrow directly is the last resort, and requires confirming with the user first: name the object, thetype_of_objectyou intend to use, and why the automatic paths in step 5 didn't apply. Never create one silently — see "Backfilling a missingtransl_objectby hand" below for the mechanics once confirmed.
About to add a new transl_object? Stop. Re-run the bare-id search from step 3 across the
lookup table's candidates first. In the overwhelming majority of cases the row already exists —
either under a type_of_object you weren't expecting, or holding placeholder text that was
mistaken for "missing."
Bulk-importing translations in one call — check for a custom task first
Some models have a rare, custom-built task that upserts a whole batch of translations in one JSON
call instead of writing each row individually. For the full workflow, JSON payload shape, and its
verified gotchas, read references/bulk_import.md before assuming this path is or isn't available
in the model you're working with.
The two core entities
transl_object — one row per translatable object
| Field | Type | Notes |
|---|---|---|
model_id, branch_id |
key | |
type_of_object |
key, Edm.Int32 enum |
Which kind of model concept this is — see below. |
transl_object_id |
key, string | The object's own ID (a column's col_id, a table's tab_id, a domain element's elemnt_id, …). Not qualified by owner — see the naming gotcha below. |
transl_object_description |
string | Free-text description of the translation object itself (not a translation). |
insert_user/insert_date_time/update_user/update_date_time |
trace | |
generated_by_control_proc_id |
string | Set when the object/its translations were produced by a control procedure rather than authored directly. |
Bound tasks: task_generate_transl_objects (zero params — backfills a missing transl_object for
an object that already exists in the model but has none yet, e.g. something created outside the
normal flow via a dynamic model), task_show_history, task_unlink_generated_object.
transl_object carries roughly ninety detail_ref_transl_object_<parent> navigation properties (to
col, tab, dom_elemnt/elemnt, task, report, menu, cube_view, screen_type,
conditional_layout, role, and many more) — this is how a transl_object row is actually reached
from its owning model object; don't try to derive transl_object_id from the parent's key structure
by convention, confirm it via the correct navigation property or by reading transl_object_id
directly off a query result.
transl_object_transl — one row per object per language
| Field | Type | Shown where (per platform docs, cross-checked live) |
|---|---|---|
model_id, branch_id, type_of_object, transl_object_id |
key | Same as parent transl_object. |
appl_lang_id |
key, string | IETF tag, e.g. en-US, nl-NL. |
transl |
string | General-purpose single label; the fallback used wherever no more specific field is set. |
transl_form |
string | Form labels, formlists, task/report parameter pop-ups. |
transl_grid |
string | Grid column headers. |
transl_card_list |
string | Card list headers (undocumented in the platform docs pulled for this skill, but a real, distinct field — confirm intent with the user before assuming it always mirrors transl). For what card_list_label/card_list_order_no/etc. actually control on the card itself, see thinkwise_software_factory_subject_components. |
transl_plural |
string | List/detail tab headers, card list headers — see Plural forms below. |
tooltip_text |
string | Hover hint; basic HTML allowed. |
help_text |
string | Longer-form, feeds Help screens — see Help text below. |
approval_status |
Edm.Byte enum: not_yet_approved=0, approved=1, disapproved=2 |
Per-language, per-object review state. |
feedback |
string | Reviewer's note when disapproving. |
insert_user/insert_date_time/update_user/update_date_time |
trace | |
generated_by_control_proc_id |
string |
Which text fields are actually live depends on type_of_object. The API marks each of
transl/transl_form/transl_grid/transl_card_list/transl_plural as editable-and-mandatory or
hidden per record, and this varies by the owning object's type_of_object rather than being fixed —
confirmed live: a tab (0) row carries transl+transl_plural; a col (1) row carries
transl+transl_form+transl_grid+transl_card_list (no transl_plural); a task_parmtr (32) row
carries only transl+transl_form; most other types (dom_elemnt, task, list_bar, menu,
scheduler_view, …) carry transl alone. Fetch the record first and set only the fields it reports
as editable/mandatory — writing a fixed set of "all five fields" on every object either fails on
fields the object doesn't have, or silently sets fields the UI never shows.
Bound tasks (all zero-parameter except the one noted): task_enrichment_auto_transl ("translate with
AI" for this one row — drafts from an already-translated language), task_generate_transl_objects,
task_mark_transl_object_transl_approved, task_mark_transl_object_transl_disapproved (mandatory
feedback string parameter — cannot disapprove without a reason), task_show_history,
task_transl_selected_objects ("translate by ID" — derives a label from the object ID: underscores
to spaces, first letter capitalized), task_unlink_generated_object.
Verified live — table activity in model INSIGHTS, branch MAIN — surfaced three points made
elsewhere in this skill at once:
- Eight configured languages, not the shipped six — including a regional variant (
pt-BR, not plainpt) and a non-Latin script (ja-JP). transl_pluralauthored independently per language, not derived fromtransl(see Plural forms below).help_textfilled for only 2 of the 8 languages (en-US,nl-NL) — see Help text below.
Detecting untranslated objects — look for the bracketed placeholder, not a blank field
A newly generated transl_object_transl row is not blank. The platform auto-fills every mandatory
text field with the object's own ID wrapped in square brackets — [activity], [employee_id],
[employee_schedule_add_activity] — as a fallback label, and that bracketed placeholder is what
still shows in the Software Factory's own UI for text nobody has actually translated. Confirmed live: querying
transl_object_transl for one language in a small custom model returned zero rows with transl
null or empty, but fifty-five rows across transl/transl_form/transl_grid/transl_card_list/
transl_plural matching startswith(field,'[') and endswith(field,']'). That bracket test is the
real "still needs translating" query — a null/empty check alone will under-report almost everything.
Exception, also confirmed live: a gui_object (type_of_object = 13) with transl set to a
single literal space (not empty, not bracketed) was not a missed translation — it was an
intentionally blank label on a layout/spacer control, where a real caption would introduce an
unwanted header in the UI. Before "fixing" any blank-looking (not bracketed) translation, check
whether the owning object is this kind of spacer/filler control rather than assuming it's simply
untranslated.
type_of_object — one enum, ~150 values, spanning almost the whole model
type_of_object is not specific to tables/columns/domain elements — it is the same enum used to key
every translatable concept in the platform, and it grows across platform versions. Always fetch
the current values live — get_entity_definition (or the connector's equivalent metadata call)
against transl_object/transl_object_transl's type_of_object property/domain — rather than
trusting any hardcoded list, including the one in the reference file below.
For the full type_of_object value table and its known traps (tab_label, list_bar_grp,
cube_field, rename-orphan, and more), read references/type_of_object_lookup.md before writing a
type_of_object filter or interpreting a lookup result.
Naming — the object ID is the translation key
Because task_transl_selected_objects ("translate by ID") derives a label straight from
transl_object_id, a well-named object is half-translated before anyone types anything —
sales_order_line_status becomes "Sales order line status" for free.
Gotcha, confirmed live: transl_object_id for a column is the bare column ID — not qualified by
table. Querying the real INSIGHTS model for col rows with col_id eq 'description' returns ten
different tables (activity, country, declaration, declaration_lines, employee_function,
employee_item, hour, hours_with_customer, meeting, project, …) all sharing that column name.
Querying transl_object_transl for type_of_object eq 1 and transl_object_id eq 'description'
returns exactly one row per language — meaning all ten tables' description columns render the
same translated text, and editing any one of them changes it everywhere at once. This is by design
when the columns really do mean the same thing (a generic "description" field genuinely should read
identically everywhere) — but it's a live trap the moment two same-named columns need to diverge.
Two fixes, confirmed against the platform's own translation model:
- Rename the column so its ID is specific to what it actually holds (
loon_component_codeinstead of a barecodereused elsewhere with different meaning) — the default choice, and it also sits well withtask_transl_selected_objectsauto-deriving a better label for free. - Point
col.alt_transl_col_idat a different column's ID to deliberately share a translation with something else, or add a genuinely new column and translate it independently, if the source column must keep its generic ID for another reason (e.g. it's shared through a domain on purpose).
Domain elements (type_of_object = dom_elemnt, value 2): the transl_object_id is the element's
own ID. Real IDs pulled live from INSIGHTS include both plain descriptive IDs (approved,
approve_working_hours) and numeric-prefixed ones (1_project, 2_customer) — the latter pattern
sacrifices some of the "ID carries all descriptive meaning, database value stays a meaningless
sequential integer" convention (see the thinkwise_datamodeling_guidelines skill's Domain elements
section) for explicit, stable ordering. Match whatever convention the model already uses rather than
mixing both within one domain.
Avoid the literal word "ID" in translated text
Translated labels (transl/transl_form/transl_grid/transl_card_list/…) should read as natural
language for an end user, not restate the database naming convention. An object like employee_id
should normally translate to "Employee", not "Employee ID" — the underlying column name already
encodes that it's an identifier; the label doesn't need to repeat it. Only keep "ID" in the
translated text when it's genuinely the only unambiguous option — e.g. an external reference number
where "ID" (or a more specific term like "order number") is the actual meaning being conveyed, or a
screen that shows a name and a numeric identifier side by side and needs the second one distinguished
from the first.
Plural forms — authored per language, not derived
transl_plural is its own field, filled independently per appl_lang_id — the live activity
example above shows eight different, grammatically-correct plural strings (Aktivitäten,
Activities, Actividades, Activités, Attività, アクティビティ, Activiteiten, Atividades),
none of which the platform derived from transl. Where it's used: list/detail tab headers, card list
headers — anywhere the UI names a collection of rows rather than one row.
Link tables shown as a detail tab get a directional plural. Per platform docs: write
transl_plural as singular/plural (e.g. Persons/Companies for a person↔company link table) — the
UI shows the second half when the tab is opened from the "singular" side (a person's record shows a
"Companies" tab) and the first half from the other side (a company's record shows a "Persons" tab).
Because pluralization grammar differs per language (English's irregular plurals, German/Dutch
compounding, gendered agreement in French/Spanish/Portuguese/Italian), a plural correct in one
language says nothing about another. If task_enrichment_auto_transl (AI) is used to backfill a
plural into a new language, treat it as a draft, not a commit — review it before approving, same as
any other AI-drafted translation.
Verify after a combined write, confirmed live: another instance of the general "a multi-field
write can silently drop one field" behavior in thinkwise_datamodeling_guidelines's quirks section —
setting transl and transl_plural together in one write occasionally left transl_plural reverted
back to equal the just-set singular transl instead of the plural value that was also specified in
the same write — reproducible on 2 of 6 table records in one session, not a one-off typo. Re-read the
field back after this kind of combined write and re-apply transl_plural if it doesn't match before
committing; don't assume a multi-field write that returned success actually applied every field as
given. Re-tested and fixed, verified live: setting transl_plural after transl in the same
combined call avoided the revert entirely — both fields held correctly with no follow-up patch needed.
Order the properties this way instead of budgeting for a second corrective write.
Help text — leave approval_status review aside, default to empty
help_text is a distinct, longer-form field tied to Help screens — not a longer tooltip. The live
activity example makes the real cost concrete: help_text is filled with a genuine, well-written
definition-plus-example for exactly two of the eight configured languages (en-US, nl-NL); the
other six (de-DE, es-ES, fr-FR, it-IT, ja-JP, pt-BR) simply have none. That's not
necessarily a bug — it may be a deliberate call that only two languages' user bases need the extra
explanation — but it is exactly the failure mode to design against: every help_text filled in one
language is an implicit promise to either fill it in every other configured language, or accept that
most of your users never see it.
Convention: leave help_text empty by default. Reach for it only when a field's behavior is
genuinely non-obvious — a calculation the label can't convey, a workaround, a non-obvious
precondition. A clear object name (see Naming above) plus a short tooltip_text covers the large
majority of fields. Before filling in help_text for a new object, either commit to authoring it for
every language the branch has configured (query branch_appl_lang to know how many that is — see
below) or flag to the user that it will be English/source-language-only for now.
Multiple languages
Adding a new language? Check whether it's officially supported before translating anything by
hand. Thinkwise ships a pre-translated base model per officially supported GUI language
(GUI_TRANSL_<lang>, plus a narrower <RDBMS>_MSG_TRANSL_<lang> for database error text on some
RDBMS/language combos) — linking and merging that base model back-fills the platform's own standard
translations for free. Manual translation via this skill is then only needed for the application's
own objects. See references/officially_supported_languages.md for the live-verified language
↔ base-model mapping and the full link → merge → generate recipe before doing any manual work on a
newly-added branch_appl_lang.
appl_lang (in a manage_datamodel-style domain, model-independent) is the global catalogue:
appl_lang_id (the IETF tag, e.g. en-US, key) and appl_lang_description. branch_appl_lang
(in manage_translation) links one of those to a specific (model_id, branch_id) — it's how a branch
declares which languages it actually supports; keyed by (model_id, branch_id, appl_lang_id), no
translatable fields of its own beyond generated_by_control_proc_id. Query branch_appl_lang first
to know how many languages a help_text decision (above) or a plural-review pass (above) actually
needs to cover — don't assume the shipped six (nl-NL/en-US/de-DE/fr-FR/es-ES/pt-PT); the
live INSIGHTS example has eight, including it-IT, ja-JP, and pt-BR instead of pt-PT.
Cleaning up orphaned translation objects. branch_appl_lang carries a zero-parameter bound task,
task_delete_unused_transl_objects ("Delete unused translation objects — Delete unused translation
objects for the entire branch"), verified live: it deletes every transl_object/transl_object_transl
row whose underlying model object no longer exists, branch-wide, in one call. The most common source of
these orphans is renaming something — a rename task changes an object's id but doesn't clean up the
old id's translation rows, which then sit around indefinitely holding stale bracket-placeholder text
under an id nothing points to anymore. Reach for this task after any rename, or as a periodic branch-wide
sweep, rather than hunting down a specific orphaned row by hand once you happen to notice one. This
task can be entirely invisible without the right Software Factory role/rights on the connected
account — confirmed live: branch_appl_lang's bound-task list came back empty and a direct lookup by
this task's exact name returned a hard "not found," indistinguishable from the task simply not
existing, until the account's rights were adjusted — see thinkwise_datamodeling_guidelines's
role/rights quirk for the general version of this trap.
Generating translation objects. New objects normally get a transl_object automatically on
creation — confirmed live on tab's own task_create_tab_variant, which carries a
generate_transl_object: Edm.Boolean parameter controlling exactly this. For anything created without
going through such a task (most commonly dynamic-model objects), call the relevant
task_generate_transl_objects variant afterward — it exists both as a generic bound task on
transl_object/transl_object_transl and as dozens of narrower unbound variants scoped to a specific
parent (generate_transl_objects_help_index_help_index_id,
generate_transl_objects_tab_check_constraint_check_constraint_id,
generate_transl_objects_module_grp_module_grp_id, and many more) — use
search_domain_capabilities in sf/manage_translation to find the variant matching the parent object
type actually in play rather than assuming the generic one always applies.
Confirm the row is actually missing before reaching for this recipe. Whether a dependent-record-added
list_bar_grp/list_bar_item gets an auto-generated transl_object for free is inconsistent across
sessions — one session found zero rows immediately after creating and committing the group/item (the
case this recipe was built for), another found a real bracket-placeholder already there with no extra
steps, for the same kind of object created the same way. Run the bare-id query from step 3 at the top
of this skill first; only fall through to hand-backfilling below if that query genuinely comes back
empty.
Backfilling a missing transl_object by hand — the working recipe, verified live for a
list_bar_item (a case earlier assumed to be a dead end):
- Add the
transl_objectrow directly (model_id,branch_id,type_of_object,transl_object_idmatching the target object's own id) — this succeeds as a plain add, no bound task needed. - Run the generic
task_generate_transl_objects, bound to that exacttransl_objectrow you just created (zero params). This is the key step that's easy to skip: it's not the narrow per-parent-type unbound variant (generate_transl_objects_<parent>_<parent>_id-shaped) — that narrow variant can fail on its own hidden mandatorybranch_idwith no exposed way to target one record. The generic bound task ontransl_objectsucceeded where the narrow variant didn't, and produced the bracket-placeholdertransl_object_translrow for every configured language. - Edit that
transl_object_translrow normally (it's now an existing row addressed by its full key includingappl_lang_id, not a fresh add) — the fields patch and commit like any other translation.
Why hand-adding transl_object_transl directly (skipping step 2) fails: on a brand-new add,
appl_lang_id shows as hidden-yet-mandatory and rejects any attempt to patch it — a real, confirmed
dead end for that specific path. The fix isn't to abandon the API and go to the Software Factory's own
UI, it's to route through step 2's generate task instead of trying to hand-craft the child row.
If a future case still has no working path after trying all three steps above, then treat manual translation directly in the Software Factory as the practical fallback — but confirm that by actually attempting this recipe first, since it resolved what previously looked unrecoverable.
Review workflow. approval_status starts not_yet_approved on a new/changed translation.
task_mark_transl_object_transl_approved (zero params) approves it; disapproving requires
task_mark_transl_object_transl_disapproved with a mandatory feedback string — there is no way to
disapprove without leaving a reason. Treat approval_status as the gate for whether a set of
translations is safe to consider final, especially before a merge — an edited, previously-approved
translation resets to not_yet_approved (per platform docs; not independently re-verified live in
this session), so a stale "approved" count is a real risk signal, not just paperwork.
Fallback language. Referenced in platform documentation (used when a user's own language has no
translations in a given application) but its storage location was not found during live
verification in either sf/manage_translation or sf/manage_datamodel — it did not surface as a
field on branch, appl_lang, or branch_appl_lang, and no sf_configuration-style entity was
reachable from either domain in this session. Confirm its actual entity/field (or that it's a setting
only exposed in the Software Factory's own UI, with no exposed API surface) live before scripting
anything that depends on it, rather than assuming a name from documentation alone.
Parameterized labels. Per platform docs, transl_form (and the group/section label fields
surfaced via col/report_parmtr/task_parmtr's form_next_grp_label/grid_next_grp_label
lookups above — see thinkwise_software_factory_subject_components for what those group/section
fields actually do on a Form or Grid) can carry {placeholder} tokens resolved with locale-aware
date/number formatting.
Not recursive, resolved exactly once, and an undefined/null parameter is silently dropped rather than
breaking the string — not independently re-verified live in this session, treat as documented
behavior to confirm if a parameterized label misbehaves.
Pre-flight checklist
- Adding a new language that's on the officially supported list? Link its
GUI_TRANSL_<lang>base model (and matching<RDBMS>_MSG_TRANSL_<lang>if one exists for the branch's RDBMS), merge it into the work model, and generate — before doing any manual translation. Seereferences/officially_supported_languages.md. - Never create a
transl_objectas your first move. Search first: query the baretransl_object_idacross every plausibletype_of_objectfrom the lookup table above, not just the obvious one, and look for an existing (possibly[bracketed]) row before concluding one is missing. Hand-creating atransl_objectis a last resort that requires confirming with the user first — see "Before anything else: find the existing object" above. - Confirm the domain keys before querying:
transl_object/transl_object_transl/branch_appl_langin amanage_translation-style domain,appl_langin amanage_datamodel-style domain — don't assumesf/manage_translation/sf/manage_datamodelhold on every connector. transl_object_idfor a column is the bare column ID, not qualified by its table — before editing a column's translation, check whether other tables reuse the samecol_id(querycolfiltered on thatcol_idacross the model) and confirm the edit is meant to apply everywhere it's used, or rename/pointalt_transl_col_idelsewhere first.- Check for
alt_transl_*/*_next_grp_label/*_next_tab_label/*_confirm_button/*_cancel_buttonon the parent object before concluding an object has only one translation — many do not. form_next_grp_label/next_tab_labelresolve againsttype_of_object = tab_label(33), not whatever type the owning object is — confirmed live. Don't translate the owning column/parameter's owntype_of_objectrow and assume that's the group/tab-section label; verify whichtype_of_objectactually holds a[bracketed]placeholder for that literal label text before writing to it. Committing to the wrong type_of_object succeeds with no error and leaves the real UI-visible label untranslated.- A
list_bar_grp's real translation istype_of_object = list_bar(12), notlist_bar_grp(78) — confirmed live, the same shape of mismatch astab_labelabove. Query alltype_of_objectvalues for the bare group id and cross-checkinsert_date_timeagainst the group's own creation to find the real, auto-generated row before translating.list_bar_item(79) has no navigation property ontransl_objectat all and no known working consumer — a table/report-linked menu item just shows the linked object's own translation. tile_grp/module_grp/tab_taskdo not share thelist_bar_grptrap — confirmed live.tile_grp(87) andmodule_grp(6) group headers translate under their own type directly, no redirect.tile(84) has the same "no working path" shape aslist_bar_item.tab_task(110) has notransl_objectnavigation property at all — a table-bound task's label is the parenttask's (11) own translation, not atab_task-scoped row.- A
cube_field's label (25) is independent of itscol_idcolumn's own translation (1) — translating one does nothing for the other, and multiplecube_fieldrows sharing onecol_ideach need their own translation.cube_viewistype_of_object = 26. - A rename task cascades functional references but not translations — after renaming a
translatable object through its dedicated rename task, check for and delete the old id's orphaned
transl_object/transl_object_translrows rather than assuming the rename handled them. transl_pluralis never derived — author it explicitly per language, and for a link table shown as a detail tab, use thesingular/pluraldirectional format.- Avoid the literal word "ID" in translated text (e.g.
employee_id→ "Employee", not "Employee ID") — only keep it when it's the sole unambiguous option. - To find objects that still need translating, look for
[object_id]-bracketed placeholder text (see "Detecting untranslated objects") — not blank/null fields — and don't reflexively overwrite a literal single-space value, which can be an intentional blank spacer label. - Field editability (
transl/transl_form/transl_grid/transl_card_list/transl_plural) depends ontype_of_object— fetch the record and set only what it reports as editable, don't assume all five apply. - Set
translfirst andtransl_plurallast in the same combined write — verified live, this avoids the revert that a separate follow-up patch was previously needed to catch. - Default
help_textto empty. Filling it in commits to either covering every language on the branch (branch_appl_lang) or knowingly leaving the rest without it — ask the user whether to commit to authoringhelp_textfor every configuredbranch_appl_lang, or to explicitly accept English/source-language-only coverage, before writing anyhelp_text, rather than picking one silently. task_mark_transl_object_transl_disapprovedrequires afeedbackstring — it will not commit without one.- New object with no translations showing up? Run the matching
task_generate_transl_objectsvariant for its parent type (viasearch_domain_capabilities) before assuming something else is broken — most object-creation tasks already do this via agenerate_transl_object-style parameter, so check that first. - No
transl_objectexists at all yet? Add it by hand, then run the generic zero-paramtask_generate_transl_objectsbound to that row (not a narrow per-parent-type variant) — see "Backfilling a missingtransl_objectby hand" above. Don't jump to hand-addingtransl_object_transldirectly; that path'sappl_lang_idfield is hidden-yet-mandatory on add and will block the commit. - Fallback-language configuration's storage location is unconfirmed — don't script against a guessed field/entity name for it; verify live first (or confirm with the user that it's only exposed in the Software Factory's own UI).