--- eip: 8346 title: Translation Files for ERC-7730 Descriptors description: Translation file format and integrity mechanism for ERC-7730 Clear Signing descriptors localization. author: Alex Forshtat (@forshtat) discussions-to: https://ethereum-magicians.org/t/erc-8346-translation-files-for-erc-7730-descriptors/29072 status: Draft type: Standards Track category: ERC created: 2026-07-22 requires: 7730, 8176 --- ## Abstract [ERC-7730](./eip-7730.md) defines a schema format that allows describing intents and inputs of Ethereum transactions in a human-readable way. This format necessarily includes a significant amount of plain text in strings and templates, which are authored in English. This specification defines the translation file format for ERC-7730 descriptors, how a translation resource is bound to the descriptor and locale it translates, how its integrity is checked, and how wallets look up localized strings at render time. ## Motivation The main purpose of the Clear Signing standard is to define a mechanism that can explain the true meaning and impact behind the bytes being signed by the users' wallets. This equally includes both software and hardware wallets and must not introduce unnecessary trusted parties like automated translation services into the transaction signing process. This means that the text-based part of the Clear Signing description needs to be provided in a language the user is able to understand to achieve the level of clarity required for making an important financial decision. English-only approach to Clear Signing may prove insufficient for a very significant portion of Ethereum users worldwide. Therefore, ERC-7730 needs to support translation of Clear Signing descriptors into multiple languages without embedding the full translation directly in ERC-7730 descriptor file format. Translations may also be produced by parties other than the descriptor author. A single descriptor may eventually be translated into dozens of languages, and requiring the descriptor author to review and republish the descriptor for each of them would not scale. ## Specification ### Translation file format A translation file is a JSON document with the following fields: | Field | Required | Description | |------------------|-----------|--------------------------------------------------------------------| | `$schema` | Yes | URI of the translation file schema. | | `$locale` | Yes | Canonical BCP-47 language tag. | | `descriptorHash` | See below | Hash binding this file to the descriptor it translates. | | `includes` | No | Map from namespace to candidate references to a shared package. | | `translations` | Yes | Flat map from translation key to translated value. | ```json { "$schema": "./erc8346-v3.0.0-next.schema.json", "$locale": "fr", "descriptorHash": "0x14da251e322186245d812ff3fd0b1e9b404c5896964f9dbce2bd2bd898abbe84", "includes": { "erc20": [ { "uri": "./example-erc20.fr.json" } ] }, "translations": { "mytoken.transfer.interpolated_intent": "Envoyer {value} à {to}", "erc20.transfer.to_label": "Bénéficiaire" } } ``` In this example, the local `translations` map carries the descriptor's own key (`mytoken.transfer.interpolated_intent` — the interpolated intent embeds this descriptor's placeholders and phrasing, so no shared package can provide it), while the descriptor's `erc20.*` keys resolve through the included package. The one `erc20.transfer.to_label` entry is an override: this descriptor prefers "Bénéficiaire" over the package's "Destinataire", per the precedence in [Namespaces and includes](#namespaces-and-includes). See [`example-main.fr.json`](../assets/erc-8346/example-main.fr.json) for a complete example, [`example-erc20.fr.json`](../assets/erc-8346/example-erc20.fr.json) for the shared package it includes, and [`erc8346-v3.0.0-next.schema.json`](../assets/erc-8346/erc8346-v3.0.0-next.schema.json) for the JSON schema of the translation file format. The schema's version tag strictly tracks ERC-7730's schema versioning. All BCP-47 language tags — in `$locale` and anywhere else tags appear in descriptors or translation files — MUST use canonical BCP-47 casing (e.g. `fr`, `zh-Hant`, `pt-BR`). A translation resource used as the translation of a descriptor MUST carry a `descriptorHash` equal to the hash of that descriptor, computed according to [ERC-8176](./eip-8176.md). ### Key format Translation keys MUST match the pattern: ``` ^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$ ``` That is: one or more dot-separated segments, each segment starting with a lowercase ASCII letter and containing only lowercase ASCII letters, digits, and underscores (e.g. `erc20.transfer.amount_label`, `swap.confirm_intent`). Keys are only in scope for the descriptor being translated, meaning that uniqueness across unrelated descriptors is not required and carries no meaning: the same key in two descriptors refers to two unrelated strings, each resolved against that descriptor's own translation resources. Within a single descriptor, after resolving ERC-7730 `includes`, each key MUST be associated with exactly one literal. Validators MUST reject descriptors that attach the same key to different literals. A `Key` property is only valid alongside its literal sibling — `labelKey` with `label`, `intentKey` with `intent`, and so on. The literal is the authoritative English source text and the terminal fallback, so validators MUST reject descriptors carrying a `Key` without the corresponding literal. ### Namespaces and includes A namespace is the leading dot-segment of a key — i.e. `erc20` in `erc20.transfer.intent`, `swap` in `swap.confirm_intent`. The `includes` field lets a translation file delegate a namespace to a shared package — a translation file maintained separately, for example, one canonical French vocabulary for ERC-20 descriptors. This allows many descriptors' translations to reuse all translations in a namespace instead of each repeating its own copy. Each `includes` entry maps a namespace to an array of one or more candidate references to the same package; a wallet uses the first candidate it can resolve to a valid package. A shared package is a translation file in the format defined by this specification, with these constraints: * Its `$locale` MUST equal the including file's `$locale`. * It carries no `descriptorHash`. * It MUST NOT contain an `includes` field of its own: includes resolve one level deep. * It is subject to the same [integrity requirements](#translation-resource-integrity) as any translation resource. A key `k` with namespace `ns` resolves as follows: 1. If the including file's own `translations` map defines `k`, use that value. This lets a file override a shared term when its descriptor's context requires different wording. 2. Otherwise, if `includes` has an entry for `ns`, look up `k` in the resolved shared package's `translations` map. Only entries whose namespace is `ns` are considered. There is no further fallback: a key that resolves through neither step renders the whole resource invalid, per [Validity](#validity). ### Placeholders Some strings contain field-value placeholders in the form `{fieldPath}`. The set of placeholders in a translated string MUST be exactly the set of placeholders in its English source: each placeholder MUST appear verbatim, none may be removed, and none may be added. Word order may be rearranged around placeholders to suit the target language. For example: ``` "pool.add_liquidity.interpolated_intent": en: "You are providing {amount} as liquidity to {poolName}" uk: "Ви надаєте ліквідність до {poolName} на суму {amount}" ``` This requirement is a security measure: a translation that omits a placeholder withholds information from the user, and one that adds a placeholder causes the wallet to interpolate field values that the descriptor author did not reference in that string. ### Object-valued translations Some ERC-7730 constructs carry structured display text rather than a single string. Their translation entries are objects instead of strings. *Object-form intents.* An ERC-7730 `intent` may be a JSON object of label/value pairs. The translation value for its `intentKey` is an object mapping each key of the English intent object to an object with `label` and `value` members holding the translated pair: ```json { "staking.withdraw.intent": { "Native Staking": { "label": "Staking natif", "value": "Retirer" }, "Rewards": { "label": "Récompenses", "value": "Consensus et exécution" } } } ``` The translation object's key set MUST exactly equal the key set of the English intent object. *Enumerations.* The display values of an ERC-7730 `metadata.enums` entry are translated as an object mapping each raw enumeration value to its translated display string; the key set MUST exactly equal the key set of the English enumeration. ```json { "lending.interest_rate_mode": { "1": "stable", "2": "variable" } } ``` A shape mismatch — a string entry where an object is expected or vice versa, or a key set differing from the English source — violates this section's constraints. ### Translation resource integrity Translation resource producers attest resources using the Ethereum Attestation Service similarly to [ERC-8176](./eip-8176.md)'s descriptor attestation mechanism, under a dedicated attestation schema. Wallets MUST NOT display strings from a translation resource — including a shared package — that lacks a valid attestation of its `translationHash` from an attester the wallet trusts. #### Translation hash computation To compute the `translationHash` of a translation resource: 1. Let `T` be the parsed JSON translation resource object. 2. Serialize `T` to a byte string using the JSON Canonicalization Scheme. 3. Compute the Keccak-256 hash of the resulting byte string. 4. Encode the hash as a `0x`-prefixed lowercase hexadecimal string (66 characters total). Wallets MUST compute the `translationHash` of the fetched resource themselves and match it against attested hashes. #### Canonical EAS schema | Field | Value | |--------------|----------------------------------------------| | Schema | `bytes32 translationHash` | | Schema UID | TBD | | Resolver | `0x0000000000000000000000000000000000000000` | | Revocable | `true` | | EAS Contract | `0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587` | | Chain | Ethereum mainnet (`chainId = 1`) | ### Discovery Wallets may obtain candidate translation resources for a descriptor from any source. Because descriptor binding and [integrity](#translation-resource-integrity) are enforced on the resource itself, discovery channels need not be trusted, and third parties can publish and attest translations for new locales without any change to the descriptor. ### Validity A translation resource `R` is valid as the translation of descriptor `D` into locale `L` if and only if all of the following hold: 1. `R` validates against the translation file schema. 2. `R.$locale` is a canonically cased BCP-47 tag equal to `L`. 3. `R.descriptorHash` equals the hash of `D` computed according to [ERC-8176](./eip-8176.md). 4. `R` carries a valid attestation per [Translation resource integrity](#translation-resource-integrity). 5. Every entry of `R.includes` resolves to a shared package that itself satisfies the shared package constraints and integrity requirements. 6. `R` is complete: every translation key referenced by `D` (after resolving ERC-7730 `includes`) resolves per [Namespaces and includes](#namespaces-and-includes). 7. Every resolved value passes the [placeholder](#placeholders) and [shape](#object-valued-translations) checks against its English source. Wallets MUST evaluate these checks themselves at render time rather than relying on publication-time validation. Any single failure invalidates `R` entirely, and wallets MUST NOT display individual strings from an invalid resource: translated screens are all-or-nothing, and strings from different locales are never mixed within one descriptor rendering. ### Lookup semantics 1. The wallet builds an ordered locale preference chain using BCP-47 matching and custom preferences.\ The default BCP-47 tag resolution relies on widening the match conditions, e.g.: `zh-Hant-HK` → `zh-Hant` → `zh`.\ Users SHOULD be able to define their own preferences in their wallets, e.g.: `sk` → `cs` → `pl`.\ The chain always terminates in the descriptor's literal English strings. 2. For each locale `L` in the chain, the wallet enumerates candidate resources for `(D, L)` from its [discovery](#discovery) sources, fetches each in turn, and checks [validity](#validity). The first valid resource is selected. Any failure — unreachable URI, schema violation, hash or attestation mismatch, incompleteness — moves on to the next candidate, then to the next locale. 3. If a resource was selected, every field carrying a `Key` property is displayed using its resolved translation. Fields with no `Key` property are not translatable under this specification and are always displayed using their literal value; descriptor authors SHOULD therefore key either all of a descriptor's user-facing strings or none, so that translated renderings are not mixed-language. 4. If no valid resource exists for any preferred locale, the wallet displays the descriptor's literal English strings and SHOULD show a single notice that no translation was available for the user's preferred locales — not a per-string warning. ## Rationale ### Dot-namespaced snake_case Namespacing keys by descriptor family makes keys self-describing and gives `includes` a routing prefix: a wallet can tell which package a key like `erc20.transfer.intent` may resolve against just from its leading segment. snake_case segments match existing key-naming conventions and keep the [Key format](#key-format) pattern ASCII-only and unambiguous regardless of case. Because keys are descriptor-scoped, authors are free to pick readable names without coordinating with anyone. ### All-or-nothing validity Falling back to the English literal for each individually missing key would produce mixed-language renderings on the signing screen: some fields in the user's language, others in English. This degrades comprehension in the exact context this specification exists to protect, and hardware wallets lack the screen space for meaningful per-string warnings. Treating the entire resource as valid or invalid guarantees that every rendering is in a single language. It also makes completeness a property that publication tooling can verify mechanically before a translation is distributed. ### Independent attestation instead of descriptor-pinned hashes An alternative design would pin the expected `translationHash` of each translation inside the descriptor's `$localization` references, so that translation integrity is covered by the descriptor's own [ERC-8176](./eip-8176.md) attestation. This couples every translation to the descriptor release cycle: adding a language or correcting a translation would require editing and re-attesting the descriptor, which does not scale to many locales maintained by independent translation services. Binding in the opposite direction — the translation carries the descriptor's hash and is attested on its own — provides the same descriptor-to-translation binding while allowing any party to publish and attest a new locale without involving the descriptor author. ### No plural or gender forms The flat string map with verbatim placeholders deliberately excludes ICU-MessageFormat-style plural and gender selection. Placeholder values in ERC-7730 are formatted field values — amounts, addresses, dates — where grammatical agreement rarely affects meaning, and the flat form is implementable on constrained hardware wallets and mechanically verifiable against the English source. Translators should phrase strings so they read correctly regardless of the interpolated value. A future revision may add plural support if practice shows it is needed. ### EAS attestations instead of a generic integrity standard [ERC-7730](./eip-7730.md) descriptors are expected to be attested via [ERC-8176](./eip-8176.md) using the Ethereum Attestation Service. Mirroring that same mechanism for translation resources allows wallets to share the attestation-verification code path with the same multi-attester, revocation, and wallet-trust-policy semantics for both descriptors and their translations. ## Backwards Compatibility This specification is new and additive only. The `$localization` field and the `Key` properties it builds on are optional in ERC-7730. Existing ERC-7730 descriptors continue to function and are not affected by this proposal. ## Security Considerations Translation resources are fetched from the same kind of untrusted hosts as the descriptors that reference them and are subject to the same [registry poisoning](./eip-7730.md#registry-poisoning) concerns discussed in ERC-7730. Two requirements defend against these attacks: a resource without a valid attestation from a trusted attester is never displayed, and the `descriptorHash` binding prevents replaying an attested translation against a descriptor other than the one its attester reviewed. A malicious discovery mechanism or host can therefore deny a translation but not alter what the user is shown. Because wallets evaluate [validity](#validity) at render time, a translation that slips past publication-time tooling — for example, one violating the exact-set [placeholder](#placeholders) rule — is still caught before anything is displayed. A shared package included by a translation file is attested independently and may be re-attested with changed content after the including file was reviewed, changing the combined rendering. Wallets accept this by construction — both attesters must be trusted — but attesters of including files should be aware that their attestation covers the reference to the shared package, not the package's future contents. Attestation verification MUST occur within the trust domain of the display. A hardware wallet that receives pre-resolved strings from a companion application is trusting that application unless verification happens on-device or the strings are delivered through a channel the device can authenticate, such as vendor-signed translation packs. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).