diff --git a/doc/reference/templates.md b/doc/reference/templates.md
index 1e88a94b0..551ac531b 100644
--- a/doc/reference/templates.md
+++ b/doc/reference/templates.md
@@ -56,17 +56,19 @@ extensions.
 
 For reference, here is a list of all standard QWeb directives:
 
-| Name                           | Description                                                     |
-| ------------------------------ | --------------------------------------------------------------- |
-| `t-esc`                        | [Outputting safely a value](#outputting-data)                   |
-| `t-out`                        | [Outputting value, possibly without escaping](#outputting-data) |
-| `t-set`, `t-value`             | [Setting variables](#setting-variables)                         |
-| `t-if`, `t-elif`, `t-else`,    | [conditionally rendering](#conditionals)                        |
-| `t-foreach`, `t-as`            | [Loops](#loops)                                                 |
-| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes)                       |
-| `t-call`                       | [Rendering sub templates](#sub-templates)                       |
-| `t-debug`, `t-log`             | [Debugging](#debugging)                                         |
-| `t-translation`                | [Disabling the translation of a node](translations.md)          |
+| Name                           | Description                                                             |
+| ------------------------------ | ----------------------------------------------------------------------- |
+| `t-esc`                        | [Outputting safely a value](#outputting-data)                           |
+| `t-out`                        | [Outputting value, possibly without escaping](#outputting-data)         |
+| `t-set`, `t-value`             | [Setting variables](#setting-variables)                                 |
+| `t-if`, `t-elif`, `t-else`,    | [conditionally rendering](#conditionals)                                |
+| `t-foreach`, `t-as`            | [Loops](#loops)                                                         |
+| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes)                               |
+| `t-call`                       | [Rendering sub templates](#sub-templates)                               |
+| `t-debug`, `t-log`             | [Debugging](#debugging)                                                 |
+| `t-translation`                | [Disabling the translation of a node](translations.md)                  |
+| `t-translation-context`        | [Context of translations within a node](translations.md)                |
+| `t-translation-context-*`      | [Context of translation for a specific node attribute](translations.md) |
 
 The component system in Owl requires additional directives, to express various
 needs. Here is a list of all Owl specific directives:
diff --git a/doc/reference/translations.md b/doc/reference/translations.md
index 52fd1e092..ce996f9d3 100644
--- a/doc/reference/translations.md
+++ b/doc/reference/translations.md
@@ -1,17 +1,28 @@
 # 🦉 Translations 🦉
 
 If properly setup, Owl can translate all rendered templates. To do
-so, it needs a translate function, which takes a string and returns a string.
+so, it needs a translate function, which takes
+
+- a string (the term to translate)
+- a string (the translation context of the term)
+  and returns a string.
 
 For example:
 
 ```js
 const translations = {
-  hello: "bonjour",
-  yes: "oui",
-  no: "non",
+  fr: {
+    hello: "bonjour",
+    yes: "oui",
+    no: "non",
+  },
+  pt: {
+    hello: "bom dia",
+    yes: "sim",
+    no: "não",
+  },
 };
-const translateFn = (str) => translations[str] || str;
+const translateFn = (str, ctx) => translations[ctx]?.[str] || str;
 
 const app = new App(Root, { templates, tranaslateFn });
 // ...
@@ -27,6 +38,11 @@ Once setup, all rendered templates will be translated using `translateFn`:
   `placeholder`, `label` and `alt`,
 - translating text nodes can be disabled with the special attribute `t-translation`,
   if its value is `off`.
+- the translate function receives as second parameter a context that can be used
+  to contextualized the translation. That context can be set globally on a node
+  and its children by using `t-translation-context`. If a specific node
+  attribute `x` needs another context, that context can be specified with a
+  special directive `t-translation-context-x`.
 
 So, with the above `translateFn`, the following templates:
 
@@ -46,6 +62,22 @@ will be rendered as:
 <input placeholder="bonjour" other="yes"/>
 ```
 
+and the following template:
+
+```xml
+<div t-translation-context="fr" title="hello">hello</div>
+<div>Are you sure?</div>
+<input t-translation-context-placeholder="pt" placeholder="hello" other="yes"/>
+```
+
+will be rendered as:
+
+```xml
+<div title="bonjour">bonjour</div>
+<div>Are you sure?</div>
+<input placeholder="bom dia" other="yes"/>
+```
+
 Note that the translation is done during the compilation of the template, not
 when it is rendered.
 
diff --git a/src/compiler/code_generator.ts b/src/compiler/code_generator.ts
index 4cbab34ec..13cb3cca9 100644
--- a/src/compiler/code_generator.ts
+++ b/src/compiler/code_generator.ts
@@ -24,6 +24,7 @@ import {
   ASTTOut,
   ASTTPortal,
   ASTTranslation,
+  ASTTranslationContext,
   ASTTSet,
   ASTType,
   Attrs,
@@ -35,7 +36,7 @@ type BlockType = "block" | "text" | "multi" | "list" | "html" | "comment";
 const whitespaceRE = /\s+/g;
 
 export interface Config {
-  translateFn?: (s: string) => string;
+  translateFn?: (s: string, translationCtx: string) => string;
   translatableAttributes?: string[];
   dev?: boolean;
 }
@@ -171,6 +172,7 @@ interface Context {
   forceNewBlock: boolean;
   isLast?: boolean;
   translate: boolean;
+  translationCtx: string;
   tKeyExpr: string | null;
   nameSpace?: string;
   tModelSelectedExpr?: string;
@@ -185,6 +187,7 @@ function createContext(parentCtx: Context, params?: Partial<Context>): Context {
       index: 0,
       forceNewBlock: true,
       translate: parentCtx.translate,
+      translationCtx: parentCtx.translationCtx,
       tKeyExpr: null,
       nameSpace: parentCtx.nameSpace,
       tModelSelectedExpr: parentCtx.tModelSelectedExpr,
@@ -263,7 +266,7 @@ export class CodeGenerator {
   target = new CodeTarget("template");
   templateName?: string;
   dev: boolean;
-  translateFn: (s: string) => string;
+  translateFn: (s: string, translationCtx: string) => string;
   translatableAttributes: string[] = TRANSLATABLE_ATTRS;
   ast: AST;
   staticDefs: { id: string; expr: string }[] = [];
@@ -303,6 +306,7 @@ export class CodeGenerator {
       forceNewBlock: false,
       isLast: true,
       translate: true,
+      translationCtx: "",
       tKeyExpr: null,
     });
     // define blocks and utility functions
@@ -457,9 +461,9 @@ export class CodeGenerator {
       .join("");
   }
 
-  translate(str: string): string {
+  translate(str: string, translationCtx: string): string {
     const match = translationRE.exec(str) as any;
-    return match[1] + this.translateFn(match[2]) + match[3];
+    return match[1] + this.translateFn(match[2], translationCtx) + match[3];
   }
 
   /**
@@ -501,6 +505,8 @@ export class CodeGenerator {
         return this.compileTSlot(ast, ctx);
       case ASTType.TTranslation:
         return this.compileTTranslation(ast, ctx);
+      case ASTType.TTranslationContext:
+        return this.compileTTranslationContext(ast, ctx);
       case ASTType.TPortal:
         return this.compileTPortal(ast, ctx);
     }
@@ -542,7 +548,7 @@ export class CodeGenerator {
 
     let value = ast.value;
     if (value && ctx.translate !== false) {
-      value = this.translate(value);
+      value = this.translate(value, ctx.translationCtx);
     }
     if (!ctx.inPreTag) {
       value = value.replace(whitespaceRE, " ");
@@ -631,7 +637,8 @@ export class CodeGenerator {
           }
         }
       } else if (this.translatableAttributes.includes(key)) {
-        attrs[key] = this.translateFn(ast.attrs[key]);
+        const attrTranslationCtx = ast.attrsTranslationCtx?.[key] || ctx.translationCtx;
+        attrs[key] = this.translateFn(ast.attrs[key], attrTranslationCtx);
       } else {
         expr = `"${ast.attrs[key]}"`;
         attrName = key;
@@ -1104,7 +1111,7 @@ export class CodeGenerator {
       let value: string;
       if (ast.defaultValue) {
         const defaultValue = toStringExpression(
-          ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue
+          ctx.translate ? this.translate(ast.defaultValue, ctx.translationCtx) : ast.defaultValue
         );
         if (ast.value) {
           value = `withDefault(${expr}, ${defaultValue})`;
@@ -1139,9 +1146,15 @@ export class CodeGenerator {
    * "some-prop"       "state"          "'some-prop': ctx['state']"
    * "onClick.bind"    "onClick"        "onClick: bind(ctx, ctx['onClick'])"
    */
-  formatProp(name: string, value: string): string {
+  formatProp(
+    name: string,
+    value: string,
+    attrsTranslationCtx: { [name: string]: string } | null,
+    translationCtx: string
+  ): string {
     if (name.endsWith(".translate")) {
-      value = toStringExpression(this.translateFn(value));
+      const attrTranslationCtx = attrsTranslationCtx?.[name] || translationCtx;
+      value = toStringExpression(this.translateFn(value, attrTranslationCtx));
     } else {
       value = this.captureExpression(value);
     }
@@ -1163,8 +1176,14 @@ export class CodeGenerator {
     return `${name}: ${value || undefined}`;
   }
 
-  formatPropObject(obj: { [prop: string]: any }): string[] {
-    return Object.entries(obj).map(([k, v]) => this.formatProp(k, v));
+  formatPropObject(
+    obj: { [prop: string]: any },
+    attrsTranslationCtx: { [name: string]: string } | null,
+    translationCtx: string
+  ): string[] {
+    return Object.entries(obj).map(([k, v]) =>
+      this.formatProp(k, v, attrsTranslationCtx, translationCtx)
+    );
   }
 
   getPropString(props: string[], dynProps: string | null): string {
@@ -1181,7 +1200,9 @@ export class CodeGenerator {
     let { block } = ctx;
     // props
     const hasSlotsProp = "slots" in (ast.props || {});
-    const props: string[] = ast.props ? this.formatPropObject(ast.props) : [];
+    const props: string[] = ast.props
+      ? this.formatPropObject(ast.props, ast.propsTranslationCtx, ctx.translationCtx)
+      : [];
 
     // slots
     let slotDef: string = "";
@@ -1205,7 +1226,13 @@ export class CodeGenerator {
           params.push(`__scope: "${scope}"`);
         }
         if (ast.slots[slotName].attrs) {
-          params.push(...this.formatPropObject(ast.slots[slotName].attrs!));
+          params.push(
+            ...this.formatPropObject(
+              ast.slots[slotName].attrs!,
+              ast.slots[slotName].attrsTranslationCtx,
+              ctx.translationCtx
+            )
+          );
         }
         const slotInfo = `{${params.join(", ")}}`;
         slotStr.push(`'${slotName}': ${slotInfo}`);
@@ -1332,7 +1359,9 @@ export class CodeGenerator {
       key = this.generateComponentKey(key);
     }
 
-    const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
+    const props = ast.attrs
+      ? this.formatPropObject(ast.attrs, ast.attrsTranslationCtx, ctx.translationCtx)
+      : [];
     const scope = this.getPropString(props, dynProps);
     if (ast.defaultContent) {
       const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
@@ -1365,6 +1394,15 @@ export class CodeGenerator {
     }
     return null;
   }
+  compileTTranslationContext(ast: ASTTranslationContext, ctx: Context): string | null {
+    if (ast.content) {
+      return this.compileAST(
+        ast.content,
+        Object.assign({}, ctx, { translationCtx: ast.translationCtx })
+      );
+    }
+    return null;
+  }
   compileTPortal(ast: ASTTPortal, ctx: Context): string {
     if (!this.staticDefs.find((d) => d.id === "Portal")) {
       this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts
index 5dbf0078f..760b85345 100644
--- a/src/compiler/parser.ts
+++ b/src/compiler/parser.ts
@@ -27,6 +27,7 @@ export const enum ASTType {
   TSlot,
   TCallBlock,
   TTranslation,
+  TTranslationContext,
   TPortal,
 }
 
@@ -56,6 +57,7 @@ export interface ASTDomNode {
   tag: string;
   content: AST[];
   attrs: Attrs | null;
+  attrsTranslationCtx: Attrs | null;
   ref: string | null;
   on: EventHandlers | null;
   model: TModelInfo | null;
@@ -127,6 +129,7 @@ interface SlotDefinition {
   scope: string | null;
   on: EventHandlers | null;
   attrs: Attrs | null;
+  attrsTranslationCtx: Attrs | null;
 }
 
 export interface ASTComponent {
@@ -136,6 +139,7 @@ export interface ASTComponent {
   dynamicProps: string | null;
   on: EventHandlers | null;
   props: { [name: string]: string } | null;
+  propsTranslationCtx: { [name: string]: string } | null;
   slots: { [name: string]: SlotDefinition } | null;
 }
 
@@ -143,6 +147,7 @@ export interface ASTSlot {
   type: ASTType.TSlot;
   name: string;
   attrs: Attrs | null;
+  attrsTranslationCtx: Attrs | null;
   on: EventHandlers | null;
   defaultContent: AST | null;
 }
@@ -168,6 +173,12 @@ export interface ASTTranslation {
   content: AST | null;
 }
 
+export interface ASTTranslationContext {
+  type: ASTType.TTranslationContext;
+  content: AST | null;
+  translationCtx: string;
+}
+
 export interface ASTTPortal {
   type: ASTType.TPortal;
   target: string;
@@ -192,6 +203,7 @@ export type AST =
   | ASTLog
   | ASTDebug
   | ASTTranslation
+  | ASTTranslationContext
   | ASTTPortal;
 
 // -----------------------------------------------------------------------------
@@ -245,6 +257,7 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
     parseTOutNode(node, ctx) ||
     parseTKey(node, ctx) ||
     parseTTranslation(node, ctx) ||
+    parseTTranslationContext(node, ctx) ||
     parseTSlot(node, ctx) ||
     parseComponent(node, ctx) ||
     parseDOMNode(node, ctx) ||
@@ -368,6 +381,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
 
   const nodeAttrsNames = node.getAttributeNames();
   let attrs: ASTDomNode["attrs"] = null;
+  let attrsTranslationCtx: ASTDomNode["attrsTranslationCtx"] = null;
   let on: EventHandlers | null = null;
   let model: TModelInfo | null = null;
 
@@ -428,6 +442,10 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
       throw new OwlError(`Invalid attribute: '${attr}'`);
     } else if (attr === "xmlns") {
       ns = value;
+    } else if (attr.startsWith("t-translation-context-")) {
+      const attrName = attr.slice(22);
+      attrsTranslationCtx = attrsTranslationCtx || {};
+      attrsTranslationCtx[attrName] = value;
     } else if (attr !== "t-name") {
       if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
         throw new OwlError(`Unknown QWeb directive: '${attr}'`);
@@ -450,6 +468,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
     tag: tagName,
     dynamicTag,
     attrs,
+    attrsTranslationCtx,
     on,
     ref,
     content: children,
@@ -609,7 +628,15 @@ function parseTCall(node: Element, ctx: ParsingContext): AST | null {
     if (ast && ast.type === ASTType.TComponent) {
       return {
         ...ast,
-        slots: { default: { content: tcall, scope: null, on: null, attrs: null } },
+        slots: {
+          default: {
+            content: tcall,
+            scope: null,
+            on: null,
+            attrs: null,
+            attrsTranslationCtx: null,
+          },
+        },
       };
     }
   }
@@ -744,9 +771,14 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
   let on: ASTComponent["on"] = null;
 
   let props: ASTComponent["props"] = null;
+  let propsTranslationCtx: ASTComponent["propsTranslationCtx"] = null;
   for (let name of node.getAttributeNames()) {
     const value = node.getAttribute(name)!;
-    if (name.startsWith("t-")) {
+    if (name.startsWith("t-translation-context-")) {
+      const attrName = name.slice(22);
+      propsTranslationCtx = propsTranslationCtx || {};
+      propsTranslationCtx[attrName] = value;
+    } else if (name.startsWith("t-")) {
       if (name.startsWith("t-on-")) {
         on = on || {};
         on[name.slice(5)] = value;
@@ -794,12 +826,17 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
       const slotAst = parseNode(slotNode, ctx);
       let on: SlotDefinition["on"] = null;
       let attrs: Attrs | null = null;
+      let attrsTranslationCtx: Attrs | null = null;
       let scope: string | null = null;
       for (let attributeName of slotNode.getAttributeNames()) {
         const value = slotNode.getAttribute(attributeName)!;
         if (attributeName === "t-slot-scope") {
           scope = value;
           continue;
+        } else if (attributeName.startsWith("t-translation-context-")) {
+          const attrName = attributeName.slice(22);
+          attrsTranslationCtx = attrsTranslationCtx || {};
+          attrsTranslationCtx[attrName] = value;
         } else if (attributeName.startsWith("t-on-")) {
           on = on || {};
           on[attributeName.slice(5)] = value;
@@ -809,7 +846,7 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
         }
       }
       slots = slots || {};
-      slots[name] = { content: slotAst, on, attrs, scope };
+      slots[name] = { content: slotAst, on, attrs, attrsTranslationCtx, scope };
     }
 
     // default slot
@@ -817,10 +854,25 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
     slots = slots || {};
     // t-set-slot="default" has priority over content
     if (defaultContent && !slots.default) {
-      slots.default = { content: defaultContent, on, attrs: null, scope: defaultSlotScope };
+      slots.default = {
+        content: defaultContent,
+        on,
+        attrs: null,
+        attrsTranslationCtx: null,
+        scope: defaultSlotScope,
+      };
     }
   }
-  return { type: ASTType.TComponent, name, isDynamic, dynamicProps, props, slots, on };
+  return {
+    type: ASTType.TComponent,
+    name,
+    isDynamic,
+    dynamicProps,
+    props,
+    propsTranslationCtx,
+    slots,
+    on,
+  };
 }
 
 // -----------------------------------------------------------------------------
@@ -834,12 +886,17 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
   const name = node.getAttribute("t-slot")!;
   node.removeAttribute("t-slot");
   let attrs: Attrs | null = null;
+  let attrsTranslationCtx: Attrs | null = null;
   let on: ASTComponent["on"] = null;
   for (let attributeName of node.getAttributeNames()) {
     const value = node.getAttribute(attributeName)!;
     if (attributeName.startsWith("t-on-")) {
       on = on || {};
       on[attributeName.slice(5)] = value;
+    } else if (attributeName.startsWith("t-translation-context-")) {
+      const attrName = attributeName.slice(22);
+      attrsTranslationCtx = attrsTranslationCtx || {};
+      attrsTranslationCtx[attrName] = value;
     } else {
       attrs = attrs || {};
       attrs[attributeName] = value;
@@ -849,11 +906,16 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
     type: ASTType.TSlot,
     name,
     attrs,
+    attrsTranslationCtx,
     on,
     defaultContent: parseChildNodes(node, ctx),
   };
 }
 
+// -----------------------------------------------------------------------------
+// Translation
+// -----------------------------------------------------------------------------
+
 function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
   if (node.getAttribute("t-translation") !== "off") {
     return null;
@@ -865,6 +927,23 @@ function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
   };
 }
 
+// -----------------------------------------------------------------------------
+// Translation Context
+// -----------------------------------------------------------------------------
+
+function parseTTranslationContext(node: Element, ctx: ParsingContext): AST | null {
+  const translationCtx = node.getAttribute("t-translation-context");
+  if (!translationCtx) {
+    return null;
+  }
+  node.removeAttribute("t-translation-context");
+  return {
+    type: ASTType.TTranslationContext,
+    content: parseNode(node, ctx),
+    translationCtx,
+  };
+}
+
 // -----------------------------------------------------------------------------
 // Portal
 // -----------------------------------------------------------------------------
diff --git a/src/runtime/template_set.ts b/src/runtime/template_set.ts
index 4e750e418..5be0b4c07 100644
--- a/src/runtime/template_set.ts
+++ b/src/runtime/template_set.ts
@@ -12,7 +12,7 @@ const bdom = { text, createBlock, list, multi, html, toggler, comment };
 export interface TemplateSetConfig {
   dev?: boolean;
   translatableAttributes?: string[];
-  translateFn?: (s: string) => string;
+  translateFn?: (s: string, translationCtx: string) => string;
   templates?: string | Document | Record<string, string>;
   getTemplate?: (s: string) => Element | Function | string | void;
   customDirectives?: customDirectives;
@@ -27,7 +27,7 @@ export class TemplateSet {
   rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
   templates: { [name: string]: Template } = {};
   getRawTemplate?: (s: string) => Element | Function | string | void;
-  translateFn?: (s: string) => string;
+  translateFn?: (s: string, translationCtx: string) => string;
   translatableAttributes?: string[];
   Portal = Portal;
   customDirectives: customDirectives;
diff --git a/tests/compiler/__snapshots__/translation.test.ts.snap b/tests/compiler/__snapshots__/translation.test.ts.snap
index 6e493cb33..b7c1ea7c6 100644
--- a/tests/compiler/__snapshots__/translation.test.ts.snap
+++ b/tests/compiler/__snapshots__/translation.test.ts.snap
@@ -1,5 +1,128 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`translation context body of t-sets are translated in context 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  let { isBoundary, withDefault, setContextValue } = helpers;
+  
+  return function template(ctx, node, key = \\"\\") {
+    ctx = Object.create(ctx);
+    ctx[isBoundary] = 1
+    setContextValue(ctx, \\"label\\", \`traduit\`);
+    const b2 = text(ctx['label']);
+    return multi([b2]);
+  }
+}"
+`;
+
+exports[`translation context default slot params and content translated in context 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  let { callSlot } = helpers;
+  
+  let block1 = createBlock(\`<div><block-child-0/></div>\`);
+  
+  function defaultContent1(ctx, node, key = \\"\\") {
+    return text(\` foo \`);
+  }
+  
+  return function template(ctx, node, key = \\"\\") {
+    const b3 = callSlot(ctx, node, key, 'default', false, {param: \`param\`,title: \`título\`}, defaultContent1.bind(this));
+    return block1([], [b3]);
+  }
+}"
+`;
+
+exports[`translation context props with modifier .translate are translated in context 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  const comp1 = app.createComponent(\`ChildComponent\`, true, false, false, []);
+  
+  return function template(ctx, node, key = \\"\\") {
+    return comp1({text: \`jeu\`}, key + \`__1\`, node, this, null);
+  }
+}"
+`;
+
+exports[`translation context props with modifier .translate are translated in context 2`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  
+  let block1 = createBlock(\`<span><block-text-0/></span>\`);
+  
+  return function template(ctx, node, key = \\"\\") {
+    let txt1 = ctx['props'].text;
+    return block1([txt1]);
+  }
+}"
+`;
+
+exports[`translation context slot attrs and text contents are translated in context 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  let { capture, markRaw } = helpers;
+  const comp1 = app.createComponent(\`ChildComponent\`, true, true, false, []);
+  
+  function slot1(ctx, node, key = \\"\\") {
+    return text(\`jeu\`);
+  }
+  
+  return function template(ctx, node, key = \\"\\") {
+    const ctx1 = capture(ctx);
+    return comp1({slots: markRaw({'a': {__render: slot1.bind(this), __ctx: ctx1, title: \`título\`}})}, key + \`__1\`, node, this, null);
+  }
+}"
+`;
+
+exports[`translation context slot attrs and text contents are translated in context 2`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  let { callSlot } = helpers;
+  
+  let block1 = createBlock(\`<div><block-child-0/></div>\`);
+  
+  return function template(ctx, node, key = \\"\\") {
+    const b2 = callSlot(ctx, node, key, 'a', false, {});
+    return block1([], [b2]);
+  }
+}"
+`;
+
+exports[`translation context translation of attributes in context 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  
+  let block1 = createBlock(\`<div title=\\"titre\\" label=\\"game\\"/>\`);
+  
+  return function template(ctx, node, key = \\"\\") {
+    return block1();
+  }
+}"
+`;
+
+exports[`translation context translation of text in context 1`] = `
+"function anonymous(app, bdom, helpers
+) {
+  let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+  
+  let block2 = createBlock(\`<div>word</div>\`);
+  let block3 = createBlock(\`<div>mot</div>\`);
+  
+  return function template(ctx, node, key = \\"\\") {
+    const b2 = block2();
+    const b3 = block3();
+    return multi([b2, b3]);
+  }
+}"
+`;
+
 exports[`translation support body of t-sets are translated 1`] = `
 "function anonymous(app, bdom, helpers
 ) {
diff --git a/tests/compiler/parser.test.ts b/tests/compiler/parser.test.ts
index c264ba548..de3db0bf5 100644
--- a/tests/compiler/parser.test.ts
+++ b/tests/compiler/parser.test.ts
@@ -43,6 +43,7 @@ describe("qweb parser", () => {
       dynamicTag: null,
       content: [],
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: null,
       model: null,
@@ -70,6 +71,7 @@ describe("qweb parser", () => {
       tag: "div",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: null,
       model: null,
@@ -84,6 +86,7 @@ describe("qweb parser", () => {
       tag: "div",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: null,
       model: null,
@@ -98,6 +101,7 @@ describe("qweb parser", () => {
       tag: "div",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: null,
       model: null,
@@ -109,6 +113,7 @@ describe("qweb parser", () => {
           tag: "span",
           dynamicTag: null,
           attrs: null,
+          attrsTranslationCtx: null,
           on: null,
           ref: null,
           model: null,
@@ -128,6 +133,7 @@ describe("qweb parser", () => {
           tag: "div",
           dynamicTag: null,
           attrs: null,
+          attrsTranslationCtx: null,
           on: null,
           ref: null,
           model: null,
@@ -139,6 +145,7 @@ describe("qweb parser", () => {
           tag: "span",
           dynamicTag: null,
           attrs: null,
+          attrsTranslationCtx: null,
           on: null,
           ref: null,
           model: null,
@@ -156,6 +163,7 @@ describe("qweb parser", () => {
       tag: "div",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: null,
       model: null,
@@ -181,6 +189,7 @@ describe("qweb parser", () => {
       tag: "div",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: null,
       model: null,
@@ -201,6 +210,7 @@ describe("qweb parser", () => {
       tag: "div",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: null,
       model: null,
@@ -223,6 +233,7 @@ describe("qweb parser", () => {
       tag: "div",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: null,
       model: null,
@@ -246,6 +257,7 @@ describe("qweb parser", () => {
           tag: "span",
           dynamicTag: null,
           attrs: null,
+          attrsTranslationCtx: null,
           on: null,
           ref: null,
           model: null,
@@ -262,6 +274,7 @@ describe("qweb parser", () => {
       tag: "div",
       dynamicTag: null,
       attrs: { class: "abc" },
+      attrsTranslationCtx: null,
       on: null,
       ref: null,
       model: null,
@@ -280,6 +293,7 @@ describe("qweb parser", () => {
         height: "90px",
         width: "100px",
       },
+      attrsTranslationCtx: null,
       content: [
         {
           attrs: {
@@ -290,6 +304,7 @@ describe("qweb parser", () => {
             stroke: "green",
             "stroke-width": "1",
           },
+          attrsTranslationCtx: null,
           content: [],
           dynamicTag: null,
           model: null,
@@ -312,6 +327,7 @@ describe("qweb parser", () => {
       parse(`<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/></g>`)
     ).toEqual({
       attrs: null,
+      attrsTranslationCtx: null,
       content: [
         {
           attrs: {
@@ -322,6 +338,7 @@ describe("qweb parser", () => {
             stroke: "green",
             "stroke-width": "1",
           },
+          attrsTranslationCtx: null,
           content: [],
           dynamicTag: null,
           model: null,
@@ -348,6 +365,7 @@ describe("qweb parser", () => {
       tag: "div",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: null,
       content: [
@@ -356,6 +374,7 @@ describe("qweb parser", () => {
           tag: "pre",
           dynamicTag: null,
           attrs: null,
+          attrsTranslationCtx: null,
           on: null,
           ref: null,
           content: [],
@@ -391,6 +410,7 @@ describe("qweb parser", () => {
       tag: "span",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: null,
       model: null,
@@ -413,6 +433,7 @@ describe("qweb parser", () => {
       tag: "div",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: null,
       model: null,
@@ -455,6 +476,7 @@ describe("qweb parser", () => {
       tag: "div",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: null,
       model: null,
@@ -469,6 +491,7 @@ describe("qweb parser", () => {
       tag: "div",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: null,
       model: null,
@@ -489,6 +512,7 @@ describe("qweb parser", () => {
       tag: "div",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: null,
       model: null,
@@ -530,6 +554,7 @@ describe("qweb parser", () => {
         tag: "div",
         dynamicTag: null,
         attrs: null,
+        attrsTranslationCtx: null,
         on: null,
         ref: null,
         model: null,
@@ -607,6 +632,7 @@ describe("qweb parser", () => {
         tag: "div",
         dynamicTag: null,
         attrs: null,
+        attrsTranslationCtx: null,
         on: null,
         ref: null,
         model: null,
@@ -626,6 +652,7 @@ describe("qweb parser", () => {
             tag: "h1",
             dynamicTag: null,
             attrs: null,
+            attrsTranslationCtx: null,
             on: null,
             ref: null,
             model: null,
@@ -639,6 +666,7 @@ describe("qweb parser", () => {
         tag: "h2",
         dynamicTag: null,
         attrs: null,
+        attrsTranslationCtx: null,
         on: null,
         ref: null,
         model: null,
@@ -685,6 +713,7 @@ describe("qweb parser", () => {
         {
           type: ASTType.DomNode,
           attrs: null,
+          attrsTranslationCtx: null,
           on: null,
           tag: "div",
           dynamicTag: null,
@@ -705,6 +734,7 @@ describe("qweb parser", () => {
         {
           type: ASTType.DomNode,
           attrs: null,
+          attrsTranslationCtx: null,
           on: null,
           tag: "div",
           dynamicTag: null,
@@ -742,6 +772,7 @@ describe("qweb parser", () => {
       tag: "div",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: null,
       model: null,
@@ -811,6 +842,7 @@ describe("qweb parser", () => {
         tag: "div",
         dynamicTag: null,
         attrs: null,
+        attrsTranslationCtx: null,
         on: null,
         ref: null,
         model: null,
@@ -853,6 +885,7 @@ describe("qweb parser", () => {
         tag: "span",
         dynamicTag: null,
         attrs: null,
+        attrsTranslationCtx: null,
         on: null,
         ref: null,
         model: null,
@@ -887,6 +920,7 @@ describe("qweb parser", () => {
           tag: "span",
           dynamicTag: null,
           attrs: null,
+          attrsTranslationCtx: null,
           on: null,
           ref: null,
           model: null,
@@ -920,6 +954,7 @@ describe("qweb parser", () => {
           "t-att-selected": "category.id==options.active_category_id",
           "t-att-value": "category.id",
         },
+        attrsTranslationCtx: null,
         on: null,
         ref: null,
         model: null,
@@ -940,6 +975,7 @@ describe("qweb parser", () => {
     ).toEqual({
       type: ASTType.DomNode,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: null,
       model: null,
@@ -987,6 +1023,7 @@ describe("qweb parser", () => {
         ref: null,
         model: null,
         attrs: null,
+        attrsTranslationCtx: null,
         ns: null,
         content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }],
       },
@@ -1010,6 +1047,7 @@ describe("qweb parser", () => {
         name: "Comp",
         dynamicProps: null,
         props: null,
+        propsTranslationCtx: null,
         slots: null,
         on: null,
       },
@@ -1099,6 +1137,7 @@ describe("qweb parser", () => {
       tag: "div",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: null,
       model: null,
@@ -1139,6 +1178,7 @@ describe("qweb parser", () => {
       tag: "button",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: { click: "add" },
       ref: null,
       model: null,
@@ -1175,6 +1215,7 @@ describe("qweb parser", () => {
       tag: "select",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: null,
       content: [
@@ -1183,6 +1224,7 @@ describe("qweb parser", () => {
           tag: "option",
           dynamicTag: null,
           attrs: { value: "1" },
+          attrsTranslationCtx: null,
           on: null,
           ref: null,
           content: [],
@@ -1212,6 +1254,7 @@ describe("qweb parser", () => {
       tag: "select",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: null,
       content: [
@@ -1220,6 +1263,7 @@ describe("qweb parser", () => {
           tag: "option",
           dynamicTag: null,
           attrs: { "t-att-value": "valueVar" },
+          attrsTranslationCtx: null,
           on: null,
           ref: null,
           content: [],
@@ -1251,6 +1295,7 @@ describe("qweb parser", () => {
       name: "MyComponent",
       dynamicProps: null,
       props: null,
+      propsTranslationCtx: null,
       on: null,
       slots: null,
       isDynamic: false,
@@ -1263,6 +1308,7 @@ describe("qweb parser", () => {
       name: "MyComponent",
       dynamicProps: null,
       props: { a: "1", b: "'b'" },
+      propsTranslationCtx: null,
       isDynamic: false,
       on: null,
       slots: null,
@@ -1275,6 +1321,7 @@ describe("qweb parser", () => {
       name: "MyComponent",
       dynamicProps: "state",
       props: { a: "1" },
+      propsTranslationCtx: null,
       isDynamic: false,
       on: null,
       slots: null,
@@ -1287,6 +1334,7 @@ describe("qweb parser", () => {
       name: "MyComponent",
       dynamicProps: null,
       props: null,
+      propsTranslationCtx: null,
       isDynamic: false,
       on: { click: "someMethod" },
       slots: null,
@@ -1329,12 +1377,14 @@ describe("qweb parser", () => {
       name: "MyComponent",
       dynamicProps: null,
       props: null,
+      propsTranslationCtx: null,
       isDynamic: false,
       on: null,
       slots: {
         default: {
           content: { type: ASTType.Text, value: "foo" },
           attrs: null,
+          attrsTranslationCtx: null,
           on: null,
           scope: null,
         },
@@ -1350,12 +1400,14 @@ describe("qweb parser", () => {
       name: "MyComponent",
       dynamicProps: null,
       props: null,
+      propsTranslationCtx: null,
       isDynamic: false,
       on: null,
       slots: {
         default: {
           content: { type: ASTType.Text, value: "foo" },
           attrs: { param: "param" },
+          attrsTranslationCtx: null,
           on: null,
           scope: null,
         },
@@ -1370,6 +1422,7 @@ describe("qweb parser", () => {
       isDynamic: false,
       dynamicProps: null,
       props: null,
+      propsTranslationCtx: null,
       on: null,
       slots: {
         default: {
@@ -1381,6 +1434,7 @@ describe("qweb parser", () => {
                 tag: "span",
                 dynamicTag: null,
                 attrs: null,
+                attrsTranslationCtx: null,
                 content: [],
                 ref: null,
                 model: null,
@@ -1392,6 +1446,7 @@ describe("qweb parser", () => {
                 tag: "div",
                 dynamicTag: null,
                 attrs: null,
+                attrsTranslationCtx: null,
                 content: [],
                 ref: null,
                 model: null,
@@ -1401,6 +1456,7 @@ describe("qweb parser", () => {
             ],
           },
           attrs: null,
+          attrsTranslationCtx: null,
           on: null,
           scope: null,
         },
@@ -1415,9 +1471,11 @@ describe("qweb parser", () => {
       name: "MyComponent",
       on: null,
       props: null,
+      propsTranslationCtx: null,
       slots: {
         mySlot: {
           attrs: null,
+          attrsTranslationCtx: null,
           content: null,
           on: null,
           scope: null,
@@ -1434,9 +1492,16 @@ describe("qweb parser", () => {
       isDynamic: false,
       dynamicProps: null,
       props: null,
+      propsTranslationCtx: null,
       on: null,
       slots: {
-        name: { content: { type: ASTType.Text, value: "foo" }, attrs: null, on: null, scope: null },
+        name: {
+          content: { type: ASTType.Text, value: "foo" },
+          attrs: null,
+          attrsTranslationCtx: null,
+          on: null,
+          scope: null,
+        },
       },
     });
   });
@@ -1448,11 +1513,13 @@ describe("qweb parser", () => {
       isDynamic: false,
       dynamicProps: null,
       props: null,
+      propsTranslationCtx: null,
       on: null,
       slots: {
         name: {
           content: { type: ASTType.Text, value: "foo" },
           attrs: { param: "param" },
+          attrsTranslationCtx: null,
           on: null,
           scope: null,
         },
@@ -1469,12 +1536,14 @@ describe("qweb parser", () => {
       isDynamic: false,
       dynamicProps: null,
       props: null,
+      propsTranslationCtx: null,
       on: null,
       slots: {
         name: {
           content: { type: ASTType.Text, value: "foo" },
           on: { click: "doStuff" },
           attrs: null,
+          attrsTranslationCtx: null,
           scope: null,
         },
       },
@@ -1493,16 +1562,24 @@ describe("qweb parser", () => {
       name: "MyComponent",
       dynamicProps: null,
       props: null,
+      propsTranslationCtx: null,
       isDynamic: false,
       on: null,
       slots: {
         default: {
           content: { type: ASTType.Text, value: " " },
           attrs: null,
+          attrsTranslationCtx: null,
+          on: null,
+          scope: null,
+        },
+        name: {
+          content: { type: ASTType.Text, value: "foo" },
+          attrs: null,
+          attrsTranslationCtx: null,
           on: null,
           scope: null,
         },
-        name: { content: { type: ASTType.Text, value: "foo" }, attrs: null, on: null, scope: null },
       },
     });
   });
@@ -1518,11 +1595,24 @@ describe("qweb parser", () => {
       name: "MyComponent",
       dynamicProps: null,
       props: null,
+      propsTranslationCtx: null,
       isDynamic: false,
       on: null,
       slots: {
-        a: { content: { type: ASTType.Text, value: "foo" }, attrs: null, on: null, scope: null },
-        b: { content: { type: ASTType.Text, value: "bar" }, attrs: null, on: null, scope: null },
+        a: {
+          content: { type: ASTType.Text, value: "foo" },
+          attrs: null,
+          attrsTranslationCtx: null,
+          on: null,
+          scope: null,
+        },
+        b: {
+          content: { type: ASTType.Text, value: "bar" },
+          attrs: null,
+          attrsTranslationCtx: null,
+          on: null,
+          scope: null,
+        },
       },
     });
   });
@@ -1533,6 +1623,7 @@ describe("qweb parser", () => {
       name: "myComponent",
       dynamicProps: null,
       props: null,
+      propsTranslationCtx: null,
       isDynamic: true,
       on: null,
       slots: null,
@@ -1545,6 +1636,7 @@ describe("qweb parser", () => {
       name: "mycomponent",
       dynamicProps: null,
       props: { a: "1", b: "'b'" },
+      propsTranslationCtx: null,
       isDynamic: true,
       on: null,
       slots: null,
@@ -1557,6 +1649,7 @@ describe("qweb parser", () => {
       name: "mycomponent",
       dynamicProps: "state",
       props: { a: "1" },
+      propsTranslationCtx: null,
       isDynamic: true,
       on: null,
       slots: null,
@@ -1587,12 +1680,14 @@ describe("qweb parser", () => {
       name: "MyComponent",
       dynamicProps: null,
       props: null,
+      propsTranslationCtx: null,
       isDynamic: false,
       on: null,
       slots: {
         default: {
           content: { body: null, name: "subTemplate", type: ASTType.TCall, context: null },
           attrs: null,
+          attrsTranslationCtx: null,
           scope: null,
           on: null,
         },
@@ -1613,11 +1708,13 @@ describe("qweb parser", () => {
       name: "MyComponent",
       dynamicProps: null,
       props: null,
+      propsTranslationCtx: null,
       isDynamic: false,
       on: null,
       slots: {
         default: {
           attrs: null,
+          attrsTranslationCtx: null,
           on: null,
           scope: null,
           content: {
@@ -1626,11 +1723,13 @@ describe("qweb parser", () => {
             name: "Child",
             dynamicProps: null,
             props: null,
+            propsTranslationCtx: null,
             on: null,
             slots: {
               brol: {
                 content: { type: ASTType.Text, value: "coucou" },
                 attrs: null,
+                attrsTranslationCtx: null,
                 scope: null,
                 on: null,
               },
@@ -1654,11 +1753,13 @@ describe("qweb parser", () => {
       name: "MyComponent",
       dynamicProps: null,
       props: null,
+      propsTranslationCtx: null,
       isDynamic: false,
       on: null,
       slots: {
         default: {
           attrs: null,
+          attrsTranslationCtx: null,
           on: null,
           scope: null,
           content: {
@@ -1667,11 +1768,13 @@ describe("qweb parser", () => {
             name: "Child",
             dynamicProps: null,
             props: null,
+            propsTranslationCtx: null,
             on: null,
             slots: {
               brol: {
                 content: { type: ASTType.Text, value: "coucou" },
                 attrs: null,
+                attrsTranslationCtx: null,
                 on: null,
                 scope: null,
               },
@@ -1691,6 +1794,7 @@ describe("qweb parser", () => {
       type: ASTType.TSlot,
       name: "default",
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       defaultContent: null,
     });
@@ -1701,6 +1805,7 @@ describe("qweb parser", () => {
       type: ASTType.TSlot,
       name: "header",
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       defaultContent: { type: ASTType.Text, value: "default content" },
     });
@@ -1711,6 +1816,7 @@ describe("qweb parser", () => {
       type: ASTType.TSlot,
       name: "default",
       attrs: null,
+      attrsTranslationCtx: null,
       on: { "click.prevent": "doSomething" },
       defaultContent: null,
     });
@@ -1728,6 +1834,7 @@ describe("qweb parser", () => {
         tag: "div",
         dynamicTag: null,
         attrs: null,
+        attrsTranslationCtx: null,
         on: null,
         ref: null,
         model: null,
@@ -1746,6 +1853,7 @@ describe("qweb parser", () => {
         tag: "div",
         dynamicTag: null,
         attrs: null,
+        attrsTranslationCtx: null,
         on: null,
         ref: null,
         model: null,
@@ -1765,6 +1873,7 @@ describe("qweb parser", () => {
       tag: "div",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: "name",
       model: null,
@@ -1779,6 +1888,7 @@ describe("qweb parser", () => {
       tag: "div",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: "name",
       model: null,
@@ -1795,6 +1905,7 @@ describe("qweb parser", () => {
       tag: "div",
       dynamicTag: null,
       attrs: null,
+      attrsTranslationCtx: null,
       on: null,
       ref: "name",
       model: null,
@@ -1831,6 +1942,7 @@ describe("qweb parser", () => {
       body: {
         content: {
           attrs: null,
+          attrsTranslationCtx: null,
           content: [
             {
               type: ASTType.Text,
@@ -1859,6 +1971,92 @@ describe("qweb parser", () => {
     });
   });
 
+  // ---------------------------------------------------------------------------
+  // t-translation-context
+  // ---------------------------------------------------------------------------
+
+  test('t-translation-context="fr"', async () => {
+    expect(parse(`<t t-translation-context="fr">word</t>`)).toEqual({
+      type: ASTType.TTranslationContext,
+      content: {
+        type: ASTType.Text,
+        value: "word",
+      },
+      translationCtx: "fr",
+    });
+
+    expect(parse(`<div t-translation-context="fr">word</div>`)).toEqual({
+      content: {
+        attrs: null,
+        attrsTranslationCtx: null,
+        content: [
+          {
+            type: 0,
+            value: "word",
+          },
+        ],
+        dynamicTag: null,
+        model: null,
+        ns: null,
+        on: null,
+        ref: null,
+        tag: "div",
+        type: ASTType.DomNode,
+      },
+      translationCtx: "fr",
+      type: ASTType.TTranslationContext,
+    });
+  });
+
+  // ---------------------------------------------------------------------------
+  // t-translation-context-attr
+  // ---------------------------------------------------------------------------
+
+  test('t-translation-context="fr" and t-translation-context-title="pt" for a div attr title', async () => {
+    expect(
+      parse(
+        `<div t-translation-context="fr" title="hello" t-translation-context-title="pt">word</div>`
+      )
+    ).toEqual({
+      content: {
+        attrs: { title: "hello" },
+        attrsTranslationCtx: { title: "pt" },
+        content: [
+          {
+            type: 0,
+            value: "word",
+          },
+        ],
+        dynamicTag: null,
+        model: null,
+        ns: null,
+        on: null,
+        ref: null,
+        tag: "div",
+        type: ASTType.DomNode,
+      },
+      translationCtx: "fr",
+      type: ASTType.TTranslationContext,
+    });
+  });
+
+  test('t-translation-context-title="fr" for component prop title', async () => {
+    expect(parse(`<Comp title="hello" t-translation-context-title="fr" />`)).toEqual({
+      dynamicProps: null,
+      isDynamic: false,
+      name: "Comp",
+      on: null,
+      props: {
+        title: "hello",
+      },
+      propsTranslationCtx: {
+        title: "fr",
+      },
+      slots: null,
+      type: ASTType.TComponent,
+    });
+  });
+
   // ---------------------------------------------------------------------------
   // t-model
   // ---------------------------------------------------------------------------
@@ -1866,6 +2064,7 @@ describe("qweb parser", () => {
     expect(parse(`<input t-model="state.stuff" />`)).toEqual({
       type: ASTType.DomNode,
       attrs: null,
+      attrsTranslationCtx: null,
       content: [],
       on: null,
       ref: null,
@@ -1886,6 +2085,7 @@ describe("qweb parser", () => {
     expect(parse(`<input t-model="state['stuff']" />`)).toEqual({
       type: ASTType.DomNode,
       attrs: null,
+      attrsTranslationCtx: null,
       content: [],
       on: null,
       ref: null,
@@ -1906,6 +2106,7 @@ describe("qweb parser", () => {
     expect(parse(`<input t-model.lazy.trim.number="state.stuff" />`)).toEqual({
       type: ASTType.DomNode,
       attrs: null,
+      attrsTranslationCtx: null,
       content: [],
       on: null,
       ref: null,
@@ -1927,6 +2128,7 @@ describe("qweb parser", () => {
   expect(parse(`<textarea t-model="state.stuff" />`)).toEqual({
     type: ASTType.DomNode,
     attrs: null,
+    attrsTranslationCtx: null,
     content: [],
     on: null,
     ref: null,
@@ -1947,6 +2149,7 @@ describe("qweb parser", () => {
   expect(parse(`<input type="checkbox" t-model="state.stuff" />`)).toEqual({
     type: ASTType.DomNode,
     attrs: { type: "checkbox" },
+    attrsTranslationCtx: null,
     content: [],
     on: null,
     ref: null,
@@ -1967,6 +2170,7 @@ describe("qweb parser", () => {
   expect(parse(`<input type="radio" t-model="state.stuff" />`)).toEqual({
     type: ASTType.DomNode,
     attrs: { type: "radio" },
+    attrsTranslationCtx: null,
     content: [],
     on: null,
     ref: null,
@@ -1987,6 +2191,7 @@ describe("qweb parser", () => {
   expect(parse(`<input type="radio" t-model.lazy.trim.number="state.stuff" />`)).toEqual({
     type: ASTType.DomNode,
     attrs: { type: "radio" },
+    attrsTranslationCtx: null,
     content: [],
     on: null,
     ref: null,
@@ -2012,6 +2217,7 @@ describe("qweb parser", () => {
     expect(parse(`<div t-tag="theTag" />`)).toEqual({
       type: ASTType.DomNode,
       attrs: null,
+      attrsTranslationCtx: null,
       content: [],
       on: null,
       ref: null,
diff --git a/tests/compiler/translation.test.ts b/tests/compiler/translation.test.ts
index 852f8d210..dd3009f59 100644
--- a/tests/compiler/translation.test.ts
+++ b/tests/compiler/translation.test.ts
@@ -86,7 +86,7 @@ describe("translation support", () => {
 
     await mount(SomeComponent, fixture, { translateFn });
     expect(fixture.innerHTML).toBe("<div> mot </div>");
-    expect(translateFn).toHaveBeenCalledWith("word");
+    expect(translateFn).toHaveBeenCalledWith("word", "");
   });
 
   test("translation works, even if initial string has inner consecutive white space", async () => {
@@ -97,7 +97,7 @@ describe("translation support", () => {
     const translateFn = jest.fn((expr: string) => (expr === "some  word" ? "un mot" : expr));
 
     await mount(SomeComponent, fixture, { translateFn });
-    expect(translateFn).toHaveBeenCalledWith("some  word");
+    expect(translateFn).toHaveBeenCalledWith("some  word", "");
     expect(fixture.innerHTML).toBe("<div>un mot</div>");
   });
 
@@ -171,3 +171,126 @@ describe("translation support", () => {
     expect(fixture.innerHTML).toBe("translated");
   });
 });
+
+describe("translation context", () => {
+  test("translation of text in context", async () => {
+    class SomeComponent extends Component {
+      static template = xml`
+        <div>word</div>
+        <div t-translation-context="fr">word</div>
+      `;
+    }
+
+    const translateFn = jest.fn((expr: string, translationCtx: string) =>
+      translationCtx === "fr" ? (expr === "word" ? "mot" : expr) : expr
+    );
+
+    await mount(SomeComponent, fixture, { translateFn });
+    expect(fixture.innerHTML).toBe("<div>word</div><div>mot</div>");
+    expect(translateFn).toHaveBeenCalledWith("word", "");
+    expect(translateFn).toHaveBeenCalledWith("word", "fr");
+  });
+  test("translation of attributes in context", async () => {
+    class SomeComponent extends Component {
+      static template = xml`
+        <div t-translation-context="en" t-translation-context-title="fr" title="title" label="game"/>
+      `;
+    }
+
+    const translateFn = jest.fn((expr: string, translationCtx: string) =>
+      translationCtx === "fr" ? (expr === "title" ? "titre" : expr) : expr
+    );
+
+    await mount(SomeComponent, fixture, { translateFn });
+    expect(fixture.innerHTML).toBe(`<div title="titre" label="game"></div>`);
+    expect(translateFn).toHaveBeenCalledWith("title", "fr");
+    expect(translateFn).toHaveBeenCalledWith("game", "en");
+  });
+  test("body of t-sets are translated in context", async () => {
+    class SomeComponent extends Component {
+      static template = xml`
+        <t t-set="label" t-translation-context="fr">untranslated</t>
+        <t t-esc="label"/>`;
+    }
+
+    const translateFn = jest.fn((expr: string, translationCtx: string) =>
+      translationCtx === "fr" ? "traduit" : expr
+    );
+
+    await mount(SomeComponent, fixture, { translateFn });
+    expect(fixture.innerHTML).toBe("traduit");
+    expect(translateFn).toHaveBeenCalledWith("untranslated", "fr");
+  });
+  test("props with modifier .translate are translated in context", async () => {
+    class ChildComponent extends Component {
+      static props = ["text"];
+      static template = xml`<span t-esc="props.text"/>`;
+    }
+
+    class SomeComponent extends Component {
+      static components = { ChildComponent };
+      static template = xml`
+        <ChildComponent text.translate="game" t-translation-context-text.translate="fr" />`;
+    }
+
+    const translateFn = jest.fn((expr: string, translationCtx: string) =>
+      translationCtx === "fr" ? "jeu" : expr
+    );
+
+    await mount(SomeComponent, fixture, { translateFn });
+    expect(fixture.innerHTML).toBe("<span>jeu</span>");
+    expect(translateFn).toHaveBeenCalledWith("game", "fr");
+  });
+  test("slot attrs and text contents are translated in context", async () => {
+    class ChildComponent extends Component {
+      static template = xml`
+        <div t-translation-context="ja">
+          <t t-slot="a"/>
+        </div>`;
+    }
+
+    class SomeComponent extends Component {
+      static components = { ChildComponent };
+      static template = xml`
+        <ChildComponent t-translation-context="fr">
+          <t t-set-slot="a" title.translate="title" t-translation-context-title.translate="pt">game</t>
+        </ChildComponent>
+        `;
+    }
+
+    const translateFn = jest.fn((expr: string, translationCtx: string) =>
+      translationCtx === "fr" ? "jeu" : translationCtx === "pt" ? "título" : expr
+    );
+
+    await mount(SomeComponent, fixture, { translateFn });
+    expect(fixture.innerHTML).toBe("<div>jeu</div>");
+    expect(translateFn).toHaveBeenCalledWith("game", "fr");
+    expect(translateFn).toHaveBeenCalledWith("title", "pt");
+  });
+  test("default slot params and content translated in context", async () => {
+    class SomeComponent extends Component {
+      static template = xml`
+        <div>
+          <t
+            t-slot="default"
+            t-translation-context="fr"
+            param.translate="param"
+            title.translate="title"
+            t-translation-context-title.translate="pt"
+          >
+            foo
+          </t>
+        </div>`;
+    }
+
+    const translateFn = jest.fn((expr: string, translationCtx: string) =>
+      translationCtx === "pt" ? "título" : expr
+    );
+
+    await mount(SomeComponent, fixture, { translateFn });
+    expect(fixture.innerHTML).toBe("<div> foo </div>");
+    expect(translateFn).toHaveBeenCalledWith("foo", "fr");
+    expect(translateFn).toHaveBeenCalledWith("param", "fr");
+    expect(translateFn).toHaveBeenCalledWith("title", "pt");
+  });
+});