diff --git a/crates/engine/src/graphic_templates_tests.rs b/crates/engine/src/graphic_templates_tests.rs
--- a/crates/engine/src/graphic_templates_tests.rs
+++ b/crates/engine/src/graphic_templates_tests.rs
@@ -40,6 +40,25 @@ fn clip_of(v: &Value) -> ClipId {
     ClipId(v["clip"].as_u64().unwrap())
 }
 
+#[test]
+fn template_exports_keep_distinct_non_ascii_names() {
+    let mut s = demo("gt-unicode-export");
+    let c = clip_of(&s.execute("graphics.newText", json!({"text": "Headline"})).unwrap());
+    let first = s.execute("graphics.template.export", json!({"clip": c.0, "name": "片头"})).unwrap();
+    let first_path = first["path"].as_str().unwrap();
+    let first_bytes = std::fs::read(first_path).unwrap();
+    let second = s.execute("graphics.template.export", json!({"clip": c.0, "name": "片尾"})).unwrap();
+    assert_ne!(first["path"], second["path"], "distinct names must not overwrite the first export");
+    assert_ne!(first["id"], second["id"], "distinct templates need distinct library identities");
+    assert_eq!(std::fs::read(first_path).unwrap(), first_bytes);
+    let second_file: Value = serde_json::from_slice(&std::fs::read(second["path"].as_str().unwrap()).unwrap()).unwrap();
+    assert_eq!(second_file["name"], "片尾");
+    let installed = s.execute("graphics.template.list", json!({"source": "user"})).unwrap();
+    for exported in [&first, &second] {
+        assert!(installed.as_array().unwrap().iter().any(|entry| entry["id"] == exported["id"] && entry["name"] == exported["name"]));
+    }
+}
+
 #[test]
 fn template_export_install_apply_round_trip() {
     let mut s = demo("gt-export");
diff --git a/crates/project/src/gtemplate.rs b/crates/project/src/gtemplate.rs
--- a/crates/project/src/gtemplate.rs
+++ b/crates/project/src/gtemplate.rs
@@ -223,12 +223,12 @@ pub enum TemplateError {
     Invalid(String),
 }
 
-/// Lower-case slug of a name (`My Lower Third` → `my-lower-third`).
+/// Lower-case slug of a name (`My Lower Third` → `my-lower-third`), retaining Unicode letters and digits.
 pub fn slug(name: &str) -> String {
     let mut s = String::new();
     for c in name.chars() {
-        if c.is_ascii_alphanumeric() {
-            s.push(c.to_ascii_lowercase());
+        if c.is_alphanumeric() {
+            s.extend(c.to_lowercase());
         } else if !s.ends_with('-') && !s.is_empty() {
             s.push('-');
         }
@@ -744,6 +744,16 @@ mod tests {
         assert!(matches!(t.validate(), Err(TemplateError::Invalid(_))));
     }
 
+    #[test]
+    fn template_slugs_preserve_non_ascii_names() {
+        assert_eq!(slug("片头"), "片头");
+        assert_eq!(slug("片尾"), "片尾");
+        assert_eq!(slug("Écran Été"), "écran-été");
+        assert_eq!(slug("My Lower Third!"), "my-lower-third");
+        assert_eq!(slug(" ../片头\\片尾! "), "片头-片尾");
+        assert_eq!(slug("---"), "template");
+    }
+
     #[test]
     fn instantiates_on_other_frame_sizes() {
         let t = builtin_templates().into_iter().find(|t| t.id == "builtin:title-centered").unwrap();
