# HG changeset patch
# Node ID 4309a1010d8a498b6cad06b4fb71b5a14c6ec175
# Parent  24b0650762af1a5fca3b7e8a94a14ffb95d03ed2
diff --git a/xeme/meson.build b/xeme/meson.build
--- a/xeme/meson.build
+++ b/xeme/meson.build
@@ -4,6 +4,7 @@
   'xemeextension.c',
   'xemeextensionmap.c',
   'xemeinputstream.c',
+  'xemelocalizedstring.c',
   'xememessage.c',
   'xemeoutputstream.c',
   'xemestanza.c',
@@ -19,6 +20,7 @@
   'xemeextension.h',
   'xemeextensionmap.h',
   'xemeinputstream.h',
+  'xemelocalizedstring.h',
   'xememessage.h',
   'xemeoutputstream.h',
   'xemestanza.h',
diff --git a/xeme/tests/meson.build b/xeme/tests/meson.build
--- a/xeme/tests/meson.build
+++ b/xeme/tests/meson.build
@@ -3,6 +3,7 @@
   'extension',
   'extensionmap',
   'inputstream',
+  'localizedstring',
   'message',
   'outputstream',
   'stanza',
diff --git a/xeme/tests/testlocalizedstring.c b/xeme/tests/testlocalizedstring.c
new file mode 100644
--- /dev/null
+++ b/xeme/tests/testlocalizedstring.c
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2023-2025 Xeme Developers
+ *
+ * Xeme is the legal property of its developers, whose names are too
+ * numerous to list here. Please refer to the AUTHORS file distributed
+ * with this source distribution
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this library; if not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+
+#include <xeme.h>
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_xeme_localized_string_new(void) {
+	XemeLocalizedString *l10nstr = NULL;
+
+	l10nstr = xeme_localized_string_new("en-us", "hiya!");
+	g_assert_true(XEME_IS_LOCALIZED_STRING(l10nstr));
+
+	g_assert_finalize_object(l10nstr);
+}
+
+static void
+test_xeme_localized_string_properties(void) {
+	XemeLocalizedString *l10nstr = NULL;
+	char *lang = NULL;
+	char *str = NULL;
+
+	l10nstr = g_object_new(
+		XEME_TYPE_LOCALIZED_STRING,
+		"language", "en-us",
+		"string", "strung",
+		NULL);
+
+	g_assert_true(XEME_IS_LOCALIZED_STRING(l10nstr));
+
+	g_object_get(
+		l10nstr,
+		"language", &lang,
+		"string", &str,
+		NULL);
+
+	g_assert_cmpstr(lang, ==, "en-us");
+	g_clear_pointer(&lang, g_free);
+
+	g_assert_cmpstr(str, ==, "strung");
+	g_clear_pointer(&str, g_free);
+
+	g_assert_finalize_object(l10nstr);
+}
+
+static void
+test_xeme_localized_string_equal(void) {
+	XemeLocalizedString *l10nstr1 = NULL;
+	XemeLocalizedString *l10nstr2 = NULL;
+
+	g_assert_true(xeme_localized_string_equal(l10nstr1, l10nstr2));
+
+	l10nstr1 = xeme_localized_string_new(NULL, "foo");
+	g_assert_false(xeme_localized_string_equal(l10nstr1, l10nstr2));
+
+	l10nstr2 = xeme_localized_string_new(NULL, "foo");
+	g_assert_true(xeme_localized_string_equal(l10nstr1, l10nstr2));
+
+	xeme_localized_string_set_language(l10nstr1, "en-us");
+	g_assert_false(xeme_localized_string_equal(l10nstr1, l10nstr2));
+
+	xeme_localized_string_set_language(l10nstr2, "en-us");
+	g_assert_true(xeme_localized_string_equal(l10nstr1, l10nstr2));
+
+	xeme_localized_string_set_string(l10nstr1, "bar");
+	g_assert_false(xeme_localized_string_equal(l10nstr1, l10nstr2));
+
+	xeme_localized_string_set_string(l10nstr2, "bar");
+	g_assert_true(xeme_localized_string_equal(l10nstr1, l10nstr2));
+
+	g_assert_finalize_object(l10nstr1);
+	l10nstr1 = NULL;
+	g_assert_false(xeme_localized_string_equal(l10nstr1, l10nstr2));
+
+	g_assert_finalize_object(l10nstr2);
+	l10nstr2 = NULL;
+	g_assert_true(xeme_localized_string_equal(l10nstr1, l10nstr2));
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+int
+main(int argc, char *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+
+	g_test_add_func("/xeme/localized-string/new",
+	                test_xeme_localized_string_new);
+	g_test_add_func("/xeme/localized-string/properties",
+	                test_xeme_localized_string_properties);
+
+	g_test_add_func("/xeme/localized-string/equal",
+	                test_xeme_localized_string_equal);
+
+	return g_test_run();
+}
diff --git a/xeme/xemelocalizedstring.c b/xeme/xemelocalizedstring.c
new file mode 100644
--- /dev/null
+++ b/xeme/xemelocalizedstring.c
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2023-2025 Xeme Developers
+ *
+ * Xeme is the legal property of its developers, whose names are too
+ * numerous to list here. Please refer to the AUTHORS file distributed
+ * with this source distribution
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this library; if not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+#include <birb.h>
+
+#include "xemelocalizedstring.h"
+
+#include "xemestring.h"
+
+struct _XemeLocalizedString {
+	GObject parent;
+
+	char *lang;
+	char *str;
+};
+
+enum {
+	PROP_0,
+	PROP_LANGUAGE,
+	PROP_STRING,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, 0};
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_FINAL_TYPE(XemeLocalizedString, xeme_localized_string, G_TYPE_OBJECT)
+
+static void
+xeme_localized_string_finalize(GObject *obj) {
+	XemeLocalizedString *l10nstr = XEME_LOCALIZED_STRING(obj);
+
+	g_clear_pointer(&l10nstr->lang, g_free);
+	g_clear_pointer(&l10nstr->str, g_free);
+
+	G_OBJECT_CLASS(xeme_localized_string_parent_class)->finalize(obj);
+}
+
+static void
+xeme_localized_string_get_property(GObject *obj, guint param_id, GValue *value,
+                                   GParamSpec *pspec)
+{
+	XemeLocalizedString *l10nstr = XEME_LOCALIZED_STRING(obj);
+
+	switch(param_id) {
+	case PROP_LANGUAGE:
+		g_value_set_string(value, xeme_localized_string_get_language(l10nstr));
+		break;
+	case PROP_STRING:
+		g_value_set_string(value, xeme_localized_string_get_string(l10nstr));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+xeme_localized_string_set_property(GObject *obj, guint param_id,
+                                   const GValue *value, GParamSpec *pspec)
+{
+	XemeLocalizedString *l10nstr = XEME_LOCALIZED_STRING(obj);
+
+	switch(param_id) {
+	case PROP_LANGUAGE:
+		xeme_localized_string_set_language(l10nstr, g_value_get_string(value));
+		break;
+	case PROP_STRING:
+		xeme_localized_string_set_string(l10nstr, g_value_get_string(value));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+xeme_localized_string_init(G_GNUC_UNUSED XemeLocalizedString *l10nstr) {
+}
+
+static void
+xeme_localized_string_class_init(XemeLocalizedStringClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->finalize = xeme_localized_string_finalize;
+	obj_class->get_property = xeme_localized_string_get_property;
+	obj_class->set_property = xeme_localized_string_set_property;
+
+	/**
+	 * XemeLocalizedString:language:
+	 *
+	 * The language for this string.
+	 *
+	 * This may be %NULL.
+	 *
+	 * Since: 0.1
+	 */
+	properties[PROP_LANGUAGE] = g_param_spec_string(
+		"language", NULL, NULL,
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * XemeLocalizedString:string:
+	 *
+	 * The string.
+	 *
+	 * Since: 0.1
+	 */
+	properties[PROP_STRING] = g_param_spec_string(
+		"string", NULL, NULL,
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+gboolean
+xeme_localized_string_equal(XemeLocalizedString *l10nstr1,
+                            XemeLocalizedString *l10nstr2)
+{
+	if(l10nstr1 == l10nstr2) {
+		return TRUE;
+	}
+
+	if(l10nstr1 == NULL || l10nstr2 == NULL) {
+		return FALSE;
+	}
+
+	if(xeme_str_equal(l10nstr1->lang, l10nstr2->lang)) {
+		if(xeme_str_equal(l10nstr1->str, l10nstr2->str)) {
+			return TRUE;
+		}
+	}
+
+	return FALSE;
+}
+
+const char *
+xeme_localized_string_get_language(XemeLocalizedString *l10nstr) {
+	g_return_val_if_fail(XEME_IS_LOCALIZED_STRING(l10nstr), NULL);
+
+	return l10nstr->lang;
+}
+
+const char *
+xeme_localized_string_get_string(XemeLocalizedString *l10nstr) {
+	g_return_val_if_fail(XEME_IS_LOCALIZED_STRING(l10nstr), NULL);
+
+	return l10nstr->str;
+}
+
+XemeLocalizedString *
+xeme_localized_string_new(const char *lang, const char *str) {
+	g_return_val_if_fail(!xeme_str_is_empty(str), NULL);
+
+	return g_object_new(
+		XEME_TYPE_LOCALIZED_STRING,
+		"language", lang,
+		"string", str,
+		NULL);
+}
+
+void
+xeme_localized_string_set_language(XemeLocalizedString *l10nstr,
+                                   const char *lang)
+{
+	g_return_if_fail(XEME_IS_LOCALIZED_STRING(l10nstr));
+
+	if(g_set_str(&l10nstr->lang, lang)) {
+		g_object_notify_by_pspec(G_OBJECT(l10nstr), properties[PROP_LANGUAGE]);
+	}
+}
+
+void
+xeme_localized_string_set_string(XemeLocalizedString *l10nstr, const char *str)
+{
+	g_return_if_fail(XEME_IS_LOCALIZED_STRING(l10nstr));
+	g_return_if_fail(!xeme_str_is_empty(str));
+
+	if(g_set_str(&l10nstr->str, str)) {
+		g_object_notify_by_pspec(G_OBJECT(l10nstr), properties[PROP_STRING]);
+	}
+}
diff --git a/xeme/xemelocalizedstring.h b/xeme/xemelocalizedstring.h
new file mode 100644
--- /dev/null
+++ b/xeme/xemelocalizedstring.h
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2023-2025 Xeme Developers
+ *
+ * Xeme is the legal property of its developers, whose names are too
+ * numerous to list here. Please refer to the AUTHORS file distributed
+ * with this source distribution
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this library; if not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef XEME_LOCALIZED_STRING_H
+#define XEME_LOCALIZED_STRING_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include "xemeversion.h"
+
+G_BEGIN_DECLS
+
+/**
+ * XemeLocalizedString:
+ *
+ * A string that has language associated with it.
+ *
+ * Since: 0.1
+ */
+
+#define XEME_TYPE_LOCALIZED_STRING (xeme_localized_string_get_type())
+
+XEME_AVAILABLE_IN_0_1
+G_DECLARE_FINAL_TYPE(XemeLocalizedString, xeme_localized_string, XEME,
+                     LOCALIZED_STRING, GObject)
+
+/**
+ * xeme_localized_string_equal:
+ * @l10nstr2: (nullable): the second instance
+ *
+ * Checks if two localized strings are equal.
+ *
+ * This does not take case folding into consideration.
+ *
+ * Returns: true if the two languages and strings are equal.
+ *
+ * Since: 0.1
+ */
+XEME_AVAILABLE_IN_0_1
+gboolean xeme_localized_string_equal(XemeLocalizedString *l10nstr1, XemeLocalizedString *l10nstr2);
+
+/**
+ * xeme_localized_string_get_language:
+ *
+ * Gets the language of the string.
+ *
+ * Returns: (nullable): The language.
+ *
+ * Since: 0.1
+ */
+XEME_AVAILABLE_IN_0_1
+const char *xeme_localized_string_get_language(XemeLocalizedString *str);
+
+/**
+ * xeme_localized_string_get_string:
+ *
+ * Gets the string.
+ *
+ * Returns: (nullable): The string.
+ *
+ * Since: 0.1
+ */
+XEME_AVAILABLE_IN_0_1
+const char *xeme_localized_string_get_string(XemeLocalizedString *str);
+
+/**
+ * xeme_localized_string_new:
+ * @lang: (nullable): the language for the string
+ * @str: the string.
+ *
+ * Creates a new localized string.
+ *
+ * Returns: (transfer full): The string.
+ *
+ * Since: 0.1
+ */
+XEME_AVAILABLE_IN_0_1
+XemeLocalizedString *xeme_localized_string_new(const char *lang, const char *str);
+
+/**
+ * xeme_localized_string_set_language:
+ * @lang: (nullable): the language
+ *
+ * Sets the language for a string.
+ *
+ * Since: 0.1
+ */
+XEME_AVAILABLE_IN_0_1
+void xeme_localized_string_set_language(XemeLocalizedString *l10nstr, const char *lang);
+
+/**
+ * xeme_localized_string_set_string:
+ * @str: the string
+ *
+ * Sets the string.
+ *
+ * Since: 0.1
+ */
+XEME_AVAILABLE_IN_0_1
+void xeme_localized_string_set_string(XemeLocalizedString *l10nstr, const char *str);
+
+G_END_DECLS
+
+#endif /* XEME_LOCALIZED_STRING_H */
