Miško Hevery
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    # $localize - tagged template design Design a localization system which can be used for: - `Goal-A`: Angular i18n or any other application/library. *(Not directly tied to Angular projects)* - `Goal-B`: Can be used without a compile step. *(Easy to adopt, important for quick turnaround in dev mode)* - `Goal-C`: Supports runtime translation evaluation. - `Goal-D`: Supports compile time translation inlining. - `Goal-E`: Supports message extraction for generating translation files :::info "translation evaluation" is the process of rendering a localized message with translated text at runtime. ::: :::info "translation inlining" is the process of replacing the original localization marker and message with a translated message, leaving no trace of the original i18n localization marker. ::: ### Additional design docs * [Global import migration](https://hackmd.io/I6obNkZmQNKhw7KlgcgYHg) * [Message id support](https://hackmd.io/33M5Wb-JT7-0fneA0JuHPA) * [Legacy message id handling](https://hackmd.io/EQF4_-atSXK4XWg8eAha2g) ### Prior Work - [Design: Angular translation service](https://docs.google.com/document/d/1h_y3mJ6kULNEoM2CymCgiqt5WMaLNm0cIkrhcLY30TY) - [skolmer/es2015-i18n-tag: ES2015 template literal tag for i18n and l10n (translation and internationalization)](https://github.com/skolmer/es2015-i18n-tag) ## Summary of Proposal ```typescript $localize `:greeting:Hello ${name}:title:`; ``` `$localize` is a global [tagged-template handler](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_templates) function that works by marking messages that need to be translated. - The `$localize` function is not tied in any way to Angular and can be used independently of the Angular framework. (See `Goal-A`). - The `$localize` function will work without any further processing. (See `Goal-B`) This is important for quick turnaround in development mode. - The `$localize` function is global because we want to make sure that it can be easily identified in the output code even after passing through minification and obfuscation tools (such as web pack). This will make it easy achieve `Goal-D` and easily identify and inline messages when compile-time inlining. - An extractor tool can be run over javascript files to collect all of the strings for translation. `Goal-E` ### Translations The main point of tagging messages in code for translation is the ability to replace them with translated messages later. Consider the following tagged message: ```typescript alert($localize`Hello World`); ``` #### Runtime evaluation If we load a set of French translations like this: ```typescript loadTranslations({ // Assume that 123456789 is the message id 123456789: 'Bonjour le monde', }); ``` The alert would display `Bonjour le monde`. The above example is referred to as `runtime inlining` and it supports `Goal-C`. #### Compile-time inlining An alternative is to process the bundled source code with a command line tool which identifies the `$localize` tagged strings and converts the code in-place. Therefore the inlined code would look like: ```typescript alert('Bonjour le monde'); ``` Notice: - That `$localize` is completely removed as part of the compile time in lining. - That the compile-time inlining can run on any input, but it is specifically designed to be able to take production output which is minimized and chunked into multiple files, such as from web-pack or rollup with terser. ## Detailed Design ### Source format In its simplest form the translation string would be: ```typescript $localize `Hello ${name}`; ``` However when extracting localization strings it is often necessary to have description and attribute names. For this reason a more complete form would be: ```typescript $localize `:meaning|description:@@custom_id:Hello ${name}:placeholder_name:`; ``` The additional meta-information useful for translation is stored in "meta-blocks". This is done so that metadata is not removed from production build by the minifier. The implication of this is that the runtime evaluation and compile-time inlining processes must remove these metadata blocks. ### Pass-through behavior It is important for the code to run without any additional processing. For this reason `localize` is implemented with a minimal runtime which needs to be present in any application containing `$localize` tagged messages. This minimal implementation is attached to the global object by adding the following "side-effecty" import: ```typescript import '@angular/localize/init'; ``` ### Runtime translation The minimal pass-through implementation checks for the existence of a method called `$localize.translate()`. If this exists then it will call it to get a translated version of the message. This is the basis of runtime translation support. ```typescript $localize.translate( messageParts: TemplateStringsArray, expressions: readonly any[]): [TemplateStringsArray, readonly any[]]; ``` :::info A message to be translated consists of `messageParts`, which contain the static strings parts of a message, and `expressions` which are the values to be substituted in between each message part. Both are passed to the `$localize.translate()` function because a translation may re-order the `expressions` for a particular locale. ::: There is a basic runtime translation implementation provided. It is configured by importing and calling `loadTranslations()`. ```typescript= import {loadTranslations} from '@angular/localize'; loadTranslations(localizationMap: { [messageId: string]: string }): void; ``` Calling this function will ensure that the `$localize.translate()` function is initialized and configured to use the given translations. ### Command line tools In addition to runtime the localization library provides a set of CLI tools which can perform extraction and infusion of the translated strings back into the source. #### Translation message extraction :::info Extraction does not need to be implemented for v9. For v9 the translation files can be generated by the Angular CLI (e.g. `ng xi18n`), which uses the current ViewEngine compiler to extract messages directly from templates. ::: Extraction is performed with `localize-extract` binary. It is responsible for scanning the source files of the project and looking for the `$localize` tagged messages as described in [Source Format](#Source-format) section. #### Compile-time translation inlining Translation inlining is performed by the `localize-translate` binary. The command is responsible for searching the file for the `$localize` tagged messages and replacing them with the translated messages. This process completely removes the need for and `localize` function implementation. ```typescript= alert($localize`Hello ${name}`); ``` will translate into ```typescript= alert('Bonjour ' + name + ''); ``` Notice that `$localize` has been completely removed in the process. ## Angular integration Given an angular template such as this: ```htmlmixed= <h1 i18n>Hello {{name}}!</h1> ``` The Angular compiler generates code which includes this snippet. ```typescript= var $I18N_7$; if (ngI18nClosureMode) { // ... Google specific message localization } else { $I18N_7$ = $localize`Hello ${"\uFFFD0\uFFFD"}:INTERPOLATION:!`; } ``` ## Implementation goals There are a number of pieces to be implemented (or taken into consideration but deferred till after v9) as part of this `$localize` design. Some will be implemented by the Angular Framework team and some by the Angular tooling team. Below are the implementation goals for the Framework team. ### v9.0.0 | Feature | Description | | -------- | -------- | | **Passthru infusion** | Provide basic `$localize` implementation which only returns the string so that dev-mode does not need any additional processing. | | **Compile-time infusion** | Implement a Babel plugin that can map localized message to a specified translation during compilation. This will be managed by the CLI. | | **Update ngtsc** | Modify ngtsc to generate `$localize` tagged template expressions rather than calls to `ɵɵi18nLocalize()` | ### Post v9.0.0 | Feature | Description | | -------- | -------- | | **Message extraction** | Don't implement extraction. Just use existing template XLIFF we already have. This means that we will only be able to translate templates (not strings in source code). This is fine since v8 Works the same way. (VERIFY THIS WORKS) | | **Run-time infusion** | Implement a version of `$localize` that supports loading translations at run-time and maps localized messages to the relevant translation. | ## Implementation details The features are implemented in a reusable manner to allow them to be put together in different ways to support a variety of use-cases. For example, as stand-alone tools, or integration with build pipe-lines other than Babel. ### Folder layout ``` angular/angular/packages localize init/index.ts src/ localize/ tools/ utils/ translate.ts test ... ``` The folders are described below: * `init/index.ts`: attaches the `$localize` function to the global object. * `src/localize/`: the implementation of the `$localize()` function used in `init`. * `src/tools`: contains the compile-time inliner and extraction tools * `src/utils/`: contains reusable functions that are shared between the other parts of the library. Some may be exported publicly for 3rd party use. * `src/translate.ts`: contains the `loadTranslations()` and `$localize.translate()` functions. The `localize` folder is a top level Angular package (distributed as an npm package). `localize/init` is also an entry-point into the package. ## Outstanding questions #### Code sharing Some utilities are common to more than one Angular package. For example, `utf8Encode()` is used in `i18n` and `compiler`. How should we avoid code duplication of this code? I.E. how to share code (via Bazel depdendencies or npm package dependencies?) :::info Code that needs to be shared within `@angular/localize` is exposed as a secondary entry-point at `@angular/localize/utils`. This is the cleanest way to share code since the `run_time` entry-point is in Angular package format but the `compile_time` entry-point is a node.js package format. ::: :::warning Currently the `compile_time` entry-point relies upon code inside `@angular/compiler` to parse the translation files. So that package must be a peer dependency (or optional?). There is an open question about what to do about this. ::: ## Alternative proposals ### Deployment proposal (July 24) The following proposal should provide a way to avoid having to include an import in application code. *It is subject to confirming some technical details for webpack.* 1) Always include a minimal viable (not just passthru) implementation of the global `$localize` function in `@angular/core` as standard. > [name=Miško Hevery] > - Including it in `@angular/core` may be too late, since it is possible for application call to call `$localize` before `@angular/core` is loaded. > - I think in `ngDevMode` we should create `$localize` which will throw an error if someone access it with a message to add `$localize` shim. (This may be missed for abover reason.) > - This also implies that `@angular/core` has side-effect, and and I am not sure what weird implications that would have down the line. > - This also breaks the runtime i18n which requires that we need to load the messages before the application `.js` is loaded (because runtime i18n implies that we don't want to have multiple `.js` files) 2) During the production builds (in webpack), check for the existence of `$localize` calls in the entire program (i.e. all the bundles). If none are found then remove the global function from the program. 3) During compile-time infusion, remove the global function from the program. This approach gives us the following benefits: * There is no need to add an import for the global `$localize` in user code - ever! - not even in `polyfill.ts`. * In development mode, there is no extra processing required to support i18n as the `$localize` function is always available whether or not you are using i18n. * If there is no i18n being used (i.e. not `%localize` tagged strings) then the global function is stripped out in production mode - a form of tree shaking. * If not explicitly providing translations, despite using `i18n` tags, even production builds will work as expected. * If using compile-time infusion, the global function will be stripped out as part of the infusion process. * Full run-time infusion is just the same as the pass-thru case, except that translations are provided through a loading mechanism. * Application developers can use `$localize` to programmatically tag strings straight away with no further effort in the framework. There is only one `$localize` function for all runtime scenarios (pass-thru and run-time infusion): ```typescript= declare const $localize: Localize; declare interface Localize { (messageParts: TemplateStringsArray, ...expressions: any[]): string; /** * A map of message/id to translated message. Used in runtime infusion to hold the translations. * */ map: {[key: string]: string[]}; /** * A function that will return a string identifier for the given messageParts */ hash(messageParts: string[]|TemplateStringsArray): string; } ``` This interface provides a `map` property and a `hash` method. The `map` holds the currently loaded translations. If there are none then `$localize` falls back to the pass-thru mode. This is a minimal implementation to support all pass-thru and run-time infusion modes. Implementing runtime infusion in an application, is simply a case of loading up translations. The stategies for doing so are still TBD. ### extra script in a global fn proposal (July 24) ```html <html> ... <body> <script src="runtime.ca5745c8d8fbdbaf630b.js" type="module"</script> <script src="polyfills.60185d0cfe1893b52a44.js" type="module"></script> <script src="i18n-fr.893b52a4460185d0cfe1.js" type="module"></script> <script src="main.deadf89a6d615608cfde.js" type="module"></script> </body> </html> ``` ```typescript= (function(global: any) { const $localize: Localize = function localize(messageParts: TemplateStringsArray): string { const id = $localize.hash(messageParts); const translation = $localize.map[id] || messageParts; let message: string = translation[0]; for (let i = 1; i < translation.length; i++) { message += arguments[i] + translation[i]; } return message; }; $localize.map = {}; $localize.hash = messageParts => messageParts.join(''); return global['$localize'] = $localize; })(typeof window !== 'undefined' && window || typeof self !== 'undefined' && self || global); ``` ### takeways - use side-effecty import to load `$localize` - use side-effecty import to load `$localize.load` for runtime localization of strings. - placeholders are passed as expressions - for new users it is reasonable that we ask them to install a new package - installation should be as simple as `ng add @angular/localize` and include modification of the polyfills.ts - and possibly also configuration of the infusion step in the build pipeline (angular.json) - this might be a separate schematic that would require `ng generate` invocation rather than a default one invoked by `ng add`. - we need to ensure that we give a compiler error instructing the user to `ng add @angular/localize` if the shim is not loaded - `ngtsc` can do this by detecting if $localize type is defined in the global scope - we should also give a runtime error if the shim is not loaded - `@angular/core` in `ngDevMode` adds `$localize` to global scope (if not already defined), and this implementation will throw error if someone tries to use it instructing them ho `ng add @angular/localize`. - for existing projects we need to detect if the project uses i18n and if it does invoke `ng add @angular/localize` - we can detect i18n usage by scanning metadata of application ~~(and possibly node_modules)~~ and look for `/\wi18n\w/` expression. - we might need to modify angular.json to teach users about how to use the new localization pipeline - for existing projects that are upgrading we should try to convert the existing angular.json with i18n build config to the new pipeline (or ensure that the old pipeline invocation is still compatible). ### Non-global proposal Building on an [idea by Trotyl in GitHub](https://github.com/angular/angular/pull/31609#discussion_r311094455)... The idea is that we can avoid having to use a global in the source code by providing a two-step compilation process for compile-time inlining. :::info **Conclusion** We have decided not to adopt this proposal and to stick with the plain global function design. **Publishing libraries** The introduction of two steps would cause problems with publishing libraries (e.g. to npm) that support i18n. Since the library would be bundled, the first step would need to be run before publication - leaving the global marker in place - which would then prevent the run-time local import approach from working. The only way around this would be to fall back on the global approach, which is what we are proposing already. **Complicated build pipe-line** Further, this approach requires each application project's build pipeline to be aware of the two steps. While we could build a TS/webpack based approach, we are unlikely to provide other build tool plugins and also it would require the application developer to understand this tooling to configure their build appropriately even if they were available. ::: #### An example Source code before compilation: ```typescript= import {$localize} from '@angular/localize'; alert($localize`Hello World`); ``` Source code after TS compilation [step-1] (i.e. in ES2015 bundle): ```typescript= alert(ɵlocalize`Hello World`); ``` Source code after i18n inlining [step-2] (i.e in locale specific bundle): ```typescript alert(`Bonjour le monde`); ``` #### Run-time/pass-through localization To use localization in your app: * Add a normal `import {$localize} from '@angular/localize';` to source code that requires i18n (i.e. user code and in our generated templates). * This import actually refers to the run-time localization code. * If you do nothing else then you just need to add a dependency on `@angular/localize` to your package.json and you get run-time inlining. * The template compiler would add local imports of `$localize` as necessary where `i18n` tags have been found in the template #### Compile-time inlining To do compile-time inlining: * Turn on a flag in ngc (similar to `enableIvy`, say `inlineI18n`). * A TS transform will convert all the locally imported `$localize` identifiers into a global identifier (i.e. it would go through and rename all `$localize` identifiers with some global like `ɵlocalize`) and also remove all the `import {$localize} from '@angular/localize';` statements). * this global `ɵlocalize` has no implementation but acts as a marker that finds its way through the compilation process untouched by minifiers etc into the bundles. * Run the actual compile-time inlining on the bundles containing the global, just like we planned already. #### Benefits This will result in the following benefits: * There is no need to add anything to `polyfill.ts` or other files outside of CLI setups. * If you don’t use i18n then nothing has to change in your app or build setup. * backwards compatibility for v8 projects * if you use `i18n` tags in your templates, then you just need add `@angular/localize` to your package.json dependencies and you have run-time/pass-through localization with no more work. * minimal change to v8 projects * if you want to use compile-time inlining then you just turn on the `inlineI18n` flag and ngtsc/CLI work together to generate the bundles with no trace of the original `$localize` code. * no extra deployment cost (i.e. in polyfills.ts) for projects that use compile-time inlining. * It is possible to support i18n in multipe Angular apps on a single browser page using run-time inlining. * Each app will get its own copy of the runtime `$localize` code. #### External users For non-Angular users of `@angular/localize`: * The `@angular/localize` is a required dependency - but no other Angular packages. * This package will contain the run-time localization plus the utilities for doing compile-time inlining. * Run-time inlining works out of the box. * Just add `import {$localize} from '@angular/localize';` and build as normal. * Compile-time inlining requires the two-step compilation process * First step: * if compiling with TypeScript then add the `ts.Transform` used in ngtsc to the compiler configuration (published as part of `@angular/localize`). * if not using TS (e.g. compiling with Babel) then add a Babel plugin to the compiler configuration (possibly published by Angular team). * Second step: * the second step is the Angular agnostic inlining tool (i.e. a wrapper around a Babel plugin) so this can be reused as-is. * If there is no compiler (e.g. plain JavaScript), then steps 1 and 2 can effectively be joined together to provide compile-time inlining in a tool that runs directly on the source JS.

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully