载入中...
搜索中...
未找到
LocalizationDocument.cpp
浏览该文件的文档.
2
3#include <set>
4#include <utility>
5
7namespace {
8
9std::set<std::string> placeholders(const std::string& text) {
10 std::set<std::string> result;
11 for (size_t start = text.find('{'); start != std::string::npos; start = text.find('{', start + 1)) {
12 const size_t end = text.find('}', start + 1);
13 if (end == std::string::npos) break;
14 const std::string name = text.substr(start + 1, end - start - 1);
15 if (!name.empty()) result.insert(name);
16 start = end;
17 }
18 return result;
19}
20
21void issue(LocalizationAnalysis& result, const char* rule, DiagnosticSeverity severity, const std::string& key,
22 const std::string& locale, const std::string& message) {
25 message + " [key=" + key + ", locale=" + locale + "]"));
26 if (severity == DiagnosticSeverity::Error) result.status = EditorStatus::Failed;
27}
28
29const EditorValue* field(const EditorValue& value, const char* key) {
30 const auto* object = value.getIf<EditorValue::Object>();
31 if (!object) return nullptr;
32 const auto found = object->find(key);
33 return found == object->end() ? nullptr : &found->second;
34}
35
36} // namespace
37
38Result<void> LocalizationDocument::addRow(std::string key, std::string sourceText, std::string context) {
39 if (key.empty() || sourceText.empty())
40 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.localization.invalid-source"),
41 "Localization key and source text are required");
42 if (rows_.contains(key))
43 return eve::editing::failed<void>(EditorStatus::Conflict, RuleId("editor.localization.duplicate-key"),
44 "Localization key already exists: " + key);
46 row.key = key;
47 row.context = std::move(context);
48 row.sourceText = std::move(sourceText);
49 rows_.emplace(std::move(key), std::move(row));
50 ++revision_;
51 return eve::editing::applied<void>();
52}
53
55 if (rows_.erase(key) == 0)
56 return eve::editing::failed<void>(EditorStatus::NotFound, RuleId("editor.localization.key-not-found"),
57 "Localization key was not found: " + key);
58 ++revision_;
59 return eve::editing::applied<void>();
60}
61
62Result<void> LocalizationDocument::setVariant(const std::string& key, std::string locale,
64 auto row = rows_.find(key);
65 if (row == rows_.end())
66 return eve::editing::failed<void>(EditorStatus::NotFound, RuleId("editor.localization.key-not-found"),
67 "Localization key was not found: " + key);
68 if (locale.empty() || variant.voiceDuration < 0.0)
69 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.localization.invalid-variant"),
70 "Locale is required and voice duration cannot be negative");
71 static const std::set<std::string> statuses{"", "missing", "recording", "review", "approved"};
72 if (!statuses.contains(variant.voiceStatus))
73 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.localization.invalid-voice-status"),
74 "Unknown voice production status: " + variant.voiceStatus);
75 row->second.variants[std::move(locale)] = std::move(variant);
76 ++revision_;
77 return eve::editing::applied<void>();
78}
79
80std::vector<LocalizationRow> LocalizationDocument::rows() const {
81 std::vector<LocalizationRow> result;
82 result.reserve(rows_.size());
83 for (const auto& [key, row] : rows_) {
84 static_cast<void>(key);
85 result.push_back(row);
86 }
87 return result;
88}
89
90LocalizationAnalysis LocalizationDocument::analyze(const std::vector<std::string>& requiredLocales,
91 bool requireVoice) const {
93 std::set<std::string> uniqueLocales;
94 for (const std::string& locale : requiredLocales) {
95 if (locale.empty() || !uniqueLocales.insert(locale).second) {
96 issue(result, "editor.localization.invalid-locale", DiagnosticSeverity::Error, "", locale,
97 "Required locales must be unique and non-empty");
98 continue;
99 }
101 coverage.locale = locale;
102 coverage.total = static_cast<int>(rows_.size());
103 for (const auto& [key, row] : rows_) {
104 const auto found = row.variants.find(locale);
105 if (found == row.variants.end() || found->second.text.empty()) {
106 issue(result, "editor.localization.missing-translation", DiagnosticSeverity::Error, key, locale,
107 "Required translation is missing");
108 continue;
109 }
110 const LocalizationVariant& variant = found->second;
111 ++coverage.translated;
112 if (placeholders(row.sourceText) != placeholders(variant.text))
113 issue(result, "editor.localization.placeholder-mismatch", DiagnosticSeverity::Error, key, locale,
114 "Translation placeholders do not match source text");
115 if (!variant.voiceAsset.empty()) ++coverage.voiced;
116 if (variant.voiceStatus == "approved") ++coverage.approved;
117 if (variant.voiceAsset.empty() && variant.voiceDuration > 0.0)
118 issue(result, "editor.localization.orphan-voice-duration", DiagnosticSeverity::Warning, key, locale,
119 "Voice duration exists without a voice asset");
120 if (!variant.voiceAsset.empty() && variant.voiceDuration <= 0.0)
121 issue(result, "editor.localization.missing-voice-duration", DiagnosticSeverity::Warning, key, locale,
122 "Voice asset has no measured duration");
123 if (requireVoice && variant.voiceAsset.empty())
124 issue(result, "editor.localization.missing-voice", DiagnosticSeverity::Error, key, locale,
125 "Required localized voice asset is missing");
126 }
127 result.locales.push_back(std::move(coverage));
128 }
129 return result;
130}
131
134 for (const auto& [key, row] : rows_) {
135 EditorValue::Object variants;
136 for (const auto& [locale, variant] : row.variants)
137 variants[locale] = EditorValue::Object{{"text", variant.text},
138 {"voiceAsset", variant.voiceAsset},
139 {"voiceStatus", variant.voiceStatus},
140 {"voiceDuration", variant.voiceDuration}};
141 rows.emplace_back(EditorValue::Object{
142 {"key", key}, {"context", row.context}, {"sourceText", row.sourceText}, {"variants", std::move(variants)}});
143 }
144 return EditorValue::Object{{"schemaVersion", int64_t{1}}, {"rows", std::move(rows)}};
145}
146
148 const auto* schema = field(snapshot, "schemaVersion");
149 const auto* rowsValue = field(snapshot, "rows");
150 const auto* version = schema ? schema->getIf<int64_t>() : nullptr;
151 const auto* rows = rowsValue ? rowsValue->getIf<EditorValue::Array>() : nullptr;
152 if (!version || *version != 1 || !rows)
153 return eve::editing::failed<void>(EditorStatus::Unsupported, RuleId("editor.localization.invalid-snapshot"),
154 "Localization snapshot schema is unsupported");
155 LocalizationDocument candidate;
156 for (const EditorValue& rowValue : *rows) {
157 const auto* keyValue = field(rowValue, "key");
158 const auto* sourceValue = field(rowValue, "sourceText");
159 const auto* contextValue = field(rowValue, "context");
160 const auto* variantsValue = field(rowValue, "variants");
161 const auto* key = keyValue ? keyValue->getIf<std::string>() : nullptr;
162 const auto* source = sourceValue ? sourceValue->getIf<std::string>() : nullptr;
163 const auto* context = contextValue ? contextValue->getIf<std::string>() : nullptr;
164 const auto* variants = variantsValue ? variantsValue->getIf<EditorValue::Object>() : nullptr;
165 if (!key || !source || !context || !variants || !candidate.addRow(*key, *source, *context).ok())
166 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.localization.invalid-row"),
167 "Localization snapshot contains an invalid source row");
168 for (const auto& [locale, variantValue] : *variants) {
169 const auto* textValue = field(variantValue, "text");
170 const auto* assetValue = field(variantValue, "voiceAsset");
171 const auto* statusValue = field(variantValue, "voiceStatus");
172 const auto* durationValue = field(variantValue, "voiceDuration");
173 const auto* text = textValue ? textValue->getIf<std::string>() : nullptr;
174 const auto* asset = assetValue ? assetValue->getIf<std::string>() : nullptr;
175 const auto* status = statusValue ? statusValue->getIf<std::string>() : nullptr;
176 const auto* duration = durationValue ? durationValue->getIf<double>() : nullptr;
177 if (!text || !asset || !status || !duration ||
178 !candidate.setVariant(*key, locale, {*text, *asset, *status, *duration}).ok())
179 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.localization.invalid-variant"),
180 "Localization snapshot contains an invalid locale variant");
181 }
182 }
183 rows_ = std::move(candidate.rows_);
184 ++revision_;
185 return eve::editing::applied<void>();
186}
187
188} // namespace eve::localization_editing
double value
Duration start
float duration
std::string variant
std::string message
std::uint32_t key
wgpu::PopErrorScopeStatus status
std::string text
std::string name
bool found
const UnitySourceAsset & source
const VegetationPresetContext & context
bool ok() const noexcept
Whether this result represents a non-failure outcome.
Definition Result.h:480
Move-only operation result carrying either a value or Status.
Definition Result.h:155
Deterministic owning value tree shared by authoring hosts.
std::vector< Value > Array
std::map< std::string, Value > Object
const T * getIf() const
Return an immediately borrowed typed value.
UI-neutral localization table with deterministic persistence and QA.
Result< void > loadSnapshot(const EditorValue &snapshot)
Atomically replace data from a validated persisted snapshot.
Result< void > setVariant(const std::string &key, std::string locale, LocalizationVariant variant)
Replace one locale variant, validating production metadata.
LocalizationAnalysis analyze(const std::vector< std::string > &requiredLocales, bool requireVoice=false) const
Analyze locale coverage, placeholders and optional voice requirements.
Result< void > removeRow(const std::string &key)
Remove a source key and all translated variants.
EditorValue snapshotValue() const
Capture deterministic data for conflict-safe document persistence.
std::vector< LocalizationRow > rows() const
Return rows in deterministic key order.
Result< void > addRow(std::string key, std::string sourceText, std::string context={})
Insert a unique source key.
Diagnostic ruleDiagnostic(eve::DiagnosticCode code, RuleId rule, DiagnosticSeverity severity, std::string message)
Build a common diagnostic carrying an open editing rule identity.
Severity
Stable severity of a diagnostic, independent of its display text.
Definition Diagnostic.h:20
Completeness analysis result with key/locale-scoped diagnostics.
std::vector< LocalizationLocaleCoverage > locales
Aggregate production coverage for one locale.
One stable localization key and all locale variants.
One locale's editable translation and voice-production metadata.
std::string key
Reconciliation key; defaults to id when empty.
Definition Widget.h:19
std::string text
Definition Widget.h:20