41 std::map<std::string, std::string, std::less<>>
messages;
50[[nodiscard]]
inline std::optional<std::string>
catalogueLocale(std::string_view fileId) {
55 std::string_view
const locale =
57 auto const letter = [](
char const symbol) {
58 return (symbol >=
'a' && symbol <=
'z') || (symbol >=
'A' && symbol <=
'Z');
60 bool const valid = letter(locale.front()) && std::ranges::all_of(locale, [&letter](
char const symbol) {
61 return letter(symbol) || (symbol >=
'0' && symbol <=
'9') || symbol ==
'-';
63 return valid ? std::optional<std::string>{std::string{locale}} : std::nullopt;
74[[nodiscard]]
inline std::expected<Catalogue, std::string>
parseCatalogue(std::string_view
text) {
77 return std::unexpected(json.error());
79 auto const*
const object = std::get_if<Json::object_t>(&json->data);
80 if (
object ==
nullptr) {
81 return std::unexpected(std::string{
"a catalogue is an object of message keys"});
84 return std::unexpected(std::move(*reason));
87 return std::unexpected(
"a catalogue holds more than " + std::to_string(
kMaxCatalogueKeys) +
" keys");
90 for (
auto const& [key, value] : *
object) {
92 return std::unexpected(
"a message key is 1 to " + std::to_string(
kMaxNameBytes) +
" bytes");
94 auto const*
const pattern = std::get_if<std::string>(&value.data);
95 if (pattern ==
nullptr) {
96 return std::unexpected(
"the message `" + key +
"` is not a string");
99 catalogue.
dropped.push_back(
"`" + key +
"`: " + parsed.error());
102 catalogue.
messages.emplace(key, *pattern);
114 auto next = std::make_shared<Table>(*_table);
115 next->insert_or_assign(std::move(locale), std::move(catalogue.
messages));
116 _table = std::move(next);
121 [[nodiscard]] std::vector<std::string>
locales()
const {
122 std::vector<std::string> out;
123 out.reserve(_table->size());
124 for (
auto const& [locale, messages] : *_table) {
125 static_cast<void>(messages);
126 out.push_back(locale);
133 [[nodiscard]] std::vector<std::string>
const&
problems() const noexcept {
return _problems; }
137 void note(std::string line) { _problems.push_back(std::move(line)); }
144 [[nodiscard]] std::optional<std::string>
lookup(std::string_view key, std::string_view locale)
const {
145 return find(*_table, key, locale);
152 return [
table = _table](std::string_view key, std::string_view locale) {
return find(*
table, key, locale); };
156 using Table = std::map<std::string, std::map<std::string, std::string, std::less<>>, std::less<>>;
159 [[nodiscard]]
static std::optional<std::string> find(
Table const&
table, std::string_view key,
160 std::string_view locale) {
161 std::string_view tag = locale;
163 if (
auto const messages =
table.find(tag); messages !=
table.end()) {
164 if (
auto const message = messages->second.find(key); message != messages->second.end()) {
165 return message->second;
168 auto const dash = tag.rfind(
'-');
169 if (dash == std::string_view::npos) {
172 tag = tag.substr(0, dash);
176 std::shared_ptr<Table const> _table = std::make_shared<Table const>();
177 std::vector<std::string> _problems;
189 std::map<std::string, std::string, std::less<>>
const& files) {
197 catalogues.
note(
"`" + file.
id +
"`: a catalogue's id is i18n/<locale>.json");
200 auto const bytes = files.find(file.
hash);
201 if (bytes == files.end()) {
202 catalogues.
note(
"`" + file.
id +
"`: not among the verified files");
207 catalogues.
note(
"`" + file.
id +
"`: " + catalogue.error());
210 for (std::string
const& dropped : catalogue->dropped) {
211 catalogues.
note(
"`" + file.
id +
"`: " + dropped);
213 catalogues.
add(std::move(*locale), std::move(*catalogue));
225 std::string_view key, std::string_view fallback) {
227 std::vector<std::string>
const locales = catalogues.
locales();
228 std::vector<std::string> out;
229 auto const format = [](std::string_view pattern, std::string
const& locale) -> std::optional<std::string> {
236 if (locales.empty()) {
237 out.push_back(format(fallback,
"en").value_or(std::string{fallback}));
240 out.reserve(locales.size());
241 for (std::string
const& locale : locales) {
242 std::optional<std::string>
text;
243 if (
auto const pattern = catalogues.
lookup(key, locale)) {
244 text = format(*pattern, locale);
247 text = format(fallback, locale);
249 out.push_back(
text.value_or(std::string{fallback}));
265 std::string_view fallback, std::string_view query) {
267 auto const fold = [](std::string_view
text) {
268 std::string out{
text};
269 std::ranges::transform(out, out.begin(), [](
char const symbol) {
270 return symbol >=
'A' && symbol <=
'Z' ? static_cast<char>(symbol -
'A' +
'a') : symbol;
274 std::string
const needle = fold(query);
276 texts.emplace_back(fallback);
277 return std::ranges::any_of(texts, [&](std::string
const&
text) {
return fold(
text).contains(needle); });
280namespace detail::catalogue {
284 std::optional<std::string>
text;
285 std::optional<std::string>
key;
294 if (std::string
const*
const text = std::get_if<std::string>(&title.data)) {
298 if (
auto const*
const object = std::get_if<Json::object_t>(&title.data)) {
299 for (
auto const& [name, value] : *
object) {
300 std::string
const*
const text = std::get_if<std::string>(&value.data);
301 if (
text ==
nullptr) {
306 }
else if (name ==
"fallback") {
318[[nodiscard]]
inline std::optional<std::string>
formatPattern(std::string_view pattern, std::string
const& locale) {
338 std::string_view locale) {
343 std::string
const where{locale};
344 if (parts.
key && catalogues !=
nullptr) {
345 if (
auto const pattern = catalogues->
lookup(*parts.
key, where)) {
347 return std::move(*
text);
369 return {*parts.
text};
371 if (parts.
key && catalogues !=
nullptr) {
What the UI service's client and server share (spec 5 §12): content hashes, the bundle's manifest,...
std::optional< std::string > mustUnderstand(Json const &json, std::string_view where)
The must-understand key (!name) of one object, if it has one.
constexpr std::string_view kMessagesFile
A message catalogue.
constexpr std::size_t kMaxNameBytes
The longest name: an id, a kind, a vocabulary, a host name, an application id or version.
std::vector< std::string > catalogTitles(CatalogEntry const &entry, MessageCatalogues const *catalogues)
A catalog entry's title in every locale the catalogues hold: what a search over screen titles compare...
bool matchesInAnyLocale(MessageCatalogues const &catalogues, std::string_view key, std::string_view fallback, std::string_view query)
Whether a message contains query in any locale the catalogues hold, or in its fallback: a navigator's...
std::optional< std::string > catalogueLocale(std::string_view fileId)
Reads the locale from a catalogue's id.
std::expected< Catalogue, std::string > parseCatalogue(std::string_view text)
Parses a catalogue: a flat JSON object of message key to MessageFormat 2 pattern.
std::optional< std::string > formatPattern(std::string_view pattern, std::string const &locale)
A message pattern formatted with no arguments.
MessageCatalogues loadCatalogues(Manifest const &manifest, std::map< std::string, std::string, std::less<> > const &files)
Reads every message catalogue a verified bundle lists.
constexpr std::string_view kCatalogueSuffix
The id suffix of a message catalogue.
constexpr std::size_t kMaxCatalogueKeys
The most keys one catalogue may hold.
TitleParts titleParts(Json const &title)
Splits a catalog title into its parts.
std::string catalogTitle(CatalogEntry const &entry, MessageCatalogues const *catalogues, std::string_view locale)
A catalog entry's title in one locale, as a shell's tab or header shows it.
constexpr std::string_view kCataloguePrefix
The id prefix of a message catalogue: i18n/LOCALE.json.
std::vector< std::string > textInEveryLocale(MessageCatalogues const &catalogues, std::string_view key, std::string_view fallback)
A message formatted in every locale the catalogues hold: what a search over titles compares against.
The message catalogues of one bundle, by locale, and the provider t resolves through.
void note(std::string line)
Records a loading problem.
render::TranslationProvider provider() const
A provider over the catalogues as they are now. It shares them, so it stays valid after this object i...
std::vector< std::string > locales() const
The locales with a catalogue, sorted.
std::vector< std::string > const & problems() const noexcept
What went wrong while loading: a catalogue left out, or a pattern dropped. Each line names the file.
std::optional< std::string > lookup(std::string_view key, std::string_view locale) const
A message's pattern in a locale: the exact locale's catalogue, then its language's (de-AT,...
void add(std::string locale, Catalogue catalogue)
Adds, or replaces, a locale's catalogue.
The MessageFormat 2 subset ui/1 accepts for t(key, fallback, args), and the CLDR plural rules its ....
FormattedMessage formatMessage(MessagePattern const &message, ValueRecord const &args, MessageLocale const &locale)
Formats a parsed message with named arguments.
std::expected< MessagePattern, std::string > parseMessage(std::string_view text)
Parses a message in the ui/1 subset of MessageFormat 2.
The renderer-side translation-catalog seam over morph's derived message keys (morph::forms::i18n,...
std::function< std::optional< std::string >(std::string_view key, std::string_view bcp47Locale)> TranslationProvider
A host-supplied catalog lookup: key/bcp47Locale -> translated text, or std::nullopt on a miss.
One catalog entry (spec 6 §8).
Json title
The title: a string, or a message reference such as {"t": "lims.sample.title", "fallback": "…"}...
std::string id
The screen's id.
One locale's messages, by key, with the patterns already parsed.
std::vector< std::string > dropped
One line per key whose pattern is outside the subset; those keys show their fallback.
std::map< std::string, std::string, std::less<> > messages
The pattern text of each key whose pattern is in the ui/1 subset.
std::string hash
The file's content hash.
std::string kind
screen, app, theme, messages, component or icon; a client fetches no kind it does not know.
std::string id
The file's id: a screen's document id, app, a theme's, a catalogue's or a component's name.
A bundle's manifest: every file the application serves, by its hash (spec 5 §12, "Content addressing"...
std::vector< ManifestFile > files
The files, sorted by kind then id.
The locale a message is formatted for.
Keyed rows of cells under column headers. Each row's view is a Row whose children are the cells,...
A catalog title's parts: a plain string, or the message key of a {"t": …} reference and its fallback.
std::optional< std::string > text
A plain string title.
std::optional< std::string > fallback
A message reference's fallback.
std::optional< std::string > key
A message reference's key.
The UI document's typed values (ui::Value), its types (ui::Type), their JSON form,...
std::expected< Json, std::string > parseJson(std::string_view text)
Parses JSON text into the document layer's JSON tree.
glz::generic_u64 Json
The JSON tree the document layer reads and writes: glaze's generic value in its integer-preserving mo...
std::map< std::string, Value, std::less<> > ValueRecord
A record value: field name to value, ordered by name.
Node text(Text spec)
Static or bound text.
Node table(std::vector< TableColumn > columns, std::function< std::vector< RowT >()> rows, std::function< Key(RowT const &)> keyOf, std::function< std::vector< Node >(reactive::Signal< RowT > const &)> cells, TableOptions options={})
A table of keyed rows: one Row of cells per row, under the column headers.