Angular: i18n:能够在模板之外使用翻译字符串

创建于 2016-09-07  ·  204评论  ·  资料来源: angular/angular

我正在提交... (用“x”勾选)

[x] feature request

当前行为
https://github.com/angular/angular/issues/9104#issuecomment -244909246

我不认为这是可能的,就像我之前说的它只适用于静态文本,它不会解析 js 代码上的文本,只有模板

预期/期望的行为
能够使用 API 翻译代码中任何地方使用的字符串。

重现问题

预期的行为是什么?
我引用$translate.instant的用法来展示真实的用例:

  • 自定义渲染文本:
if (data._current_results === data._total) {
                content = this.$translate.instant('CLIPPINGS__LIST__SUMMARY_ALL', {'num': data._current_results});
            } else {
                if (undefined === data._total) {
                    data._total = '...';
                }

                content = this.$translate.instant('CLIPPINGS__LIST__SUMMARY', {
                    'num': data._current_results,
                    'total': data._total
                });
            }

            // Put 'mentions' first
            data = angular.merge({}, {
                mentions: mentions
            }, data);

            _.each(data, (value:number, key:string):void => {
                if (value) {
                    details += value + ' ' + this.$translate.instant('CLIPPINGS__LIST__SUMMARY_TYPE_' + key) + ', ';
                }
            });

            if (details) {
                details = '(' + _.trim(details, ', ') + ')';
            }

            content = content.replace(':details', details);

更多示例:

  • 从 HTML 呈现报告的导出图像中获取图像的文件名:
getExportImageName(hideExtension:boolean):string {
        let fileName:string;

        fileName = this.$translate.instant('D_CHART_FACET_authors__EXPORT_FILENAME', {
            'profileName': this.ExportService.GetProfileName(),
            'period': this.ExportService.GetPeriodString(this.SearchFilter.GetPeriodFromInterval())
        });

        if (!Boolean(hideExtension)) {
            fileName += '.png';
        }

        return fileName;
    }
  • 有时您正在翻译,有时使用模型数据(在模板中可能非常冗长):
private _getTitle():string {
        if (this.inShareColumn) {
            return this.$translate.instant('COMPARISONS__SHARE_COLUMN_share_of_voice_TITLE');
        } else if (this.inTotalsColumn) {
            return this.$translate.instant('COMPARISONS__TOTAL_COLUMN_share_of_voice_TITLE');
        } else {
            return _.get<string>(this.group, 'profileName', '');
        }
    }
  • 使用第三方图表插件(Highcharts)
this.chart = new Highcharts.Chart(<any>{
            title: {
                text: this.$translate.instant('REPORTS_BLOG_MAPPING_CHART_TITLE_tone').toUpperCase(),
            },
            xAxis: {
                title: {
                    text: this.$translate.instant('REPORTS_BLOG_MAPPING_CHART_TITLE_tone_xaxis')
                }
            },
            yAxis: {
                min: 0,
                title: {
                    text: this.$translate.instant('REPORTS_BLOG_MAPPING_CHART_TITLE_tone_yaxis')
                }
            },
            plotOptions: {
                scatter: {
                    tooltip: {
                        headerFormat: '<b>{point.key}</b><br>',
                        pointFormat: '{point.y} ' + this.$translate.instant('REPORTS_BLOG_MAPPING_CHART_mentions')
                    }
                }
            }
        });
  • 设置配置变量并在没有管道的情况下呈现文本,因为它并不总是翻译后的字符串
this.config = {
            requiredList: true,
            bannedList: false,
            allowSpaces: false,
            allowComma: false,
            colorsType: false,
            defaultEnterAction: 'required',
            requiredTooltip: this.$translate.instant('D_CLIPPING_TAGS__REQUIRED_TOOLTIP'),
            bannedTooltip: this.$translate.instant('D_CLIPPING_TAGS__BANNED_TOOLTIP')
        };
  • 设置 window.title :) :
SetWindowTitle(title:string) {
        if (!!title) {
            this.$window.document.title = this.$translate.instant(title);
        }
    }
  • 自定义日期格式:
dateHuman(date:Date):string {
        return date.getDate() + ' ' + this.$translate.instant('GLOBAL_CALENDAR_MONTH_' + date.getMonth())
            + ' ' + date.getFullYear();
    }
  • 根据翻译的值对事物进行排序:
// Sort types
            tmpTypes = _.sortBy(tmpTypes, (type:string):string => {
                // 'MISC' at the end
                if ('MISC' === type) {
                    return 'zzzzz';
                }

                return this.$translate.instant('FACET_phrases2__TYPE_' + type);
            });
GetSortedLanguages():IFacetLangDetectedCommonServiceLanguageObject[] {
        // We have to sort by translated languages!
        return _.sortBy(_.map(this.facetOptions, (item:string):any => {
            return {
                key: item,
                label: this.$translate.instant('FACET_langDetected_' + item),
                cssStyle: (_.includes(['english', 'catalan', 'spanish', 'french', 'italian'], item))
                    ? {'font-weight': 'bold'} : null,
                flag: _.get(this.lutFlags, item, null)
            };
        }), (item):string => {
            return item.label.toLowerCase();
        });
    }
  • 将原始数据导出到 CSV 或 Excel 并转换值:
getDataExportStacked(inputData:any):any {
        let exportData = angular.copy(inputData);

        if (angular.isArray(exportData) && exportData.length) {
            exportData[0].name = this.$translate.instant('CLIPPINGS__CHARTS_volume_TITLE');

            exportData[0].data = _.map(exportData[0].data, (inputDataItem:any):any => {
                return {
                    'label': inputDataItem.association.profileName,
                    'value': inputDataItem.value
                };
            });
        }

        return exportData;
    }
  • 将配置字符串设置为第三方插件:
UpdateCalendarStrings():void {
        Highcharts.setOptions({
            lang: {
                months: [
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_January'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_February'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_March'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_April'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_May'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_June'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_July'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_August'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_September'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_October'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_November'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_December')
                ],
                shortMonths: [
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_SHORT_Jan'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_SHORT_Feb'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_SHORT_Mar'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_SHORT_Apr'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_SHORT_May'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_SHORT_Jun'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_SHORT_Jul'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_SHORT_Aug'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_SHORT_Sep'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_SHORT_Oct'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_SHORT_Nov'),
                    this.$translate.instant('GLOBAL_CALENDAR_MONTH_SHORT_Dec')
                ],
                weekdays: [
                    this.$translate.instant('GLOBAL_CALENDAR_DAY_Sunday'),
                    this.$translate.instant('GLOBAL_CALENDAR_DAY_Monday'),
                    this.$translate.instant('GLOBAL_CALENDAR_DAY_Tuesday'),
                    this.$translate.instant('GLOBAL_CALENDAR_DAY_Wednesday'),
                    this.$translate.instant('GLOBAL_CALENDAR_DAY_Thursday'),
                    this.$translate.instant('GLOBAL_CALENDAR_DAY_Friday'),
                    this.$translate.instant('GLOBAL_CALENDAR_DAY_Saturday')
                ]
            }
        });
    }


改变行为的动机/用例是什么?
能够翻译模板外的字符串。

请告诉我们您的环境:

  • 角度版本: 2.0.0-rc.6
  • 浏览器: [全部]
  • 语言: [TypeScript 2.0.2 | ES5 | 系统JS]

@vicb

i18n feature high

最有用的评论

我认为这是一个真正的精彩,在实施之前 i18n 还没有准备好使用。
例如,我无法在代码中设置翻译后的验证消息

所有204条评论

我认为这是一个真正的精彩,在实施之前 i18n 还没有准备好使用。
例如,我无法在代码中设置翻译后的验证消息

@Mattes83你试过这种方式吗?

<p [hidden]="!alertManagerRowForm.controls.sendTo.hasError('validateEmail')" class="help-error">
    {{ 'ALERTMANAGER__FORM__FIELD__sendTo__ERROR__validateEmail' | translate }}
</p>

:confused: 文档告诉你在代码中有错误消息是多么好,并且翻译支持需要模板中的任何内容。 看来还是需要沟通。

@marcalj我知道这种方式......但我需要在我的 ts 文件中包含本地化字符串。
@manklu我完全同意

我认为这是一个真正的精彩,在实施之前 i18n 还没有准备好使用。
例如,我无法在代码中设置翻译后的验证消息

是的,这里也一样,我刚刚从 ng2-translate 切换到 Angular2 i18n,因为我更喜欢使用 OOTB 模块,而且提取翻译更容易(ng2-translate 更耗时 IMO)
在这个阶段,我无法翻译从我的服务中引发的错误消息。 也没有解决方法。

如果有人想开始设计规范,那会很有帮助(即在 Google Docs 上)。

它需要列出所有案例。 想了想,觉得有必要

_.('static text');
_.('with {{ parameter }}', {parameter: "parameter" });
_.plural(count, {'few': '...'});
_.select(on, {'value': '...'});

嗨,伙计们,很棒的功能要求:+1:

是否有任何建议的解决方法来翻译 *.ts 文本?

@fbobbio我一直在做的是在模板中创建隐藏元素,例如。
<span class="translation" #trans-foo i18n>foo</span>

使用以下方法绑定它们:
@ViewChild('trans-foo) transFoo : ElementRef;

然后检索翻译值
transFoo.nativeElement.textContent

看起来迟钝,但适合我的需要。

由于 ng-xi18n 已经处理了整个 TS 代码,为什么不为 (string-)properties 实现一个像@i18n()这样的装饰器呢? 然后可以用转换后的值填充这些值,例如@Input()与单向数据绑定一起使用。
如果无法从代码中轻松提取未翻译的值,只需将其放在参数中,如下所示:

@i18n( {
  source : 'Untranslated value',
  description: 'Some details for the translator'
} )
public set translatedProperty( value : string ) {
   this._translatedProperty = value;
}

并在没有翻译目标时将源输入属性。

这是整个国际化周期中必不可少的项目,IMO。

在我的商店里,我们习惯于使用“类似ng-xi18n”的工具(xgettext 的扩展),它会爬取所有类型的源文件,寻找标记的文本以放入翻译人员的字典文件中。

我喜欢 HTML 模板中简洁明了的 i18n 标记,我期待 Typescript 代码也一样。

@vicb除了您的用例之外,我还在考虑支持TS代码中插值字符串本地化的可能性。 但是,可能需要重写 TS 代码以支持这种情况。 这会是一种有效的方法吗?

这是阻止我们使用开箱即用的方法来翻译 Angular 2 提供的主要功能。 我们有许多元数据驱动的组件,它们的键来自一些未存储在 HTML 中的元数据。 如果我们可以通过管道或以编程方式针对可用的 TRANSLATIONS 进行翻译,我们可以让这些组件显示正确的字符串。

同时,由于这个限制,我们仅限于 ng-translate 之类的东西。

我解决这个问题的方法是在整个应用程序中使用的服务中添加一个空的“lang”对象。 然后我应用一个指令来读取 div 中的所有 span 并将值存储在该对象中。 我将所有字符串放在页面底部的模板中,并带有隐藏属性。 然后可以从模板或组件访问该字符串。 这很丑陋,您可以轻松地覆盖具有相同 ID 的条目。 但总比没有好。

服务

@Injectable()
export class AppService {

    //Language string object.
    private _lang:Object = new Object();
    get lang():Object {
        return this._lang;
    }
}

指示

@Directive({
    selector: '[lang]'
})
export class LangDirective implements OnInit {

    constructor(
        public element: ElementRef,
        public app: AppService) {
    }

    ngOnInit() {
        let ele = this.element.nativeElement;
        for (var i = 0; i < ele.children.length; i++) {
            let id = ele.children[i].getAttribute('id');
            let value = ele.children[i].innerHTML;
            this.app.lang[id]=value;
        }
    }
}

模板

<button>{{app.lang.myButtonText}}</button>
<div lang hidden >
    <span id="myButtonText" i18n="Test Button">Testing</span>
</div>

你好@lvlbmeunier

感谢您在我们等待正式实施期间提供解决方法的建议。 我试图实施您的解决方案,但我似乎无法让动态翻译键得到识别。 我试图这样做:

<p *ngFor="let d of dataEntry">{{app.lang[d.name]}}</p>
<div lang hidden >
<span id="{{d.name}}" *ngFor="let d of dataEntry" i18n>{{d.name}}</span>
</div>

这些新密钥不会出现在我的 xliff 文件中。 是否有可能通过您的解决方案实现这一目标?

我从未尝试过,但我几乎可以肯定 xliff 文件的构建不会运行代码。 在 i18n 中具有动态值将与概念相反。 如果您确切知道列表中的所有名称,则它们都应该独立声明,而不是在 for 循环中。

手动添加密钥是可行的,但这是不切实际的。 就我而言,我从 API 中获得了数百个需要翻译的文本键。

您可以运行代码并获取源代码并将其复制到文件中。 x18n 文件的生成是基于静态文件的。

使用4.0.0-beta您可以将 id 分配给i18n ,例如 mainTitle:

<span i18n="title@@mainTitle">

有了这个,我们至少可以为 JIT 编译器创建一个包含我们所有“额外”翻译的虚拟组件(它不需要添加到 app html,只需添加 app 模块)。

对于初学者,我们不仅将提供程序添加到编译器,还将添加到应用程序模块:

// bootstrap.ts
getTranslationProviders().then(providers => {
    const options = { providers };
    // here we pass "options.providers" to "platformBrowserDynamic" as extra providers.
    // otherwise when we inject the token TRANSLATIONS it will be empty. The second argument of
   // "bootstrapModule" will assign the providers to the compiler and not our AppModule
    platformBrowserDynamic(<Provider[]>options.providers).bootstrapModule(AppModule, options);
});

然后我们将创建一个虚拟组件来容纳我们额外的翻译,不要忘记将该组件添加到declarationsAppModule中。 这是为了让ng-xi18n可以找到 html(我认为)并将其添加到翻译文件中。

//tmpI18N.ts
import {Component} from '@angular/core';

@Component({
    selector: 'tmpI18NComponent',
    moduleId: module.id,
    templateUrl: 'tmp.i18n.html'
})
export class TmpI18NComponent {
}

将我们的翻译添加到tmp.i18n.html

<!-- tmp.i18n.html -->
<span i18n="test@@mainTitle">
    test {{something}}
</span>

现在我们可以创建一个服务来获取我们的翻译:

import {Injectable, Inject, TRANSLATIONS} from '@angular/core';
import {I18NHtmlParser, HtmlParser, Xliff} from '@angular/compiler';

@Injectable()
export class I18NService {
    private _source: string;
    private _translations: {[name: string]: any};

    constructor(
        @Inject(TRANSLATIONS) source: string
    ) {
        let xliff = new Xliff();
        this._source = source;
        this._translations = xliff.load(this._source, '');
    }

    get(key: string, interpolation: any[] = []) {
        let parser = new I18NHtmlParser(new HtmlParser(), this._source);
        let placeholders = this._getPlaceholders(this._translations[key]);
        let parseTree = parser.parse(`<div i18n="@@${key}">content ${this._wrapPlaceholders(placeholders).join(' ')}</div>`, 'someI18NUrl');

        return this._interpolate(parseTree.rootNodes[0]['children'][0].value, this._interpolationWithName(placeholders, interpolation));
    }

    private _getPlaceholders(nodes: any[]): string[] {
        return nodes
            .filter((node) => node.hasOwnProperty('name'))
            .map((node) => `${node.name}`);
    }

    private _wrapPlaceholders(placeholders: string[]): string[] {
        return placeholders
            .map((node) => `{{${node}}}`);
    }

    private _interpolationWithName(placeholders: string[], interpolation: any[]): {[name: string]: any} {
        let asObj = {};

        placeholders.forEach((name, index) => {
            asObj[name] = interpolation[index];
        });

        return asObj;
    }

    private _interpolate(pattern: string, interpolation: {[name: string]: any}) {
        let compiled = '';
        compiled += pattern.replace(/{{(\w+)}}/g, function (match, key) {
            if (interpolation[key] && typeof interpolation[key] === 'string') {
                match = match.replace(`{{${key}}}`, interpolation[key]);
            }
            return match;
        });

        return compiled;
    }
}

现在我们可以做类似的事情:

export class AppComponent {

    constructor(i18nService: I18NService) {
        // Here we pass value that should be interpolated in our tmp template as a array and 
        // not an object. This is due to the fact that interpolation in the translation files (xlf for instance)
        // are not named. They will have names such as `<x id="INTERPOLATION"/>
        // <x id="INTERPOLATION_1"/>`. And so on.
        console.log(i18nService.get('mainTitle', ['magic']));
    }
}

这是一个 hacky 解决方法。 但至少我们可以利用翻译文件,按键获取,插值,并且不必在我们的应用程序中有隐藏的 html。

注意:当前的@angular/compiler-cli NPM 包 4.0.0-beta 的依赖版本不正确@angular/tsc-wrapped 。 它指向 0.4.2 它应该是 0.5.0。 @vicb这很容易解决吗? 还是我们应该等待下一个版本?

@fredrikredflag 太棒了! 那么AOT呢?

@ghidoz AOT 是另一个故事。 我们想要做的是预编译所有翻译,以便我们可以通过密钥获取它们。 但是由于ngc将用正确的翻译替换所有i18n ,我们无法利用它。 找不到任何包含来自ngc的已解析翻译的公开选项/属性。 我们可以使用与 JIT 相同的动态方法,通过获取 xlt 文件的方式仍然在TRANSLATION令牌上提供它。 然而,这违背了 AOT 的目的。

不要为任何生产应用程序这样做

/// bootstrap.aot.ts
function fetchLocale() {
    const locale = 'sv';
    const noProviders: Object[] = [];

    const translationFile = `./assets/locale/messages.${locale}.xlf`;
    return window['fetch'](translationFile)
        .then(resp => resp.text())
        .then( (translations: string ) => [
            { provide: TRANSLATIONS, useValue: translations },
            { provide: TRANSLATIONS_FORMAT, useValue: 'xlf' },
            { provide: LOCALE_ID, useValue: locale }
        ])
        .catch(() => noProviders);
}

fetchLocale().then(providers => {
    const options = { providers };
    platformBrowser(<Provider[]>options.providers).bootstrapModuleFactory(AppModuleNgFactory);
});

目前关于如何实现这一点的思考:

@Component()
class MyComp {
  // description, meaning and id are constants
  monday = __('Monday', {description?, meaning?, id?});
}
  • 因为我们不能用这样的结构影响 DOM 结构,所以我们可以支持运行时翻译——会有一个二进制版本,并且解析会在运行时发生,
  • 我们还可以像今天对模板所做的那样支持静态翻译 - 字符串将在编译时被替换,性能更好,但每个语言环境只有一个应用版本,
  • 稍后我们可能会在模板中支持__(...)
  • 这将通过 2.3 中可用的 TS 转换器实现 - 我们之前可能有一个原型。

/cc @ocombe

我想__()是用于一个还没有名字的方法(并且该方法实际上不会是__ ,对吧?)

对于您可能在您的类中使用的静态翻译,这是一个好主意,而且我认为实现起来非常简单。

没有__()是名字,你不喜欢吗:)

它通常用于翻译框架 - 但如果您更喜欢其他名称,您可以使用import {__ as olivier} from '@angular/core'

eeeeeeeh 这不是很自我解释:D
它看起来像一个非常私人的功能

我也不喜欢 ___ 方法名称 :) 我曾想象它会是一项服务?
不管怎样,很高兴看到进步👍

我喜欢 __()! :D 非常 gettext-y 并且很短。

在 JS 世界中,我只有使用@ocombes ng2-translate 的经验,其中使用 this._translateService.instant() 标记所有文本会使代码与更短的替代方案相比更难阅读,就像这里提出的那样。

没有什么能阻止你重命名 ng2 翻译服务__你知道 :)

实际上,我不知道如何将 DI 服务方法包装在一个简单的函数中 - 无论如何,没关系,这超出了本期的范围:-) 我的评论只是对使用 __ 的 +1,因为众所周知,如果你我曾经使用过其他翻译框架,至少在 PHP 世界中是这样。

好吧,也许___ 很好,比“this.translationService.getTranslation()”要短得多。
是的,如果你愿意,你可以重命名它。

那么i18n(...)而不是__(...)呢?

请停止关于名字的争论,这不是重点。 谢谢。

@vicbmonday = __('Monday', {description: 'First day of the week', id: 'firstDatOfWeek'});是否可以通过 id 来解决它,即:

__('@<strong i="8">@firstDatOfWeek</strong>') // Monday

Monday也是它定义的类的本地变量,还是可以从任何地方解析? 我正在考虑常见的翻译,例如“关闭”、“打开”等。

我想知道在官方版本支持之前,你们推荐与 AOT一起使用的最佳解决方法是什么?

问候,
肖恩

Angular 2 厨房水槽:http: //ng2.javascriptninja.io
和来源@ https://github.com/born2net/Angular-kitchen-sink

@vicb任何消息,在那边 ^

在实现此功能之前,我正在使用属性翻译功能

import {Component, Input, OnInit} from '@angular/core';

@Component({
    selector: 'app-sandbox',
    templateUrl: 'sandbox.html'
})
export class SandboxComponent implements OnInit {
    @Input()
    public title: string;

    constructor() {
    }

    ngOnInit() {
        console.log('Translated title ', this.title);
    }
}

从父组件模板:

<app-sandbox i18n-title title="Sandbox"></app-sandbox>

这是一种解决方法,但我仍然认为它是迄今为止最干净的。 它使您可以访问 ts 中的已翻译title ts

我开始研究这个功能,期待一个设计文档很快:-)

这将是最受期待的功能

这是一个好消息。 谢谢@ocombe

设计文档可在此处获得: https ://goo.gl/jQ6tQf
任何反馈表示赞赏,谢谢。

亲爱的,会尽快阅读!

谢谢@ocombe

我已经阅读了文件。 美丽的总结。

能够从 ts 文件中提取字符串听起来很棒。

我不确定 Typescript 升级何时可用于 Angular4。

这意味着这个功能应该已经可以使用几个月了,因为几乎每个人都遇到了这个限制,至少要再过 3/6 个月才能使用这个解决方案。

我现在可能不是唯一一个使用 ngx-translate 在我的控制器中管理翻译的人,而我依靠 angular i18n 来制作模板。

将所有内容合并到 i18n 中的目标显然很棒。

但是除了在理解如何在 Angular 框架中实现 i18n 方面的简化之外,与当前的 ngx-translate 实现相比,这种方法真的会提高性能吗?

根据我现在的理解,唯一的好处是能够从控制器中提取字符串。

现在还有一个很大的限制是多元化,请确保在发布整个升级时它可以正常工作。 我已经阅读了很多与 i18n 相关的票证,其中 xliff 和 xmb 都不能正确处理这个问题。 这是我需要切换回 ngx-translate 以提供有效解决方案的另一种情况。

@ocombe感谢您提供详细的设计文档,对于此功能请求,这看起来是一个非常好的解决方案。 当前的设计解决了我试图解决的用例。

一个问题:JIT 模式下的 I18N 服务和 AOT 模式下的 TS 2.2.x Transformer 的设计。 我是否正确假设转换器将使用静态转换替换对服务方法的调用,并删除对 I18N 服务的所有剩余引用?

@Thommas Angular 4 将使用TypeScript 2.1(这意味着您必须升级),但实际上您已经可以在 Angular 2 或 4 中使用任何更新版本的 TypeScript(例如 2.2.1)。

就 ngx-translate 的性能提升而言,这取决于您是否使用 AOT。 如果您使用 AOT,那么这是一种收获,但您可能不会看到其中的差异。 在 JIT(无 AOT)中不会有任何收益(实际上,这取决于您使用get可观察对象还是同步instant方法,可观察对象的成本高于简单的函数调用)。
我在你的代码中谈论 i18n 。 如果我们谈论的是模板,那么如果您使用 Angular i18n(在 AOT 和 JIT 中),就会有真正的可见收益,因为它不使用绑定。 您使用的翻译越多,您获得的就越多。

我将确保复数化的工作方式与模板的工作方式完全相同。 现在在模板中有什么问题吗?

@schmuli只有在您使用 AOT 时,Angular 才能替换您的代码。 如果您不这样做,那么服务调用将在您的代码中保持不变。
但是如果您使用 AOT,那么服务调用将被静态转换替换(只有您可能使用的变量将保持动态)。 我不确定我们是否会删除对 I18n 服务的引用,因为它将来可能用于其他事情,一旦我开始编写代码,我将不得不看看转换器有什么可能。 您是否正在考虑这将成为问题的用例?

@ocombe我想不出留下代码会成为问题的用例,除非我们考虑不会使用额外的DI注入(这是一个真正的问题吗?)。

考虑到您所写的该服务将来可能用于其他事情,以及通过更改用户代码可能引入错误,我会说最好避免完全删除代码。

也许使用该服务来获取 AOT 中的当前语言环境会很有用。 例如,考虑您的服务器可以返回多种语言的文本的情况,因此您需要将当前语言环境包含在请求中。 但是,您始终可以使用虚拟翻译字符串作为解决方法。

@diego0020您已经可以获取语言环境,只需从核心注入LOCALE_ID

@ocombe Pluralization不是用 xliff 实现的,谷歌以外的任何人都不知道如何将它与 xmb 一起使用。 另见: https ://github.com/angular/angular/issues/13780

无论如何,现在没有什么能阻止我们的 i18n 实现,现在使用复数管道的 ngx-translate 可以做到。 希望一切在 NG4 中看起来会更好,并且文档将反映改进。 谢谢。

@Thommas ICU 与 xliff 将很快修复: https ://github.com/angular/angular/pull/15068,如果您想知道如何使用,它在文档中: https ://angular.io/docs/

请道歉,有ETA吗? 因为我听说它将在 ng4.0.0 版本中提供,但没有! 谢谢

抄送@ocombe

我们仍在进行设计,有很多意想不到的事情需要考虑(比如图书馆如何发布自己的翻译),...
预计 4.2 会出现这种情况,但不能保证。

好的,谢谢@ocombe ,我有两个问题:

  1. 实际上,我们可以翻译模板中包含插值的文本,例如:
    <span i18n>This is {{myValue}}</span>
  1. 我问是因为我实际上无法提取,我不知道为什么会收到此错误:
    Could not mark an element as translatable inside a translatable section

@istiti

  1. 是的你可以。
  2. 您需要注意 i18n 指令的位置,它应该直接放在包含文本的元素上,而不是放在任何其他兄弟元素上。
    例如:
    <div i18n><span i18n>my text</span></div> - 这不好
    <div><span i18n>my text</span></div> - 这很好

@royiHalp
谢谢

  1. 好的如何用插值翻译?
  2. 当提取 ngc 得到我的文件、行和代码片段但我没有将 i18n 包裹在其他 i18n 中
    真的很难找到我的哪个父母有意外的 i18n 属性

@istiti

  1. 您可以像任何其他元素一样执行此操作,只需像您一样添加 i18n 指令:
    <span i18n>This is {{myValue}}</span>
    messages.xlf 文件中的结果将是:
    <source>This is <x id="INTERPOLATION"/></source>
    现在,当您将其翻译到不同的语言环境时,您应该将<x id="INTERPOLATION"/>放在句子中的正确位置,以帮助其他人理解您的意思,您可以像这样向 i18n 指令添加描述:
  1. 根据我的经验,错误Could not mark an element as translatable inside a translatable section正如我所解释的那样,我记得如果我仔细阅读错误,我可以注意到我在哪个文件中遇到了问题。

@fredrikredflag 非常感谢!

你的代码超级有用! 我只需要解决一个问题,因为现在xliff.load似乎返回了一个不同的对象,因此需要将this._translations调整为:

const loaded = xliff.load(this._source, '');
this._translations = loaded['i18nNodesByMsgId'] ? loaded['i18nNodesByMsgId'] : {};

如果我们要求一个不存在的密钥,则在get方法中进行次要验证:

const placeholders = this._getPlaceholders(this._translations[key] ? this._translations[key] : []);

另外,我认为空的 i18n 组件是树摇晃的,所以我必须在相应的组件模板中包含以下字符串:

<span hidden i18n="@@MY_STRING_1">String 1</span>
<span hidden i18n="@@MY_STRING_2">String 2</span>

幸运的是,我的 miniApp 只需要一些字符串,因此它在使用 AoT 的生产环境中效果很好。
再次感谢!!!

P/S:Martin Roob 的xliffmerge工具是目前必须使用的工具,他的TinyTranslator也是 B-)

我使用 AoT 编译,我的项目支持两种语言:英语和俄语。 我通过使用环境配置找到了该问题的临时解决方案。

environments/environment.ts文件包含:

import { messagesEn } from '../messages/messages-en';

export const environment = {
  production: false
};

export const messages = messagesEn;

还有另外两个文件environment.prod-en.tsenvironment.prod-ru.ts具有下一个内容:

import { messagesEn } from '../messages/messages-en';

export const environment = {
  production: true
};

export const messages = messagesEn;

对于俄语:

import { messagesRu } from '../messages/messages-ru';

export const environment = {
  production: true
};

export const messages = messagesRu;

每个消息文件都可以包含如下内容:

export const messages = {
  MessageKey: 'Translation',
  AnotherMessageKey: 'Translation',
  Group: {
    MessageKey: 'Translation',
    AnotherMessageKey: 'Translation',
  }
};

在我的代码(组件、服务等)中,我只导入消息:

import { messages } from '../../environments/environment';

并使用它们:

alert(messages.MessageKey);

.angular-cli.json中,我指定了下一个生产环境:

"environments": {
  "dev": "environments/environment.ts",
  "prod-en": "environments/environment.prod-en.ts",
  "prod-ru": "environments/environment.prod-ru.ts"
}

有用!

@alex-chuev 你用 JIT 试过了吗?

@ocombe任何粗略估计什么时候可以使用?

顺便说一句,很棒的功能:)

在对编译器进行更改之前它一直处于暂停状态,所以不是 4.2,我仍然希望为 4.3 做这个

任何人都知道如何在参数值中实现 i18n,例如在以下示例中的 [title] 中:
所以换句话说,给 HELLO 这个词添加翻译

问候

肖恩

如果 HELLO 只是您在 HTML 模板中键入的字符串,您可以执行以下操作:
```html

````
该文档有一个例子。 看这里: https://angular.io/docs/ts/latest/cookbook/i18n.html#! #translate-属性

如果您必须绑定到组件中的字符串属性,则必须等待此功能,或者实现自定义的翻译方式(据我所知)。

@ocombe

我们仍在进行设计,有很多意想不到的事情需要考虑(比如图书馆如何发布自己的翻译),...

这是非常重要的。 我希望它也是这个功能的一部分。 因为目前我看不到 3rd-party 库如何为您的应用程序提供翻译。 在构建过程中,您只能指定 1 个 XLIFF 文件,而且您似乎必须为您的语言预先构建库才能翻译它。

是的,我也在等! 这对于面向非英语观众的项目非常重要!

这绝对是我们想要支持的东西,团队正在努力重构编译器,这将使我们能够做到这一点:-)

是否有针对哪个版本的估计? 我在某处读到它可能在 4.3 左右?

由于必要的重构(和重大更改),恐怕不会在 v5 之前

Whaaaat 😳 哦,不,我在等你说的 4.2 的这个功能,现在它是 v5 ☹️ 我在想 v5 将是动态语言更改,而不是每个版本都发布,但无论如何希望有一天,即使每个版本都有 36k 不同的构建语言。 我只是想知道v5是什么时候? 谢谢

@ocombe

@istiti我说这不是保证日期;-)
发布时间表在这里: https ://github.com/angular/angular/blob/master/docs/RELEASE_SCHEDULE.md
v5 的最终版本是 2017-09-18,但之前会有 beta 和 RC

为每个组件创建一个语言环境文件是否有任何更改? 意味着,创建块messages.xlf ,例如: messages.{component}.{locale}.xlf并且例如只有messages.{component}.xlf用于默认语言。

还没有

由于构建时间@ocombe,损坏它是严格的最小值

我很困惑......这个变更请求/变更是否正式?
这里有同样的问题,翻译不仅仅适用于模板。
拥有 redux、自定义树视图组件等......全部从代码中的 javascript 对象生成,而不是每个模板

它是官方的,它即将到来,但我们必须首先对编译器进行其他深入的更改,这就是为什么它还没有为 4.3 做好准备

是否知道该功能将在哪个确切版本可用/计划? 4.3.1, 4.3.2 ... 4.3.X ?

4.x 分支没有更多的主要/次要版本(只有补丁:https://github.com/angular/angular/blob/master/docs/RELEASE_SCHEDULE.md),这意味着这将适用于 5.x ,不确定是哪一个。

这仍然有望进入 5.x 吗? 我在测试版中看不到它。 谢谢

可能是 v50.x 而不是 v5.x 😂

5.x 是的,但很可能不会出现在 5.0 中

乐。 3 août 2017 à 12:56,vltr [email protected]一个écrit:

可能是 v50.x 而不是 v5.x 😂


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/angular/angular/issues/11405#issuecomment-319936876
或使线程静音
https://github.com/notifications/unsubscribe-auth/AAQMorXMbyI8l6K3QA4jmXEKawiEC46xks5sUad0gaJpZM4J2pkr
.

@ocombe ,我对使它工作的过程非常感兴趣,而编译器目前的工作方式如何使它成为一项更艰巨的任务。 你对时间线的样子有什么清楚的吗,或者这仍然可能不是 5.0,而是一些 5.x?

@arackow最有可能在 5.x 中, @vicb正在对编译器进行最后的更改,这最终将使这成为可能

@ocombe ...是否有任何设计文档描述了模板之外的字符串解决方案的概念? 我们正处于项目开发阶段,很高兴知道它,因此我们将能够以某种方式准备我们的临时解决方案,然后切换到最终的 Angular 语法会容易得多。

我们有一个设计文档,但它是基于以前版本的编译器,它可能不是我们最终要做的实现。 一旦我们对新的实现有了更好的想法,我们将制作一个新的公共设计文档

@ocombe我必须 +1 这个功能。 现在非常需要它大声笑:+1:感谢你们制作了惊人的工具! :)

据我了解当前的设计文档,AOT 库的作者需要提供使用该库的各种应用程序所需的所有语言的翻译。
我是否正确理解了这一点?

这是一个很好的观点@gms1。
在这里测试了这个。
作为库作者,您只需添加 i18n 标记。
您不应该将您的库编译为“ ngfactories ”(请参阅​​ Jason Aden 在 ng-conf 2017 上的演讲),因此不会替换翻译标签。 这样,当您运行ng xi18n命令时,您还将在 xliff 中获得 node_modules 文件夹中文件的翻译。

所以不,您不必提供翻译,而是添加具有有用含义的 i18n 标签。

引用设计文档:

对于 AOT,源代码被转换以通过静态翻译替换服务调用。 为此,我们将使用 TypeScript 转换器。

那么,如果 Angular(AOT)库通常以转译形式发布,这意味着什么? 我没有在本文档中找到对 .metadata.json 的任何引用

您不应该将您的库编译为最终代码,而是准备由开发人员使用您的代码进行编译。 看看@jasonaden在这里提到了什么。 这也意味着您可以通过添加 i18n 标记使您的库可翻译。

@gms1正如我所说:

我们有一个设计文档,但它是基于以前版本的编译器,它可能不是我们最终要做的实现。

并且 AOT 编译器正在针对 v5 进行广泛更改,libs 应该更容易发布“aot-ready”代码。
对于 i18n,我们也希望使其尽可能简单/灵活,可能类似于:库可以发布您可以使用的翻译文件,但您也可以根据需要覆盖,并且您应该能够提供如果库默认不支持其他语言

感谢@ocombe的澄清!

@ocombe这是否意味着 ngx-translate 的功能将在角度本身中可用?

@montreal91 ngx-translate 的任何部分都不能按原样实现为 angular,我的 lib 有一个非常幼稚的方法,它“大部分”工作,但我们希望框架更高效。
话虽如此,角度将提供类似的功能(即使最终实现会有所不同)。

这是我在基于 (https://github.com/PointInside/ng2-toastr) 开发 UI 通知系统时的方法 (FLUX)。 问题是通知内容是在服务调用中定义的,而不是由angular i18n系统(xi18n)管理的模板

这不是我想要的,但它是一个 _working_ 解决方案。 您可以从以下代码片段中_提取_想法。 希望能帮助到你 :)

任何地方

// this call in any other component, service... 
// "info" method parameter is the "id" of the HTML element in template NotificationComponent.html
this.notificationActionCreator.success("myMessage");

通知组件.ts

@Component({
    selector: 'notification-cmp',
    templateUrl: '../../public/html/NotificationComponent.html'
})
export class NotificationComponent implements OnInit, OnDestroy, AfterViewInit {

    notificationMap: Map<string, any> = new Map();
    subscription: Subscription;

    constructor(private toastr: ToastsManager,
                private vcr: ViewContainerRef,
                private store: NotificationStore,
                private elementRef: ElementRef) {

        this.toastr.setRootViewContainerRef(vcr);
    }

    ngOnInit() {
        this.subscription = this.store.payload
            .subscribe((payload: NotificationStorePayload) => this.handleStorePayload(payload));
    }

    ngOnDestroy(): void {
        this.store.destroy();
        if (null != this.subscription) {
            this.subscription.unsubscribe();
        }
    }

    ngAfterViewInit(): void {
        this.elementRef.nativeElement.querySelectorAll("div[notification-title][notification-text]")
            .forEach(el => this.notificationMap.set(el.id, {
                    title: el.attributes["notification-title"].value,
                    text: el.attributes["notification-text"].value,
                })
            );
    }

    handleStorePayload(payload: NotificationStorePayload): void {

        if (null != payload.action) {
            let notification = this.notificationMap.get(payload.notification.id);

            switch (payload.notification.type) {
                case NotificationType.SUCCESS:
                    this.toastr.success(notification.text, notification.title);
                    break;
                case NotificationType.INFO:
                    this.toastr.info(notification.text, notification.title);
                    break;
                case NotificationType.WARNING:
                    this.toastr.warning(notification.text, notification.title);
                    break;
                case NotificationType.ERROR:
                    this.toastr.error(notification.text, notification.title);
                    break;
            }
        }
    }
}

NotificationComponent.html

<div hidden
     id="myMessage"
     i18n-notification-title="@@notificationTitleMyMessage"
     i18n-notification-text="@@notificationTextMyMessage"
     notification-title="Greeting"
     notification-text="Hello world"></div>

<div hidden
     id="error"
     i18n-notification-title="@@notificationTitleError"
     i18n-notification-text="@@notificationTextError"
     notification-title="Error"
     notification-text="Something went wrong!"></div>

既然 Angular 5 已经发布,我们是否有任何转机,何时将集成 i18n 模块上的更改?

看起来这个功能取决于对 Angular 编译器的更改,但负责编译器的团队似乎不太感兴趣。 看起来没有任务或 PR 或提交:-( 。抱歉发泄了,但是这个描述所有其他翻译框架中存在的非常基本功能的功能请求现在已经有一年多了。我已经给出了希望这个基本功能能够到来。至少对于 5.x...

小伙伴们保持冷静。 在 AOT 编译器中实现并不是一件愚蠢的事情。 有一点信任, @ocombe说:

“它最有可能在 5.x 中, @vicb正在对编译器进行最后的更改,这最终将使这成为可能”

是的,我们现在正在开发运行时 i18n,这是第一步。
运行时 i18n 意味着:所有语言的一个包,AOT 编译的库可以使用 i18n,也许以后可以在运行时更改语言。 这也意味着我们可以在运行时进行翻译,这是代码内部翻译所需要的(因为我们需要一个运行时解析器来进行翻译等......)。
完成后,下一个优先级将是代码内部的翻译。

但是在我们将编译器更改为使用 typescript 转换器之前,我们无法执行运行时 i18n(已在 5.0 中完成)

知道哪个 5.x 版本会有哪些 i18n 改进吗? 5.0.0 本身发布了什么?

5.0:

  • 不使用 intl API 的新 i18n 管道(日期/数字/百分比/货币)修复了 20 个错误,因为现在它们在所有浏览器中的工作方式相同 + 一些其他改进(语言环境参数、新的附加格式、时区参数日期管道,...)
  • 用于库和组件的新 i18n api 函数: https ://next.angular.io/api?query=getlocale
  • 更好地与 cli 集成

5.1 或 5.2(如果没有意外的阻止程序):

  • 运行时 i18n

5.x:

  • i18n 代码翻译(不仅仅是模板)

以及我们将在此过程中添加但尚未决定的其他内容(您可以关注 https://github.com/angular/angular/issues/16477)。

只是好奇的想法,虽然我有点晚了,只是考虑到TS文件中的i18n支持,我们为什么不考虑注释语法? (我发现上面也有人提到过)

示例是:
@i18n (id='message1')
message1 = "这是一条需要翻译的消息";

@i18n (id='dynamicMessage')
dynamicMessage = "这是一条带有 {0} 的动态消息";

使用 dynamicMessage 的注解,我们可以通过注解向这个字符串实例注入一个函数,名称为format ,然后我们可以像下面这样使用:
var finalMessage = dynamicMessage.format('value1')

类似的方法可以用于处理命名参数等。

我想问题不在于如何打造良好的开发者体验。 问题是关于编译。

如果将 message1 更改为另一个字符串会怎样? 那么所有 i18n 变量都需要始终为常量。 我不认为使用变量会起作用。 例如,Symfony 有一个带有翻译功能的翻译服务,它在 JS 语法中会像这样工作: let translated = this.translator.trans('Hello %name%', [{'%name%': nameVariable}]);这将导致: <source>Hello %name%</source> <target>Bonjour %name%</target>

@MickL当前 -aot 翻译的重点是,制作一个最小且高效的程序。 这就是为什么动态翻译的方法还不存在的原因。
我坚信当前的方法非常好,因为我们没有使用动态服务来翻译字符串。 这意味着如果您知道某些文本是静态的,请不要使用服务以这种方式进行翻译。

动态翻译的问题很简单,如果你每次显示字符串时都要翻译它。 确实不是什么好办法,
为什么?

  1. 因为当您不使用它时,您的内存中有多种语言的字典。
  2. 每次显示动态文本时,您都需要转到地图并找到一个键,然后找到语言翻译。 每次都是因为您有“主要语言”。

在我看来,未来的解决方案应该是:

  1. 后端为您提供要翻译的文本。
  2. 替换/(或从当前方法从后端下载)您以当前/主要语言显示的每个文本。 (replace一切都得到时间,但你不需要每次想要显示一个简单的标签时都找到两个键)。
  3. 只有真正的动态文本才能被替换和/或请求到后端。

想想看,如果你真的有动态文本来显示你可以从后端接收它,如果你真的需要那个“动态文本”的副本,你总是可以使用缓存。

在我看来,没有必要再讨论这个问题了。 实际上有一个人(Olivier Combe)在 i18n 上全职工作。 AOT 是非常特殊的,在使这个问题几乎成为可能之前必须做很多工作。 很快我们就会有动态翻译:不再需要单独构建每种语言! 完成后,我们稍后将在代码中进行翻译(本期)。 他说,从现在开始,希望这个问题得到解决的道路还需要半年时间。

如果您有兴趣,请查看他在 2017 年 11 月 7 日关于 Angular Connect 的演讲,内容是关于 Angular 中 i18n 的现在和未来: https ://youtu.be/DWet6RvhHWI?t=21m12s

这闻起来像是打字稿标记的模板字符串的一个很好的用例......

这个味道一般。 我每个月都会访问该线程一次以检查进度,并且由于期望值降低而不再感到失望。 开发人员唯一想要的就是在代码中拥有与模板中相同的字符串翻译选项。 由于缺少功能,我的代码充满了隐藏的翻译模板字符串。 这最终是通过服务还是注释来完成是无关紧要的,如果需要额外的毫秒也没关系。 当我从代码中挑选一个字符串时,我已经在观察一些变化。 唯一的要求是对代码进行解析,并且相关字符串以与我的基于模板的翻译相同的格式出现在相同的文件中。

@eliasre如果您急于进行代码翻译,可以尝试https://github.com/ngx-translate/i18n-polyfill
没有保证它会是一样的,实际上它可能会有所不同(因为更容易使用),现在可能存在的限制......它会在你的应用程序中添加 ~80ko 来使用那个库

如果您不想等待,可以使用 ngx-translate

@ocombe您是否介意提供有关进度的更新,以便我们在等待决定使用哪些插件或框架时可以跟踪和计划相应的开发时间表? 这将非常有帮助和赞赏

@eliasre我认为谷歌希望立即做出好的解决方案。 未来不会发生重大变化。 我同意这比我们想象的要花更长的时间。

不要打扰可怜的@ocombe ,他工作非常非常努力!
继续努力吧! :)

我不是开发此功能的人,现在是假期,我会在知道更多信息后立即通知您

谢谢@ocombe

感谢@ocombe ,任何更新都将在此不胜感激。 在过去的几个月里,我一直在拼命等待运行 i18n,而最后的构建 ETA 是 5.2。 您能在此处获得更新的 ETA 后立即通知我们吗?

抱歉这么坚持,但目前构建 20 种语言的速度非常慢。

由于 Angular 5.2 已经发布,不幸的是我找不到任何与 i18n 相关的东西(或者我忽略了什么?)...... - @ocombe也许你可以更新我们的发布计划? 非常感谢您在 i18n 上的努力!

@ocombe不是我们在这里等待的人...查看此评论中的 youtube 链接: https ://github.com/angular/angular/issues/11405#issuecomment -343933617

规划不断发展。 每当我给出一个可能的发布日期时,事实证明其他东西优先或成为必需,它再次改变了日期。 我知道这对等待这个功能的你们来说是多么令人失望,我正在尽我所能提高优先级。
我们明天开会讨论 i18n 的积压工作/计划,如果我有任何新情况,我会通知您。

@ocombe如果你本月发布 i18n,我会给你买 24 包精酿啤酒

如果你问 Angular Elements 和库支持在做什么,我会添加 24 瓶德国啤酒和另外 24 瓶。 该死的不了解正在发生的事情是可怕的。

@ocombe ,你有这个千载难逢的机会获得 72 瓶昂贵的精酿啤酒,让开发社区开心。 如果您完成这些事情,我的朋友,我们将履行我们的承诺。

@MickL Angualr Elments,据我了解,它将允许我们构建非 SPA 页面并将 Angular 组件用作本机元素(也用作自部件)

是的,我知道。 这是每个人都在等待的另外 2 个功能,但没有人知道状态是什么以及何时期待它......

如果有人承诺添加最后 27 瓶啤酒,我们就可以开始倒数了!
http://www.99-bottles-of-beer.net/language-javascript-1948.html

我会添加它们, @ocombe :命名你最喜欢的品牌:D

编辑:有人创建了一个类似于名为 beerbounty 的 eth bounty 的服务:p

正如所承诺的,有一点更新:我们仍然打算在 v6 中发布运行时 i18n,这给了我们 5 周的时间。 它会很短,可能会在最后一个版本中添加,并且会在标志后面。
运行时 i18n 应该带有代码翻译,因为无论如何它也适用于模板(它应该使用相同的机制)。
话虽如此,还不确定该服务是否会 100% 准备就绪,如果我们认为这是正确的选择,我们可能会推迟它以确保在发布后不会立即破坏它。 但无论如何它都会遵循运行时 i18n(这是我们的路线图),我们不需要主要版本来发布它,因为它会在标志后面,而且它不应该破坏任何东西)。

什么会阻止我们及时发布它:

  • 如果我们偶然发现了一个意想不到的问题(你知道开发是如何工作的)
  • 如果它破坏了内部的谷歌应用程序,因为我们在某个地方搞砸了
  • 如果有最重要的事情出现,我们必须转移注意力
  • 如果它所依赖的新事物之一被阻塞或延迟(运行时 i18n 取决于团队其他成员正在处理的一些主要内部更改)
  • 资深 angularJS 开发者,使用 PascalPrecht 的翻译库 3 年。
  • 让我兴奋。 Angular 2 菜鸟。 天真地浏览https://angular.io/guide/i18n
  • 成功地将 i18n 整合到我的专业项目中
  • 花一整天时间弄乱所有前端硬编码的标签,把它移植到一个 100+kb 的 xlf 文件
  • 需要在 ng 服务中翻译原始标签
  • 在谷歌上查找它,在这里
  • 找出在Angular 6时代之前没有解决方案。
  • 我的
    mfw

@Macadoshis你现在可以使用我的 polyfill 库: https ://github.com/ngx-translate/i18n-polyfill

@Macadoshis现在想象一下我们自 alpha.46 以来所经历的一切! ;)

感谢您将i18n- polyfill 指向我@ocombe ,但我会选择ngx-translate ,因为它支持翻译键的异步加载。
TRANSLATIONS提供程序的i18n-polyfill工厂似乎只支持在引导前已知的固定语言环境的同步原始加载。

// sync loading
return require(`raw-loader!../locale/messages.${locale}.xlf`);
// @ngx-translate/i18n-polyfill/esm5/ngx-translate-i18n-polyfill.js
Tokenizer.prototype._advance = function () {
    //...
    this._index++;
    // "this._input" needs to be a string and can't handle a Promise or an Observable
    this._peek = this._index >= this._length ? $EOF : this._input.charCodeAt(this._index);
    this._nextPeek = this._index + 1 >= this._length ? $EOF : this._input.charCodeAt(this._index + 1);
}

@ocombe您编写的 polyfill 与正在发布的内容有多接近? 我们应该注意什么或准备什么? 另外,您是否介意列出在版本 6 中发布的 i18n 功能列表,以及 i18n 的测试情况如何?

我暂时没有关于代码翻译服务的更多细节,我们可能会在下周工作(我要去山景城)。 我所知道的是,在幕后它将类似于闭包库中的goog.getMsg (因为它是谷歌目前在内部使用的): https ://github.com/google/closure-library

在 v6 中,我们将发布运行时 i18n:一个适用于所有语言环境的捆绑包,在运行时解析翻译,如果我们有时间可能还会翻译代码(否则很快就会出现)。 所有这些都将在一个标志后面,因为它需要使用不会经过实战测试的新渲染器(称为 ng-ivy)。

谢谢! @ocombe ...听起来真不错!
模板翻译的语法会有重大变化吗? 我现在正在考虑为我们的项目添加翻译——但不想在几周内完全重做。

不,模板语法是一样的

感谢@ocombe的好消息,这是非常有希望的。 我们可以从 v6 发布的 ng-ivy 渲染器中期待什么样的生产准备? 我知道它不会经过实战测试,但它仍然可以用于生产,并且可以在所有主要的几乎当前版本的浏览器上运行,对吧?

您可以在此处关注进度: https ://github.com/angular/angular/issues/21706
我不认为一切都会为 v6 做好准备,因为它处于标志之下,即使在 v6 发布之后我们也会继续推进(这不是重大变化或任何东西)
老实说,我不确定所有这些东西是什么:D
我知道来自 cli 的 hello world 应用程序已经在运行。
我认为其中一些东西需要从新渲染器的优化中受益,但它可能可以在没有检查所有内容的情况下工作
话虽如此,这也意味着它还没有准备好生产,暂时不要把你的产品押在它上面。 它不会在内部谷歌应用程序上进行测试,我们可能需要做一些重大更改来修复问题。 使用新渲染器由您自行决定,风险自负

v6-beta4 中是否存在这些内容?

不,这不对。 检查此路线图,此功能被 i18n 运行时阻止。

更新@ocombe

运行时 i18n 的第一个 PR 已合并到 master 中,以及我们将用来测试功能的 hello world 演示应用程序。 它在运行时工作,理论上支持代码翻译,即使还没有服务。
目前它的支持非常少(静态字符串),我们正在努力添加新功能(我将在下周进行提取工作,然后是带有占位符和变量的动态字符串)。
之后,我们将提供代码翻译服务。
一旦新功能完成,它就会合并到 master 中,您不必等待新的专业。

@ocombe i heart i18n 功能应该已经在 v4.3 中了! 发布了这么多,但 i18n 上什么都没有。 Angular 团队经理可以在上面分配更多的工作/开发人员吗?我可以理解你一个人,或者两个开发人员不能快速进行,这个 i 18n 功能是必须具备的功能,可以将 Angular 伪装成业务应用程序/大型应用程序,当然可以快速构建这两个功能对于大型应用程序来说是最重要的,你不这么认为吗? 谢谢

我认为我们可以对@ocombe说的唯一一件事就是感谢他为这个功能付出的所有努力。 我敢肯定,如果没有更多的开发人员不是因为他没有要求他们。 就让他做吧,我相信它发布后我们不会后悔的。

仅供参考...

我有一个用于导入和导出字符串的替代工具,最初是为与 Aurelia 一起使用而开发的,其中包括对存储在json文件中的字符串的支持——它非常方便,因为 Webpack 允许您直接导入json文件到您的ts文件中,使您的代码可以轻松访问这些字符串。

该工具也适用于 Angular 模板,并且还有许多其他方便的功能 - 如果您愿意,可以使用它,或者随意借鉴它的想法来改进 Angular 工具。 还有一个基于此的 Webpack 加载器 - 请参阅文档中的链接。

https://www.npmjs.com/package/gulp-translate

我们有一个更简单的问题,我不确定我们是否需要即将推出的功能(或@ocombe 的 polyfill)。

我们用常量定义所有反应式表单的结构,我们在其中设置每个表单元素的属性。 例如:

loginForm = [
  {
    type: 'email',
    placeholder: 'EMAILPLACEHOLDER' // "Your email"
  },
  {
    type: 'password,
    placeholder: 'PASSWORDPLACEHOLDER' // "Your password"
  }
];

目前我们使用ngx-translate ,所以在模板中我们像这样翻译字符串:

<input *ngFor="let ele of loginForm" [type]="ele.type" [placeholder]="ele.placeholder | translate">

我们现在想迁移到 Angular i18n。 但是,由于并非我们所有的字符串都在模板中,因此我们似乎不能开箱即用地使用 Angular i18n。

不过,我们的情况看起来比需要在运行时检索翻译的情况更简单。 我们所有的文本都是在常量中预定义的,并且可以在编译时替换,就像 x18n 目前对模板中的字符串所做的那样。

例如,我们可以有一个更复杂的字符串,如下所示

placeholder: 'I18N:description|meaning@<strong i="16">@PASSWORDPLACEHOLDER</strong>:Your password'

在编译时,字符串可以替换为:

placeholder: 'Your password'

这是否可以考虑实施?

您绝对可以使用运行时 i18n 来做到这一点,您可以在管道中使用新的 i18n 服务,例如将占位符转换为他们的翻译

我注意到这个线程已经打开了一段时间,它显示了以简单的方式在 Angular 中开发多语言网站的主要兴趣,密切关注 HTML 和组件代码文本。
ocombe 请不要忘记在完成时向所有订阅者发送警报 - 我知道到 2018 年 5 月即将到来。感谢您的友好贡献!

@ocombe我们应该使用 Ivy 来使用这个运行时服务,还是它也可以与旧引擎一起使用?

它只会与常春藤一起使用,你现在不能使用它,因为它还没有完成

感谢您的辛勤工作@ocombe

关于我们多久可以使用这些新功能的任何消息?

Ivy 现在可以在 Angular 6 中使用编译器标志启用,处于预发布状态。 目前这些新的 18n 功能是否可以使用 Ivy 预发行版?

还没有,但我正在取得良好的进展(现在全职工作)。 一旦它可以在 master 中进行测试,我将在这里发布更新

非常感谢@ocombe ! 很多人都从你的辛勤工作中受益。 我们都非常感谢!

@ocombe ts 中的静态翻译是否仍在计划中? 我的意思是编译时表达式替换,可能是打字稿转换。
如果没有,我们是否能够在不提取的情况下将自定义 ts 转换插入ng build管道?

我不确定你的意思是@TinyMan吗? 您的意思是能够在构建时合并翻译(就像现在一样),或者能够在 ts 代码中使用翻译(除了我们已经拥有的模板翻译)?

@ocombe我的意思是能够在 ts 中使用翻译,但不能通过服务或 DI / 运行时翻译。 我的意思是编译时翻译,但用于打字稿。 像 C 预处理器。 我认为这是按照vicb 评论计划的:

  • 我们还可以像今天对模板所做的那样支持静态翻译 - 字符串将在编译时被替换,性能更好,但每个语言环境只有一个应用版本,

例如,我希望能够编写:

export const Notifications = {
    newComment: i18n("New comment notification@@newComment", "New comment on your story !"),
    newStory: i18n("New story notification@@newStory", "{{0}} wrote a new story !"),
}

然后这被转换为(在fr中):

export const Notifications = {
    newComment: "Un nouveau commentaire a été ajouté a votre article !",
    newStory: "{{0}} a écrit un nouvel article !",
}

目前为此目的,我使用带有自定义标记的 ngx-translate 进行提取(在此示例中i18n ),当我需要显示消息时,我必须调用this.translate.instant(Notifications.newStory, ["TinyMan"])来进行翻译+插值。 我的意思是我们需要能够在不调用翻译服务的情况下编写Notifications.newComment 。 对于字符串插值,我们可以有一个只做 ICU 和插值的插值方法(模板字符串已经被翻译)

Notification.newStory // {{0}} a écrit un nouvel article, static string, no runtime translation
template(Notification.newStory, "TinyMan") // TinyMan a écrit un nouvel article !

在这里,我们只是摆脱了翻译 HTTP 请求和服务开销。

我希望它澄清?

您所描述的确实在 i18n 的路线图上。
目前, https://github.com/ngx-translate/i18n-polyfill将完成这项工作。

@TinyMan我还不确定它是通过服务还是全局函数。 两者都是可能的,但服务也有很多优点:您可以模拟它进行测试,或者用您自己的替换它,您还可以更改每个模块/组件的行为
我知道 Google 在内部将使用 Closure i18n(通过goog.getMsg )这是一个全局函数,并且该函数很可能会在编译期间基于全局标志被服务替换。 我会尝试看看该标志是否也可以在外部使用,但我不明白为什么不能。 但如果你使用它,它将用于模板代码翻译

@ocombe我也想在这里表示感谢。 我非常感谢您在这方面所做的工作,这对我的工作非常有帮助。 一个问题:有没有一种方法,或者_将_有一种方法来进行 AOT 编译,它只生成一组捆绑文件,在查找文本时,在运行时引用 xlf 以获得正确的字符串?

所以本质上你需要每种语言 1 个 xlf 文件加上 5 或 6 个捆绑文件。 现在,如果我们有 10 种语言和 AOT 编译它的 50 多个文件:每种语言的一组捆绑文件......

我不知道这样的事情的努力或复杂性,但如果有 aot 编译但只有一组文件会很好。

是的,这就是我们要用 ivy 做的,运行时 i18n
捆绑您的应用程序一次,并在运行时加载翻译(您可以延迟加载翻译)

@ocombe那个。 是。 极好的。

这让我感到难以置信的快乐。 谢谢!

如果没有翻译外部模板的能力, i18n功能对我来说完全没用。

@ocombe你好奥利维尔!
任何更新?

谢谢!

如果它完成了,那没关系,因为我们必须等待 Ivy 完成,这计划在 Angular 7(2018 年 9 月 / 10 月)

到 Angular 7,这个问题将是两年前的 LOL。
无论如何,常春藤一直是推迟这个新功能最多的东西......

@ocombe ,很高兴听到。 出于这个原因,我们目前被困在使用我们从头开始推出的遗留 i18n 服务。 无法在 JS 中做到这一点使得我们很难为我们做一些简单的事情:

  • 动态组件
  • 与我们必须提供一些本地化文本的第三方服务集成
  • 显示动态确认模式
  • 我们的 api 返回错误types 。 在我们的上下文中,我们需要动态定位这些错误,而模板驱动的方法会很笨拙。
  • 我们想按照 Angular 团队的建议使用TitleService ,但是没有办法提供本地化文本!

我有点好奇 Angular 团队目前是如何处理这个问题的......

@vincentjames501
在我们公司,我们尝试和你现在做的一样,但是很麻烦,所以我们最终使用了之前在这个帖子中提到的i18n-polyfill ,到目前为止它工作得很好。
我们唯一的缺点是应用程序包的大小。 我们有三个翻译文件,每个包都包含其中的所有文件(我们还没有找到一种方法来仅使用它使用的翻译来生成包)。 但所有这一切都只是令人讨厌的问题,我们希望 Ivy 出局后能解决这些问题。
干杯,

我已经包含了一个翻译映射并将其重新导出到每种语言的 environment.ts 文件中。 它可以正常工作,不包含在所有捆绑包中,并且不需要外部库。 我唯一的问题是,由于每个站点不同,我无法为所有语言使用相同的服务人员,如果有人切换语言,我的站点无法判断他是否已经注册推送通知......

@ocombe Angular 6 有什么新闻吗?

如上所述,它不会出现在 Angular v6 中。 我们依赖计划用于 v7 的 Ivy。

@ocombe我可以建议您锁定此线程,以便只有团队可以在适当的时候提供有意义的更新,我们避免您不断回答的重复性问题?

我与当前 Angular 一起使用的技巧是使用ng-template并以编程方式创建嵌入式视图:

<ng-template #messagetotranslate i18n>This message should be translated</ng-template>

然后在组件 ts 文件中:

@ViewChild('messagetotranslate') messagetotranslate: TemplateRef<any>;

createTranslatedMessage(): string {
  return this.messagetotranslate.createEmbeddedView({}).rootNodes[0].textContent;
}

createTranslatedMessage方法返回翻译后的消息。 尽管我使用模板来声明不是最佳的消息,但这使得 CLI 翻译工具可以将其注册到 xlf 文件中,并且我有一种以编程方式获取翻译后的消息以供在外部模板使用的方法。

希望 api 也适用于路线! 例如

export const routes: Routes = [
 {
  path: '',
  component: HomeComponent,
  data: {
   title: i18n('Home'),
   breadcrumb: i18n('Home')
  }
 }
]

如果这个功能发布了,会不会比现在的 i18n api 有很大的变化? 您会建议我现在使用 i18n 开始我的项目,还是应该在 Angular 7 发布时等待新的 i18n Api?

任何更新我们什么时候可以获得运行时和动态翻译支持?

运行时翻译已完成并合并(使用 ivy),但编译器端尚未完成。
我现在正在研究 ICU 表达式。
我还没有开始编写动态翻译(服务),但这可能是我在 ICU 表达式之后要做的下一件事。

@vincentjames501我们可以将 i18n-polyfills 与 ICU 表达式一起使用吗?我没有解决方案如何实现它

@ocombe ,您能否澄清运行时翻译与动态翻译之间的区别?

可能没有官方面额,但我看到它的方式是:

  • 运行时:翻译是在运行时解决的,这意味着不在构建期间(就像我们目前所做的那样)。 它适用于所有类型的翻译(模板或代码)
  • 动态意味着您只知道在运行时要翻译什么,而在构建时无法做到。 它主要适用于使用服务的“代码中”翻译。 我猜你可以认为 ICU 表达式是动态的,因为它们依赖于一个变量,但你仍然可以在构建时计算所有可能的翻译,因为案例的数量是有限的。

我推迟了向我们正在开发的当前项目添加 i18n 支持,但我们离发布越来越近了,知道这是否会在 Angular 7 中成为最终版本吗? 还是我应该考虑其他添加翻译的方式?

@andrei-tatar i18n 自 Angular 2 以来运行良好。唯一的缺点:

  • 无代码翻译(本期)-> 如果需要,请使用 i18n-polyfill
  • 如果您构建 AOT(推荐),则必须为每种语言分别构建应用程序
  • 由于每个应用程序都是单独构建的,因此在切换语言时您必须重新加载页面

对于最后两点,您可以改用@ngx-translate。 但它的工作方式与 Angular 内置的 i18n 不同,因此以后的更新可能需要一些时间。 对于 polyfill,更新将是没有时间的,可能没有重大更改。

@ocombe

我们如何使用 Angular i18n 功能处理条件运算符

https://github.com/shobhit12345

我们使用 ngSwitch 模板化所有消息,因此我们可以将 i18n-tags 附加到
他们分别。

隐藏显示历史

我们经常使用这种方法来解决丢失的动态翻译。
稍微想一想,可以表达出惊人数量的文本
这种方式在模板中。 一个常见的模式是有一个消息数组,
然后将其与 ngFor 和 ngSwitch 一起使用来模板化消息,
像这样的东西:

ts

messages = [ '这是第一条消息', '这是第二条消息' ]

html


i18n="@@firstMesssage">这是第一条消息
i18n="@@secondMesssage">这是第二条消息

它有点冗长 - 但它有效!


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/angular/angular/issues/11405#issuecomment-415731284
或使线程静音
https://github.com/notifications/unsubscribe-auth/AGwM6jcWIpwtGxhkH1fwnVXNagRxmMnoks5uT-LxgaJpZM4J2pkr
.

如果它完成了,那没关系,因为我们必须等待 Ivy 完成,这计划在 Angular 7(2018 年 9 月 / 10 月)

已经是十月了。 您认为我们会在 10 月底之前推出新功能吗?

@lizzymendivil根据https://is-angular-ivy-ready.firebaseapp.com常春藤已完成 65%,而且似乎不太可能在 30 天内完成

是的,ivy 不会在一个月内完成。

@ocombe你能不能把它锁定,这样只有你可以发布更新。 获得所有“什么时候完成?”的所有通知有点烦人注释

@ocombe似乎常春藤现在大约 94%,你认为这可能会在年底前准备好吗?

我不这么认为。 功能就绪且无错误(= 可用)是非常不同的。 我们现在主要在修复问题。

@ocombe我们可以相信 i18n 在 angular 8 之前出现吗?

我不这么认为。 功能就绪且无错误(= 可用)是非常不同的。 我们现在主要在修复问题。

好的,谢谢及时回复,非常感谢。

@ocombe的任何更新,到我们应该期待什么时候(我在最后几条评论中看到年末)“代码翻译”部分与“模板翻译”一起可用,以支持 Angular 的 I18N? 我们认为将 ngx-translate-polyfill 与 Angular I18n 功能一起使用,但似乎 xlf 支持不适用于使用https://github.com/biesbjerg/ngx-translate-extract CLI 合并现有 xlf 文件.

谢谢 !

@Abekonge

我们使用 ngSwitch 对所有消息进行模板化,因此我们可以将 i18n-tags 单独附加到它们。
它有点冗长 - 但它有效!

感谢您的建议! 这似乎是最容易理解的。 我的问题是,在数组具有“很多”值(例如 10 或更多)的情况下,您是否仍然使用此实现? 似乎代码会变得非常庞大。

我不知道我们有没有这么多,但为什么没有。 如果消息在多个地方使用,我们会制作小的 i18n 组件,基本上只是 switch 和 i18n 标签。

大卫

书房 1 月 23 日。 2019 吉隆坡。 19.24 skrev Andrew Bissada通知@github.com:

@Abekonge

我们使用 ngSwitch 对所有消息进行模板化,因此我们可以将 i18n-tags 单独附加到它们。
它有点冗长 - 但它有效!

感谢您的建议! 这似乎是最容易理解的。 我的问题是,在数组具有“很多”值(例如 10 或更多)的情况下,您是否仍然使用此实现? 似乎代码会变得非常庞大。


你收到这个是因为你被提到了。
直接回复此电子邮件,在 GitHub 上查看它,或将线程静音。

如何在验证器类中使用 i18n-polifills。 我无法通过 i18n 服务使用隐蔽验证器消息。

//在组件中
this.crForm= this.fb.group({
用户名: [''],
电子密码:[''],
}, { 验证器:新的 AccValidator(this.i18n).validate()}
);

//验证器

导入 { AbstractControl, ValidationErrors, FormGroup, FormControl } from '@angular/forms';
从“@ngx-translate/i18n-polyfill”导入 { I18n };
导出类 AccValidator{
构造函数(i18n:I18n)
{

}
validate() {       
    return (group: FormGroup): createAccountError => {
        if (group) {
            this.i18n("test");
}}}

低于错误

RROR 错误:未捕获(承诺中):TypeError:i18n 不是函数
类型错误:i18n 不是函数
在 FormGroup.eval [作为验证器] (acc-validator.ts:9)
在 FormGroup.AbstractControl._runValidator (forms.js:3433)
在 FormGroup.AbstractControl.updateValueAndValidity (forms.js:3387)
在新的 FormGroup (forms.js:4326)
在 FormBuilder.group (forms.js:7864)

@ocombe

如何在 .ts 文件中使用 i18n-polyfills 和 const

出口常量光泽模型= () => [
{
标题:'测试数据',
活跃:真实,
数据: [

        [
            {
                title: 'test data',
                data: "test data."
            },
            {
                title: 'test data',
                data: "test data"
            },
            {
                title: 'test data',
                data: "test data"
            },
            {
                title: 'test data',
                data: "test data."
            },
            {
                title: 'Past Due',
                data: "test data."
            }

]]}];


{VAR_SELECT, select, Medical {Medical} 牙科 {Dental} 医疗和牙科 {Medical 和牙科} 灾难性和牙科 {Catastrophic 和牙科} 灾难性 {Catastrophic} }
{VAR_SELECT, select, Medical {Medical} 牙科 {Dental} Medical### & Dental {Medical y Dental}
灾难性 ### 和牙科{灾难性 y 牙科} 灾难性 {灾难性} }

当我使用带有特殊字符的 ICU 表达式时,它不会转换。

@ocombe如何使用 i18n-polyfills 处理面包屑?

导出 const 路线:路线 = [
{
小路: '',
组件:家庭组件,
数据: {
标题:“家”,
面包屑:“家”
}
}
]

@ocombe

如何在 .ts 文件中使用 i18n-polyfills 和 const

出口常量光泽模型= () => [
{
标题:'测试数据',
活跃:真实,
数据: [

        [
            {
                title: 'test data',
                data: "test data."
            },
            {
                title: 'test data',
                data: "test data"
            },
            {
                title: 'test data',
                data: "test data"
            },
            {
                title: 'test data',
                data: "test data."
            },
            {
                title: 'Past Due',
                data: "test data."
            }

]]}];

你可以这样使用
{
数据:'测试数据',
标题:this.i18n({ value: 'Name', id: 'name' }),
}
像这样在组件构造函数中注入 I18n polyfill
私人 i18n:I18n
在 ts 文件中使用翻译时,您需要使用 ngx-extractor 和 xliffmerge 来翻译这些 ts 文件。 https://www.npmjs.com/package/ngx-i18nsupport

@JanneHarju
它不起作用,没有获取 xlf 文件中的数据。

这是我的翻译脚本:
"translate-i18n": "ng xi18n --output-path locale && ngx-extractor -i src/**/*.ts projects/**/*.ts -f xlf -o src/locale/messages.xlf && xliffmerge --profile xliffmerge.json fi en",

这是 xliffmerge.json

{
  "xliffmergeOptions": {
    "srcDir": "src/locale",
    "genDir": "src/locale",
    "i18nFile": "messages.xlf",
    "defaultLanguage": "en",
    "useSourceAsTarget": true
  }
}

@JanneHarju
是的,我使用相同的配置,并且能够在 xlf 文件中提取 .ts 消息。

但是我在不同的 .ts 文件中有 const

出口常量光泽模型= () => [
{
}

我正在导入组件,当我尝试将 i18n 与 const 一起使用时,它没有提取值。

如果你使用像提供者这样的常量值怎么办?

{
    provide: glossayModel,
    useFactory: () => glossayModel()
}

代码可能会被破坏,但你可能会明白。

如何翻译面包屑

常量路线:路线= [
{
路径:'./enroll-new.component',
组件:注册新组件,
数据: {
面包屑:“注册”
},
}
]

你们可以停止使用这个线程作为 i18n-polyfill 或其他问题的支持线程吗? 如果您对 i18n-polyfill 有疑问,请在该回购中提出问题。
@ocombe也许你可以锁定这个话题? 几乎所有重要的事情都应该涵盖。
您计划发布本主题中提到的 i18n 支持的 Angular 版本是什么?

是的,暂时锁定。
我们正在努力让 ivy 与 v8 一起作为可选测试版提供。 运行时 i18n 将可用于 google 闭包(google 内部使用的),这将让我们对其进行调试(这是一个非常大的变化)。 对于其他所有人,您将能够使用 i18n 测试 ivy,但您将无法加载翻译。 该应用程序将使用用于编码该应用程序的原始语言运行。
实际翻译所需的运行时服务当前返回未翻译的文本。 由于我们都在努力修复现有代码的错误,因此新功能将仅限于发布后。 一旦 v8 发布,我们将继续努力,这应该是我的第一个任务。
一旦我们有一些消息,我将添加更新并解锁此线程。

我们正在处理的新$localize标记值得以编程方式使用。

例如

const name = 'World';
$localize `Hello ${name}:name!`;

然后将能够为Hello {$name}!提供可以在编译时(或运行时)替换的翻译。

这现在可用(如果没有记录)。

此页面是否有帮助?
0 / 5 - 0 等级