You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-08-07 17:03:01 +03:00
i18n: include context when looking for translation keys
This commit is contained in:
@@ -13,10 +13,12 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use mas_i18n::{translations::TranslationTree, Message};
|
use mas_i18n::{translations::TranslationTree, Message};
|
||||||
|
use minijinja::machinery::Span;
|
||||||
|
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
keys: Vec<Key>,
|
keys: Vec<Key>,
|
||||||
func: String,
|
func: String,
|
||||||
|
current_file: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
@@ -24,9 +26,14 @@ impl Context {
|
|||||||
Self {
|
Self {
|
||||||
keys: Vec::new(),
|
keys: Vec::new(),
|
||||||
func,
|
func,
|
||||||
|
current_file: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_current_file(&mut self, file: &str) {
|
||||||
|
self.current_file = Some(file.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
pub fn record(&mut self, key: Key) {
|
pub fn record(&mut self, key: Key) {
|
||||||
self.keys.push(key);
|
self.keys.push(key);
|
||||||
}
|
}
|
||||||
@@ -39,6 +46,28 @@ impl Context {
|
|||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
for translatable in &self.keys {
|
for translatable in &self.keys {
|
||||||
let message = Message::from_literal(translatable.default_value());
|
let message = Message::from_literal(translatable.default_value());
|
||||||
|
|
||||||
|
let location = translatable.location.as_ref().map(|location| {
|
||||||
|
if location.span.start_line == location.span.end_line {
|
||||||
|
format!(
|
||||||
|
"{}:{}:{}-{}",
|
||||||
|
location.file,
|
||||||
|
location.span.start_line,
|
||||||
|
location.span.start_col,
|
||||||
|
location.span.end_col
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{}:{}:{}-{}:{}",
|
||||||
|
location.file,
|
||||||
|
location.span.start_line,
|
||||||
|
location.span.start_col,
|
||||||
|
location.span.end_line,
|
||||||
|
location.span.end_col
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let key = translatable
|
let key = translatable
|
||||||
.key
|
.key
|
||||||
.split('.')
|
.split('.')
|
||||||
@@ -48,12 +77,23 @@ impl Context {
|
|||||||
None
|
None
|
||||||
});
|
});
|
||||||
|
|
||||||
if translation_tree.set_if_not_defined(key, message) {
|
if translation_tree.set_if_not_defined(key, message, location) {
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
count
|
count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_key_location(&self, mut key: Key, span: Span) -> Key {
|
||||||
|
if let Some(file) = &self.current_file {
|
||||||
|
key.location = Some(Location {
|
||||||
|
file: file.to_owned(),
|
||||||
|
span,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
key
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -62,15 +102,26 @@ pub enum Kind {
|
|||||||
Plural,
|
Plural,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Location {
|
||||||
|
file: String,
|
||||||
|
span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Key {
|
pub struct Key {
|
||||||
kind: Kind,
|
kind: Kind,
|
||||||
key: String,
|
key: String,
|
||||||
|
location: Option<Location>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Key {
|
impl Key {
|
||||||
pub fn new(kind: Kind, key: String) -> Self {
|
pub fn new(kind: Kind, key: String) -> Self {
|
||||||
Self { kind, key }
|
Self {
|
||||||
|
kind,
|
||||||
|
key,
|
||||||
|
location: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_value(&self) -> String {
|
pub fn default_value(&self) -> String {
|
||||||
|
@@ -82,6 +82,7 @@ fn main() {
|
|||||||
let template = std::fs::read_to_string(&path).expect("Failed to read template");
|
let template = std::fs::read_to_string(&path).expect("Failed to read template");
|
||||||
match minijinja::parse(&template, relative.as_str()) {
|
match minijinja::parse(&template, relative.as_str()) {
|
||||||
Ok(ast) => {
|
Ok(ast) => {
|
||||||
|
context.set_current_file(relative.as_str());
|
||||||
minijinja::find_in_stmt(&mut context, &ast).unwrap();
|
minijinja::find_in_stmt(&mut context, &ast).unwrap();
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -103,6 +104,7 @@ fn main() {
|
|||||||
let mut file = File::options()
|
let mut file = File::options()
|
||||||
.write(true)
|
.write(true)
|
||||||
.read(false)
|
.read(false)
|
||||||
|
.truncate(true)
|
||||||
.open(
|
.open(
|
||||||
options
|
options
|
||||||
.existing
|
.existing
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
pub use minijinja::machinery::parse;
|
pub use minijinja::machinery::parse;
|
||||||
use minijinja::{
|
use minijinja::{
|
||||||
machinery::ast::{Call, Const, Expr, Macro, Stmt},
|
machinery::ast::{Call, Const, Expr, Macro, Spanned, Stmt},
|
||||||
ErrorKind,
|
ErrorKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -113,7 +113,11 @@ fn find_in_macro<'a>(context: &mut Context, macro_: &'a Macro<'a>) -> Result<(),
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_in_call<'a>(context: &mut Context, call: &'a Call<'a>) -> Result<(), minijinja::Error> {
|
fn find_in_call<'a>(
|
||||||
|
context: &mut Context,
|
||||||
|
call: &'a Spanned<Call<'a>>,
|
||||||
|
) -> Result<(), minijinja::Error> {
|
||||||
|
let span = call.span();
|
||||||
if let Expr::Var(var_) = &call.expr {
|
if let Expr::Var(var_) = &call.expr {
|
||||||
if var_.id == context.func() {
|
if var_.id == context.func() {
|
||||||
let key = call
|
let key = call
|
||||||
@@ -134,14 +138,18 @@ fn find_in_call<'a>(context: &mut Context, call: &'a Call<'a>) -> Result<(), min
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
context.record(Key::new(
|
let key = Key::new(
|
||||||
if has_count {
|
if has_count {
|
||||||
crate::key::Kind::Plural
|
crate::key::Kind::Plural
|
||||||
} else {
|
} else {
|
||||||
crate::key::Kind::Message
|
crate::key::Kind::Message
|
||||||
},
|
},
|
||||||
key.to_owned(),
|
key.to_owned(),
|
||||||
));
|
);
|
||||||
|
|
||||||
|
let key = context.set_key_location(key, span);
|
||||||
|
|
||||||
|
context.record(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -80,4 +80,4 @@ text = @{ (!start ~ ANY)+ }
|
|||||||
|
|
||||||
start = _{ "%" }
|
start = _{ "%" }
|
||||||
number = @{ ASCII_NONZERO_DIGIT ~ ASCII_DIGIT* }
|
number = @{ ASCII_NONZERO_DIGIT ~ ASCII_DIGIT* }
|
||||||
ident = @{ ASCII_ALPHA ~ ASCII_ALPHANUMERIC* }
|
ident = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* }
|
||||||
|
@@ -12,10 +12,17 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use std::{collections::BTreeMap, ops::Deref};
|
use std::{
|
||||||
|
collections::{BTreeMap, BTreeSet},
|
||||||
|
ops::Deref,
|
||||||
|
};
|
||||||
|
|
||||||
use icu_plurals::PluralCategory;
|
use icu_plurals::PluralCategory;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{
|
||||||
|
de::{MapAccess, Visitor},
|
||||||
|
ser::SerializeMap,
|
||||||
|
Deserialize, Deserializer, Serialize, Serializer,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::sprintf::Message;
|
use crate::sprintf::Message;
|
||||||
|
|
||||||
@@ -30,20 +37,137 @@ fn plural_category_as_str(category: PluralCategory) -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
pub type TranslationTree = Tree;
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum TranslationTree {
|
#[derive(Debug, Clone, Deserialize, Default)]
|
||||||
Message(Message),
|
pub struct Metadata {
|
||||||
Children(BTreeMap<String, TranslationTree>),
|
#[serde(skip)]
|
||||||
|
// We don't want to deserialize it, as we're resetting it every time
|
||||||
|
// This then generates the `context` field when serializing
|
||||||
|
pub context_locations: BTreeSet<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TranslationTree {
|
impl Serialize for Metadata {
|
||||||
fn default() -> Self {
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
Self::Children(BTreeMap::new())
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let context = self
|
||||||
|
.context_locations
|
||||||
|
.iter()
|
||||||
|
.map(String::as_str)
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
let mut map = serializer.serialize_map(None)?;
|
||||||
|
|
||||||
|
if !context.is_empty() {
|
||||||
|
map.serialize_entry("context", &context)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(description) = &self.description {
|
||||||
|
map.serialize_entry("description", description)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
map.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TranslationTree {
|
impl Metadata {
|
||||||
|
fn add_location(&mut self, location: String) {
|
||||||
|
self.context_locations.insert(location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct Tree {
|
||||||
|
inner: BTreeMap<String, Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Node {
|
||||||
|
metadata: Option<Metadata>,
|
||||||
|
value: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum Value {
|
||||||
|
Tree(Tree),
|
||||||
|
Leaf(Message),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Tree {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct TreeVisitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for TreeVisitor {
|
||||||
|
type Value = Tree;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str("map")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||||
|
where
|
||||||
|
A: MapAccess<'de>,
|
||||||
|
{
|
||||||
|
let mut tree: BTreeMap<String, Node> = BTreeMap::new();
|
||||||
|
let mut metadata_map: BTreeMap<String, Metadata> = BTreeMap::new();
|
||||||
|
|
||||||
|
while let Some(key) = map.next_key::<String>()? {
|
||||||
|
if let Some(name) = key.strip_prefix('@') {
|
||||||
|
let metadata = map.next_value::<Metadata>()?;
|
||||||
|
metadata_map.insert(name.to_owned(), metadata);
|
||||||
|
} else {
|
||||||
|
let value = map.next_value::<Value>()?;
|
||||||
|
tree.insert(
|
||||||
|
key,
|
||||||
|
Node {
|
||||||
|
metadata: None,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, meta) in metadata_map {
|
||||||
|
if let Some(node) = tree.get_mut(&key) {
|
||||||
|
node.metadata = Some(meta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Tree { inner: tree })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_any(TreeVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Tree {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let mut map = serializer.serialize_map(None)?;
|
||||||
|
|
||||||
|
for (key, value) in &self.inner {
|
||||||
|
map.serialize_entry(key, &value.value)?;
|
||||||
|
if let Some(meta) = &value.metadata {
|
||||||
|
map.serialize_entry(&format!("@{key}"), meta)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
map.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tree {
|
||||||
/// Get a message from the tree by key.
|
/// Get a message from the tree by key.
|
||||||
///
|
///
|
||||||
/// Returns `None` if the requested key is not found.
|
/// Returns `None` if the requested key is not found.
|
||||||
@@ -51,7 +175,7 @@ impl TranslationTree {
|
|||||||
pub fn message(&self, key: &str) -> Option<&Message> {
|
pub fn message(&self, key: &str) -> Option<&Message> {
|
||||||
let keys = key.split('.');
|
let keys = key.split('.');
|
||||||
let node = self.walk_path(keys)?;
|
let node = self.walk_path(keys)?;
|
||||||
let message = node.as_message()?;
|
let message = node.value.as_message()?;
|
||||||
Some(message)
|
Some(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,19 +189,20 @@ impl TranslationTree {
|
|||||||
let keys = key.split('.');
|
let keys = key.split('.');
|
||||||
let node = self.walk_path(keys)?;
|
let node = self.walk_path(keys)?;
|
||||||
|
|
||||||
let subtree = match node {
|
let subtree = match &node.value {
|
||||||
TranslationTree::Message(message) => return Some(message),
|
Value::Leaf(message) => return Some(message),
|
||||||
TranslationTree::Children(tree) => tree,
|
Value::Tree(tree) => tree,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(node) = subtree.get(plural_category_as_str(category)) {
|
let node = if let Some(node) = subtree.inner.get(plural_category_as_str(category)) {
|
||||||
let message = node.as_message()?;
|
node
|
||||||
Some(message)
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback to the "other" category
|
// Fallback to the "other" category
|
||||||
let message = subtree.get("other")?.as_message()?;
|
subtree.inner.get("other")?
|
||||||
Some(message)
|
};
|
||||||
}
|
|
||||||
|
let message = node.value.as_message()?;
|
||||||
|
Some(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
@@ -85,48 +210,100 @@ impl TranslationTree {
|
|||||||
&mut self,
|
&mut self,
|
||||||
path: I,
|
path: I,
|
||||||
value: Message,
|
value: Message,
|
||||||
|
location: Option<String>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let mut path = path.into_iter();
|
// We're temporarily moving the tree out of the struct to be able to nicely
|
||||||
let Some(next) = path.next() else {
|
// iterate on it
|
||||||
if let TranslationTree::Message(_) = self {
|
let mut fake_root = Node {
|
||||||
return false;
|
metadata: None,
|
||||||
}
|
value: Value::Tree(Tree {
|
||||||
|
inner: std::mem::take(&mut self.inner),
|
||||||
*self = TranslationTree::Message(value);
|
}),
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match self {
|
let mut node = &mut fake_root;
|
||||||
TranslationTree::Message(_) => panic!("cannot set a value on a message node"),
|
for key in path {
|
||||||
TranslationTree::Children(children) => children
|
match &mut node.value {
|
||||||
.entry(next.deref().to_owned())
|
Value::Tree(tree) => {
|
||||||
.or_default()
|
node = tree.inner.entry(key.deref().to_owned()).or_insert(Node {
|
||||||
.set_if_not_defined(path, value),
|
metadata: None,
|
||||||
|
value: Value::Tree(Tree::default()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Value::Leaf(_) => {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let replaced = match &node.value {
|
||||||
|
Value::Tree(tree) => {
|
||||||
|
assert!(
|
||||||
|
tree.inner.is_empty(),
|
||||||
|
"Trying to overwrite a non-empty tree"
|
||||||
|
);
|
||||||
|
|
||||||
|
node.value = Value::Leaf(value);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Value::Leaf(_) => {
|
||||||
|
// Do not overwrite existing values
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(location) = location {
|
||||||
|
node.metadata
|
||||||
|
.get_or_insert(Metadata::default())
|
||||||
|
.add_location(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the original tree at the end of the function
|
||||||
|
match fake_root {
|
||||||
|
Node {
|
||||||
|
value: Value::Tree(tree),
|
||||||
|
..
|
||||||
|
} => self.inner = tree.inner,
|
||||||
|
_ => panic!("Tried to replace the root node"),
|
||||||
|
};
|
||||||
|
|
||||||
|
replaced
|
||||||
}
|
}
|
||||||
|
|
||||||
fn walk_path<K: Deref<Target = str>, I: IntoIterator<Item = K>>(
|
fn walk_path<K: Deref<Target = str>, I: IntoIterator<Item = K>>(
|
||||||
&self,
|
&self,
|
||||||
path: I,
|
path: I,
|
||||||
) -> Option<&TranslationTree> {
|
) -> Option<&Node> {
|
||||||
let mut path = path.into_iter();
|
let mut iterator = path.into_iter();
|
||||||
let Some(next) = path.next() else {
|
let Some(next) = iterator.next() else {
|
||||||
return Some(self);
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
match self {
|
self.walk_path_inner(next, iterator)
|
||||||
TranslationTree::Message(_) => None,
|
|
||||||
TranslationTree::Children(tree) => {
|
|
||||||
let child = tree.get(&*next)?;
|
|
||||||
child.walk_path(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn walk_path_inner<K: Deref<Target = str>, I: Iterator<Item = K>>(
|
||||||
|
&self,
|
||||||
|
next_key: K,
|
||||||
|
mut path: I,
|
||||||
|
) -> Option<&Node> {
|
||||||
|
let next = self.inner.get(next_key.deref())?;
|
||||||
|
|
||||||
|
match path.next() {
|
||||||
|
Some(next_key) => match &next.value {
|
||||||
|
Value::Tree(tree) => tree.walk_path_inner(next_key, path),
|
||||||
|
Value::Leaf(_) => None,
|
||||||
|
},
|
||||||
|
None => Some(next),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Value {
|
||||||
fn as_message(&self) -> Option<&Message> {
|
fn as_message(&self) -> Option<&Message> {
|
||||||
match self {
|
match self {
|
||||||
TranslationTree::Message(message) => Some(message),
|
Value::Leaf(message) => Some(message),
|
||||||
TranslationTree::Children(_) => None,
|
Value::Tree(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -19,7 +19,7 @@ limitations under the License.
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="flex-1 flex items-center justify-center">
|
<section class="flex-1 flex items-center justify-center">
|
||||||
<div class="w-64 flex flex-col gap-2">
|
<div class="w-64 flex flex-col gap-2">
|
||||||
<h1 class="text-xl font-semibold">Unexpected error</h1>
|
<h1 class="text-xl font-semibold">{{ _("error.unexpected") }}</h1>
|
||||||
{% if code %}
|
{% if code %}
|
||||||
<p class="font-semibold font-mono">
|
<p class="font-semibold font-mono">
|
||||||
{{ code }}
|
{{ code }}
|
||||||
|
@@ -22,8 +22,7 @@ limitations under the License.
|
|||||||
<div class="my-2 mx-8">
|
<div class="my-2 mx-8">
|
||||||
<h1 class="my-2 text-5xl font-semibold leading-tight">{{ _("app.human_name") }}</h1>
|
<h1 class="my-2 text-5xl font-semibold leading-tight">{{ _("app.human_name") }}</h1>
|
||||||
<p class="text-lg">
|
<p class="text-lg">
|
||||||
OpenID Connect discovery document:
|
{{ _("app.technical_description", discovery_url=discovery_url) }}
|
||||||
<a class="cpd-link" data-kind="primary" href="{{ discovery_url }}">{{ discovery_url }}</a>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@@ -1,6 +1,26 @@
|
|||||||
{
|
{
|
||||||
"app": {
|
"app": {
|
||||||
"human_name": "Matrix Authentication Service",
|
"human_name": "Matrix Authentication Service",
|
||||||
"name": "matrix-authentication-service"
|
"@human_name": {
|
||||||
|
"context": "pages/index.html:23:63-82",
|
||||||
|
"description": "Human readable name of the application"
|
||||||
|
},
|
||||||
|
"name": "matrix-authentication-service",
|
||||||
|
"@name": {
|
||||||
|
"context": "app.html:26:14-27, base.html:32:31-44",
|
||||||
|
"description": "Name of the application"
|
||||||
|
},
|
||||||
|
"technical_description": "OpenID Connect discovery document: <a class=\"cpd-link\" data-kind=\"primary\" href=\"%(discovery_url)s\">%(discovery_url)s</a>",
|
||||||
|
"@technical_description": {
|
||||||
|
"context": "pages/index.html:25:11-70",
|
||||||
|
"description": "Introduction text displayed on the home page"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"unexpected": "Unexpected error",
|
||||||
|
"@unexpected": {
|
||||||
|
"context": "pages/error.html:22:41-62",
|
||||||
|
"description": "Error message displayed when an unexpected error occurs"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user