HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
po_parser.hpp
1// Copyright Take Vos 2020.
2// Distributed under the Boost Software License, Version 1.0.
3// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
4
5#pragma once
6
7#include "po_translations.hpp"
8#include "../i18n/i18n.hpp"
9#include "../file/file.hpp"
10#include "../parser/parser.hpp"
11#include "../macros.hpp"
12#include <string>
13#include <vector>
14#include <filesystem>
15#include <ranges>
16
17hi_export_module(hikogui.l10n.po_parser);
18
19hi_export namespace hi { inline namespace v1 {
20
21namespace detail {
22
23template<std::input_iterator It, std::sentinel_for<It> ItEnd>
24[[nodiscard]] constexpr std::tuple<std::string, size_t, std::string> parse_po_line(It& it, ItEnd last, std::string_view path)
25{
26 hi_assert(it != last);
27
28 auto name = std::string{};
29 if ((*it == token::id)) {
30 name = static_cast<std::string>(*it++);
31 } else {
32 throw parse_error(
33 std::format("{}: Expecting a keyword at start of each line, got {}", token_location(it, last, path), *it));
34 }
35
36 auto index = 0_uz;
37 if ((*it == '[')) {
38 ++it;
39
40 if (it != last and *it == token::integer) {
41 index = static_cast<size_t>(*it++);
42 } else {
43 throw parse_error(std::format(
44 "{}: Expecting an integer literal as an index for {}, got {}", token_location(it, last, path), name, *it));
45 }
46
47 if (it != last and *it == ']') {
48 ++it;
49 } else {
50 throw parse_error(std::format(
51 "{}: The index on {} must terminate with a bracket ']', got {}", token_location(it, last, path), name, *it));
52 }
53 }
54
55 auto value = std::string{};
56 if (it != last and (*it == token::sstr or *it == token::dstr)) {
57 value = static_cast<std::string>(*it++);
58 } else {
59 throw parse_error(
60 std::format("{}: Expecting a string value after {}, got {}", token_location(it, last, path), name, *it));
61 }
62
63 while (it != last and (*it == token::sstr or *it == token::dstr)) {
64 // Concatenating string literals.
65 value += static_cast<std::string>(*it++);
66 }
67
68 return {name, index, value};
69}
70
71template<std::input_iterator It, std::sentinel_for<It> ItEnd>
72[[nodiscard]] constexpr std::optional<po_translation> parse_po_translation(It& it, ItEnd last, std::string_view path)
73{
74 po_translation r;
75
76 while (it != last) {
77 if (r.msgstr.empty()) {
78 // If there have been no "msgstr" keywords, then capture information in the translation.
79 auto [name, index, value] = parse_po_line(it, last, path);
80
81 if (name == "msgctxt") {
82 r.msgctxt = value;
83
84 } else if (name == "msgid") {
85 r.msgid = value;
86
87 } else if (name == "msgid_plural") {
88 r.msgid_plural = value;
89
90 } else if (name == "msgstr") {
91 if (index >= r.msgstr.size()) {
92 r.msgstr.resize(index + 1);
93 }
94 r.msgstr[index] = value;
95
96 } else {
97 throw parse_error(
98 std::format("{}: Line starts with unexpected keyword {}", token_location(it, last, path), name));
99 }
100
101 } else if ((*it == token::id) and (*it == "msgstr")) {
102 // After the first "msgstr" keyword there may be others, but another keyword will start a new translation.
103 auto [name, index, value] = parse_po_line(it, last, path);
104
105 if (index >= r.msgstr.size()) {
106 r.msgstr.resize(index + 1);
107 }
108 r.msgstr[index] = value;
109
110 } else {
111 // The current keyword is not a msgstr, so return the translation captured.
112 return r;
113 }
114 }
115
116 return std::nullopt;
117}
118
119constexpr void parse_po_header(po_translations& r, std::string_view header)
120{
121 using namespace std::literals;
122
123 for (auto const line : std::views::split(header, "\\n"sv)) {
124 if (line.empty()) {
125 // Skip empty header lines.
126 continue;
127 }
128
129 auto split_line = make_vector<std::string_view>(std::views::split(line, ":"sv));
130 if (split_line.size() < 2) {
131 throw parse_error(std::format("Unknown header '{}'", std::string_view{line}));
132 }
133
134 auto const name = to_lower(strip(split_line.front()));
135 split_line.erase(split_line.begin());
136 auto const value = strip(join(split_line, ":"));
137
138 if (name == "language") {
139 r.language = language_tag{value};
140 }
141 }
142}
143
144} // namespace detail
145
146template<std::input_iterator It, std::sentinel_for<It> ItEnd>
147[[nodiscard]] constexpr po_translations parse_po(It it, ItEnd last, std::string_view path)
148{
149 po_translations r;
150
151 auto token_it = lexer<lexer_config::sh_style()>.parse(it, last);
152
153 while (token_it != std::default_sentinel) {
154 if (auto result = detail::parse_po_translation(token_it, std::default_sentinel, path)) {
155 if (not result->msgid.empty()) {
156 r.translations.push_back(*result);
157
158 } else if (result->msgstr.size() == 1) {
159 // If a translation has an empty msgid, then the msgstr contain headers.
160 detail::parse_po_header(r, result->msgstr.front());
161
162 } else {
163 throw parse_error(std::format("{}: Unknown .po syntax.", token_location(token_it, path)));
164 }
165 }
166 }
167
168 return r;
169}
170
171[[nodiscard]] constexpr po_translations parse_po(std::string_view text, std::string_view path)
172{
173 return parse_po(text.begin(), text.end(), path);
174}
175
176[[nodiscard]] inline po_translations parse_po(std::filesystem::path const& path)
177{
178 return parse_po(as_string_view(file_view{path}), path.string());
179}
180
181}} // namespace hi::inline v1
Defines the file class.
STL namespace.
The HikoGUI namespace.
Definition array_generic.hpp:20
hi_export constexpr std::string token_location(It &it, ItEnd last, std::string_view path) noexcept
Create a location string for error messages.
Definition token.hpp:163
DOXYGEN BUG.
Definition algorithm_misc.hpp:20