Compare commits

..

2 Commits

Author SHA1 Message Date
boyned//Kampfkarren
7e60406373 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
2026-10-05 18:02:34 -07:00
boatbomber
95262a02e9 Fix missing version links in CHANGELOG (#1331)
Makes the version headers clickable links, consistent with the others.
2026-10-01 23:52:23 -07:00
9 changed files with 82 additions and 19 deletions

View File

@@ -31,6 +31,10 @@ Making a new release? Simply add the new header with the version and date undern
## 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)
* 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])
@@ -38,6 +42,7 @@ Making a new release? Simply add the new header with the version and date undern
* Fixed `rojo sourcemap --absolute` emitting verbatim (`\\?\`) paths on Windows, which broke require types in luau-lsp. ([#1290])
* The plugin now disables the `Check for Updates` setting if you block access to `api.github.com`. ([#1297])
[7.7.1]: https://github.com/rojo-rbx/rojo/releases/tag/v7.7.1
[#1290]: https://github.com/rojo-rbx/rojo/pull/1290
[#1297]: https://github.com/rojo-rbx/rojo/pull/1297
@@ -60,6 +65,7 @@ Making a new release? Simply add the new header with the version and date undern
* `rojo serve` now validates the `Host`/`Origin` headers to protect the local/private server against DNS rebinding, gates `/api/open` to local clients, and warns when bound to a network-reachable address. The accepted hosts can be extended with the `--allowed-hosts` option or a project's `serveAllowedHosts` field, for example to reach a network-exposed server by hostname. ([#1270])
* Fixed syncback not removing stale `$properties` entries when Studio resets a property to its engine default. ([#1244])
[7.7.0]: https://github.com/rojo-rbx/rojo/releases/tag/v7.7.0
[#1176]: https://github.com/rojo-rbx/rojo/pull/1176
[#1179]: https://github.com/rojo-rbx/rojo/pull/1179
[#1192]: https://github.com/rojo-rbx/rojo/pull/1192

View File

@@ -10,7 +10,7 @@ expression: contents
<Item class="LocalizationTable" referent="1">
<Properties>
<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>
</Item>
</Item>

View File

@@ -10,7 +10,7 @@ expression: contents
<Item class="LocalizationTable" referent="1">
<Properties>
<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>
</Item>
</Item>

View File

@@ -10,7 +10,7 @@ expression: contents
<Item class="LocalizationTable" referent="1">
<Properties>
<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>
</Item>
</Item>

View File

@@ -162,13 +162,11 @@ pub fn syncback_csv_init<'sync>(
///
/// We manually deserialize into this table from CSV, but let serde_json handle
/// serialization.
#[derive(Debug, Default, Serialize, Deserialize)]
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LocalizationEntry<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
key: Option<Cow<'a, str>>,
#[serde(skip_serializing_if = "Option::is_none")]
context: Option<Cow<'a, str>>,
// 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.
//
// 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>>,
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<Cow<'a, str>>,
// We use a BTreeMap here to get deterministic output order.
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.
///
/// 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);
}
let encoded =
serde_json::to_string(&entries).context("Could not encode JSON for localization table")?;
let encoded = serde_json::to_string(&sort_localization_entries(entries))
.context("Could not encode JSON for localization table")?;
Ok(encoded)
}
@@ -249,11 +308,9 @@ fn localization_to_csv(csv_contents: &str) -> anyhow::Result<Vec<u8>> {
let mut out = Vec::new();
let mut writer = csv::Writer::from_writer(&mut out);
let mut csv: Vec<LocalizationEntry> =
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 csv: Vec<LocalizationEntry> = sort_localization_entries(
serde_json::from_str(csv_contents).context("cannot decode JSON from localization table")?,
);
let mut headers = vec!["Key", "Source", "Context", "Example"];
// We want both order and a lack of duplicates, so we use a BTreeSet.

View File

@@ -20,5 +20,5 @@ name: foo
class_name: LocalizationTable
properties:
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: []

View File

@@ -29,5 +29,5 @@ name: root
class_name: LocalizationTable
properties:
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: []

View File

@@ -29,5 +29,5 @@ name: root
class_name: LocalizationTable
properties:
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: []

View File

@@ -20,5 +20,5 @@ name: foo
class_name: LocalizationTable
properties:
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: []