Skip to content

[messages] Fix incomplete translations, reordered placeholders, and placeholders next to plurals - #1120

Open
mosuem wants to merge 4 commits into
mainfrom
fixTranslationAlignment
Open

mosuem wants to merge 4 commits into
mainfrom
fixTranslationAlignment

Conversation

@mosuem

@mosuem mosuem commented Oct 30, 2026

Copy link
Copy Markdown
Member

Note

This PR was generated by an AI coding agent (Jetski) on behalf of @mosuem.

Fixes several bugs that make translated messages render incorrectly. They all
show up as soon as translations come from a translation pipeline, where
translations are often incomplete and reorder placeholders.

Bugs

Case Before After
A translation is missing a message (fr.arb has no banana) Lookups after the gap shift: banana returns "Cerise", cherry throws a RangeError Falls back to the template's message
A translation has a message which is not in the template Same shift Dropped, with a warning
A translation reorders placeholders (Bonjour {last} {first}) Bonjour Ada Lovelace Bonjour Lovelace Ada
A message mixes placeholders with a plural or select ({name} has {count, plural, ...}) The placeholder is dropped: has 3 messages Ada has 3 messages
A cleaner on a message with arguments Ignored, or a RangeError if it changes the length Applied to each literal segment, arguments are untouched

Changes

  • messages_builder: groupIntoFamilies (new, lib/message_family.dart)
    groups the ARB files by context and aligns every translation with its
    template: the same messages in the same order (missing ones fall back to
    the template, extra ones are dropped), with argument indices remapped to the
    template's placeholder order. Both the data file builder and the code
    generator use it, so data files and generated code agree.
  • messages_builder: all data files of a context now share a hash of the
    template's message names and placeholders. Updating a translation no longer
    changes the generated code; changing the template's API still does.
  • messages_builder: MessageParser.parseNode combines text and placeholders
    next to plurals and selects into StringMessages, instead of dropping the
    placeholders.
  • messages: StringMessage.generateString applies the cleaner to each
    literal segment.
  • Regenerated pkgs/messages/example; only the hashes change.

The commits can be reviewed one by one.

Testing

  • New tests: messages/test/string_message_test.dart,
    messages_builder/test/message_parser_test.dart, and
    messages_builder/test/message_family_test.dart. The cleaner and parser
    tests fail on main.
  • dart analyze --fatal-infos, dart format, and dart test pass for
    messages, messages_builder, messages_serializer, and
    messages_shrinker. The example regenerates without a diff and runs.

Notes

  • The package:messages workflow currently fails on main because of an
    unrelated info in examples_flutter/my_application/lib/main.dart
    (package:flutter/material.dart is deprecated on Flutter beta).
  • This overlaps with the draft Update messages #1081, which touches the same files in
    messages_builder. I will rebase that one on top of this.

The cleaner was ignored for messages with arguments, and threw a
RangeError if it changed the length of the text. It is now applied to
each literal segment, and the arguments are inserted unchanged.
Placeholders were dropped from messages which also contain a plural or
select at the same level, such as `{name} has {count, plural, ...}`.
Text and placeholders between plurals and selects are now combined into
StringMessages.
The generated code looks up messages by their index in the template and
passes arguments in the order of the template's placeholders, but each
data file was serialized on its own. So a translation which was missing
a message, or had an extra one, returned the wrong messages, and a
translation which reordered placeholders got the wrong arguments.

Translations are now aligned with their template: missing messages fall
back to the template, extra messages are dropped, and argument indices
are remapped to the template's placeholder order.

All data files of a context now share a hash of the template's message
names and placeholders, so updating a translation does not change the
generated code.
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses several critical issues in the message translation pipeline, specifically targeting incorrect rendering caused by incomplete translations, reordered placeholders, and complex message structures involving plurals or selects. By introducing a family-based alignment system, the builder now ensures that all translations strictly adhere to the template's structure and argument ordering. Additionally, the parser and generator logic have been improved to correctly handle placeholders adjacent to conditional logic and to ensure deterministic code generation.

Highlights

  • Message Alignment: Introduced groupIntoFamilies to align translation files with their templates, ensuring consistent message ordering, handling missing messages via fallback, and dropping extraneous entries.
  • Placeholder Handling: Fixed bugs where placeholders were dropped when appearing alongside plurals or selects, and ensured argument indices are correctly remapped when translations reorder placeholders.
  • Deterministic Generation: Updated data file hashing to depend only on template structure, preventing unnecessary code regeneration when only translations are updated.
  • Message Cleaning: Updated StringMessage.generateString to apply cleaners to literal segments individually, preserving argument integrity.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@github-actions

Copy link
Copy Markdown

PR Health

Breaking changes ✔️
Package Change Current Version New Version Needed Version Looking good?

This check can be disabled by tagging the PR with skip-breaking-check.

Coverage ✔️
File Coverage

This check for test coverage is informational (issues shown here will not fail the PR).

This check can be disabled by tagging the PR with skip-coverage-check.

Changelog Entry ✔️
Package Changed Files

Changes to files need to be accounted for in their respective changelogs.

This check can be disabled by tagging the PR with skip-changelog-check (for example, for non-user-visible changes when the package version is already at -wip).

License Headers ✔️
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
Files
no missing headers

All source files should start with a license header.

This check can be disabled by tagging the PR with skip-license-check.

API leaks ✔️

The following packages contain symbols visible in the public API, but not exported by the library. Export these symbols or remove them from your publicly visible API.

Package Leaked API symbol Leaking sources

This check can be disabled by tagging the PR with skip-leaking-check.

Unused Dependencies ✔️
Package Status
None No packages found to check.

For details on how to fix these, see dependency_validator.

This check can be disabled by tagging the PR with skip-unused-dependencies-check.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request aligns translations with their templates to ensure consistent message ordering, correct argument indexing (even when placeholders are reordered), and stable hashes that only depend on the template's structure. It also fixes an issue where placeholders were dropped from messages containing plurals or selects, and updates StringMessage.generateString to apply the cleaner function to each literal segment. Feedback highlights a potential non-determinism issue in groupIntoFamilies due to file system loading order, which could lead to flaky builds and unstable generated filenames; sorting the files by path is recommended to ensure deterministic builds.

Comment on lines +44 to +54
List<MessageFamily> groupIntoFamilies(
List<LocatedMessageFile> messageFiles, {
void Function(String message)? warn,
}) {
final groups = messageFiles
.groupListsBy((messageFile) => findTemplate(messageFiles, messageFile));
return [
for (final MapEntry(key: template, value: members) in groups.entries)
_alignFamily(template, members, warn),
];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The order of messageFiles can be non-deterministic because it is loaded from the file system (via Directory.list()). This non-determinism can propagate to the order of MessageFamily groups returned by groupIntoFamilies. In builder.dart, if the message context is null, the generator uses a counter to name the files (e.g., m${counter++}_messages.g.dart). A non-deterministic family order will result in non-deterministic filenames across builds, causing flaky builds and unnecessary git diffs. Sorting messageFiles by their path at the beginning of groupIntoFamilies ensures deterministic grouping and file generation.

Suggested change
List<MessageFamily> groupIntoFamilies(
List<LocatedMessageFile> messageFiles, {
void Function(String message)? warn,
}) {
final groups = messageFiles
.groupListsBy((messageFile) => findTemplate(messageFiles, messageFile));
return [
for (final MapEntry(key: template, value: members) in groups.entries)
_alignFamily(template, members, warn),
];
}
List<MessageFamily> groupIntoFamilies(
List<LocatedMessageFile> messageFiles, {
void Function(String message)? warn,
}) {
final sortedFiles = messageFiles.toList()..sortBy((file) => file.path);
final groups = sortedFiles
.groupListsBy((messageFile) => findTemplate(sortedFiles, messageFile));
return [
for (final MapEntry(key: template, value: members) in groups.entries)
_alignFamily(template, members, warn),
];
}

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant