mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-11 10:04:54 +00:00
Consistent ordering of CSV fields (#1333)
Paired with https://github.com/rojo-rbx/rbx-dom/pull/668 This fixes a bug where the first sync of a place with LocalizationTables would always fail diff, since Roblox computes it at runtime
This commit is contained in:
committed by
GitHub
parent
95262a02e9
commit
7e60406373
@@ -31,6 +31,10 @@ Making a new release? Simply add the new header with the version and date undern
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
* LocalizationTables no longer report as changed when nothing changes. [(#1333)]
|
||||||
|
|
||||||
|
[#1333]: https://github.com/rojo-rbx/rojo/pull/1333
|
||||||
|
|
||||||
## [7.7.1] (October 1st, 2026)
|
## [7.7.1] (October 1st, 2026)
|
||||||
|
|
||||||
* Fixed `$path` values that point outside the project folder failing to match `syncRule`s on Windows, which broke `rojo sourcemap` with a "could not be turned into a Roblox Instance" error. ([#1290])
|
* Fixed `$path` values that point outside the project folder failing to match `syncRule`s on Windows, which broke `rojo sourcemap` with a "could not be turned into a Roblox Instance" error. ([#1290])
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ expression: contents
|
|||||||
<Item class="LocalizationTable" referent="1">
|
<Item class="LocalizationTable" referent="1">
|
||||||
<Properties>
|
<Properties>
|
||||||
<string name="Name">normal</string>
|
<string name="Name">normal</string>
|
||||||
<string name="Contents">[{"key":"Count","example":"A number demonstrating issue 145","source":"3","values":{"es":"7"}}]</string>
|
<string name="Contents">[{"example":"A number demonstrating issue 145","key":"Count","source":"3","values":{"es":"7"}}]</string>
|
||||||
</Properties>
|
</Properties>
|
||||||
</Item>
|
</Item>
|
||||||
</Item>
|
</Item>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ expression: contents
|
|||||||
<Item class="LocalizationTable" referent="1">
|
<Item class="LocalizationTable" referent="1">
|
||||||
<Properties>
|
<Properties>
|
||||||
<string name="Name">normal</string>
|
<string name="Name">normal</string>
|
||||||
<string name="Contents">[{"key":"Language.Name","source":"English","values":{}},{"key":"Language.Region","source":"United States","values":{}},{"key":"Label.Thickness","source":"Thickness","values":{}},{"key":"Label.Opacity","source":"Opacity","values":{}},{"key":"Toolbar.Undo","source":"Undo","values":{}},{"key":"Toolbar.Redo","source":"Redo","values":{}},{"key":"Toolbar.Camera","source":"Top-down camera","values":{}},{"key":"Toolbar.Saves","source":"Saved drawings","values":{}},{"key":"Toolbar.Preferences","source":"Settings","values":{}},{"key":"Toolbar.Mode.Vector","source":"Vector mode","values":{}},{"key":"Toolbar.Mode.Pixel","source":"Pixel mode","values":{}}]</string>
|
<string name="Contents">[{"key":"Label.Opacity","source":"Opacity","values":{}},{"key":"Label.Thickness","source":"Thickness","values":{}},{"key":"Language.Name","source":"English","values":{}},{"key":"Language.Region","source":"United States","values":{}},{"key":"Toolbar.Camera","source":"Top-down camera","values":{}},{"key":"Toolbar.Mode.Pixel","source":"Pixel mode","values":{}},{"key":"Toolbar.Mode.Vector","source":"Vector mode","values":{}},{"key":"Toolbar.Preferences","source":"Settings","values":{}},{"key":"Toolbar.Redo","source":"Redo","values":{}},{"key":"Toolbar.Saves","source":"Saved drawings","values":{}},{"key":"Toolbar.Undo","source":"Undo","values":{}}]</string>
|
||||||
</Properties>
|
</Properties>
|
||||||
</Item>
|
</Item>
|
||||||
</Item>
|
</Item>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ expression: contents
|
|||||||
<Item class="LocalizationTable" referent="1">
|
<Item class="LocalizationTable" referent="1">
|
||||||
<Properties>
|
<Properties>
|
||||||
<string name="Name">normal</string>
|
<string name="Name">normal</string>
|
||||||
<string name="Contents">[{"key":"Ack","example":"An exclamation of despair","source":"Ack!","values":{"es":"¡Ay!"}}]</string>
|
<string name="Contents">[{"example":"An exclamation of despair","key":"Ack","source":"Ack!","values":{"es":"¡Ay!"}}]</string>
|
||||||
</Properties>
|
</Properties>
|
||||||
</Item>
|
</Item>
|
||||||
</Item>
|
</Item>
|
||||||
|
|||||||
@@ -162,13 +162,11 @@ pub fn syncback_csv_init<'sync>(
|
|||||||
///
|
///
|
||||||
/// We manually deserialize into this table from CSV, but let serde_json handle
|
/// We manually deserialize into this table from CSV, but let serde_json handle
|
||||||
/// serialization.
|
/// serialization.
|
||||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct LocalizationEntry<'a> {
|
struct LocalizationEntry<'a> {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
key: Option<Cow<'a, str>>,
|
key: Option<Cow<'a, str>>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
context: Option<Cow<'a, str>>,
|
context: Option<Cow<'a, str>>,
|
||||||
|
|
||||||
// Roblox writes `examples` for LocalizationTable's Content property, which
|
// Roblox writes `examples` for LocalizationTable's Content property, which
|
||||||
@@ -176,16 +174,77 @@ struct LocalizationEntry<'a> {
|
|||||||
// This is reported here: https://devforum.roblox.com/t/2908720.
|
// This is reported here: https://devforum.roblox.com/t/2908720.
|
||||||
//
|
//
|
||||||
// To support their mistake, we support an alias named `examples`.
|
// To support their mistake, we support an alias named `examples`.
|
||||||
#[serde(skip_serializing_if = "Option::is_none", alias = "examples")]
|
#[serde(alias = "examples")]
|
||||||
example: Option<Cow<'a, str>>,
|
example: Option<Cow<'a, str>>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
source: Option<Cow<'a, str>>,
|
source: Option<Cow<'a, str>>,
|
||||||
|
|
||||||
// We use a BTreeMap here to get deterministic output order.
|
// We use a BTreeMap here to get deterministic output order.
|
||||||
values: BTreeMap<Cow<'a, str>, Cow<'a, str>>,
|
values: BTreeMap<Cow<'a, str>, Cow<'a, str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guarantee a specific order of both fields and entries so that diff always match up
|
||||||
|
impl<'a> Serialize for LocalizationEntry<'a> {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
let mut btree = BTreeMap::new();
|
||||||
|
|
||||||
|
if let Some(key) = &self.key {
|
||||||
|
btree.insert(
|
||||||
|
"key",
|
||||||
|
serde_json::to_value(key).map_err(serde::ser::Error::custom)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(context) = &self.context {
|
||||||
|
btree.insert(
|
||||||
|
"context",
|
||||||
|
serde_json::to_value(context).map_err(serde::ser::Error::custom)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(example) = &self.example {
|
||||||
|
btree.insert(
|
||||||
|
"example",
|
||||||
|
serde_json::to_value(example).map_err(serde::ser::Error::custom)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(source) = &self.source {
|
||||||
|
btree.insert(
|
||||||
|
"source",
|
||||||
|
serde_json::to_value(source).map_err(serde::ser::Error::custom)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
btree.insert(
|
||||||
|
"values",
|
||||||
|
serde_json::to_value(&self.values).map_err(serde::ser::Error::custom)?,
|
||||||
|
);
|
||||||
|
|
||||||
|
btree.serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sort_localization_entries(mut entries: Vec<LocalizationEntry>) -> Vec<LocalizationEntry> {
|
||||||
|
entries.sort_by(|a, b| {
|
||||||
|
a.key
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.cmp(&b.key.as_deref().unwrap_or_default())
|
||||||
|
.then_with(|| {
|
||||||
|
a.source
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.cmp(&b.source.as_deref().unwrap_or_default())
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
entries
|
||||||
|
}
|
||||||
|
|
||||||
/// Normally, we'd be able to let the csv crate construct our struct for us.
|
/// Normally, we'd be able to let the csv crate construct our struct for us.
|
||||||
///
|
///
|
||||||
/// However, because of a limitation with Serde's 'flatten' feature, it's not
|
/// However, because of a limitation with Serde's 'flatten' feature, it's not
|
||||||
@@ -236,8 +295,8 @@ fn convert_localization_csv(contents: &[u8]) -> anyhow::Result<String> {
|
|||||||
entries.push(entry);
|
entries.push(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
let encoded =
|
let encoded = serde_json::to_string(&sort_localization_entries(entries))
|
||||||
serde_json::to_string(&entries).context("Could not encode JSON for localization table")?;
|
.context("Could not encode JSON for localization table")?;
|
||||||
|
|
||||||
Ok(encoded)
|
Ok(encoded)
|
||||||
}
|
}
|
||||||
@@ -249,11 +308,9 @@ fn localization_to_csv(csv_contents: &str) -> anyhow::Result<Vec<u8>> {
|
|||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
let mut writer = csv::Writer::from_writer(&mut out);
|
let mut writer = csv::Writer::from_writer(&mut out);
|
||||||
|
|
||||||
let mut csv: Vec<LocalizationEntry> =
|
let csv: Vec<LocalizationEntry> = sort_localization_entries(
|
||||||
serde_json::from_str(csv_contents).context("cannot decode JSON from localization table")?;
|
serde_json::from_str(csv_contents).context("cannot decode JSON from localization table")?,
|
||||||
|
);
|
||||||
// TODO sort this better
|
|
||||||
csv.sort_by(|a, b| a.source.partial_cmp(&b.source).unwrap());
|
|
||||||
|
|
||||||
let mut headers = vec!["Key", "Source", "Context", "Example"];
|
let mut headers = vec!["Key", "Source", "Context", "Example"];
|
||||||
// We want both order and a lack of duplicates, so we use a BTreeSet.
|
// We want both order and a lack of duplicates, so we use a BTreeSet.
|
||||||
|
|||||||
@@ -20,5 +20,5 @@ name: foo
|
|||||||
class_name: LocalizationTable
|
class_name: LocalizationTable
|
||||||
properties:
|
properties:
|
||||||
Contents:
|
Contents:
|
||||||
String: "[{\"key\":\"Ack\",\"example\":\"An exclamation of despair\",\"source\":\"Ack!\",\"values\":{\"es\":\"¡Ay!\"}}]"
|
String: "[{\"example\":\"An exclamation of despair\",\"key\":\"Ack\",\"source\":\"Ack!\",\"values\":{\"es\":\"¡Ay!\"}}]"
|
||||||
children: []
|
children: []
|
||||||
|
|||||||
@@ -29,5 +29,5 @@ name: root
|
|||||||
class_name: LocalizationTable
|
class_name: LocalizationTable
|
||||||
properties:
|
properties:
|
||||||
Contents:
|
Contents:
|
||||||
String: "[{\"key\":\"Ack\",\"example\":\"An exclamation of despair\",\"source\":\"Ack!\",\"values\":{\"es\":\"¡Ay!\"}}]"
|
String: "[{\"example\":\"An exclamation of despair\",\"key\":\"Ack\",\"source\":\"Ack!\",\"values\":{\"es\":\"¡Ay!\"}}]"
|
||||||
children: []
|
children: []
|
||||||
|
|||||||
@@ -29,5 +29,5 @@ name: root
|
|||||||
class_name: LocalizationTable
|
class_name: LocalizationTable
|
||||||
properties:
|
properties:
|
||||||
Contents:
|
Contents:
|
||||||
String: "[{\"key\":\"Ack\",\"example\":\"An exclamation of despair\",\"source\":\"Ack!\",\"values\":{\"es\":\"¡Ay!\"}}]"
|
String: "[{\"example\":\"An exclamation of despair\",\"key\":\"Ack\",\"source\":\"Ack!\",\"values\":{\"es\":\"¡Ay!\"}}]"
|
||||||
children: []
|
children: []
|
||||||
|
|||||||
@@ -20,5 +20,5 @@ name: foo
|
|||||||
class_name: LocalizationTable
|
class_name: LocalizationTable
|
||||||
properties:
|
properties:
|
||||||
Contents:
|
Contents:
|
||||||
String: "[{\"key\":\"Ack\",\"example\":\"An exclamation of despair\",\"source\":\"Ack!\",\"values\":{\"es\":\"¡Ay!\"}}]"
|
String: "[{\"example\":\"An exclamation of despair\",\"key\":\"Ack\",\"source\":\"Ack!\",\"values\":{\"es\":\"¡Ay!\"}}]"
|
||||||
children: []
|
children: []
|
||||||
|
|||||||
Reference in New Issue
Block a user