diff --git a/Cargo.lock b/Cargo.lock
index 0d509f55ce2e..204a0113274d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -492,6 +492,37 @@ dependencies = [
  "miniz_oxide",
 ]
 
+[[package]]
+name = "fluent-build"
+version = "0.0.0"
+dependencies = [
+ "expect-test",
+ "fluent-syntax",
+ "proc-macro2",
+ "quote",
+ "test-fixture",
+ "test-utils",
+]
+
+[[package]]
+name = "fluent-files"
+version = "0.0.0"
+dependencies = [
+ "expect-test",
+ "fluent-build",
+ "test-fixture",
+ "test-utils",
+]
+
+[[package]]
+name = "fluent-syntax"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d"
+dependencies = [
+ "thiserror 1.0.69",
+]
+
 [[package]]
 name = "foldhash"
 version = "0.1.4"
@@ -967,6 +998,7 @@ dependencies = [
  "cov-mark",
  "either",
  "expect-test",
+ "fluent-files",
  "hir",
  "ide-db",
  "itertools 0.14.0",
diff --git a/Cargo.toml b/Cargo.toml
index d9c57b4e44da..273dc3961359 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -52,6 +52,8 @@ debug = 2
 # local crates
 base-db = { path = "./crates/base-db", version = "0.0.0" }
 cfg = { path = "./crates/cfg", version = "0.0.0", features = ["tt"] }
+fluent-build = { path = "./crates/fluent-build", version = "0.0.0" }
+fluent-files = { path = "./crates/fluent-files", version = "0.0.0" }
 hir = { path = "./crates/hir", version = "0.0.0" }
 hir-def = { path = "./crates/hir-def", version = "0.0.0" }
 hir-expand = { path = "./crates/hir-expand", version = "0.0.0" }
@@ -113,6 +115,7 @@ dissimilar = "1.0.10"
 dot = "0.1.4"
 either = "1.15.0"
 expect-test = "1.5.1"
+fluent-syntax = "0.11.1"
 indexmap = { version = "2.8.0", features = ["serde"] }
 itertools = "0.14.0"
 libc = "0.2.171"
@@ -127,9 +130,11 @@ object = { version = "0.36.7", default-features = false, features = [
   "macho",
   "pe",
 ] }
+proc-macro2 = "1"
 process-wrap = { version = "8.2.0", features = ["std"] }
 pulldown-cmark-to-cmark = "10.0.4"
 pulldown-cmark = { version = "0.9.6", default-features = false }
+quote = "1"
 rayon = "1.10.0"
 salsa = "0.19"
 rustc-hash = "2.1.1"
diff --git a/crates/fluent-build/Cargo.toml b/crates/fluent-build/Cargo.toml
new file mode 100644
index 000000000000..bd9788bbcf0d
--- /dev/null
+++ b/crates/fluent-build/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "fluent-build"
+version = "0.0.0"
+repository.workspace = true
+description = "Utils for build-time support for fluent files"
+
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+rust-version.workspace = true
+
+[lib]
+
+[dependencies]
+fluent-syntax.workspace = true
+proc-macro2.workspace = true
+quote.workspace = true
+
+# local deps
+
+[dev-dependencies]
+expect-test = "1.5.1"
+
+# local deps
+test-utils.workspace = true
+test-fixture.workspace = true
+
+[lints]
+workspace = true
+
diff --git a/crates/fluent-build/src/error.rs b/crates/fluent-build/src/error.rs
new file mode 100644
index 000000000000..d74aa912f8f4
--- /dev/null
+++ b/crates/fluent-build/src/error.rs
@@ -0,0 +1,24 @@
+use std::io;
+
+pub type FluentParserError =
+    (fluent_syntax::ast::Resource<String>, Vec<fluent_syntax::parser::ParserError>);
+
+#[derive(Debug)]
+pub enum FluentBuildError {
+    Io(io::Error),
+    Parser(FluentParserError),
+    /// Panic errors
+    Custom(String),
+}
+
+impl From<io::Error> for FluentBuildError {
+    fn from(value: io::Error) -> Self {
+        Self::Io(value)
+    }
+}
+
+impl From<FluentParserError> for FluentBuildError {
+    fn from(value: FluentParserError) -> Self {
+        Self::Parser(value)
+    }
+}
diff --git a/crates/fluent-build/src/lib.rs b/crates/fluent-build/src/lib.rs
new file mode 100644
index 000000000000..e4f69131bbe8
--- /dev/null
+++ b/crates/fluent-build/src/lib.rs
@@ -0,0 +1,189 @@
+pub mod error;
+
+use std::path::PathBuf;
+use std::{env, fs, io};
+
+use error::FluentBuildError;
+use fluent_syntax::ast;
+use proc_macro2::TokenStream;
+use quote::{format_ident, quote};
+
+fn get_cargo_manifest() -> String {
+    env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| panic!("Running on non cargo environment"))
+}
+
+pub fn build_fluent_src() -> io::Result<()> {
+    let mut output = "//! This file is generated by `fluent-build`".to_owned();
+
+    for entry in fs::read_dir("src")? {
+        let entry = entry?.path();
+
+        if !entry.extension().is_none_or(|e| e.to_string_lossy().to_string() == "ftl") {
+            continue;
+        }
+
+        let output_mod = entry
+            .file_stem()
+            .unwrap()
+            .to_string_lossy()
+            .to_string()
+            .replace("-", "_")
+            .replace(".", "_");
+        let output_file =
+            entry.with_extension("rs").file_name().unwrap().to_string_lossy().to_string();
+
+        output +=
+            &format!("\n#[path = \"{output_file}\"]\nmod {output_mod};\npub use {output_mod}::*;");
+
+        build_fluent_file(entry).map_err(|e| match e {
+            FluentBuildError::Io(error) => return error,
+            FluentBuildError::Parser(_) => todo!("parse error"),
+            FluentBuildError::Custom(c) => panic!("{c}"),
+        })?;
+    }
+
+    let absolute_path = PathBuf::from(get_cargo_manifest()).join("src/generated.rs");
+    fs::write(absolute_path, output)?;
+
+    Ok(())
+}
+
+pub fn build_fluent_file(relative_path: PathBuf) -> Result<(), FluentBuildError> {
+    let absolute_path = PathBuf::from(get_cargo_manifest()).join(relative_path);
+    let resource = fs::read_to_string(&absolute_path)
+        .map_err(|e| FluentBuildError::Custom(format!("{absolute_path:#?}: {e}")))?;
+
+    for esc in ["\\n", "\\\"", "\\'"] {
+        if resource.contains(esc) {
+            return Err(FluentBuildError::Custom(format!(
+                "invalid escape `{esc}` in Fluent resource.\nFluent does not interpret these escape sequences (<https://projectfluent.org/fluent/guide/special.html>)"
+            )));
+        }
+    }
+
+    // Use parse_runtime because we don't need debug info
+    let resource = fluent_syntax::parser::parse_runtime(resource)?.body;
+
+    let entries = resource
+        .into_iter()
+        .filter_map(|entry| {
+            let (name, value, attributes, comment) = match entry {
+                ast::Entry::Message(ast::Message {
+                    id,
+                    value: Some(value),
+                    attributes,
+                    comment,
+                }) => (id.name, value, attributes, comment),
+                ast::Entry::Term(ast::Term { id, value, attributes, comment }) => {
+                    (id.name, value, attributes, comment)
+                }
+                _ => return None,
+            };
+
+            let children = attributes
+                .into_iter()
+                .map(|ast::Attribute { id, value }| {
+                    entry_to_code(format!("{name}-{}", id.name), value, None)
+                })
+                .collect::<Vec<_>>();
+
+            let mut code = entry_to_code(name, value, comment).to_string();
+
+            for child in children {
+                code.push('\n');
+                code += &child.to_string();
+            }
+
+            Some(code)
+        })
+        .collect::<Vec<_>>();
+
+    let code = entries.join("\n");
+    let result_file = absolute_path.with_extension("rs");
+
+    fs::write(result_file, code)?;
+
+    Ok(())
+}
+
+fn entry_to_code(
+    name: String,
+    pattern: ast::Pattern<String>,
+    comment: Option<ast::Comment<String>>,
+) -> TokenStream {
+    let name = format_ident!("{}", name.replace("-", "_"));
+
+    let is_dynamic =
+        pattern.elements.iter().any(|elm| matches!(elm, ast::PatternElement::Placeable { .. }));
+
+    let comment = comment
+        .map(|comment| {
+            let content = comment.content.into_iter().map(|c| quote! {#[doc = #c]});
+            quote! {#(#content)*}
+        })
+        .unwrap_or(quote! {});
+
+    if is_dynamic {
+        let (args, format_str) = pattern.elements.into_iter().fold(
+            (Vec::new(), String::new()),
+            |(mut args, mut body), elm| {
+                resolve_pattern_element(&mut args, &mut body, elm);
+                (args, body)
+            },
+        );
+
+        quote! {
+            #comment
+            pub fn #name(#(#args),*) -> String {
+                format!(#format_str)
+            }
+        }
+    } else {
+        // Collect all static elements
+        let body = pattern
+            .elements
+            .into_iter()
+            .filter_map(|elm| match elm {
+                ast::PatternElement::TextElement { value } => Some(value),
+                _ => None,
+            })
+            .collect::<String>();
+
+        quote! {
+            #comment
+            #[allow(non_upper_case_globals)]
+            pub const #name: &str = #body;
+        }
+    }
+}
+
+fn resolve_pattern_element(
+    args: &mut Vec<TokenStream>,
+    body: &mut String,
+    elm: ast::PatternElement<String>,
+) {
+    match elm {
+        ast::PatternElement::TextElement { value } => {
+            *body += &value;
+        }
+        ast::PatternElement::Placeable { expression } => match expression {
+            ast::Expression::Select { .. } => todo!(),
+            ast::Expression::Inline(inline_expression) => match inline_expression {
+                ast::InlineExpression::StringLiteral { .. } => todo!(),
+                ast::InlineExpression::NumberLiteral { .. } => todo!(),
+                ast::InlineExpression::FunctionReference { .. } => todo!(),
+                ast::InlineExpression::MessageReference { .. } => todo!(),
+                ast::InlineExpression::TermReference { .. } => todo!(),
+                ast::InlineExpression::VariableReference { id } => {
+                    let arg = format_ident!("{}", id.name);
+                    args.push(quote! { #arg : impl ::std::fmt::Display });
+
+                    body.push('{');
+                    body.push_str(&id.name);
+                    body.push('}');
+                }
+                ast::InlineExpression::Placeable { .. } => todo!("Expression inception"),
+            },
+        },
+    }
+}
diff --git a/crates/fluent-files/.gitignore b/crates/fluent-files/.gitignore
new file mode 100644
index 000000000000..38cf000dd543
--- /dev/null
+++ b/crates/fluent-files/.gitignore
@@ -0,0 +1,2 @@
+src/*.rs
+!src/lib.rs
diff --git a/crates/fluent-files/Cargo.toml b/crates/fluent-files/Cargo.toml
new file mode 100644
index 000000000000..232eb470dbfa
--- /dev/null
+++ b/crates/fluent-files/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "fluent-files"
+version = "0.0.0"
+repository.workspace = true
+description = "Collection of translated files"
+
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+rust-version.workspace = true
+
+[lib]
+
+[build-dependencies]
+# local deps
+fluent-build.workspace = true
+
+[dev-dependencies]
+expect-test = "1.5.1"
+
+# local deps
+test-utils.workspace = true
+test-fixture.workspace = true
+
+[lints]
+workspace = true
+
+
diff --git a/crates/fluent-files/build.rs b/crates/fluent-files/build.rs
new file mode 100644
index 000000000000..966bc4492532
--- /dev/null
+++ b/crates/fluent-files/build.rs
@@ -0,0 +1,7 @@
+fn main() {
+    println!("cargo::rerun-if-changed=src/");
+    let result = fluent_build::build_fluent_src();
+    if let Err(r) = result {
+        panic!("{r:#?}")
+    }
+}
diff --git a/crates/fluent-files/src/await_outside_of_async.ftl b/crates/fluent-files/src/await_outside_of_async.ftl
new file mode 100644
index 000000000000..e42c4d821711
--- /dev/null
+++ b/crates/fluent-files/src/await_outside_of_async.ftl
@@ -0,0 +1,2 @@
+await-outside-of-async-code = E0728
+await-outside-of-async = `await` is used inside {$ctx}, which is not an `async` context
diff --git a/crates/fluent-files/src/lib.rs b/crates/fluent-files/src/lib.rs
new file mode 100644
index 000000000000..14ed671113d3
--- /dev/null
+++ b/crates/fluent-files/src/lib.rs
@@ -0,0 +1,2 @@
+mod generated;
+pub use generated::*;
diff --git a/crates/ide-diagnostics/Cargo.toml b/crates/ide-diagnostics/Cargo.toml
index 96be51e1b266..dc54f1824939 100644
--- a/crates/ide-diagnostics/Cargo.toml
+++ b/crates/ide-diagnostics/Cargo.toml
@@ -19,6 +19,7 @@ serde_json.workspace = true
 tracing.workspace = true
 
 # local deps
+fluent-files.workspace = true
 stdx.workspace = true
 syntax.workspace = true
 cfg.workspace = true
diff --git a/crates/ide-diagnostics/src/handlers/await_outside_of_async.rs b/crates/ide-diagnostics/src/handlers/await_outside_of_async.rs
index 92ca7a74184f..90be9b68a694 100644
--- a/crates/ide-diagnostics/src/handlers/await_outside_of_async.rs
+++ b/crates/ide-diagnostics/src/handlers/await_outside_of_async.rs
@@ -10,8 +10,8 @@ pub(crate) fn await_outside_of_async(
     let display_range =
         adjusted_display_range(ctx, d.node, &|node| Some(node.await_token()?.text_range()));
     Diagnostic::new(
-        crate::DiagnosticCode::RustcHardError("E0728"),
-        format!("`await` is used inside {}, which is not an `async` context", d.location),
+        crate::DiagnosticCode::RustcHardError(fluent_files::await_outside_of_async_code),
+        fluent_files::await_outside_of_async(&d.location),
         display_range,
     )
 }