Repository navigation
[messages] Fix incomplete translations, reordered placeholders, and placeholders next to plurals - #1120
[messages] Fix incomplete translations, reordered placeholders, and placeholders next to plurals#1120mosuem wants to merge 4 commits into
Conversation
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.
Summary of ChangesHello, 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
Using Gemini Code AssistThe 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
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 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
|
PR HealthBreaking changes ✔️
This check can be disabled by tagging the PR with 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 Changelog Entry ✔️
Changes to files need to be accounted for in their respective changelogs. This check can be disabled by tagging the PR with License Headers ✔️
All source files should start with a license header. This check can be disabled by tagging the PR with 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.
This check can be disabled by tagging the PR with Unused Dependencies ✔️
For details on how to fix these, see dependency_validator. This check can be disabled by tagging the PR with |
There was a problem hiding this comment.
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.
| 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), | ||
| ]; | ||
| } |
There was a problem hiding this comment.
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.
| 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), | |
| ]; | |
| } |
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
fr.arbhas nobanana)bananareturns "Cerise",cherrythrows aRangeErrorBonjour {last} {first})Bonjour Ada LovelaceBonjour Lovelace Ada{name} has {count, plural, ...})has 3 messagesAda has 3 messagescleaneron a message with argumentsRangeErrorif it changes the lengthChanges
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 thetemplate's message names and placeholders. Updating a translation no longer
changes the generated code; changing the template's API still does.
messages_builder:MessageParser.parseNodecombines text and placeholdersnext to plurals and selects into
StringMessages, instead of dropping theplaceholders.
messages:StringMessage.generateStringapplies thecleanerto eachliteral segment.
pkgs/messages/example; only the hashes change.The commits can be reviewed one by one.
Testing
messages/test/string_message_test.dart,messages_builder/test/message_parser_test.dart, andmessages_builder/test/message_family_test.dart. The cleaner and parsertests fail on
main.dart analyze --fatal-infos,dart format, anddart testpass formessages,messages_builder,messages_serializer, andmessages_shrinker. The example regenerates without a diff and runs.Notes
package:messagesworkflow currently fails onmainbecause of anunrelated info in
examples_flutter/my_application/lib/main.dart(
package:flutter/material.dartis deprecated on Flutter beta).messages_builder. I will rebase that one on top of this.