├── .eslintignore
├── .eslintrc.json
├── .github
├── release.yml
└── workflows
│ ├── pages-deploy.yml
│ └── pull-request.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .tool-versions
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── README_zh.md
├── dev
├── App.vue
├── components
│ ├── DemoAlert.vue
│ ├── DemoCustomTags.vue
│ ├── DemoDateFormat.vue
│ ├── DemoDirective.vue
│ ├── DemoIf.vue
│ ├── DemoMultilines.vue
│ ├── DemoPlural.vue
│ ├── DemoTemplate.vue
│ └── LanguageSelect.vue
├── gettext.d.ts
├── i18n.ts
├── index.html
├── language
│ ├── LINGUAS
│ ├── en_GB
│ │ └── app.po
│ ├── fr_FR
│ │ └── app.po
│ ├── it_IT
│ │ └── app.po
│ ├── messages.pot
│ ├── translations.json
│ └── zh_CN
│ │ └── app.po
├── main.ts
├── tsconfig.json
├── variables.css
└── vite.config.ts
├── docs
├── .vuepress
│ ├── client.ts
│ ├── config.ts
│ └── styles
│ │ └── index.scss
├── README.md
├── component.md
├── configuration.md
├── demo.md
├── directive.md
├── extraction.md
├── functions.md
├── setup.md
├── translation.md
└── zh
│ ├── README.md
│ ├── component.md
│ ├── configuration.md
│ ├── demo.md
│ ├── directive.md
│ ├── extraction.md
│ ├── functions.md
│ ├── setup.md
│ └── translation.md
├── gettext.config.js
├── package-lock.json
├── package.json
├── scripts
├── attributeEmbeddedJsExtractor.ts
├── compile.ts
├── config.ts
├── embeddedJsExtractor.ts
├── extract.ts
├── gettext_compile.ts
├── gettext_extract.ts
└── utils.ts
├── src
├── component.ts
├── directive.ts
├── index.ts
├── interpolate.ts
├── plurals.ts
├── translate.ts
├── typeDefs.ts
└── utilities.ts
├── tests
├── component.spec.ts
├── config.test.ts
├── directive.arabic.spec.ts
├── directive.spec.ts
├── interpolate.spec.ts
├── json
│ ├── component.ts
│ ├── directive.arabic.ts
│ ├── directive.ts
│ ├── plugin.config.ts
│ └── translate.ts
├── plurals.spec.ts
├── translate.spec.ts
└── utils.ts
├── tsconfig.json
├── tsup.config.ts
└── vitest.config.ts
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | distDocs
4 | coverage
5 | dev/language
6 | docs/language/translations.json
7 | docs/.vuepress/.cache
8 | docs/.vuepress/.temp
9 | .vscode
10 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "env": {
4 | "node": true
5 | },
6 | "extends": ["plugin:vue/vue3-recommended", "prettier"],
7 | "rules": {},
8 | "parserOptions": {
9 | "parser": "@typescript-eslint/parser",
10 | "sourceType": "module",
11 | "ecmaVersion": 2020
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.github/release.yml:
--------------------------------------------------------------------------------
1 | changelog:
2 | categories:
3 | - title: BREAKING CHANGES 💣
4 | labels:
5 | - breaking-change
6 | - title: Enhancements ✨
7 | labels:
8 | - enhancement
9 | - title: Fixes
10 | labels:
11 | - bug
--------------------------------------------------------------------------------
/.github/workflows/pages-deploy.yml:
--------------------------------------------------------------------------------
1 | name: pages
2 | on:
3 | push:
4 | branches:
5 | - master
6 | jobs:
7 | build-and-deploy:
8 | runs-on: ubuntu-20.04
9 | steps:
10 | - name: checkout
11 | uses: actions/checkout@v2.3.5
12 |
13 | - name: build
14 | run: |
15 | npm ci
16 | npm run docs:build
17 |
18 | - name: deploy
19 | uses: JamesIves/github-pages-deploy-action@4.1.5
20 | with:
21 | branch: pages
22 | folder: distDocs
23 |
--------------------------------------------------------------------------------
/.github/workflows/pull-request.yml:
--------------------------------------------------------------------------------
1 | name: pull request
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - master
7 | types: [opened, edited, reopened, synchronize]
8 |
9 | jobs:
10 | test:
11 | name: "test"
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: check out repo
16 | uses: actions/checkout@v3
17 | - name: setup node
18 | id: setup-node
19 | uses: actions/setup-node@v3
20 | with:
21 | node-version: 18
22 | - name: install system dependencies
23 | run: |
24 | sudo apt-get update
25 | sudo apt-get install -y gettext
26 | - name: install npm dependencies
27 | run: npm ci
28 | - name: run tests
29 | run: npm run test
30 | - name: build package
31 | run: npm run build
32 | - name: build docs
33 | run: npm run docs:build
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.tmp/
2 | /node_modules/
3 | dist
4 | distDocs
5 | npm-debug.log
6 | manifest.json
7 | *.DS_Store
8 | *~
9 | .*.swp
10 | .*.pid
11 | *.mo
12 | ./coverage
13 | .yarn
14 | yarn.lock
15 |
16 | .tm_properties
17 | .idea/
18 |
19 | docs/.vuepress/.cache
20 | docs/.vuepress/.temp
21 | .npmrc
22 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | coverage
4 | dev/language
5 | docs/language/translations.json
6 | docs/.vuepress/.cache
7 | docs/.vuepress/.temp
8 | distDocs
9 | .vscode
10 | .eslintrc.json
11 | package.json
12 | package-lock.json
13 | tsconfig.json
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "arrowParens": "always",
4 | "trailingComma": "all"
5 | }
6 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | nodejs 20.14.0
2 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib"
3 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2021 JOSHMARTIN GmbH
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 | Translate [Vue 3](http://vuejs.org) applications with [gettext](https://en.wikipedia.org/wiki/Gettext).
9 |
10 |
11 |
12 | Getting started | Demo | Documentation | 中文
13 |
14 |
15 |
16 | ## Basic usage
17 |
18 | In templates:
19 |
20 | ```jsx
21 |
22 | {{ $gettext("I'm %{age} years old!", { age: 32 }) }}
23 |
24 | ```
25 |
26 | In code:
27 |
28 | ```ts
29 | const { $gettext } = useGettext();
30 |
31 | console.log($gettext("Hello World!"));
32 | ```
33 |
34 | ## Features
35 |
36 | - simple, ergonomic API
37 | - reactive translations in Vue templates and TypeScript/JavaScript code
38 | - CLI to automatically extract messages from code files
39 | - support for pluralization and message contexts
40 |
41 | ## Contribute
42 |
43 | Please make sure your code is properly formatted (the project contains a `prettier` config) and all the tests run successfully (`npm run test`) when opening a pull request.
44 |
45 | Please specify clearly what you changed and why.
46 |
47 | ## Credits
48 |
49 | This plugin relies heavily on the work of the original [`vue-gettext`](https://github.com/Polyconseil/vue-gettext/).
50 |
51 | ## License
52 |
53 | [MIT](http://opensource.org/licenses/MIT)
54 |
--------------------------------------------------------------------------------
/README_zh.md:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | 使用 [gettext](https://en.wikipedia.org/wiki/Gettext) 国际化 [Vue 3](http://vuejs.org) 应用程序。
7 |
8 |
9 |
10 | 快速上手 | 在线演示 | 使用文档 | English
11 |
12 |
13 |
14 | ## 基本用法
15 |
16 | 模版:
17 |
18 | ```jsx
19 |
20 | {{ $gettext("I'm %{age} years old!", { age: 32 }) }}
21 |
22 | ```
23 |
24 | 代码:
25 |
26 | ```ts
27 | const { $gettext } = useGettext();
28 |
29 | console.log($gettext("Hello World!"));
30 | ```
31 |
32 | ## 特性
33 |
34 | - 简单、符合人体工学的 API 接口
35 | - 支持响应式翻译(Vue 模板和 TypeScript/JavaScript 代码)
36 | - 提供 CLI 工具自动从代码文件中提取翻译文本
37 | - 支持复数和上下文翻译
38 |
39 | ## 贡献
40 |
41 | 提交 PR 前请确保代码已经格式化(项目中已有 `prettier` 配置),而且测试(`npm run test`)已通过。
42 |
43 | 并且写清楚改了什么以及为什么要这样改。
44 |
45 |
46 | ## 致谢
47 | 本项目在很大程度上依赖于 [`vue-gettext`](https://github.com/Polyconseil/vue-gettext/) 所做的工作.
48 |
49 | ## License
50 |
51 | [MIT](http://opensource.org/licenses/MIT)
52 |
--------------------------------------------------------------------------------
/dev/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Template
7 |
8 |
9 |
10 |
11 |
Alert
12 |
13 |
14 |
15 |
16 |
Plural
17 |
18 |
19 |
20 |
21 |
Date
22 |
23 |
24 |
25 |
Custom tags
26 |
27 |
28 |
29 |
Multilines
30 |
31 |
32 |
33 |
Directive
34 |
35 |
36 |
37 |
If
38 |
39 |
40 |
41 |
42 |
43 |
80 |
81 |
110 |
--------------------------------------------------------------------------------
/dev/components/DemoAlert.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | gettext in JavaScript
6 |
7 |
8 |
9 | -
10 | {{ n }}
11 | +
12 | ngettext in JavaScript
13 |
14 |
15 |
16 |
17 |
41 |
--------------------------------------------------------------------------------
/dev/components/DemoCustomTags.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Headline 1
4 | Headline 2
5 | Headline 3
6 | Headline 4
7 | Paragraph
8 |
9 |
10 |
--------------------------------------------------------------------------------
/dev/components/DemoDateFormat.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ dateFormat(dateNow) }}
4 |
5 |
6 |
7 |
29 |
--------------------------------------------------------------------------------
/dev/components/DemoDirective.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
A random number: %{ random }
5 |
6 |
7 |
8 |
9 | Hello %{ name }
10 |
11 |
12 |
13 |
14 | -
15 |
21 | %{ count } apple
22 |
23 | +
24 |
25 |
26 |
27 |
28 |
46 |
--------------------------------------------------------------------------------
/dev/components/DemoIf.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Toggle: {{ show }}
5 | {{ $gettext("Welcome %{ name }", { name: obj.name }) }}
6 |
7 |
8 |
9 | 1
10 | 2
11 | 3
12 | [{{ obj.name }}]
13 |
14 |
This is %{ obj.name }
15 |
This is %{ obj.name }
16 |
This is %{ obj.name }
17 |
18 |
19 |
20 |
21 |
45 |
--------------------------------------------------------------------------------
/dev/components/DemoMultilines.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Forgotten your password? Enter your "email address" below, and we'll email instructions for setting a new one.
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/dev/components/DemoPlural.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | In English, '0' (zero) is always plural.
5 |
6 |
7 | -
8 | {{ n }}
9 | +
10 |
11 |
12 | {{ $ngettext("%{ n } book", "%{ n } books", n, { n }) }}
13 |
14 |
15 |
Use default singular or plural form when there is no translation.
16 |
17 | {{
18 | $ngettext(
19 | "%{ countForUntranslated } item. This is left untranslated on purpose.",
20 | "%{ countForUntranslated } items. This is left untranslated on purpose.",
21 | countForUntranslated,
22 | { countForUntranslated },
23 | )
24 | }}
25 |
26 |
27 |
28 |
29 |
51 |
--------------------------------------------------------------------------------
/dev/components/DemoTemplate.vue:
--------------------------------------------------------------------------------
1 |
2 | - {{ n }} +
3 |
4 | {{ __("use `__` as a alias of gettext") }}
5 |
6 | {{ __("use `_n` as a alias of ngettext") }}
7 | {{ _n("One apple", "%{n} apples", n, { n: `${n}` }) }}
8 |
9 |
10 | {{ __("use `_x` as a alias of pgettext") }}
11 | {{ __("hover here and see the title") }}
12 |
13 |
14 | {{ __("use `_xn` as a alias of npgettext") }}
15 | {{ _xn("list-item", "one message", "%{n} messages", n, { n: `${n}` }) }}
16 |
17 |
18 |
19 |
20 |
31 |
--------------------------------------------------------------------------------
/dev/components/LanguageSelect.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Select your language:
5 |
6 |
7 | {{ name }}
8 |
9 |
10 |
11 |
12 |
25 |
26 |
33 |
--------------------------------------------------------------------------------
/dev/gettext.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 | declare module "@vue/runtime-core" {
3 | interface ComponentCustomProperties {
4 | __: ComponentCustomProperties["$gettext"];
5 | _x: ComponentCustomProperties["$pgettext"];
6 | _n: ComponentCustomProperties["$ngettext"];
7 | _xn: ComponentCustomProperties["$npgettext"];
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/dev/i18n.ts:
--------------------------------------------------------------------------------
1 | import translations from "./language/translations.json";
2 | import { createGettext } from "/@gettext/";
3 |
4 | const gettext = createGettext({
5 | availableLanguages: {
6 | en_GB: "British English",
7 | fr_FR: "Français",
8 | it_IT: "Italiano",
9 | zh_CN: "简体中文",
10 | },
11 | defaultLanguage: "en_GB",
12 | translations: translations,
13 | setGlobalProperties: true,
14 | globalProperties: {
15 | // custom global properties name
16 | gettext: ["$gettext", "__"],
17 | ngettext: ["$ngettext", "_n"],
18 | pgettext: ["$pgettext", "_x"],
19 | npgettext: ["$npgettext", "_xn"],
20 | },
21 | });
22 |
23 | export { gettext };
24 |
--------------------------------------------------------------------------------
/dev/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | vue3-gettext example
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/dev/language/LINGUAS:
--------------------------------------------------------------------------------
1 | en_GB fr_FR it_IT zh_CN
--------------------------------------------------------------------------------
/dev/language/en_GB/app.po:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 | "Project-Id-Version: \n"
4 | "Last-Translator: Automatically generated\n"
5 | "Language-Team: none\n"
6 | "Language: en_GB\n"
7 | "MIME-Version: 1.0\n"
8 | "Content-Type: text/plain; charset=UTF-8\n"
9 | "Content-Transfer-Encoding: 8bit\n"
10 | "Generated-By: easygettext\n"
11 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
12 |
13 | #: dev/components/DemoPlural.vue:18
14 | msgid "%{ countForUntranslated } item. This is left untranslated on purpose."
15 | msgid_plural ""
16 | "%{ countForUntranslated } items. This is left untranslated on purpose."
17 | msgstr[0] ""
18 | "%{ countForUntranslated } item. This is left untranslated on purpose."
19 | msgstr[1] ""
20 | "%{ countForUntranslated } items. This is left untranslated on purpose."
21 |
22 | #: dev/components/DemoPlural.vue:12
23 | msgid "%{ n } book"
24 | msgid_plural "%{ n } books"
25 | msgstr[0] "%{ n } book"
26 | msgstr[1] "%{ n } books"
27 |
28 | #: dev/components/DemoAlert.vue:35
29 | #, fuzzy
30 | msgid "%{ n } car"
31 | msgid_plural "%{ n } cars"
32 | msgstr[0] "%{ n } book"
33 | msgstr[1] "%{ n } books"
34 |
35 | #: dev/components/DemoDirective.vue:15
36 | msgid "%{ count } apple"
37 | msgid_plural "%{ count } apples"
38 | msgstr[0] "%{ count } apple"
39 | msgstr[1] "%{ count } apples"
40 |
41 | #: dev/components/DemoDirective.vue:4
42 | msgid "A random number: %{ random } "
43 | msgstr "A random number: %{ random } "
44 |
45 | #: dev/components/DemoMultilines.vue:3
46 | msgid ""
47 | "Forgotten your password? Enter your \"email address\" below, and we'll email "
48 | "instructions for setting a new one."
49 | msgstr ""
50 | "Forgotten your password? Enter your \"email address\" below,\n"
51 | " and we'll email instructions for setting a new one."
52 |
53 | #. use jsExtractorOpts in gettext.config.js to extract custom keywords
54 | #: dev/components/DemoAlert.vue:24
55 | msgid "Good bye!"
56 | msgstr ""
57 |
58 | #: dev/components/DemoCustomTags.vue:3
59 | msgid "Headline 1"
60 | msgstr "Headline 1"
61 |
62 | #: dev/components/DemoCustomTags.vue:4
63 | msgid "Headline 2"
64 | msgstr "Headline 2"
65 |
66 | #: dev/components/DemoCustomTags.vue:5
67 | msgid "Headline 3"
68 | msgstr "Headline 3"
69 |
70 | #: dev/components/DemoCustomTags.vue:6
71 | msgid "Headline 4"
72 | msgstr "Headline 4"
73 |
74 | #: dev/components/DemoDirective.vue:9
75 | msgid "Hello %{ name } "
76 | msgstr "Hello %{ name } "
77 |
78 | #: dev/components/DemoTemplate.vue:11
79 | msgid "hover here and see the title"
80 | msgstr ""
81 |
82 | #: dev/components/DemoPlural.vue:4
83 | msgid "In English, '0' (zero) is always plural."
84 | msgstr "In English, '0' (zero) is always plural."
85 |
86 | #: dev/components/DemoTemplate.vue:7
87 | msgid "One apple"
88 | msgid_plural "%{n} apples"
89 | msgstr[0] ""
90 | msgstr[1] ""
91 |
92 | #: dev/components/DemoCustomTags.vue:7
93 | msgid "Paragraph"
94 | msgstr "Paragraph"
95 |
96 | #: dev/components/LanguageSelect.vue:4
97 | msgid "Select your language:"
98 | msgstr "Select your language:"
99 |
100 | #: dev/components/DemoIf.vue:14 dev/components/DemoIf.vue:15
101 | #: dev/components/DemoIf.vue:16
102 | msgid "This is %{ obj.name }"
103 | msgstr "This is %{ obj.name }"
104 |
105 | #: dev/components/DemoTemplate.vue:4
106 | msgid "use `__` as a alias of gettext"
107 | msgstr ""
108 |
109 | #: dev/components/DemoTemplate.vue:6
110 | msgid "use `_n` as a alias of ngettext"
111 | msgstr ""
112 |
113 | #: dev/components/DemoTemplate.vue:10
114 | msgid "use `_x` as a alias of pgettext"
115 | msgstr ""
116 |
117 | #: dev/components/DemoTemplate.vue:14
118 | msgid "use `_xn` as a alias of npgettext"
119 | msgstr ""
120 |
121 | #: dev/components/DemoIf.vue:5
122 | msgid "Welcome %{ name }"
123 | msgstr "Welcome %{ name }"
124 |
125 | #: dev/components/DemoTemplate.vue:15
126 | msgctxt "list-item"
127 | msgid "one message"
128 | msgid_plural "%{n} messages"
129 | msgstr[0] ""
130 | msgstr[1] ""
131 |
132 | #: dev/components/DemoTemplate.vue:9
133 | msgctxt "title"
134 | msgid "This is my title"
135 | msgstr ""
136 |
137 | #~ msgid "%{ nComputed } book"
138 | #~ msgid_plural "%{ nComputed } books"
139 | #~ msgstr[0] "%{ nComputed } book"
140 | #~ msgstr[1] "%{ nComputed } books"
141 |
142 | #~ msgid ""
143 | #~ "Use default singular or plural form when there is no translation. This is "
144 | #~ "left untranslated on purpose."
145 | #~ msgstr ""
146 | #~ "Use default singular or plural form when there is no translation. This is "
147 | #~ "left untranslated on purpose."
148 |
--------------------------------------------------------------------------------
/dev/language/fr_FR/app.po:
--------------------------------------------------------------------------------
1 | # French translations for vue-gettext package
2 | # Traductions françaises du paquet vue-gettext.
3 | # Copyright (C) 2016 THE vue-gettext'S COPYRIGHT HOLDER
4 | # This file is distributed under the same license as the vue-gettext package.
5 | # Automatically generated, 2016.
6 | #
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: vue-gettext 2.0.0\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2017-06-13 15:49+0200\n"
12 | "PO-Revision-Date: 2016-11-22 16:24+0100\n"
13 | "Last-Translator: Automatically generated\n"
14 | "Language-Team: none\n"
15 | "Language: fr_FR\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=UTF-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n"
20 |
21 | #: dev/components/DemoPlural.vue:18
22 | msgid "%{ countForUntranslated } item. This is left untranslated on purpose."
23 | msgid_plural ""
24 | "%{ countForUntranslated } items. This is left untranslated on purpose."
25 | msgstr[0] ""
26 | msgstr[1] ""
27 |
28 | #: dev/components/DemoPlural.vue:12
29 | msgid "%{ n } book"
30 | msgid_plural "%{ n } books"
31 | msgstr[0] "%{ n } livre"
32 | msgstr[1] "%{ n } livres"
33 |
34 | #: dev/components/DemoAlert.vue:35
35 | msgid "%{ n } car"
36 | msgid_plural "%{ n } cars"
37 | msgstr[0] "%{ n } voiture"
38 | msgstr[1] "%{ n } voitures"
39 |
40 | #: dev/components/DemoDirective.vue:15
41 | msgid "%{ count } apple"
42 | msgid_plural "%{ count } apples"
43 | msgstr[0] "%{ count } pomme"
44 | msgstr[1] "%{ count } pommes"
45 |
46 | #: dev/components/DemoDirective.vue:4
47 | msgid "A random number: %{ random } "
48 | msgstr "Un nombre aléatoire : %{ random } "
49 |
50 | #: dev/components/DemoMultilines.vue:3
51 | msgid ""
52 | "Forgotten your password? Enter your \"email address\" below, and we'll email "
53 | "instructions for setting a new one."
54 | msgstr ""
55 | "Mot de passe perdu ? Saisissez votre \"adresse électronique\" ci-dessous\n"
56 | "et nous vous enverrons les instructions pour en créer un nouveau."
57 |
58 | #. use jsExtractorOpts in gettext.config.js to extract custom keywords
59 | #: dev/components/DemoAlert.vue:24
60 | msgid "Good bye!"
61 | msgstr "Au revoir !"
62 |
63 | #: dev/components/DemoCustomTags.vue:3
64 | msgid "Headline 1"
65 | msgstr "Titre 1"
66 |
67 | #: dev/components/DemoCustomTags.vue:4
68 | msgid "Headline 2"
69 | msgstr "Titre 2"
70 |
71 | #: dev/components/DemoCustomTags.vue:5
72 | msgid "Headline 3"
73 | msgstr "Titre 3"
74 |
75 | #: dev/components/DemoCustomTags.vue:6
76 | msgid "Headline 4"
77 | msgstr "Titre 4"
78 |
79 | #: dev/components/DemoDirective.vue:9
80 | msgid "Hello %{ name } "
81 | msgstr "Bonjour %{ name } "
82 |
83 | #: dev/components/DemoTemplate.vue:11
84 | msgid "hover here and see the title"
85 | msgstr ""
86 |
87 | #: dev/components/DemoPlural.vue:4
88 | msgid "In English, '0' (zero) is always plural."
89 | msgstr "En anglais, '0' (zero) prend toujours le pluriel."
90 |
91 | #: dev/components/DemoTemplate.vue:7
92 | msgid "One apple"
93 | msgid_plural "%{n} apples"
94 | msgstr[0] ""
95 | msgstr[1] ""
96 |
97 | #: dev/components/DemoCustomTags.vue:7
98 | msgid "Paragraph"
99 | msgstr "Paragraphe"
100 |
101 | #: dev/components/LanguageSelect.vue:4
102 | msgid "Select your language:"
103 | msgstr "Sélectionner votre langage"
104 |
105 | #: dev/components/DemoIf.vue:14 dev/components/DemoIf.vue:15
106 | #: dev/components/DemoIf.vue:16
107 | msgid "This is %{ obj.name }"
108 | msgstr "C'est %{ obj.name }"
109 |
110 | #: dev/components/DemoTemplate.vue:4
111 | msgid "use `__` as a alias of gettext"
112 | msgstr ""
113 |
114 | #: dev/components/DemoTemplate.vue:6
115 | msgid "use `_n` as a alias of ngettext"
116 | msgstr ""
117 |
118 | #: dev/components/DemoTemplate.vue:10
119 | msgid "use `_x` as a alias of pgettext"
120 | msgstr ""
121 |
122 | #: dev/components/DemoTemplate.vue:14
123 | msgid "use `_xn` as a alias of npgettext"
124 | msgstr ""
125 |
126 | #: dev/components/DemoIf.vue:5
127 | msgid "Welcome %{ name }"
128 | msgstr "Bienvenue %{ name }"
129 |
130 | #: dev/components/DemoTemplate.vue:15
131 | msgctxt "list-item"
132 | msgid "one message"
133 | msgid_plural "%{n} messages"
134 | msgstr[0] ""
135 | msgstr[1] ""
136 |
137 | #: dev/components/DemoTemplate.vue:9
138 | msgctxt "title"
139 | msgid "This is my title"
140 | msgstr ""
141 |
142 | #~ msgid "%{ nComputed } book"
143 | #~ msgid_plural "%{ nComputed } books"
144 | #~ msgstr[0] "%{ nComputed } livre"
145 | #~ msgstr[1] "%{ nComputed } livres"
146 |
--------------------------------------------------------------------------------
/dev/language/it_IT/app.po:
--------------------------------------------------------------------------------
1 | # Italian translations for vue-gettext package
2 | # Traduzioni italiane per il pacchetto vue-gettext..
3 | # Copyright (C) 2016 THE vue-gettext'S COPYRIGHT HOLDER
4 | # This file is distributed under the same license as the vue-gettext package.
5 | # Automatically generated, 2016.
6 | #
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: vue-gettext 2.0.0\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2017-06-13 15:49+0200\n"
12 | "PO-Revision-Date: 2016-11-22 16:26+0100\n"
13 | "Last-Translator: Automatically generated\n"
14 | "Language-Team: none\n"
15 | "Language: it_IT\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=UTF-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
20 |
21 | #: dev/components/DemoPlural.vue:18
22 | msgid "%{ countForUntranslated } item. This is left untranslated on purpose."
23 | msgid_plural ""
24 | "%{ countForUntranslated } items. This is left untranslated on purpose."
25 | msgstr[0] ""
26 | msgstr[1] ""
27 |
28 | #: dev/components/DemoPlural.vue:12
29 | msgid "%{ n } book"
30 | msgid_plural "%{ n } books"
31 | msgstr[0] "%{ n } libbra"
32 | msgstr[1] "%{ n } libri"
33 |
34 | #: dev/components/DemoAlert.vue:35
35 | msgid "%{ n } car"
36 | msgid_plural "%{ n } cars"
37 | msgstr[0] "%{ n } auto"
38 | msgstr[1] "%{ n } auto"
39 |
40 | #: dev/components/DemoDirective.vue:15
41 | msgid "%{ count } apple"
42 | msgid_plural "%{ count } apples"
43 | msgstr[0] "%{ count } mela"
44 | msgstr[1] "%{ count } mele"
45 |
46 | #: dev/components/DemoDirective.vue:4
47 | msgid "A random number: %{ random } "
48 | msgstr "Un numero casuale: %{ random } "
49 |
50 | #: dev/components/DemoMultilines.vue:3
51 | msgid ""
52 | "Forgotten your password? Enter your \"email address\" below, and we'll email "
53 | "instructions for setting a new one."
54 | msgstr ""
55 | "Password dimenticata? Inserisci il tuo \"indirizzo email\" qui\n"
56 | "sotto, e ti invieremo istruzioni per impostarne una nuova."
57 |
58 | #. use jsExtractorOpts in gettext.config.js to extract custom keywords
59 | #: dev/components/DemoAlert.vue:24
60 | msgid "Good bye!"
61 | msgstr "Arriverdeci!"
62 |
63 | #: dev/components/DemoCustomTags.vue:3
64 | msgid "Headline 1"
65 | msgstr "Titolo 1"
66 |
67 | #: dev/components/DemoCustomTags.vue:4
68 | msgid "Headline 2"
69 | msgstr "Titolo 2"
70 |
71 | #: dev/components/DemoCustomTags.vue:5
72 | msgid "Headline 3"
73 | msgstr "Titolo 3"
74 |
75 | #: dev/components/DemoCustomTags.vue:6
76 | msgid "Headline 4"
77 | msgstr "Titolo 4"
78 |
79 | #: dev/components/DemoDirective.vue:9
80 | msgid "Hello %{ name } "
81 | msgstr "Buongiorno %{ name } "
82 |
83 | #: dev/components/DemoTemplate.vue:11
84 | msgid "hover here and see the title"
85 | msgstr ""
86 |
87 | #: dev/components/DemoPlural.vue:4
88 | msgid "In English, '0' (zero) is always plural."
89 | msgstr "In inglese, '0' (zero) è sempre al plurale."
90 |
91 | #: dev/components/DemoTemplate.vue:7
92 | msgid "One apple"
93 | msgid_plural "%{n} apples"
94 | msgstr[0] ""
95 | msgstr[1] ""
96 |
97 | #: dev/components/DemoCustomTags.vue:7
98 | msgid "Paragraph"
99 | msgstr "Paragrafo"
100 |
101 | #: dev/components/LanguageSelect.vue:4
102 | msgid "Select your language:"
103 | msgstr "Seleziona la tua lingua:"
104 |
105 | #: dev/components/DemoIf.vue:14 dev/components/DemoIf.vue:15
106 | #: dev/components/DemoIf.vue:16
107 | msgid "This is %{ obj.name }"
108 | msgstr "Questo è %{ obj.name }"
109 |
110 | #: dev/components/DemoTemplate.vue:4
111 | msgid "use `__` as a alias of gettext"
112 | msgstr ""
113 |
114 | #: dev/components/DemoTemplate.vue:6
115 | msgid "use `_n` as a alias of ngettext"
116 | msgstr ""
117 |
118 | #: dev/components/DemoTemplate.vue:10
119 | msgid "use `_x` as a alias of pgettext"
120 | msgstr ""
121 |
122 | #: dev/components/DemoTemplate.vue:14
123 | msgid "use `_xn` as a alias of npgettext"
124 | msgstr ""
125 |
126 | #: dev/components/DemoIf.vue:5
127 | msgid "Welcome %{ name }"
128 | msgstr "Benvenuto %{ name }"
129 |
130 | #: dev/components/DemoTemplate.vue:15
131 | msgctxt "list-item"
132 | msgid "one message"
133 | msgid_plural "%{n} messages"
134 | msgstr[0] ""
135 | msgstr[1] ""
136 |
137 | #: dev/components/DemoTemplate.vue:9
138 | msgctxt "title"
139 | msgid "This is my title"
140 | msgstr ""
141 |
142 | #~ msgid "%{ nComputed } book"
143 | #~ msgid_plural "%{ nComputed } books"
144 | #~ msgstr[0] "%{ nComputed } libbra"
145 | #~ msgstr[1] "%{ nComputed } libri"
146 |
--------------------------------------------------------------------------------
/dev/language/messages.pot:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 | "Content-Type: text/plain; charset=UTF-8\n"
4 |
5 | #: dev/components/DemoPlural.vue:18
6 | msgid "%{ countForUntranslated } item. This is left untranslated on purpose."
7 | msgid_plural "%{ countForUntranslated } items. This is left untranslated on purpose."
8 | msgstr[0] ""
9 | msgstr[1] ""
10 |
11 | #: dev/components/DemoPlural.vue:12
12 | msgid "%{ n } book"
13 | msgid_plural "%{ n } books"
14 | msgstr[0] ""
15 | msgstr[1] ""
16 |
17 | #: dev/components/DemoAlert.vue:35
18 | msgid "%{ n } car"
19 | msgid_plural "%{ n } cars"
20 | msgstr[0] ""
21 | msgstr[1] ""
22 |
23 | #: dev/components/DemoDirective.vue:15
24 | msgid "%{ count } apple"
25 | msgid_plural "%{ count } apples"
26 | msgstr[0] ""
27 | msgstr[1] ""
28 |
29 | #: dev/components/DemoDirective.vue:4
30 | msgid "A random number: %{ random } "
31 | msgstr ""
32 |
33 | #: dev/components/DemoMultilines.vue:3
34 | msgid "Forgotten your password? Enter your \"email address\" below, and we'll email instructions for setting a new one."
35 | msgstr ""
36 |
37 | #. use jsExtractorOpts in gettext.config.js to extract custom keywords
38 | #: dev/components/DemoAlert.vue:24
39 | msgid "Good bye!"
40 | msgstr ""
41 |
42 | #: dev/components/DemoCustomTags.vue:3
43 | msgid "Headline 1"
44 | msgstr ""
45 |
46 | #: dev/components/DemoCustomTags.vue:4
47 | msgid "Headline 2"
48 | msgstr ""
49 |
50 | #: dev/components/DemoCustomTags.vue:5
51 | msgid "Headline 3"
52 | msgstr ""
53 |
54 | #: dev/components/DemoCustomTags.vue:6
55 | msgid "Headline 4"
56 | msgstr ""
57 |
58 | #: dev/components/DemoDirective.vue:9
59 | msgid "Hello %{ name } "
60 | msgstr ""
61 |
62 | #: dev/components/DemoTemplate.vue:11
63 | msgid "hover here and see the title"
64 | msgstr ""
65 |
66 | #: dev/components/DemoPlural.vue:4
67 | msgid "In English, '0' (zero) is always plural."
68 | msgstr ""
69 |
70 | #: dev/components/DemoTemplate.vue:7
71 | msgid "One apple"
72 | msgid_plural "%{n} apples"
73 | msgstr[0] ""
74 | msgstr[1] ""
75 |
76 | #: dev/components/DemoCustomTags.vue:7
77 | msgid "Paragraph"
78 | msgstr ""
79 |
80 | #: dev/components/LanguageSelect.vue:4
81 | msgid "Select your language:"
82 | msgstr ""
83 |
84 | #: dev/components/DemoIf.vue:14
85 | #: dev/components/DemoIf.vue:15
86 | #: dev/components/DemoIf.vue:16
87 | msgid "This is %{ obj.name }"
88 | msgstr ""
89 |
90 | #: dev/components/DemoTemplate.vue:4
91 | msgid "use `__` as a alias of gettext"
92 | msgstr ""
93 |
94 | #: dev/components/DemoTemplate.vue:6
95 | msgid "use `_n` as a alias of ngettext"
96 | msgstr ""
97 |
98 | #: dev/components/DemoTemplate.vue:10
99 | msgid "use `_x` as a alias of pgettext"
100 | msgstr ""
101 |
102 | #: dev/components/DemoTemplate.vue:14
103 | msgid "use `_xn` as a alias of npgettext"
104 | msgstr ""
105 |
106 | #: dev/components/DemoIf.vue:5
107 | msgid "Welcome %{ name }"
108 | msgstr ""
109 |
110 | #: dev/components/DemoTemplate.vue:15
111 | msgctxt "list-item"
112 | msgid "one message"
113 | msgid_plural "%{n} messages"
114 | msgstr[0] ""
115 | msgstr[1] ""
116 |
117 | #: dev/components/DemoTemplate.vue:9
118 | msgctxt "title"
119 | msgid "This is my title"
120 | msgstr ""
121 |
--------------------------------------------------------------------------------
/dev/language/translations.json:
--------------------------------------------------------------------------------
1 | {"en_GB":{"%{ countForUntranslated } item. This is left untranslated on purpose.":["%{ countForUntranslated } item. This is left untranslated on purpose.","%{ countForUntranslated } items. This is left untranslated on purpose."],"%{ n } book":["%{ n } book","%{ n } books"],"%{ nComputed } book":["%{ nComputed } book","%{ nComputed } books"],"%{ count } apple":["%{ count } apple","%{ count } apples"],"A random number: %{ random } ":"A random number: %{ random } ","Forgotten your password? Enter your \"email address\" below, and we'll email instructions for setting a new one.":"Forgotten your password? Enter your \"email address\" below,\n and we'll email instructions for setting a new one.","Headline 1":"Headline 1","Headline 2":"Headline 2","Headline 3":"Headline 3","Headline 4":"Headline 4","Hello %{ name } ":"Hello %{ name } ","In English, '0' (zero) is always plural.":"In English, '0' (zero) is always plural.","Paragraph":"Paragraph","Select your language:":"Select your language:","This is %{ obj.name }":"This is %{ obj.name }","Use default singular or plural form when there is no translation. This is left untranslated on purpose.":"Use default singular or plural form when there is no translation. This is left untranslated on purpose.","Welcome %{ name }":"Welcome %{ name }"},"fr_FR":{"%{ n } book":["%{ n } livre","%{ n } livres"],"%{ n } car":["%{ n } voiture","%{ n } voitures"],"%{ nComputed } book":["%{ nComputed } livre","%{ nComputed } livres"],"%{ count } apple":["%{ count } pomme","%{ count } pommes"],"A random number: %{ random } ":"Un nombre aléatoire : %{ random } ","Forgotten your password? Enter your \"email address\" below, and we'll email instructions for setting a new one.":"Mot de passe perdu ? Saisissez votre \"adresse électronique\" ci-dessous\net nous vous enverrons les instructions pour en créer un nouveau.","Good bye!":"Au revoir !","Headline 1":"Titre 1","Headline 2":"Titre 2","Headline 3":"Titre 3","Headline 4":"Titre 4","Hello %{ name } ":"Bonjour %{ name } ","In English, '0' (zero) is always plural.":"En anglais, '0' (zero) prend toujours le pluriel.","Paragraph":"Paragraphe","Select your language:":"Sélectionner votre langage","This is %{ obj.name }":"C'est %{ obj.name }","Welcome %{ name }":"Bienvenue %{ name }"},"it_IT":{"%{ n } book":["%{ n } libbra","%{ n } libri"],"%{ n } car":["%{ n } auto","%{ n } auto"],"%{ nComputed } book":["%{ nComputed } libbra","%{ nComputed } libri"],"%{ count } apple":["%{ count } mela","%{ count } mele"],"A random number: %{ random } ":"Un numero casuale: %{ random } ","Forgotten your password? Enter your \"email address\" below, and we'll email instructions for setting a new one.":"Password dimenticata? Inserisci il tuo \"indirizzo email\" qui\nsotto, e ti invieremo istruzioni per impostarne una nuova.","Good bye!":"Arriverdeci!","Headline 1":"Titolo 1","Headline 2":"Titolo 2","Headline 3":"Titolo 3","Headline 4":"Titolo 4","Hello %{ name } ":"Buongiorno %{ name } ","In English, '0' (zero) is always plural.":"In inglese, '0' (zero) è sempre al plurale.","Paragraph":"Paragrafo","Select your language:":"Seleziona la tua lingua:","This is %{ obj.name }":"Questo è %{ obj.name }","Welcome %{ name }":"Benvenuto %{ name }"},"zh_CN":{"%{ n } book":"%{ n } 本书","%{ n } car":"%{ n } 辆车","%{ nComputed } book":"%{ nComputed } 本书","%{ count } apple":"%{ count } 个苹果","A random number: %{ random } ":"一个随机 数: %{ random } ","Forgotten your password? Enter your \"email address\" below, and we'll email instructions for setting a new one.":"忘记密码了吗?请在下面输入你的「电子邮件地址」,我们将通过电子邮件发送设置新密码的说明。","Good bye!":"再见","Headline 1":"一级标题","Headline 2":"二级标题","Headline 3":"三级标题","Headline 4":"四级标题","Hello %{ name } ":"%{ name } 你好","hover here and see the title":"在此悬浮以查看标题","In English, '0' (zero) is always plural.":"在英语中,0(零)总是复数。","Paragraph":"段落","Select your language:":"请选择语言","This is %{ obj.name }":"这是 %{ obj.name }","use `__` as a alias of gettext":"使用 `__` 作为 gettext 的别名","use `_n` as a alias of ngettext":"使用 `_n` 作为 ngettext 的别名","use `_x` as a alias of pgettext":"使用 `_x` 作为 pgettext 的别名","use `_xn` as a alias of npgettext":"使用 `_xn` 作为 npgettext 的别名","Welcome %{ name }":"欢迎 %{ name }","one message":{"list-item":"%{n} 条消息"},"This is my title":{"title":"这是我的标题"}}}
--------------------------------------------------------------------------------
/dev/language/zh_CN/app.po:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 | "Project-Id-Version: vue 3-gettext\n"
4 | "Last-Translator: Automatically generated\n"
5 | "Language-Team: none\n"
6 | "Language: zh_CN\n"
7 | "MIME-Version: 1.0\n"
8 | "Content-Type: text/plain; charset=UTF-8\n"
9 | "Content-Transfer-Encoding: 8bit\n"
10 | "Plural-Forms: nplurals=1; plural=0;\n"
11 |
12 | #: dev/components/DemoPlural.vue:18
13 | msgid "%{ countForUntranslated } item. This is left untranslated on purpose."
14 | msgid_plural ""
15 | "%{ countForUntranslated } items. This is left untranslated on purpose."
16 | msgstr[0] ""
17 |
18 | #: dev/components/DemoPlural.vue:12
19 | msgid "%{ n } book"
20 | msgid_plural "%{ n } books"
21 | msgstr[0] "%{ n } 本书"
22 |
23 | #: dev/components/DemoAlert.vue:35
24 | msgid "%{ n } car"
25 | msgid_plural "%{ n } cars"
26 | msgstr[0] "%{ n } 辆车"
27 |
28 | #: dev/components/DemoDirective.vue:15
29 | msgid "%{ count } apple"
30 | msgid_plural "%{ count } apples"
31 | msgstr[0] "%{ count } 个苹果"
32 |
33 | #: dev/components/DemoDirective.vue:4
34 | msgid "A random number: %{ random } "
35 | msgstr "一个随机 数: %{ random } "
36 |
37 | #: dev/components/DemoMultilines.vue:3
38 | msgid ""
39 | "Forgotten your password? Enter your \"email address\" below, and we'll email "
40 | "instructions for setting a new one."
41 | msgstr ""
42 | "忘记密码了吗?请在下面输入你的「电子邮件地址」,我们将通过电子邮件发送设置新"
43 | "密码的说明。"
44 |
45 | #. use jsExtractorOpts in gettext.config.js to extract custom keywords
46 | #: dev/components/DemoAlert.vue:24
47 | msgid "Good bye!"
48 | msgstr "再见"
49 |
50 | #: dev/components/DemoCustomTags.vue:3
51 | msgid "Headline 1"
52 | msgstr "一级标题"
53 |
54 | #: dev/components/DemoCustomTags.vue:4
55 | msgid "Headline 2"
56 | msgstr "二级标题"
57 |
58 | #: dev/components/DemoCustomTags.vue:5
59 | msgid "Headline 3"
60 | msgstr "三级标题"
61 |
62 | #: dev/components/DemoCustomTags.vue:6
63 | msgid "Headline 4"
64 | msgstr "四级标题"
65 |
66 | #: dev/components/DemoDirective.vue:9
67 | msgid "Hello %{ name } "
68 | msgstr "%{ name } 你好"
69 |
70 | #: dev/components/DemoTemplate.vue:11
71 | msgid "hover here and see the title"
72 | msgstr "在此悬浮以查看标题"
73 |
74 | #: dev/components/DemoPlural.vue:4
75 | msgid "In English, '0' (zero) is always plural."
76 | msgstr "在英语中,0(零)总是复数。"
77 |
78 | #: dev/components/DemoTemplate.vue:7
79 | msgid "One apple"
80 | msgid_plural "%{n} apples"
81 | msgstr[0] ""
82 |
83 | #: dev/components/DemoCustomTags.vue:7
84 | msgid "Paragraph"
85 | msgstr "段落"
86 |
87 | #: dev/components/LanguageSelect.vue:4
88 | msgid "Select your language:"
89 | msgstr "请选择语言"
90 |
91 | #: dev/components/DemoIf.vue:14 dev/components/DemoIf.vue:15
92 | #: dev/components/DemoIf.vue:16
93 | msgid "This is %{ obj.name }"
94 | msgstr "这是 %{ obj.name }"
95 |
96 | #: dev/components/DemoTemplate.vue:4
97 | msgid "use `__` as a alias of gettext"
98 | msgstr "使用 `__` 作为 gettext 的别名"
99 |
100 | #: dev/components/DemoTemplate.vue:6
101 | msgid "use `_n` as a alias of ngettext"
102 | msgstr "使用 `_n` 作为 ngettext 的别名"
103 |
104 | #: dev/components/DemoTemplate.vue:10
105 | msgid "use `_x` as a alias of pgettext"
106 | msgstr "使用 `_x` 作为 pgettext 的别名"
107 |
108 | #: dev/components/DemoTemplate.vue:14
109 | msgid "use `_xn` as a alias of npgettext"
110 | msgstr "使用 `_xn` 作为 npgettext 的别名"
111 |
112 | #: dev/components/DemoIf.vue:5
113 | msgid "Welcome %{ name }"
114 | msgstr "欢迎 %{ name }"
115 |
116 | #: dev/components/DemoTemplate.vue:15
117 | msgctxt "list-item"
118 | msgid "one message"
119 | msgid_plural "%{n} messages"
120 | msgstr[0] "%{n} 条消息"
121 |
122 | #: dev/components/DemoTemplate.vue:9
123 | msgctxt "title"
124 | msgid "This is my title"
125 | msgstr "这是我的标题"
126 |
127 | #~ msgid "%{ nComputed } book"
128 | #~ msgid_plural "%{ nComputed } books"
129 | #~ msgstr[0] "%{ nComputed } 本书"
130 |
--------------------------------------------------------------------------------
/dev/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 | import App from "./App.vue";
3 | import { gettext } from "./i18n";
4 |
5 | const app = createApp(App);
6 |
7 | app.use(gettext);
8 |
9 | app.mount("#app");
10 |
11 | export default app;
12 |
--------------------------------------------------------------------------------
/dev/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "resolveJsonModule": true,
4 | "esModuleInterop": true,
5 | "baseUrl": "./",
6 | "paths": {
7 | "/@gettext/": [
8 | "../src"
9 | ]
10 | }
11 | },
12 | "include": [
13 | "./**/*"
14 | ],
15 | }
--------------------------------------------------------------------------------
/dev/variables.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --color-grey: #e5e5e5;
3 | --color-blue: #00aff2;
4 | }
5 |
--------------------------------------------------------------------------------
/dev/vite.config.ts:
--------------------------------------------------------------------------------
1 | import * as path from "path";
2 | import vue from "@vitejs/plugin-vue";
3 | import { defineConfig } from "vite";
4 |
5 | export default defineConfig({
6 | plugins: [vue()],
7 | resolve: {
8 | alias: {
9 | "/@/": path.resolve(__dirname, "./src"),
10 | "/@gettext/": path.resolve(__dirname, "../src"),
11 | },
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/docs/.vuepress/client.ts:
--------------------------------------------------------------------------------
1 | import { defineClientConfig } from "@vuepress/client";
2 | import Demo from "../../dev/App.vue";
3 | import { gettext } from "../../dev/i18n";
4 |
5 | export default defineClientConfig({
6 | enhance({ app }) {
7 | app.use(gettext);
8 |
9 | app.component("Demo", Demo);
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/docs/.vuepress/config.ts:
--------------------------------------------------------------------------------
1 | import { viteBundler } from "@vuepress/bundler-vite";
2 | import { defaultTheme } from "@vuepress/theme-default";
3 | import path from "node:path";
4 | import { defineUserConfig } from "vuepress";
5 |
6 | export default defineUserConfig({
7 | base: "/vue3-gettext/",
8 | port: 8080,
9 | lang: "en-US",
10 | title: "Vue 3 Gettext",
11 | description: "Translate your Vue 3 applications with Gettext",
12 | bundler: viteBundler({
13 | viteOptions: {
14 | resolve: {
15 | alias: {
16 | vue: "vue/dist/vue.esm-bundler.js",
17 | "/@gettext/": path.resolve(__dirname, "../../src"),
18 | },
19 | },
20 | },
21 | }),
22 | locales: {
23 | "/": {
24 | lang: "en-US",
25 | },
26 | "/zh/": {
27 | lang: "zh-CN",
28 | title: "Vue 3 Gettext",
29 | description: "使用 Gettext 国际化你的 Vue3 应用",
30 | },
31 | },
32 | theme: defaultTheme({
33 | repo: "https://github.com/jshmrtn/vue3-gettext",
34 | navbar: [{ text: "npm", link: "https://npmjs.com/package/vue-haystack" }],
35 | locales: {
36 | "/": {
37 | selectLanguageName: "English",
38 | },
39 | "/zh/": {
40 | selectLanguageName: "简体中文",
41 | editLinkText: "在 GitHub 上编辑此页",
42 | lastUpdatedText: "上次更新",
43 | contributorsText: "贡献者",
44 | sidebar: [
45 | { link: "/zh/demo.md", text: "在线演示" },
46 | {
47 | text: "安装与设置",
48 | link: "/zh/setup.md",
49 | children: [
50 | { link: "/zh/setup.md", text: "安装步骤" },
51 | { link: "/zh/extraction.md", text: "自动抽取" },
52 | { link: "/zh/configuration.md", text: "插件配置" },
53 | ],
54 | },
55 | {
56 | text: "使用方法",
57 | link: "/zh/functions.md",
58 | children: [
59 | { link: "/zh/functions.md", text: "全局属性" },
60 | { link: "/zh/component.md", text: "组件(已废弃)" },
61 | { link: "/zh/directive.md", text: "指令(已废弃)" },
62 | ],
63 | },
64 | {
65 | text: "翻译文档说明",
66 | link: "/zh/translation.md",
67 | },
68 | ],
69 | },
70 | },
71 | sidebar: [
72 | { link: "/demo.md", text: "Demo" },
73 | {
74 | text: "Setup",
75 | link: "/setup.md",
76 | children: [
77 | { link: "/setup.md", text: "Installation" },
78 | { link: "/extraction.md", text: "Message extraction" },
79 | { link: "/configuration.md", text: "Configuration" },
80 | ],
81 | },
82 | {
83 | text: "Usage",
84 | link: "/functions.md",
85 | children: [
86 | { link: "/functions.md", text: "Functions" },
87 | { link: "/component.md", text: "Component" },
88 | { link: "/directive.md", text: "Directive" },
89 | ],
90 | },
91 | {
92 | text: "Translation",
93 | link: "/translation.md",
94 | },
95 | ],
96 | }),
97 | });
98 |
--------------------------------------------------------------------------------
/docs/.vuepress/styles/index.scss:
--------------------------------------------------------------------------------
1 | .translated {
2 | border-radius: 0.25rem;
3 | background-color: rgba(241, 245, 17, 0.1);
4 | border: 1px solid rgba(241, 245, 17, 0.5);
5 | }
6 |
7 | .warning {
8 | border-radius: 0.5rem;
9 | padding: 0.5rem;
10 | background-color: #fff3cd;
11 | border: 1px solid #ffecb5;
12 | color: #664d03;
13 | }
14 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | home: true
3 | heroText: Vue 3 Gettext
4 | tagline: Translate your Vue 3 applications with Gettext
5 | actions:
6 | - text: Demo
7 | link: /demo.html
8 | type: primary
9 | - text: Setup
10 | link: /setup.html
11 | type: secondary
12 | - text: Docs
13 | link: /setup.html
14 | type: secondary
15 | footer: MIT Licensed | Copyright © 2020-present JOSHMARTIN GmbH
16 | ---
17 |
18 | # Quick Start
19 |
20 | ```sh
21 | npm i vue3-gettext@next
22 | ```
23 |
24 | Set up gettext in your `main.ts`/`main.js`:
25 |
26 | ```javascript {main.ts/main.js}
27 | import { createGettext } from "vue3-gettext";
28 | import { createApp } from "vue";
29 | import translations from "./src/language/translations.json";
30 |
31 | const app = createApp(App);
32 | app.use(createGettext({ translations }));
33 | ```
34 |
35 | Use gettext functions in your application:
36 |
37 | ```jsx
38 | {{ $gettext("Translate me") }}
39 | ```
40 |
41 | Add scripts to your `package.json`:
42 |
43 | ```json { package.json }
44 | "scripts": {
45 | ...
46 | "gettext:extract": "vue-gettext-extract",
47 | "gettext:compile": "vue-gettext-compile",
48 | }
49 | ```
50 |
51 | `npm run gettext:extract` extracts translation keys from your code and creates `.po` files to translate.
52 |
53 | `npm run gettext:compile` compiles the translated messages from the `.po` files to a `.json` to be used in your application.
54 |
--------------------------------------------------------------------------------
/docs/component.md:
--------------------------------------------------------------------------------
1 | # ``
2 |
3 |
4 |
Deprecated
5 |
The <translate>
component and v-translate
directive have been deprecated, use the functions instead.
6 |
Since Vue 3, extracting messages from within components is awkward and error-prone as well as cause issues with server-side rendering.
7 | To make the transition easier, they will keep working until a future major release.
8 |
9 |
10 | ## Usage
11 |
12 |
13 | ```vue
14 | Hello
15 | ```
16 |
17 |
18 | `` renders a `` by default, use `tag` to override.
19 |
20 |
21 | ```vue
22 | Hello
23 | ```
24 |
25 |
26 | ### Parameters
27 |
28 | If the parameter should change dynamically, use the `:translate-params` prop (or use the `v-translate` directive).
29 |
30 |
31 | ```vue
32 | Hello %{ name }!
33 | ```
34 |
35 |
36 | ### Pluralization
37 |
38 |
39 | ```vue
40 |
45 | %{ amount } car
46 |
47 | ```
48 |
49 |
50 | ## Props
51 |
52 | | Prop | Description | Type | Default |
53 | | ----------------- | --------------------------------------------------- | ------ | ------- |
54 | | tag | HTML tag that contains the message | string | span |
55 | | translate-n | Determines what plural form to apply to the message | number | null |
56 | | translate-plural | Pluralized message | string | null |
57 | | translate-context | Gettext translation context | string | null |
58 | | translate-params | Parameters to interpolate messages | Object | null |
59 | | translate-comment | Comment for the message id | string | null |
60 |
--------------------------------------------------------------------------------
/docs/configuration.md:
--------------------------------------------------------------------------------
1 | # Configuration
2 |
3 | Once you have extracted your messages and compiled a `.json` file, you are ready to set up the gettext plugin in your `main.ts`/`main.js`:
4 |
5 | ```ts
6 | import { createGettext } from "vue3-gettext";
7 | import translations from "./language/translations.json";
8 |
9 | const gettext = createGettext({
10 | availableLanguages: {
11 | en: "English",
12 | de: "Deutsch",
13 | },
14 | defaultLanguage: "en",
15 | translations: translations,
16 | });
17 |
18 | const app = createApp(App);
19 | app.use(gettext);
20 | ```
21 |
22 | All the available options can be found in the `GetTextOptions` type, these are the default values:
23 |
24 | ```ts
25 | {
26 | availableLanguages: { en: "English" },
27 | defaultLanguage: "en",
28 | mutedLanguages: [],
29 | silent: false,
30 | translations: {},
31 | setGlobalProperties: true,
32 | globalProperties: { // custom global properties name
33 | language: ['$language'], // the plugin instance
34 | gettext: ['$gettext'], // ['$gettext', '__']
35 | pgettext: ['$pgettext'], // ['$pgettext', '_n']
36 | ngettext: ['$ngettext'], // ['$ngettext','_x']
37 | npgettext: ['$npgettext'], // ['$npgettext', '_nx']
38 | interpolate: ['$gettextInterpolate'],// deprecated
39 | },
40 | provideDirective: true,
41 | provideComponent: true,
42 | }
43 | ```
44 |
45 | ## Gotchas
46 |
47 | ### Using gettext functions outside of components
48 |
49 | If you need to have plain typescript/javascript files that must access gettext, you may simple move and export gettext from a separate file:
50 |
51 | `gettext.ts`
52 |
53 | ```ts
54 | export default createGettext({
55 | ...
56 | });
57 | ```
58 |
59 | Then import and use the functions:
60 |
61 | ```ts
62 | import gettext from "./gettext";
63 |
64 | const { $gettext } = gettext;
65 |
66 | const myTest = $gettext("My translation message");
67 | ```
68 |
--------------------------------------------------------------------------------
/docs/demo.md:
--------------------------------------------------------------------------------
1 | # Demo
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/docs/directive.md:
--------------------------------------------------------------------------------
1 | # `v-translate`
2 |
3 |
4 |
Deprecated
5 |
The <translate>
component and v-translate
directive have been deprecated, use the functions instead.
6 |
Since Vue 3, extracting messages from within components is awkward and error-prone as well as cause issues with server-side rendering.
7 | To make the transition easier, they will keep working until a future major release.
8 |
9 |
10 | ## Basic usage
11 |
12 | ```vue
13 | Hello
14 | ```
15 |
16 | ### Parameters
17 |
18 | Variables for message interpolation must be passed as value to the `v-translate` directive directly.
19 |
20 |
21 | ```vue
22 | Hello %{ name }!
23 | ```
24 |
25 |
26 | ### Pluralization
27 |
28 |
29 | ```vue
30 |
35 | %{ amount } car
36 |
37 | ```
38 |
39 |
40 | ## Attributes
41 |
42 | | Prop | Description | Type | Default |
43 | | ----------------- | -------------------------------------------------------------------------------------------- | ------- | ------- |
44 | | v-translate | **Required**. You can optionally provide an object with parameters for message interpolation | object | null |
45 | | translate-n | Determines what plural form to apply to the message | number | null |
46 | | translate-plural | Pluralized message | string | null |
47 | | translate-context | Gettext translation context | string | null |
48 | | translate-comment | Comment for the message id | string | null |
49 | | render-html | Will disable HTML escaping and render it directly | boolean | false |
50 |
--------------------------------------------------------------------------------
/docs/extraction.md:
--------------------------------------------------------------------------------
1 | # Message extraction
2 |
3 | To extract all the messages that you want translated from your application code, a bit of setup is required.
4 |
5 | ## Scripts
6 |
7 | First, add scripts to your `package.json`:
8 |
9 | ```json { package.json }
10 | "scripts": {
11 | ...
12 | "gettext:extract": "vue-gettext-extract",
13 | "gettext:compile": "vue-gettext-compile",
14 | }
15 | ```
16 |
17 | `npm run gettext:extract` extracts messages from your code and creates `.po` files.
18 |
19 | `npm run gettext:compile` compiles the translated messages from the `.po` files to a `.json` to be used in your application.
20 |
21 | Using these scripts is _theoretically_ optional if you have other means of extraction or may even want to write message files yourself.
22 |
23 | ## Configuration
24 |
25 | Before running the scripts, create a file `gettext.config.js` in your application root. This is a configuration _only_ for the scripts above. A minimal configuration may look like this:
26 |
27 | ```js
28 | module.exports = {
29 | output: {
30 | locales: ["en", "de"],
31 | },
32 | };
33 | ```
34 |
35 | You can also use a `gettext.config.mjs` file with the Ecmascript module format:
36 |
37 | ```js
38 | export default {
39 | output: {
40 | locales: ["en", "de"],
41 | },
42 | }
43 | ```
44 |
45 | Here are all the available configuration options and their defaults:
46 |
47 | ```js
48 | module.exports = {
49 | input: {
50 | path: "./src", // only files in this directory are considered for extraction
51 | include: ["**/*.js", "**/*.ts", "**/*.vue"], // glob patterns to select files for extraction
52 | exclude: [], // glob patterns to exclude files from extraction
53 | jsExtractorOpts:[ // custom extractor keyword. default empty.
54 | {
55 | keyword: "__", // only extractor default keyword such as $gettext,use keyword to custom
56 | options: { // see https://github.com/lukasgeiter/gettext-extractor
57 | content: {
58 | replaceNewLines: "\n",
59 | },
60 | arguments: {
61 | text: 0,
62 | },
63 | },
64 | },
65 | {
66 | keyword: "_n", // $ngettext
67 | options: {
68 | content: {
69 | replaceNewLines: "\n",
70 | },
71 | arguments: {
72 | text: 0,
73 | textPlural: 1,
74 | },
75 | },
76 | },
77 | ],
78 | compileTemplate: false, // do not compile tag when its lang is not html
79 | },
80 | output: {
81 | path: "./src/language", // output path of all created files
82 | potPath: "./messages.pot", // relative to output.path, so by default "./src/language/messages.pot"
83 | jsonPath: "./translations.json", // relative to output.path, so by default "./src/language/translations.json"
84 | locales: ["en"],
85 | flat: false, // don't create subdirectories for locales
86 | linguas: true, // create a LINGUAS file
87 | splitJson: false, // create separate json files for each locale. If used, jsonPath must end with a directory, not a file
88 | },
89 | };
90 | ```
91 |
92 | ## Gotchas
93 | When first extract, it will call `msginit` to create a `.po` file,
94 | this command will set the `Plural-Forms` header, if the locale is in
95 | [the embedded table](https://github.com/dd32/gettext/blob/master/gettext-tools/src/plural-table.c#L27)
96 | of msginit.
97 |
98 | Otherwise, as an experimental feature,
99 | you can instruct msginit to use the information from Unicode CLDR,
100 | by setting the `GETTEXTCLDRDIR` environment variable.
101 | The program will look for a file named
102 | `common/supplemental/plurals.xml` under that directory.
103 | You can get the CLDR data from [http://cldr.unicode.org/](http://cldr.unicode.org/).
104 | Or only download the [plurals.xml](https://raw.githubusercontent.com/unicode-org/cldr/main/common/supplemental/plurals.xml) file.
105 |
--------------------------------------------------------------------------------
/docs/functions.md:
--------------------------------------------------------------------------------
1 | # Functions
2 |
3 | Translation functions are made available globally in `` sections but can also be used in scripts.
4 |
5 | ## Functions
6 |
7 | ### `$gettext`
8 |
9 | Simple message translation
10 |
11 | ```ts
12 |
13 | {{ $gettext("My message") }}
14 |
15 | ```
16 |
17 | ```ts
18 | const { $gettext } = useGettext();
19 |
20 | $gettext("My message");
21 | ```
22 |
23 | #### Interpolation
24 |
25 | ```ts
26 |
27 | {{ $gettext("My message for %{ name }", { name: "Rudi" }) }}
28 |
29 | ```
30 |
31 | ### `$pgettext`
32 |
33 | Takes a translation context as the first argument
34 |
35 | ```ts
36 |
37 | {{ $pgettext("my_context", "My message") }}
38 |
39 | ```
40 |
41 | ```ts
42 | const { $pgettext } = useGettext();
43 |
44 | $pgettext("my_context", "My message");
45 | ```
46 |
47 | #### Interpolation
48 |
49 | ```ts
50 |
51 | {{ $pgettext("my_context", "My message for %{ name }", { name: "Rudi" }) }}
52 |
53 | ```
54 |
55 | ### `$ngettext`
56 |
57 | Can be used to pluralize messages
58 |
59 | ```vue
60 |
61 | {{ $ngettext("apple", "apples", 5) }}
62 |
63 | ```
64 |
65 | ```ts
66 | const { $ngettext } = useGettext();
67 |
68 | $ngettext("apple", "apples", 5);
69 | ```
70 |
71 | #### Interpolation
72 |
73 | ```ts
74 |
75 | {{ $ngettext("apple for %{ name }", "apples for %{ name }", 5, { name: "Rudi" }) }}
76 |
77 | ```
78 |
79 | ### `$npgettext`
80 |
81 | Can be used to pluralize messages with a translation context
82 |
83 | ```vue
84 |
85 | {{ $npgettext("my_context", "apple", "apples", 5) }}
86 |
87 | ```
88 |
89 | ```ts
90 | const { $npgettext } = useGettext();
91 |
92 | $npgettext("my_context", "apple", "apples", 5);
93 | ```
94 |
95 | #### Interpolation
96 |
97 | ```ts
98 |
99 | {{ $npgettext("my_context", "apple for %{ name }", "apples for %{ name }", 5, { name: "Rudi" }) }}
100 |
101 | ```
102 |
103 | ### `$language`
104 |
105 | Provides access to the whole plugin, for example if you want to access the current language:
106 |
107 | ```vue
108 |
109 | {{ $language.current }}
110 |
111 | ```
112 |
113 | ```ts
114 | const gettext = useGettext();
115 |
116 | console.log(gettext.current);
117 | ```
118 |
119 | ### `$gettextInterpolate`
120 |
121 |
122 | This helper function has been made redundant since the translate functions now directly take interpolation parameters.
123 |
124 |
125 | This is a helper function if you use parameters within your messages.
126 |
127 | ```vue
128 |
129 | {{ $gettextInterpolate($pgettext("My message for %{ name }"), { name: "Rudi" }) }}
130 |
131 | ```
132 |
133 | ```ts
134 | const { interpolate } = useGettext();
135 |
136 | interpolate($gettext("My message for %{ name }"), { name: "Rudi" });
137 | ```
138 |
139 | ### custom translate function name
140 | You can custom the translate function name by pass the `globalProperties` option to `createGettext`, see [Configuration](./configuration.md).
141 |
142 | For example, if you want to use the WordPress style:
143 | ```ts
144 | import { createGettext } from "vue3-gettext";
145 |
146 | const gettext = createGettext({
147 | ...
148 | globalProperties: {
149 | gettext: ['$gettext', '__'], // both support `$gettext` and `__`
150 | ngettext: ['$ngettext', '_n'],
151 | pgettext: ['$pgettext', '_x'],
152 | npgettext: ['$npgettext', '_nx'],
153 | }
154 | })
155 |
156 | ```
157 |
158 | If you got a VSCode warning `Property '{0}' does not exist on type '{1}'. ts(2339)`, consider add a `gettext.d.ts` file like this:
159 | ```ts
160 | export { };
161 | declare module '@vue/runtime-core' {
162 | interface ComponentCustomProperties {
163 | __: (msgid: string, parameters?: {
164 | [key: string]: string;
165 | }, disableHtmlEscaping?: boolean) => string;
166 | _x: (context: string, msgid: string, parameters?: {
167 | [key: string]: string;
168 | }, disableHtmlEscaping?: boolean) => string;
169 | _n: (msgid: string, plural: string, n: number, parameters?: {
170 | [key: string]: string;
171 | }, disableHtmlEscaping?: boolean) => string;
172 | _xn: (context: string, msgid: string, plural: string, n: number, parameters?: {
173 | [key: string]: string;
174 | }, disableHtmlEscaping?: boolean) => string;
175 | }
176 | }
177 | ```
178 |
179 | ## Html escaping
180 |
181 | All the translation functions escape html by default and take a `disableHtmlEscaping` as their last parameter. If your translation messages or parameters contain html tags, you will have to set this to `true` and render the message using [`v-html`](https://vuejs.org/api/built-in-directives.html#v-html):
182 |
183 | ```vue
184 |
185 |
186 |
187 | ```
188 |
189 | Be careful when using [`v-html`](https://vuejs.org/api/built-in-directives.html#v-html). Don't use it for user-provided content so you're not vulnerable for XSS attacks.
190 |
--------------------------------------------------------------------------------
/docs/setup.md:
--------------------------------------------------------------------------------
1 | # Setup
2 |
3 | ## Prerequisites
4 |
5 | Vue 3 Gettext provides scripts to automatically extract translation messages into gettext [PO files](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html) and, after translation, merge those into a JSON file that can be used in your application. You must install the GNU gettext utilities for those scripts to work:
6 |
7 | **Ubuntu/Linux:**
8 |
9 | ```sh
10 | sudo apt-get update
11 | sudo apt-get install gettext
12 | ```
13 |
14 | **macOS:**
15 |
16 | ```sh
17 | brew install gettext
18 | brew link --force gettext
19 | ```
20 |
21 | **Windows:**
22 |
23 | On Windows, you have multiple options. You can run the scripts and install gettext under WSL2 like you would with regular Ubuntu (recommended) or install gettext via mingw64 or cygwin. You may also find precompiled binaries [here](https://mlocati.github.io/articles/gettext-iconv-windows.html).
24 |
25 | ## Installation
26 |
27 | Install Vue 3 Gettext using `npm` or `yarn`:
28 |
29 | ```sh
30 | npm i vue3-gettext@next
31 | ```
32 |
--------------------------------------------------------------------------------
/docs/translation.md:
--------------------------------------------------------------------------------
1 | # Instructions of translated document
2 | ## Disclaimer
3 | Translated documents may not be maintained and are for reference only, please refer to the latest [English documents](./README.md).
4 |
5 | ## Help translate
6 | You can help translate the project's documentation by submitting a PR.
7 | Please refer to VuePress' documentation [Internationalization](https://vuepress.vuejs.org/guide/i18n.html),
8 | and modify the files in the [docs directory]((https://github.com/jshmrtn/vue3-gettext/tree/master/docs)) of this project to help translate.
9 |
--------------------------------------------------------------------------------
/docs/zh/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | home: true
3 | heroText: Vue 3 Gettext
4 | tagline: 使用 Gettext 国际化你的 Vue3 应用程序
5 | actions:
6 | - text: 在线演示
7 | link: /zh/demo.html
8 | type: primary
9 | - text: 安装与设置
10 | link: /zh/setup.html
11 | type: secondary
12 | - text: 文档
13 | link: /zh/setup.html
14 | type: secondary
15 | footer: MIT Licensed | Copyright © 2020-present JOSHMARTIN GmbH
16 | ---
17 |
18 | # 快速开始
19 | ## 1. 安装
20 |
21 | ```sh
22 | npm i vue3-gettext@next
23 | ```
24 | ## 2. 引入
25 | 在 `main.ts`/`main.js` 中设置 gettext:
26 |
27 | ```javascript {3,11}
28 | import { createGettext } from "vue3-gettext";
29 | import { createApp } from "vue";
30 | import translations from "./src/language/translations.json";
31 |
32 | const app = createApp(App);
33 | app.use(createGettext({
34 | availableLanguages: {
35 | en: "Engilish",
36 | zh: "简体中文",
37 | },
38 | defaultLanguage: 'zh',
39 | translations,
40 | }));
41 | ```
42 | 高亮的行暂时会报错,后续抽取编译后会修复它。
43 | 更多配置选项请查看 [配置](./configuration.md) 。
44 |
45 | ## 3. 使用
46 | 在应用程序中使用 gettext:
47 |
48 | ```jsx
49 | {{ $gettext("Translate me") }}
50 | ```
51 |
52 | ## 4. 自动抽取翻译与编译
53 | > 详见 [自动抽取](./extraction.md) 。
54 |
55 | **首先**,在 `package.json` 中添加 scripts:
56 |
57 | ```json { package.json }
58 | "scripts": {
59 | ...
60 | "gettext:extract": "vue-gettext-extract",
61 | "gettext:compile": "vue-gettext-compile",
62 | }
63 | ```
64 |
65 | **然后**,在根目录新建 `gettext.config.js` 配置文件。
66 | ```js
67 | module.exports = {
68 | input: {
69 | path: './src'
70 | },
71 | output: {
72 | locales: ["en", "zh"],
73 | },
74 | };
75 | ```
76 | 更多配置项(比如自定义抽取关键字)请查看 [自动抽取](./extraction.md) 。
77 |
78 | **最后**,运行如下指令自动抽取与编译。
79 |
80 | `npm run gettext:extract` 自动从代码中抽取待翻译字符串并生成 `.po` 文件。
81 |
82 | `npm run gettext:compile` 将翻译好的 `.po` 文件编译为 `.json` 文件以便在 `createGettext` 时引用。
83 |
--------------------------------------------------------------------------------
/docs/zh/component.md:
--------------------------------------------------------------------------------
1 | # ``
2 |
3 |
4 |
已过时弃用
5 |
<translate>
组件和 v-translate
指令已被弃用,请使用 翻译函数 代替。
6 |
从 Vue 3 开始,从组件中提取消息变得笨拙且容易出错,并且会导致服务器端渲染出现问题。
7 | 为了迁移方便,这些功能将会保留到之后的大版本升级。
8 |
9 |
10 | ## 用法
11 |
12 |
13 | ```vue
14 | Hello
15 | ```
16 |
17 |
18 | `` 默认渲染为 `` 标签, 可以通过 `tag` 属性覆盖默认值。
19 |
20 |
21 | ```vue
22 | Hello
23 | ```
24 |
25 |
26 | ### 参数
27 | 如果参数是动态的,可以使用 `:translate-params` 属性(或使用 `v-translate` 指令)。
28 |
29 |
30 | ```vue
31 | Hello %{ name }!
32 | ```
33 |
34 |
35 | ### 复数
36 |
37 |
38 | ```vue
39 |
44 | %{ amount } car
45 |
46 | ```
47 |
48 |
49 | ## 属性
50 |
51 | | 属性 | 说明 | 类型 | 默认值 |
52 | | ----------------- | -------------------- | ------ | ---- |
53 | | tag | 包裹翻译内容的标签 | string | span |
54 | | translate-n | 用于选择使用哪种复数形式 | number | null |
55 | | translate-plural | 复数形式 | string | null |
56 | | translate-context | 上下文 | string | null |
57 | | translate-params | 需要插值的(格式化)参数 | Object | null |
58 | | translate-comment | 翻译的注释 | string | null |
59 |
--------------------------------------------------------------------------------
/docs/zh/configuration.md:
--------------------------------------------------------------------------------
1 | # 配置
2 | 将自动抽取的 po 文件翻译完成,并编译为 json 文件后,就可以在 `main.ts`/`main.js` 中配置 gettext 插件了。
3 |
4 |
5 | ```ts
6 | import { createGettext } from "vue3-gettext";
7 | import translations from "./language/translations.json";
8 |
9 | const gettext = createGettext({
10 | availableLanguages: {
11 | en: "English",
12 | de: "Deutsch",
13 | },
14 | defaultLanguage: "en",
15 | translations: translations,
16 | });
17 |
18 | const app = createApp(App);
19 | app.use(gettext);
20 | ```
21 |
22 | 具体的选项配置可查看 `GetTextOptions` 这个类型,下面是各配置的默认值:
23 |
24 | ```ts
25 | {
26 | // 支持的语言
27 | availableLanguages: { en: "English" },
28 | // 默认语言
29 | defaultLanguage: "en",
30 | // 不打印警告信息的语言
31 | mutedLanguages: [],
32 | // 默认会打印警告信息
33 | silent: false,
34 | // 翻译后的 json 资源
35 | translations: {},
36 | // 默认会在将 $gettext 等函数注册到全局
37 | setGlobalProperties: true,
38 | // 注册这些属性到全局
39 | globalProperties: {
40 | language: ['$language'], // gettext 实例
41 | gettext: ['$gettext'], // 改成 ['$gettext', '__'] 这样支持同时使用 $gettext, __ 两种方式
42 | ngettext: ['$ngettext'],// ['$ngettext','_n'] 同理支持 $ngettext 与 _n 两种方式
43 | pgettext: ['$pgettext'],// ['$pgettext', '_x'] 这些 _x, _nx 是 WordPress 风格
44 | npgettext: ['$npgettext'],// ['$npgettext', '_nx'] 带上下文的复数翻译
45 | interpolate: ['$gettextInterpolate'], // 已废弃: gettext 内部的插值函数
46 | },
47 | provideDirective: true,
48 | provideComponent: true,
49 | }
50 | ```
51 |
52 | ## 踩坑陷阱
53 |
54 | ### 在组件外使用 gettext 函数
55 | 如果你需要在纯 typescript/javascript 文件中使用 gettext,
56 | 你可以将上述配置代码移到单独一个文件中配置并导出 gettext.
57 |
58 |
59 | `gettext.ts`:
60 | ```ts
61 | export default createGettext({
62 | ...
63 | });
64 | ```
65 |
66 | 然后在需要的地方导入并使用。
67 |
68 | `main.ts`:
69 | ```ts
70 | import gettext from "./gettext";
71 | ...
72 | const app = createApp(App);
73 |
74 | app.use(gettext);
75 | ```
76 |
77 | `other.ts`:
78 | ```ts
79 | import gettext from "./gettext";
80 |
81 | const { $gettext } = gettext;
82 |
83 | const myTest = $gettext("My translation message");
84 | ```
85 |
--------------------------------------------------------------------------------
/docs/zh/demo.md:
--------------------------------------------------------------------------------
1 | # 在线演示
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/docs/zh/directive.md:
--------------------------------------------------------------------------------
1 | # `v-translate`
2 |
3 |
4 |
已过时弃用
5 |
<translate>
组件和 v-translate
指令已被弃用,请使用 翻译函数 代替。
6 |
从 Vue 3 开始,从组件中提取消息变得笨拙且容易出错,并且会导致服务器端渲染出现问题。
7 | 为了迁移方便,这些功能将会保留到之后的大版本升级。
8 |
9 |
10 | ## 基本用法
11 |
12 | ```vue
13 | Hello
14 | ```
15 |
16 | ### 参数
17 | 用于插值的变量必须作为值直接传递给 v-translate 指令。
18 |
19 |
20 | ```vue
21 | Hello %{ name }!
22 | ```
23 |
24 |
25 | ### 复数
26 |
27 |
28 | ```vue
29 |
34 | %{ amount } car
35 |
36 | ```
37 |
38 |
39 | ## 属性
40 |
41 | | Prop | 描述 | 类型 | 默认值 |
42 | | ----------------- | -------------------------------- | ------- | ------- |
43 | | v-translate | **必须**. 值不为空时表示翻译内容的参数 | object | null |
44 | | translate-n | 用于判断使用哪种复数形式 | number | null |
45 | | translate-plural | 复数形式 | string | null |
46 | | translate-context | 上下文 | string | null |
47 | | translate-comment | 翻译的注释 | string | null |
48 | | render-html | 不转义直接输出 html | boolean | false |
49 |
--------------------------------------------------------------------------------
/docs/zh/extraction.md:
--------------------------------------------------------------------------------
1 | # 自动抽取
2 |
3 | 要使用自动抽取功能,你需要做一些如下准备工作。
4 |
5 | ## 脚本
6 | 首先,在 `package.json` 中添加 scripts 指令:
7 |
8 | ```json { package.json }
9 | "scripts": {
10 | ...
11 | "gettext:extract": "vue-gettext-extract",
12 | "gettext:compile": "vue-gettext-compile",
13 | }
14 | ```
15 |
16 | `npm run gettext:extract` 从代码中自动抽取待翻译字符串并创建为 `.po` 文件。
17 |
18 | `npm run gettext:compile` 把翻译好的 `.po` 文件编译为 `.json` 文件以便在程序中引用。
19 |
20 | 这些指令 _理论上_ 是可选的,因为如果你有其他方式可以达到同样的目的的话也未尝不可,甚至也可以手写 po 文件。
21 |
22 |
23 | ## 配置
24 | 运行这些脚本之前,你需要在根目录新建一个名为 `gettext.config.js` 的配置文件。
25 | 该文件 _只_ 会被上述脚本读取。最简示例如下所示:
26 |
27 |
28 | ```js
29 | module.exports = {
30 | output: {
31 | locales: ["en", "de"],
32 | },
33 | };
34 | ```
35 |
36 | 您也可以使用带有 Ecmascript 模块格式的 `gettext.config.mjs` 文件:
37 |
38 | ```js
39 | export default {
40 | output: {
41 | locales: ["en", "de"],
42 | },
43 | }
44 | ```
45 |
46 | 下面列出所有配置选项和默认值:
47 |
48 | ```js
49 | module.exports = {
50 | input: {
51 | path: "./src", // 只有该目录下的文件会被自动抽取
52 | include: ["**/*.js", "**/*.ts", "**/*.vue"], // 需要抽取的文件
53 | exclude: [], // 需要排除的文件
54 | jsExtractorOpts:[ // 自定义抽取关键字
55 | {
56 | keyword: "__", // 默认只抽取 $gettext 这些内置的关键字,可以通过 keyword 自定义
57 | options: { // 详细说明可查看 https://github.com/lukasgeiter/gettext-extractor
58 | content: {
59 | replaceNewLines: "\n",
60 | },
61 | arguments: {
62 | text: 0, // 这个 __ 函数的第几个参数是待翻译字符串
63 | },
64 | },
65 | },
66 | {
67 | keyword: "_n", // 对应 $ngettext
68 | options: {
69 | content: {
70 | replaceNewLines: "\n",
71 | },
72 | arguments: {
73 | text: 0, // 第0个参数是待翻译字符串
74 | textPlural: 1,// 第1个参数是复数字符串
75 | },
76 | },
77 | },
78 | ],
79 | },
80 | output: {
81 | path: "./src/language", // 抽取后生成的文件存放的路径
82 | potPath: "./messages.pot", // 相对于 output.path, 所以默认值是 "./src/language/messages.pot"
83 | jsonPath: "./translations.json", // 相对于 output.path, 所以默认值是 "./src/language/translations.json"
84 | locales: ["en"], // 需要生成哪些语言的 po 文件
85 | flat: false, // 是否为每种语言单独创建一个文件夹
86 | linguas: true, // 创建一个 LINGUAS 文件
87 | splitJson: false, // 为每种语言生成一个 json 文件,如果为 true, jsonPath 应当是一个目录路径而不是一个文件路径
88 | },
89 | };
90 | ```
91 |
92 | ## 额外说明
93 | 第一次抽取时,会调用 [`msginit`](https://www.gnu.org/software/gettext/manual/html_node/msginit-Invocation.html)
94 | 新建 `.po` 文件,该命令会自动设置 po 文件中的 `Plural-Forms` 字段。该字段表示翻译语言的复数形式。
95 |
96 | 但如果翻译语言不在该命令的
97 | [内置表](https://github.com/dd32/gettext/blob/master/gettext-tools/src/plural-table.c#L27) 中,
98 | 需要使用 [CLDR](http://cldr.unicode.org/) 数据才能判断语言的复数形式。
99 |
100 | 因此你需要设置环境变量 `GETTEXTCLDRDIR` 指向 CLDR 数据所在的目录,
101 | 并保证目录中存在 `common/supplemental/plurals.xml` 文件。
102 | 不过,你无需下载完整的 CLDR 数据,只需要下载 [plurals.xml](https://raw.githubusercontent.com/unicode-org/cldr/main/common/supplemental/plurals.xml) 即可。
103 |
--------------------------------------------------------------------------------
/docs/zh/functions.md:
--------------------------------------------------------------------------------
1 | # 全局属性
2 | 在 `` 模板中可以使用翻译函数,当然在 scripts 代码中也可以。
3 |
4 | ## 翻译函数
5 |
6 | ### `$gettext`
7 | 普通翻译函数
8 |
9 | ```vue
10 |
11 | {{ $gettext("My message") }}
12 |
13 | ```
14 |
15 | ```ts
16 | const { $gettext } = useGettext();
17 |
18 | $gettext("My message");
19 | ```
20 |
21 | #### 参数插值
22 |
23 | ```vue
24 |
25 | {{ $gettext("My message for %{ name }", { name: "Rudi" }) }}
26 |
27 | ```
28 |
29 | ### `$pgettext`
30 | 上下文翻译,第一个参数是上下文,第二个参数是待翻译内容。
31 |
32 | ```vue
33 |
34 | {{ $pgettext("my_context", "My message") }}
35 |
36 | ```
37 |
38 | ```ts
39 | const { $pgettext } = useGettext();
40 |
41 | $pgettext("my_context", "My message");
42 | ```
43 |
44 | #### 参数插值
45 |
46 | ```vue
47 |
48 | {{ $pgettext("my_context", "My message for %{ name }", { name: "Rudi" }) }}
49 |
50 | ```
51 |
52 | ### `$ngettext`
53 | 用于支持复数翻译。
54 |
55 | ```vue
56 |
57 | {{ $ngettext("apple", "apples", 5) }}
58 |
59 | ```
60 |
61 | ```ts
62 | const { $ngettext } = useGettext();
63 |
64 | $ngettext("apple", "apples", 5);
65 | ```
66 |
67 | #### 参数插值
68 |
69 | ```vue
70 |
71 | {{ $ngettext("apple for %{ name }", "apples for %{ name }", 5, { name: "Rudi" }) }}
72 |
73 | ```
74 |
75 | ### `$npgettext`
76 | 带上下文的复数翻译。
77 |
78 | ```vue
79 |
80 | {{ $npgettext("my_context", "apple", "apples", 5) }}
81 |
82 | ```
83 |
84 | ```ts
85 | const { $npgettext } = useGettext();
86 |
87 | $npgettext("my_context", "apple", "apples", 5);
88 | ```
89 |
90 | #### 参数插值
91 |
92 | ```vue
93 |
94 | {{ $npgettext("my_context", "apple for %{ name }", "apples for %{ name }", 5, { name: "Rudi" }) }}
95 |
96 | ```
97 |
98 | ### `$language`
99 | 这个属性是插件实例,可以通过这个属性访问插件实例的属性/方法。比如可以拿到当前语言:
100 |
101 |
102 | ```vue
103 |
104 | {{ $language.current }}
105 |
106 | ```
107 |
108 | ```ts
109 | const gettext = useGettext();
110 |
111 | console.log(gettext.current);
112 | ```
113 |
114 | ### `$gettextInterpolate`
115 |
116 |
117 | 这个帮助函数已经不需要使用了,因为上面的翻译函数已经支持了参数插值。
118 |
119 |
120 | 这是一个用于支持参数插值(格式化参数)的帮助函数。
121 |
122 | ```vue
123 |
124 | {{ $gettextInterpolate($pgettext("My message for %{ name }"), { name: "Rudi" }) }}
125 |
126 | ```
127 |
128 | ```ts
129 | const { interpolate } = useGettext();
130 |
131 | interpolate($gettext("My message for %{ name }"), { name: "Rudi" });
132 | ```
133 |
134 | ### 自定义翻译函数名称
135 | 你可以在 `createGettext` 时通过 `globalProperties` 选项自定义翻译函数的名称,参见 [插件配置](./configuration.md)。
136 |
137 | 例如,要使用 WordPress 的翻译名称风格,可以作如下配置:
138 | ```ts
139 | import { createGettext } from "vue3-gettext";
140 |
141 | const gettext = createGettext({
142 | ...
143 | globalProperties: {
144 | gettext: ['$gettext', '__'], // 这样支持同时使用 $gettext, __ 两种方式
145 | ngettext: ['$ngettext', '_n'],
146 | pgettext: ['$pgettext', '_x'],
147 | npgettext: ['$npgettext', '_nx'],
148 | }
149 | })
150 |
151 | ```
152 | 如果 VSCode 警告 `Property '{0}' does not exist on type '{1}'. ts(2339)`(类型“xxx”上不存在“__”属性), 请考虑新建一个 `gettext.d.ts` 文件,内容如下:
153 | ```ts
154 | export { };
155 | declare module '@vue/runtime-core' {
156 | interface ComponentCustomProperties {
157 | __: (msgid: string, parameters?: {
158 | [key: string]: string;
159 | }, disableHtmlEscaping?: boolean) => string;
160 | _x: (context: string, msgid: string, parameters?: {
161 | [key: string]: string;
162 | }, disableHtmlEscaping?: boolean) => string;
163 | _n: (msgid: string, plural: string, n: number, parameters?: {
164 | [key: string]: string;
165 | }, disableHtmlEscaping?: boolean) => string;
166 | _xn: (context: string, msgid: string, plural: string, n: number, parameters?: {
167 | [key: string]: string;
168 | }, disableHtmlEscaping?: boolean) => string;
169 | }
170 | }
171 | ```
172 |
173 | ## Html 转义
174 | 默认地,所有的翻译函数都会进行 html 转义,不过也可以通过最后一个参数 `disableHtmlEscaping` 来控制是否转义。
175 | 如果确实需要包含 html 标签,你需要指定该参数为 true 以避免转义,并且将结果通过 [`v-html`](https://cn.vuejs.org/api/built-in-directives.html#v-html) 来显示:
176 |
177 | ```vue
178 |
179 |
180 |
181 | ```
182 | 请谨慎使用 [`v-html`](https://cn.vuejs.org/api/built-in-directives.html#v-html)。不要将用户输入的内容用于 v-html,否则可能引起 XSS 攻击。
183 |
--------------------------------------------------------------------------------
/docs/zh/setup.md:
--------------------------------------------------------------------------------
1 | # 安装步骤
2 |
3 | ## 先决条件
4 | Vue 3 Gettext 提供了脚本自动抽取待翻译字符串为符合 [gettext](https://www.gnu.org/software/gettext/manual/html_node/index.html) 规范的 [PO 文件](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html) 。
5 | 并且自动将翻译后的 po 文件 merge 到一个 json 文件中,以便在应用程序中引用。
6 | 因此你必须首先安装 GNU gettext 工具包,以便这些脚本可以正常工作。
7 |
8 | **Ubuntu/Linux:**
9 |
10 | ```sh
11 | sudo apt-get update
12 | sudo apt-get install gettext
13 | ```
14 |
15 | **macOS:**
16 |
17 | ```sh
18 | brew install gettext
19 | brew link --force gettext
20 | ```
21 |
22 | **Windows:**
23 |
24 | 在 Windows 系统中,安装 gettext 有多种方式。
25 | - 你可以使用 WSL2, 像在 Ubuntu 中安装 getext 一样直接安装(推荐方式);
26 | - 也可以通过 mingw64 或者 cygwin 安装;
27 | - 还可以直接 [下载](https://mlocati.github.io/articles/gettext-iconv-windows.html) 安装。
28 |
29 |
30 | ## 安装
31 |
32 | 使用 `npm` 或者 `yarn` 安装 Vue 3 Gettext:
33 |
34 | ```sh
35 | npm i vue3-gettext@next
36 | ```
37 |
--------------------------------------------------------------------------------
/docs/zh/translation.md:
--------------------------------------------------------------------------------
1 | # 翻译文档说明
2 |
3 | ## 免责声明
4 | 翻译文档可能不被维护,仅供参考,请以最新[英文文档](../README.md)为准。
5 |
6 | ## 帮助翻译
7 | 你可以通过提交 PR 来帮助翻译本项目的文档。
8 | 请参考 VuePress 的 [多语言支持](https://vuepress.vuejs.org/zh/guide/i18n.html) 文档,
9 | 修改本项目的 [docs 目录](https://github.com/jshmrtn/vue3-gettext/tree/master/docs) 中的文件来帮助翻译。
10 |
--------------------------------------------------------------------------------
/gettext.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | input: {
3 | path: "./dev",
4 | jsExtractorOpts: [
5 | {
6 | keyword: "__",
7 | options: {
8 | content: {
9 | replaceNewLines: "\n",
10 | },
11 | arguments: {
12 | text: 0,
13 | },
14 | },
15 | },
16 | {
17 | keyword: "_n",
18 | options: {
19 | content: {
20 | replaceNewLines: "\n",
21 | },
22 | arguments: {
23 | text: 0,
24 | textPlural: 1,
25 | },
26 | },
27 | },
28 | {
29 | keyword: "_x",
30 | options: {
31 | content: {
32 | replaceNewLines: "\n",
33 | },
34 | arguments: {
35 | context: 0,
36 | text: 1,
37 | },
38 | },
39 | },
40 | {
41 | keyword: "_xn",
42 | options: {
43 | content: {
44 | replaceNewLines: "\n",
45 | },
46 | arguments: {
47 | context: 0,
48 | text: 1,
49 | textPlural: 2,
50 | },
51 | },
52 | },
53 | ],
54 | },
55 | output: {
56 | path: "./dev/language",
57 | locales: ["en_GB", "fr_FR", "it_IT", "zh_CN"],
58 | splitJson: false,
59 | },
60 | };
61 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue3-gettext",
3 | "version": "3.0.0-beta.6",
4 | "description": "Translate Vue 3 applications with gettext",
5 | "homepage": "https://jshmrtn.github.io/vue3-gettext/",
6 | "author": "Leo Zurbriggen",
7 | "license": "MIT",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/jshmrtn/vue3-gettext"
11 | },
12 | "keywords": [
13 | "gettext",
14 | "vue",
15 | "vue3",
16 | "internationalization",
17 | "i18n",
18 | "translation",
19 | "l10n",
20 | "typescript"
21 | ],
22 | "publishConfig": {
23 | "registry": "https://registry.npmjs.org"
24 | },
25 | "bin": {
26 | "vue-gettext-compile": "./dist/gettext_compile.js",
27 | "vue-gettext-extract": "./dist/gettext_extract.js"
28 | },
29 | "scripts": {
30 | "docs": "vuepress dev docs",
31 | "docs:build": "vuepress build docs -d distDocs",
32 | "docs:extract": "node --loader ts-node/esm ./scripts/gettext_extract.ts",
33 | "docs:compile": "node --loader ts-node/esm ./scripts/gettext_compile.ts",
34 | "start": "vite serve dev",
35 | "build": "tsup",
36 | "test": "npm run test:types && npm run test:lint && npm run test:unit",
37 | "test:types": "tsc --noEmit",
38 | "test:lint": "npm run test:lint:js && npm run test:lint:prettier",
39 | "test:lint:js": "./node_modules/.bin/eslint --ext .js,.ts,.vue",
40 | "test:lint:prettier": "./node_modules/.bin/prettier --check '**/**.(json|js|ts|vue)'",
41 | "test:unit": "vitest run",
42 | "test:unit:watch": "vitest",
43 | "prepublishOnly": "npm run test && npm run build",
44 | "package:publish:next": "npm publish --tag next",
45 | "package:publish": "npm publish"
46 | },
47 | "engines": {
48 | "node": ">= 18.0.0"
49 | },
50 | "peerDependencies": {
51 | "@vue/compiler-sfc": ">=3.0.0",
52 | "vue": ">=3.0.0"
53 | },
54 | "dependencies": {
55 | "chalk": "^4.1.2",
56 | "command-line-args": "^5.2.1",
57 | "cosmiconfig": "^9.0.0",
58 | "gettext-extractor": "^3.8.0",
59 | "glob": "^10.4.1",
60 | "parse5": "^6.0.1",
61 | "parse5-htmlparser2-tree-adapter": "^6.0.1",
62 | "pofile": "^1.1.4"
63 | },
64 | "devDependencies": {
65 | "@swc/core": "^1.5.28",
66 | "@types/command-line-args": "^5.2.3",
67 | "@types/glob": "^8.1.0",
68 | "@types/node": "^20.14.0",
69 | "@types/parse5": "^6.0.3",
70 | "@types/parse5-htmlparser2-tree-adapter": "^6.0.1",
71 | "@typescript-eslint/parser": "^5.33.1",
72 | "@vitejs/plugin-vue": "^5.0.5",
73 | "@vue/test-utils": "^2.4.6",
74 | "@vuepress/bundler-vite": "^2.0.0-rc.12",
75 | "@vuepress/theme-default": "^2.0.0-rc.33",
76 | "date-fns": "^2.29.1",
77 | "eslint": "^8.22.0",
78 | "eslint-config-prettier": "^8.5.0",
79 | "eslint-plugin-vue": "^8.5.0",
80 | "happy-dom": "^6.0.4",
81 | "json-loader": "^0.5.7",
82 | "markdown-it-fence": "^0.1.3",
83 | "prettier": "^3.3.0",
84 | "ts-node": "^10.9.2",
85 | "tsup": "^8.1.0",
86 | "typescript": "^5.4.5",
87 | "vite": "^5.2.12",
88 | "vitest": "^1.6.0",
89 | "vue": "^3.4.27",
90 | "vuepress": "^2.0.0-rc.12"
91 | },
92 | "exports": {
93 | "require": "./dist/index.js",
94 | "import": "./dist/index.mjs",
95 | "types": "./dist/index.d.ts"
96 | },
97 | "main": "dist/index.js",
98 | "module": "dist/index.mjs",
99 | "types": "./dist/index.d.ts",
100 | "files": [
101 | "dist",
102 | "LICENSE",
103 | "README.md"
104 | ]
105 | }
106 |
--------------------------------------------------------------------------------
/scripts/attributeEmbeddedJsExtractor.ts:
--------------------------------------------------------------------------------
1 | import { Element, IHtmlExtractorFunction } from "gettext-extractor/dist/html/parser";
2 | import { JsParser } from "gettext-extractor/dist/js/parser";
3 | import { Validate } from "gettext-extractor/dist/utils/validate";
4 |
5 | export function attributeEmbeddedJsExtractor(selector: string, jsParser: JsParser): IHtmlExtractorFunction {
6 | Validate.required.nonEmptyString({ selector });
7 | Validate.required.argument({ jsParser });
8 |
9 | return (node: any, fileName: string, _, nodeLineNumberStart) => {
10 | if (typeof (node as Element).tagName !== "string") {
11 | return;
12 | }
13 |
14 | const element = node as Element;
15 | element.attrs.forEach((attr) => {
16 | let lineNumberStart = nodeLineNumberStart;
17 | const attributeLineNumber = element.sourceCodeLocation?.attrs[attr.name]?.startLine;
18 |
19 | if (attributeLineNumber) {
20 | lineNumberStart += attributeLineNumber - 1;
21 | }
22 |
23 | jsParser.parseString(attr.value, fileName, {
24 | lineNumberStart,
25 | });
26 | });
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/scripts/compile.ts:
--------------------------------------------------------------------------------
1 | // Based on https://github.com/Polyconseil/easygettext/blob/master/src/compile.js
2 |
3 | import Pofile from "pofile";
4 | import fsPromises from "fs/promises";
5 | import { LanguageData, MessageContext, Translations } from "../src/typeDefs";
6 |
7 | /**
8 | * Returns a sanitized po data dictionary where:
9 | * - no fuzzy or obsolete strings are returned
10 | * - no empty translations are returned
11 | *
12 | * @param poItems items from the PO catalog
13 | * @returns jsonData: sanitized PO data
14 | */
15 | export const sanitizePoData = (poItems: InstanceType[]) => {
16 | const messages: LanguageData = {};
17 |
18 | for (let item of poItems) {
19 | const ctx = item.msgctxt || "";
20 | if (item.msgstr[0] && item.msgstr[0].length > 0 && !item.flags.fuzzy && !(item as any).obsolete) {
21 | if (!messages[item.msgid]) {
22 | messages[item.msgid] = {};
23 | }
24 | // Add an array for plural, a single string for singular.
25 | (messages[item.msgid] as MessageContext)[ctx] = item.msgstr.length === 1 ? item.msgstr[0] : item.msgstr;
26 | }
27 | }
28 |
29 | // Strip context from messages that have no context.
30 | for (let key in messages) {
31 | if (Object.keys(messages[key]).length === 1 && (messages[key] as MessageContext)[""]) {
32 | messages[key] = (messages[key] as MessageContext)[""];
33 | }
34 | }
35 |
36 | return messages;
37 | };
38 |
39 | export const po2json = (poContent: string) => {
40 | const catalog = Pofile.parse(poContent);
41 | if (!catalog.headers.Language) {
42 | throw new Error("No Language headers found!");
43 | }
44 | return {
45 | headers: catalog.headers,
46 | messages: sanitizePoData(catalog.items),
47 | };
48 | };
49 |
50 | export const compilePoFiles = async (localesPaths: string[]) => {
51 | const translations: Translations = {};
52 |
53 | await Promise.all(
54 | localesPaths.map(async (lp) => {
55 | const fileContent = await fsPromises.readFile(lp, { encoding: "utf-8" });
56 | const data = po2json(fileContent);
57 | const lang = data.headers.Language;
58 | if (lang && !translations[lang]) {
59 | translations[lang] = data.messages;
60 | } else {
61 | Object.assign(translations[data.headers.Language!], data.messages);
62 | }
63 | }),
64 | );
65 |
66 | return translations;
67 | };
68 |
--------------------------------------------------------------------------------
/scripts/config.ts:
--------------------------------------------------------------------------------
1 | import { cosmiconfig } from "cosmiconfig";
2 | import path from "path";
3 | import { GettextConfig, GettextConfigOptions } from "../src/typeDefs";
4 |
5 | export const loadConfig = async (cliArgs?: { config?: string }): Promise => {
6 | const moduleName = "gettext";
7 | const explorer = cosmiconfig(moduleName);
8 |
9 | let configRes;
10 | if (cliArgs?.config) {
11 | configRes = await explorer.load(cliArgs.config);
12 | if (!configRes) {
13 | throw new Error(`Config not found: ${cliArgs.config}`);
14 | }
15 | } else {
16 | configRes = await explorer.search();
17 | }
18 |
19 | const config = configRes?.config as GettextConfigOptions;
20 |
21 | const languagePath = config.output?.path || "./src/language";
22 | const joinPath = (inputPath: string) => path.join(languagePath, inputPath);
23 | const joinPathIfRelative = (inputPath?: string) => {
24 | if (!inputPath) {
25 | return undefined;
26 | }
27 | return path.isAbsolute(inputPath) ? inputPath : path.join(languagePath, inputPath);
28 | };
29 | return {
30 | input: {
31 | path: config.input?.path || "./src",
32 | include: config.input?.include || ["**/*.js", "**/*.ts", "**/*.vue"],
33 | exclude: config.input?.exclude || [],
34 | jsExtractorOpts: config.input?.jsExtractorOpts,
35 | compileTemplate: config.input?.compileTemplate || false,
36 | },
37 | output: {
38 | path: languagePath,
39 | potPath: joinPathIfRelative(config.output?.potPath) || joinPath("./messages.pot"),
40 | jsonPath:
41 | joinPathIfRelative(config.output?.jsonPath) ||
42 | (config.output?.splitJson ? joinPath("./") : joinPath("./translations.json")),
43 | locales: config.output?.locales || ["en"],
44 | flat: config.output?.flat === undefined ? false : config.output.flat,
45 | linguas: config.output?.linguas === undefined ? true : config.output.linguas,
46 | splitJson: config.output?.splitJson === undefined ? false : config.output.splitJson,
47 | },
48 | };
49 | };
50 |
--------------------------------------------------------------------------------
/scripts/embeddedJsExtractor.ts:
--------------------------------------------------------------------------------
1 | import { Element, IHtmlExtractorFunction } from "gettext-extractor/dist/html/parser";
2 | import { ElementSelectorSet } from "gettext-extractor/dist/html/selector";
3 | import { JsParser } from "gettext-extractor/dist/js/parser";
4 | import { Validate } from "gettext-extractor/dist/utils/validate";
5 | import { ChildNode, TextNode } from "parse5";
6 | import { ScriptKind } from "typescript";
7 |
8 | type Template = Element & {
9 | tagName: "template";
10 | content: { nodeName: string; childNodes: ChildNode[] };
11 | };
12 |
13 | export function embeddedJsExtractor(selector: string, jsParser: JsParser): IHtmlExtractorFunction {
14 | Validate.required.nonEmptyString({ selector });
15 | Validate.required.argument({ jsParser });
16 |
17 | const selectors = new ElementSelectorSet(selector);
18 |
19 | return (node: any, fileName: string, _, lineNumberStart: number) => {
20 | if (typeof (node as Element).tagName !== "string") {
21 | return;
22 | }
23 |
24 | const element = node as Element | Template;
25 |
26 | if (selectors.anyMatch(element)) {
27 | const children = element.nodeName === "template" ? (element as Template).content.childNodes : element.childNodes;
28 |
29 | children.forEach((childNode) => {
30 | if (childNode.nodeName === "#text") {
31 | const currentNode = childNode as TextNode;
32 |
33 | jsParser.parseString(currentNode.value, fileName, {
34 | scriptKind: ScriptKind.Deferred,
35 | lineNumberStart: currentNode.sourceCodeLocation?.startLine
36 | ? currentNode.sourceCodeLocation?.startLine + lineNumberStart - 1
37 | : lineNumberStart,
38 | });
39 | }
40 | });
41 | }
42 | };
43 | }
44 |
--------------------------------------------------------------------------------
/scripts/extract.ts:
--------------------------------------------------------------------------------
1 | import { parse, compileTemplate } from "@vue/compiler-sfc";
2 | import chalk from "chalk";
3 | import fs from "fs";
4 | import { GettextExtractor, HtmlExtractors, JsExtractors } from "gettext-extractor";
5 | import { IJsExtractorFunction } from "gettext-extractor/dist/js/parser";
6 | import { GettextConfigOptions } from "../src/typeDefs";
7 | import { attributeEmbeddedJsExtractor } from "./attributeEmbeddedJsExtractor";
8 | import { embeddedJsExtractor } from "./embeddedJsExtractor";
9 |
10 | const extractFromFiles = async (filePaths: string[], potPath: string, config: GettextConfigOptions) => {
11 | const extr = new GettextExtractor();
12 |
13 | // custom keywords
14 | const emptyExtractors = new Array();
15 | const extractors =
16 | config?.input?.jsExtractorOpts?.reduce((acc, item, index, array) => {
17 | console.log(`custom keyword: ${chalk.blueBright(item.keyword)}`);
18 | acc.push(JsExtractors.callExpression([item.keyword, `[this].${item.keyword}`], item.options));
19 | return acc;
20 | }, emptyExtractors) || emptyExtractors;
21 |
22 | const jsParser = extr.createJsParser([
23 | JsExtractors.callExpression(["$gettext", "[this].$gettext"], {
24 | content: {
25 | replaceNewLines: "\n",
26 | },
27 | arguments: {
28 | text: 0,
29 | },
30 | }),
31 | JsExtractors.callExpression(["$ngettext", "[this].$ngettext"], {
32 | content: {
33 | replaceNewLines: "\n",
34 | },
35 | arguments: {
36 | text: 0,
37 | textPlural: 1,
38 | },
39 | }),
40 | JsExtractors.callExpression(["$pgettext", "[this].$pgettext"], {
41 | content: {
42 | replaceNewLines: "\n",
43 | },
44 | arguments: {
45 | context: 0,
46 | text: 1,
47 | },
48 | }),
49 | JsExtractors.callExpression(["$npgettext", "[this].$npgettext"], {
50 | content: {
51 | replaceNewLines: "\n",
52 | },
53 | arguments: {
54 | context: 0,
55 | text: 1,
56 | textPlural: 2,
57 | },
58 | }),
59 | ...extractors,
60 | ]);
61 |
62 | const htmlParser = extr.createHtmlParser([
63 | HtmlExtractors.elementContent("translate, [v-translate]", {
64 | content: {
65 | trimWhiteSpace: true,
66 | // TODO: figure out newlines for component
67 | replaceNewLines: " ",
68 | },
69 | attributes: {
70 | textPlural: "translate-plural",
71 | context: "translate-context",
72 | comment: "translate-comment",
73 | },
74 | }),
75 | attributeEmbeddedJsExtractor("[*=*]", jsParser),
76 | embeddedJsExtractor("*", jsParser),
77 | ]);
78 |
79 | await Promise.all(
80 | filePaths.map(async (fp) => {
81 | const buffer: string = await new Promise((res, rej) =>
82 | fs.readFile(fp, "utf-8", (err, data) => {
83 | if (err) {
84 | rej(err);
85 | }
86 | res(data);
87 | }),
88 | );
89 | // TODO: make file extensions and parsers configurable
90 | if (fp.endsWith(".vue")) {
91 | const { descriptor, errors } = parse(buffer, {
92 | filename: fp,
93 | sourceRoot: process.cwd(),
94 | });
95 | if (errors.length > 0) {
96 | errors.forEach((e) => console.error(e));
97 | }
98 | if (descriptor.template) {
99 | htmlParser.parseString(`${descriptor.template.content} `, descriptor.filename, {
100 | lineNumberStart: descriptor.template.loc.start.line,
101 | transformSource: (code) => {
102 | const lang = descriptor?.template?.lang?.toLowerCase() || "html";
103 | if (!config.input?.compileTemplate || lang === "html") {
104 | return code;
105 | }
106 |
107 | const compiledTemplate = compileTemplate({
108 | filename: descriptor?.filename,
109 | source: code,
110 | preprocessLang: lang,
111 | id: descriptor?.filename,
112 | });
113 |
114 | return compiledTemplate.source;
115 | },
116 | });
117 | }
118 | if (descriptor.script) {
119 | jsParser.parseString(descriptor.script.content, descriptor.filename, {
120 | lineNumberStart: descriptor.script.loc.start.line,
121 | });
122 | }
123 | if (descriptor.scriptSetup) {
124 | jsParser.parseString(descriptor.scriptSetup.content, descriptor.filename, {
125 | lineNumberStart: descriptor.scriptSetup.loc.start.line,
126 | });
127 | }
128 | } else if (fp.endsWith(".html")) {
129 | htmlParser.parseString(buffer, fp);
130 | } else if (
131 | fp.endsWith(".js") ||
132 | fp.endsWith(".ts") ||
133 | fp.endsWith(".cjs") ||
134 | fp.endsWith(".mjs") ||
135 | fp.endsWith(".tsx")
136 | ) {
137 | jsParser.parseString(buffer, fp);
138 | }
139 | }),
140 | );
141 |
142 | extr.savePotFile(potPath);
143 | console.info(`${chalk.green("Extraction successful")}, ${chalk.blueBright(potPath)} created.`);
144 |
145 | extr.printStats();
146 | };
147 | export default extractFromFiles;
148 |
--------------------------------------------------------------------------------
/scripts/gettext_compile.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import chalk from "chalk";
4 | import commandLineArgs, { OptionDefinition } from "command-line-args";
5 | import fsPromises from "fs/promises";
6 | import path from "path";
7 | import { compilePoFiles } from "./compile";
8 | import { loadConfig } from "./config";
9 |
10 | const optionDefinitions: OptionDefinition[] = [{ name: "config", alias: "c", type: String }];
11 | let options;
12 | try {
13 | options = commandLineArgs(optionDefinitions) as {
14 | config?: string;
15 | };
16 | } catch (e) {
17 | console.error(e);
18 | process.exit(1);
19 | }
20 |
21 | (async () => {
22 | const config = await loadConfig(options);
23 | console.info(`Language directory: ${chalk.blueBright(config.output.path)}`);
24 | console.info(`Locales: ${chalk.blueBright(config.output.locales)}`);
25 | console.info();
26 | const localesPaths = config.output.locales.map((loc) =>
27 | config.output.flat ? path.join(config.output.path, `${loc}.po`) : path.join(config.output.path, `${loc}/app.po`),
28 | );
29 |
30 | await fsPromises.mkdir(config.output.path, { recursive: true });
31 | const jsonRes = await compilePoFiles(localesPaths);
32 | console.info(`${chalk.green("Compiled json")}: ${chalk.grey(JSON.stringify(jsonRes))}`);
33 | console.info();
34 | if (config.output.splitJson) {
35 | await Promise.all(
36 | config.output.locales.map(async (locale) => {
37 | const outputPath = path.join(config.output.jsonPath, `${locale}.json`);
38 | await fsPromises.writeFile(
39 | outputPath,
40 | JSON.stringify({
41 | [locale]: jsonRes[locale],
42 | }),
43 | );
44 | console.info(`${chalk.green("Created")}: ${chalk.blueBright(outputPath)}`);
45 | }),
46 | );
47 | } else {
48 | const outputPath = config.output.jsonPath;
49 | await fsPromises.writeFile(outputPath, JSON.stringify(jsonRes));
50 | console.info(`${chalk.green("Created")}: ${chalk.blueBright(outputPath)}`);
51 | }
52 | })();
53 |
--------------------------------------------------------------------------------
/scripts/gettext_extract.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import chalk from "chalk";
4 | import commandLineArgs, { OptionDefinition } from "command-line-args";
5 | import fs from "node:fs";
6 | import { glob } from "glob";
7 | import path from "node:path";
8 | import { loadConfig } from "./config";
9 | import extractFromFiles from "./extract";
10 | import { execShellCommand } from "./utils";
11 | import { GettextConfig } from "../src/typeDefs";
12 |
13 | const optionDefinitions: OptionDefinition[] = [{ name: "config", alias: "c", type: String }];
14 | let options;
15 | try {
16 | options = commandLineArgs(optionDefinitions) as {
17 | config?: string;
18 | };
19 | } catch (e) {
20 | console.error(e);
21 | process.exit(1);
22 | }
23 |
24 | var getFiles = async (config: GettextConfig) => {
25 | const allFiles = await Promise.all(
26 | config.input?.include.map((pattern) => {
27 | const searchPath = path.join(config.input.path, pattern);
28 | console.info(`Searching: ${chalk.blueBright(searchPath)}`);
29 | return glob(searchPath);
30 | }),
31 | );
32 | const excludeFiles = await Promise.all(
33 | config.input.exclude.map((pattern) => {
34 | const searchPath = path.join(config.input.path, pattern);
35 | console.info(`Excluding: ${chalk.blueBright(searchPath)}`);
36 | return glob(searchPath);
37 | }),
38 | );
39 | const filesFlat = allFiles.reduce((prev, curr) => [...prev, ...curr], [] as string[]);
40 | const excludeFlat = excludeFiles.reduce((prev, curr) => [...prev, ...curr], [] as string[]);
41 | excludeFlat.forEach((file) => {
42 | const index = filesFlat.indexOf(file);
43 | if (index !== -1) {
44 | filesFlat.splice(index, 1);
45 | }
46 | });
47 | return filesFlat;
48 | };
49 |
50 | (async () => {
51 | const config = await loadConfig(options);
52 | console.info(`Input directory: ${chalk.blueBright(config.input.path)}`);
53 | console.info(`Output directory: ${chalk.blueBright(config.output.path)}`);
54 | console.info(`Output POT file: ${chalk.blueBright(config.output.potPath)}`);
55 | console.info(`Locales: ${chalk.blueBright(config.output.locales)}`);
56 | console.info();
57 |
58 | const files = await getFiles(config);
59 | console.info();
60 | files.forEach((f) => console.info(chalk.grey(f)));
61 | console.info();
62 | await extractFromFiles(files, config.output.potPath, config);
63 |
64 | for (const loc of config.output.locales) {
65 | const poDir = config.output.flat ? config.output.path : path.join(config.output.path, loc);
66 | const poFile = config.output.flat ? path.join(poDir, `${loc}.po`) : path.join(poDir, `app.po`);
67 |
68 | fs.mkdirSync(poDir, { recursive: true });
69 | const isFile = fs.existsSync(poFile) && fs.lstatSync(poFile).isFile();
70 | if (isFile) {
71 | await execShellCommand(`msgmerge --lang=${loc} --update ${poFile} ${config.output.potPath} --backup=off`);
72 | console.info(`${chalk.green("Merged")}: ${chalk.blueBright(poFile)}`);
73 | } else {
74 | // https://www.gnu.org/software/gettext/manual/html_node/msginit-Invocation.html
75 | // msginit will set Plural-Forms header if the locale is in the
76 | // [embedded table](https://github.com/dd32/gettext/blob/master/gettext-tools/src/plural-table.c#L27)
77 | // otherwise it will read [$GETTEXTCLDRDIR/common/supplemental/plurals.xml](https://raw.githubusercontent.com/unicode-org/cldr/main/common/supplemental/plurals.xml)
78 | // so execShellCommand should pass the env(GETTEXTCLDRDIR) to child process
79 | await execShellCommand(
80 | `msginit --no-translator --locale=${loc} --input=${config.output.potPath} --output-file=${poFile}`,
81 | );
82 | fs.chmodSync(poFile, 0o666);
83 | await execShellCommand(`msgattrib --no-wrap --no-obsolete -o ${poFile} ${poFile}`);
84 | console.info(`${chalk.green("Created")}: ${chalk.blueBright(poFile)}`);
85 | }
86 | }
87 | if (config.output.linguas === true) {
88 | const linguasPath = path.join(config.output.path, "LINGUAS");
89 | fs.writeFileSync(linguasPath, config.output.locales.join(" "));
90 | console.info();
91 | console.info(`${chalk.green("Created")}: ${chalk.blueBright(linguasPath)}`);
92 | }
93 | })();
94 |
--------------------------------------------------------------------------------
/scripts/utils.ts:
--------------------------------------------------------------------------------
1 | import { exec } from "child_process";
2 |
3 | export function execShellCommand(cmd: string) {
4 | return new Promise((resolve) => {
5 | exec(cmd, { env: process.env }, (error, stdout, stderr) => {
6 | if (error) {
7 | console.warn(error);
8 | }
9 | resolve(stdout ? stdout : stderr);
10 | });
11 | });
12 | }
13 |
--------------------------------------------------------------------------------
/src/component.ts:
--------------------------------------------------------------------------------
1 | import { computed, defineComponent, getCurrentInstance, h, onMounted, ref } from "vue";
2 | import interpolate from "./interpolate";
3 | import translate from "./translate";
4 | import { useGettext } from "./utilities";
5 |
6 | /**
7 | * Translate content according to the current language.
8 | * @deprecated
9 | */
10 | export const Component = defineComponent({
11 | // eslint-disable-next-line vue/multi-word-component-names, vue/component-definition-name-casing
12 | name: "translate",
13 | props: {
14 | tag: {
15 | type: String,
16 | default: "span",
17 | },
18 | // Always use v-bind for dynamically binding the `translateN` prop to data on the parent,
19 | // i.e.: `:translate-n`.
20 | translateN: {
21 | type: Number,
22 | default: null,
23 | },
24 | translatePlural: {
25 | type: String,
26 | default: null,
27 | },
28 | translateContext: {
29 | type: String,
30 | default: null,
31 | },
32 | translateParams: {
33 | type: Object,
34 | default: null,
35 | },
36 | translateComment: {
37 | type: String,
38 | default: null,
39 | },
40 | },
41 |
42 | setup(props, context) {
43 | const isPlural = props.translateN !== undefined && props.translatePlural !== undefined;
44 | if (!isPlural && (props.translateN || props.translatePlural)) {
45 | throw new Error(
46 | `\`translate-n\` and \`translate-plural\` attributes must be used together: ${
47 | context.slots.default?.()[0]?.children
48 | }.`,
49 | );
50 | }
51 |
52 | const root = ref();
53 |
54 | const plugin = useGettext();
55 | const msgid = ref(null);
56 |
57 | onMounted(() => {
58 | if (!msgid.value && root.value) {
59 | msgid.value = root.value.innerHTML.trim();
60 | }
61 | });
62 |
63 | const translation = computed(() => {
64 | const translatedMsg = translate(plugin).getTranslation(
65 | msgid.value!,
66 | props.translateN,
67 | props.translateContext,
68 | isPlural ? props.translatePlural : null,
69 | plugin.current,
70 | );
71 |
72 | return interpolate(plugin)(translatedMsg, props.translateParams, undefined, getCurrentInstance()?.parent);
73 | });
74 |
75 | // The text must be wraped inside a root HTML element, so we use a by default.
76 | return () => {
77 | if (!msgid.value) {
78 | return h(props.tag, { ref: root }, context.slots.default ? context.slots.default() : "");
79 | }
80 | return h(props.tag, { ref: root, innerHTML: translation.value });
81 | };
82 | },
83 | });
84 |
85 | export default Component;
86 |
--------------------------------------------------------------------------------
/src/directive.ts:
--------------------------------------------------------------------------------
1 | import { DirectiveBinding, ObjectDirective, VNode, watch } from "vue";
2 | import interpolate from "./interpolate";
3 | import translate from "./translate";
4 | import { Language } from "./typeDefs";
5 |
6 | const updateTranslation = (language: Language, el: HTMLElement, binding: DirectiveBinding, vnode: VNode) => {
7 | const attrs = vnode.props || {};
8 | const msgid = el.dataset.msgid!;
9 | const translateContext = attrs["translate-context"];
10 | const translateN = attrs["translate-n"];
11 | const translatePlural = attrs["translate-plural"];
12 | const isPlural = translateN !== undefined && translatePlural !== undefined;
13 | const disableHtmlEscaping = attrs["render-html"] === "true";
14 |
15 | if (!isPlural && (translateN || translatePlural)) {
16 | throw new Error("`translate-n` and `translate-plural` attributes must be used together:" + msgid + ".");
17 | }
18 |
19 | if (!language.silent && attrs["translate-params"]) {
20 | console.warn(
21 | `\`translate-params\` is required as an expression for v-translate directive. Please change to \`v-translate='params'\`: ${msgid}`,
22 | );
23 | }
24 |
25 | const translation = translate(language).getTranslation(
26 | msgid,
27 | translateN,
28 | translateContext,
29 | isPlural ? translatePlural : null,
30 | language.current,
31 | );
32 |
33 | const context = Object.assign(binding.instance ?? {}, binding.value);
34 | const msg = interpolate(language)(translation, context, disableHtmlEscaping, null);
35 |
36 | el.innerHTML = msg;
37 | };
38 |
39 | /**
40 | * A directive to translate content according to the current language.
41 | *
42 | * Use this directive instead of the component if you need to translate HTML content.
43 | * It's too tricky to support HTML content within the component because we cannot get the raw HTML to use as `msgid`.
44 | *
45 | * This directive has a similar interface to the component, supporting
46 | * `translate-comment`, `translate-context`, `translate-plural`, `translate-n`.
47 | *
48 | * `This is Sparta !
`
49 | *
50 | * If you need interpolation, you must add an expression that outputs binding value that changes with each of the
51 | * context variable:
52 | * `I am %{ fullName } and from %{ location }
`
53 | * @deprecated
54 | */
55 | export default function directive(language: Language): ObjectDirective {
56 | const update = (el: HTMLElement, binding: DirectiveBinding, vnode: VNode) => {
57 | // Store the current language in the element's dataset.
58 | el.dataset.currentLanguage = language.current;
59 | updateTranslation(language, el, binding, vnode);
60 | };
61 | return {
62 | beforeMount(el: HTMLElement, binding: DirectiveBinding, vnode: VNode) {
63 | // Get the raw HTML and store it in the element's dataset (as advised in Vue's official guide).
64 | if (!el.dataset.msgid) {
65 | el.dataset.msgid = el.innerHTML;
66 | }
67 |
68 | watch(language, () => {
69 | update(el, binding, vnode);
70 | });
71 |
72 | update(el, binding, vnode);
73 | },
74 | updated(el: HTMLElement, binding: DirectiveBinding, vnode: VNode) {
75 | update(el, binding, vnode);
76 | },
77 | };
78 | }
79 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { App, computed, reactive, ref } from "vue";
2 | import Component from "./component";
3 | import Directive from "./directive";
4 | import interpolateRaw from "./interpolate";
5 | import translateRaw from "./translate";
6 | import type {
7 | GettextConfig,
8 | GettextConfigOptions,
9 | GetTextOptions,
10 | Language,
11 | LanguageData,
12 | Message,
13 | Translations,
14 | } from "./typeDefs";
15 | import { GetTextSymbol } from "./typeDefs";
16 | import { normalizeTranslations } from "./utilities";
17 |
18 | export { useGettext } from "./utilities";
19 | export type { Language, Message, LanguageData, Translations, GettextConfig, GettextConfigOptions, GetTextOptions };
20 |
21 | const defaultOptions: GetTextOptions = {
22 | /** all the available languages of your application. Keys must match locale names */
23 | availableLanguages: { en: "English" },
24 | defaultLanguage: "en",
25 | sourceCodeLanguage: undefined,
26 | mutedLanguages: [],
27 | silent: false,
28 | translations: {},
29 | setGlobalProperties: true,
30 | globalProperties: {
31 | language: ["$language"],
32 | gettext: ["$gettext"],
33 | pgettext: ["$pgettext"],
34 | ngettext: ["$ngettext"],
35 | npgettext: ["$npgettext"],
36 | interpolate: ["$gettextInterpolate"],
37 | },
38 | provideDirective: true,
39 | provideComponent: true,
40 | };
41 |
42 | export function createGettext(options: Partial = {}) {
43 | Object.keys(options).forEach((key) => {
44 | if (Object.keys(defaultOptions).indexOf(key) === -1) {
45 | throw new Error(`${key} is an invalid option for the translate plugin.`);
46 | }
47 | });
48 |
49 | const mergedOptions = {
50 | ...defaultOptions,
51 | ...options,
52 | };
53 |
54 | const translations = ref(normalizeTranslations(mergedOptions.translations));
55 |
56 | const gettext: Language = reactive({
57 | available: mergedOptions.availableLanguages,
58 | muted: mergedOptions.mutedLanguages,
59 | silent: mergedOptions.silent,
60 | translations: computed({
61 | get: () => {
62 | return translations.value;
63 | },
64 | set: (val: GetTextOptions["translations"]) => {
65 | translations.value = normalizeTranslations(val);
66 | },
67 | }),
68 | current: mergedOptions.defaultLanguage,
69 | sourceCodeLanguage: mergedOptions.sourceCodeLanguage,
70 | install(app: App) {
71 | // TODO: is this needed?
72 | (app as any)[GetTextSymbol] = gettext;
73 | app.provide(GetTextSymbol, gettext);
74 |
75 | if (mergedOptions.setGlobalProperties) {
76 | const globalProperties = app.config.globalProperties;
77 | let properties = mergedOptions.globalProperties.gettext || ["$gettext"];
78 | properties.forEach((p) => {
79 | globalProperties[p] = gettext.$gettext;
80 | });
81 | properties = mergedOptions.globalProperties.pgettext || ["$pgettext"];
82 | properties.forEach((p) => {
83 | globalProperties[p] = gettext.$pgettext;
84 | });
85 | properties = mergedOptions.globalProperties.ngettext || ["$ngettext"];
86 | properties.forEach((p) => {
87 | globalProperties[p] = gettext.$ngettext;
88 | });
89 | properties = mergedOptions.globalProperties.npgettext || ["$npgettext"];
90 | properties.forEach((p) => {
91 | globalProperties[p] = gettext.$npgettext;
92 | });
93 | properties = mergedOptions.globalProperties.interpolate || ["$gettextInterpolate"];
94 | properties.forEach((p) => {
95 | globalProperties[p] = gettext.interpolate;
96 | });
97 | properties = mergedOptions.globalProperties.language || ["$language"];
98 | properties.forEach((p) => {
99 | globalProperties[p] = gettext;
100 | });
101 | }
102 |
103 | if (mergedOptions.provideDirective) {
104 | app.directive("translate", Directive(gettext));
105 | }
106 | if (mergedOptions.provideComponent) {
107 | // eslint-disable-next-line vue/multi-word-component-names, vue/component-definition-name-casing
108 | app.component("translate", Component);
109 | }
110 | },
111 | }) as unknown as Language;
112 |
113 | const translate = translateRaw(gettext);
114 | const interpolate = interpolateRaw(gettext);
115 | gettext.$gettext = translate.gettext.bind(translate);
116 | gettext.$pgettext = translate.pgettext.bind(translate);
117 | gettext.$ngettext = translate.ngettext.bind(translate);
118 | gettext.$npgettext = translate.npgettext.bind(translate);
119 | gettext.interpolate = interpolate.bind(interpolate);
120 |
121 | gettext.directive = Directive(gettext);
122 | gettext.component = Component;
123 |
124 | return gettext;
125 | }
126 |
127 | export const defineGettextConfig = (config: GettextConfigOptions) => {
128 | return config;
129 | };
130 |
--------------------------------------------------------------------------------
/src/interpolate.ts:
--------------------------------------------------------------------------------
1 | import { ComponentInternalInstance } from "vue";
2 | import { Language } from "./typeDefs";
3 |
4 | const EVALUATION_RE = /[[\].]{1,2}/g;
5 |
6 | /* Interpolation RegExp.
7 | *
8 | * Because interpolation inside attributes are deprecated in Vue 2 we have to
9 | * use another set of delimiters to be able to use `translate-plural` etc.
10 | * We use %{ } delimiters.
11 | *
12 | * /
13 | * %\{ => Starting delimiter: `%{`
14 | * ( => Start capture
15 | * (?:.|\n) => Non-capturing group: any character or newline
16 | * +? => One or more times (ungreedy)
17 | * ) => End capture
18 | * \} => Ending delimiter: `}`
19 | * /g => Global: don't return after first match
20 | */
21 | const INTERPOLATION_RE = /%\{((?:.|\n)+?)\}/g;
22 |
23 | const MUSTACHE_SYNTAX_RE = /\{\{((?:.|\n)+?)\}\}/g;
24 |
25 | /**
26 | * Evaluate a piece of template string containing %{ } placeholders.
27 | * E.g.: 'Hi %{ user.name }' => 'Hi Bob'
28 | *
29 | * This is a vm.$interpolate alternative for Vue 2.
30 | * https://vuejs.org/v2/guide/migration.html#vm-interpolate-removed
31 | *
32 | * @param {String} msgid - The translation key containing %{ } placeholders
33 | * @param {Object} context - An object whose elements are put in their corresponding placeholders
34 | *
35 | * @return {String} The interpolated string
36 | */
37 | const interpolate =
38 | (plugin: Language) =>
39 | (msgid: string, context: any = {}, disableHtmlEscaping = false, parent?: ComponentInternalInstance | any) => {
40 | const silent = plugin.silent;
41 | if (!silent && MUSTACHE_SYNTAX_RE.test(msgid)) {
42 | console.warn(`Mustache syntax cannot be used with vue-gettext. Please use "%{}" instead of "{{}}" in: ${msgid}`);
43 | }
44 |
45 | const result = msgid.replace(INTERPOLATION_RE, (_match, token: string) => {
46 | const expression = token.trim();
47 | let evaluated: Object;
48 |
49 | const escapeHtmlMap = {
50 | "&": "&",
51 | "<": "<",
52 | ">": ">",
53 | '"': """,
54 | "'": "'",
55 | };
56 |
57 | // Avoid eval() by splitting `expression` and looping through its different properties if any, see #55.
58 | function getProps(obj: any, expression: string) {
59 | const arr = expression.split(EVALUATION_RE).filter((x) => x);
60 | while (arr.length) {
61 | obj = obj[arr.shift()!];
62 | }
63 | return obj;
64 | }
65 |
66 | function evalInContext(context: any, expression: string, parent: any): string {
67 | try {
68 | evaluated = getProps(context, expression);
69 | } catch (e) {
70 | // Ignore errors, because this function may be called recursively later.
71 | }
72 | if (evaluated === undefined || evaluated === null) {
73 | if (parent) {
74 | // Recursively climb the parent chain to allow evaluation inside nested components, see #23 and #24.
75 | return evalInContext(parent.ctx, expression, parent.parent);
76 | } else {
77 | console.warn(`Cannot evaluate expression: ${expression}`);
78 | evaluated = expression;
79 | }
80 | }
81 | const result = evaluated.toString();
82 | if (disableHtmlEscaping) {
83 | // Do not escape HTML, see #78.
84 | return result;
85 | }
86 | // Escape HTML, see #78.
87 | return result.replace(/[&<>"']/g, (m: string) => escapeHtmlMap[m as keyof typeof escapeHtmlMap]);
88 | }
89 |
90 | return evalInContext(context, expression, parent);
91 | });
92 |
93 | return result;
94 | };
95 |
96 | // Store this values as function attributes for easy access elsewhere to bypass a Rollup
97 | // weak point with `export`:
98 | // https://github.com/rollup/rollup/blob/fca14d/src/utils/getExportMode.js#L27
99 | interpolate.INTERPOLATION_RE = INTERPOLATION_RE;
100 | interpolate.INTERPOLATION_PREFIX = "%{";
101 |
102 | export default interpolate;
103 |
--------------------------------------------------------------------------------
/src/plurals.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Plural Forms
3 | *
4 | * This is a list of the plural forms, as used by Gettext PO, that are appropriate to each language.
5 | * http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html
6 | *
7 | * This is a replica of angular-gettext's plural.js
8 | * https://github.com/rubenv/angular-gettext/blob/master/src/plural.js
9 | */
10 | export default {
11 | getTranslationIndex: function (languageCode: string, n: number | null) {
12 | n = Number(n);
13 | n = typeof n === "number" && isNaN(n) ? 1 : n; // Fallback to singular.
14 |
15 | // Extract the ISO 639 language code. The ISO 639 standard defines
16 | // two-letter codes for many languages, and three-letter codes for
17 | // more rarely used languages.
18 | // https://www.gnu.org/software/gettext/manual/html_node/Language-Codes.html#Language-Codes
19 | if (languageCode.length > 2 && languageCode !== "pt_BR") {
20 | languageCode = languageCode.split("_")[0];
21 | }
22 |
23 | switch (languageCode) {
24 | case "ay": // Aymará
25 | case "bo": // Tibetan
26 | case "cgg": // Chiga
27 | case "dz": // Dzongkha
28 | case "fa": // Persian
29 | case "id": // Indonesian
30 | case "ja": // Japanese
31 | case "jbo": // Lojban
32 | case "ka": // Georgian
33 | case "kk": // Kazakh
34 | case "km": // Khmer
35 | case "ko": // Korean
36 | case "ky": // Kyrgyz
37 | case "lo": // Lao
38 | case "ms": // Malay
39 | case "my": // Burmese
40 | case "sah": // Yakut
41 | case "su": // Sundanese
42 | case "th": // Thai
43 | case "tt": // Tatar
44 | case "ug": // Uyghur
45 | case "vi": // Vietnamese
46 | case "wo": // Wolof
47 | case "zh": // Chinese
48 | // 1 form
49 | return 0;
50 | case "is": // Icelandic
51 | // 2 forms
52 | return n % 10 !== 1 || n % 100 === 11 ? 1 : 0;
53 | case "jv": // Javanese
54 | // 2 forms
55 | return n !== 0 ? 1 : 0;
56 | case "mk": // Macedonian
57 | // 2 forms
58 | return n === 1 || n % 10 === 1 ? 0 : 1;
59 | case "ach": // Acholi
60 | case "ak": // Akan
61 | case "am": // Amharic
62 | case "arn": // Mapudungun
63 | case "br": // Breton
64 | case "fil": // Filipino
65 | case "fr": // French
66 | case "gun": // Gun
67 | case "ln": // Lingala
68 | case "mfe": // Mauritian Creole
69 | case "mg": // Malagasy
70 | case "mi": // Maori
71 | case "oc": // Occitan
72 | case "pt_BR": // Brazilian Portuguese
73 | case "tg": // Tajik
74 | case "ti": // Tigrinya
75 | case "tr": // Turkish
76 | case "uz": // Uzbek
77 | case "wa": // Walloon
78 | // 2 forms
79 | return n > 1 ? 1 : 0;
80 | case "lv": // Latvian
81 | // 3 forms
82 | return n % 10 === 1 && n % 100 !== 11 ? 0 : n !== 0 ? 1 : 2;
83 | case "lt": // Lithuanian
84 | // 3 forms
85 | return n % 10 === 1 && n % 100 !== 11 ? 0 : n % 10 >= 2 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2;
86 | case "be": // Belarusian
87 | case "bs": // Bosnian
88 | case "hr": // Croatian
89 | case "ru": // Russian
90 | case "sr": // Serbian
91 | case "uk": // Ukrainian
92 | // 3 forms
93 | return n % 10 === 1 && n % 100 !== 11
94 | ? 0
95 | : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)
96 | ? 1
97 | : 2;
98 | case "mnk": // Mandinka
99 | // 3 forms
100 | return n === 0 ? 0 : n === 1 ? 1 : 2;
101 | case "ro": // Romanian
102 | // 3 forms
103 | return n === 1 ? 0 : n === 0 || (n % 100 > 0 && n % 100 < 20) ? 1 : 2;
104 | case "pl": // Polish
105 | // 3 forms
106 | return n === 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2;
107 | case "cs": // Czech
108 | case "sk": // Slovak
109 | // 3 forms
110 | return n === 1 ? 0 : n >= 2 && n <= 4 ? 1 : 2;
111 | case "csb": // Kashubian
112 | // 3 forms
113 | return n === 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2;
114 | case "sl": // Slovenian
115 | // 4 forms
116 | return n % 100 === 1 ? 0 : n % 100 === 2 ? 1 : n % 100 === 3 || n % 100 === 4 ? 2 : 3;
117 | case "mt": // Maltese
118 | // 4 forms
119 | return n === 1 ? 0 : n === 0 || (n % 100 > 1 && n % 100 < 11) ? 1 : n % 100 > 10 && n % 100 < 20 ? 2 : 3;
120 | case "gd": // Scottish Gaelic
121 | // 4 forms
122 | return n === 1 || n === 11 ? 0 : n === 2 || n === 12 ? 1 : n > 2 && n < 20 ? 2 : 3;
123 | case "cy": // Welsh
124 | // 4 forms
125 | return n === 1 ? 0 : n === 2 ? 1 : n !== 8 && n !== 11 ? 2 : 3;
126 | case "kw": // Cornish
127 | // 4 forms
128 | return n === 1 ? 0 : n === 2 ? 1 : n === 3 ? 2 : 3;
129 | case "ga": // Irish
130 | // 5 forms
131 | return n === 1 ? 0 : n === 2 ? 1 : n > 2 && n < 7 ? 2 : n > 6 && n < 11 ? 3 : 4;
132 | case "ar": // Arabic
133 | // 6 forms
134 | return n === 0 ? 0 : n === 1 ? 1 : n === 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5;
135 | default:
136 | // Everything else
137 | return n !== 1 ? 1 : 0;
138 | }
139 | },
140 | };
141 |
--------------------------------------------------------------------------------
/src/translate.ts:
--------------------------------------------------------------------------------
1 | import plurals from "./plurals";
2 | import { Language, LanguageData, Message, MessageContext, Parameters } from "./typeDefs";
3 |
4 | const translate = (language: Language) => ({
5 | /*
6 | * Get the translated string from the translation.json file generated by easygettext.
7 | *
8 | * @param {String} msgid - The translation key
9 | * @param {Number} n - The number to switch between singular and plural
10 | * @param {String} context - The translation key context
11 | * @param {String} defaultPlural - The default plural value (optional)
12 | * @param {String} language - The language ID (e.g. 'fr_FR' or 'en_US')
13 | *
14 | * @return {String} The translated string
15 | */
16 | getTranslation: function (
17 | msgid: string,
18 | n: number | null = 1,
19 | context: string | null = null,
20 | defaultPlural: string | null = null,
21 | languageKey?: string,
22 | parameters?: { [key: string]: string },
23 | disableHtmlEscaping = false,
24 | ) {
25 | if (languageKey === undefined) {
26 | languageKey = language.current;
27 | }
28 |
29 | const interp = (message: TString, parameters?: Parameters) =>
30 | parameters ? language.interpolate(message, parameters, disableHtmlEscaping) : message;
31 |
32 | // spacing needs to be consistent even if a web template designer adds spaces between lines
33 | msgid = msgid.trim();
34 |
35 | if (!msgid) {
36 | return ""; // Allow empty strings.
37 | }
38 |
39 | const silent = languageKey ? language.silent || language.muted.indexOf(languageKey) !== -1 : false;
40 |
41 | // Default untranslated string, singular or plural.
42 | let noTransLangKey = languageKey; // if no translation, use current lang key to get plural text.(for compatibility)
43 | if (language.sourceCodeLanguage) {
44 | // new config: if source code language has been set, use it to get plural text.
45 | noTransLangKey = language.sourceCodeLanguage;
46 | }
47 | const untranslated = defaultPlural && plurals.getTranslationIndex(noTransLangKey, n) > 0 ? defaultPlural : msgid;
48 |
49 | // `easygettext`'s `gettext-compile` generates a JSON version of a .po file based on its `Language` field.
50 | // But in this field, `ll_CC` combinations denoting a language’s main dialect are abbreviated as `ll`,
51 | // for example `de` is equivalent to `de_DE` (German as spoken in Germany).
52 | // See the `Language` section in https://www.gnu.org/software/gettext/manual/html_node/Header-Entry.html
53 | // So try `ll_CC` first, or the `ll` abbreviation which can be three-letter sometimes:
54 | // https://www.gnu.org/software/gettext/manual/html_node/Language-Codes.html#Language-Codes
55 | const pluginTranslations = language.translations;
56 | const translations: LanguageData = pluginTranslations[languageKey] || pluginTranslations[languageKey.split("_")[0]];
57 |
58 | if (!translations) {
59 | if (!silent) {
60 | console.warn(`No translations found for ${languageKey}`);
61 | }
62 | return interp(untranslated, parameters);
63 | }
64 |
65 | const getTranslationFromArray = (arr: string[]) => {
66 | let translationIndex = plurals.getTranslationIndex(languageKey!, n);
67 |
68 | // Do not assume that the default value of n is 1 for the singular form of all languages. E.g. Arabic
69 | if (arr.length === 1 && n === 1) {
70 | translationIndex = 0;
71 | }
72 |
73 | const str = arr[translationIndex];
74 | if (!str) {
75 | // If the translation is empty, use the untranslated string.
76 | if (str === "") {
77 | return interp(untranslated, parameters);
78 | }
79 |
80 | throw new Error(msgid + " " + translationIndex + " " + language.current + " " + n);
81 | }
82 |
83 | return interp(str, parameters);
84 | };
85 |
86 | const getUntranslatedMsg = () => {
87 | if (!silent) {
88 | let msg = `Untranslated ${languageKey} key found: ${msgid}`;
89 | if (context) {
90 | msg += ` (with context: ${context})`;
91 | }
92 | console.warn(msg);
93 | }
94 | return interp(untranslated, parameters);
95 | };
96 |
97 | const translateMsg = (msg: Message | MessageContext, context: string | null = null): string => {
98 | if (msg instanceof Object) {
99 | if (Array.isArray(msg)) {
100 | return getTranslationFromArray(msg);
101 | }
102 | const msgContext = context ?? "";
103 | const ctxVal = msg[msgContext];
104 | return translateMsg(ctxVal);
105 | }
106 | if (context) {
107 | return getUntranslatedMsg();
108 | }
109 | if (!msg) {
110 | return getUntranslatedMsg();
111 | }
112 | return interp(msg, parameters);
113 | };
114 |
115 | const translated = translations[msgid];
116 | return translateMsg(translated, context);
117 | },
118 |
119 | /*
120 | * Returns a string of the translation of the message.
121 | * Also makes the string discoverable by gettext-extract.
122 | *
123 | * @param {String} msgid - The translation key
124 | * @param {Object} parameters - The interpolation parameters
125 | * @param {Boolean} disableHtmlEscaping - Disable html escaping
126 | *
127 | * @return {String} The translated string
128 | */
129 | gettext: function (
130 | msgid: TString,
131 | parameters?: Parameters,
132 | disableHtmlEscaping = false,
133 | ) {
134 | return this.getTranslation(msgid, undefined, undefined, undefined, undefined, parameters, disableHtmlEscaping);
135 | },
136 |
137 | /*
138 | * Returns a string of the translation for the given context.
139 | * Also makes the string discoverable by gettext-extract.
140 | *
141 | * @param {String} context - The context of the string to translate
142 | * @param {String} msgid - The translation key
143 | * @param {Object} parameters - The interpolation parameters
144 | * @param {Boolean} disableHtmlEscaping - Disable html escaping
145 | *
146 | * @return {String} The translated string
147 | */
148 | pgettext: function (
149 | context: string,
150 | msgid: TString,
151 | parameters?: Parameters,
152 | disableHtmlEscaping = false,
153 | ) {
154 | return this.getTranslation(msgid, 1, context, undefined, undefined, parameters, disableHtmlEscaping);
155 | },
156 |
157 | /*
158 | * Returns a string of the translation of either the singular or plural,
159 | * based on the number.
160 | * Also makes the string discoverable by gettext-extract.
161 | *
162 | * @param {String} msgid - The translation key
163 | * @param {String} plural - The plural form of the translation key
164 | * @param {Number} n - The number to switch between singular and plural
165 | * @param {Object} parameters - The interpolation parameters
166 | * @param {Boolean} disableHtmlEscaping - Disable html escaping
167 | *
168 | * @return {String} The translated string
169 | */
170 | ngettext: function (
171 | msgid: TSingular,
172 | plural: TPlural,
173 | n: number,
174 | parameters?: Parameters & Parameters,
175 | disableHtmlEscaping = false,
176 | ) {
177 | return this.getTranslation(msgid, n, null, plural, undefined, parameters, disableHtmlEscaping);
178 | },
179 |
180 | /*
181 | * Returns a string of the translation of either the singular or plural,
182 | * based on the number, for the given context.
183 | * Also makes the string discoverable by gettext-extract.
184 | *
185 | * @param {String} context - The context of the string to translate
186 | * @param {String} msgid - The translation key
187 | * @param {String} plural - The plural form of the translation key
188 | * @param {Number} n - The number to switch between singular and plural
189 | * @param {Object} parameters - The interpolation parameters
190 | * @param {Boolean} disableHtmlEscaping - Disable html escaping
191 | *
192 | * @return {String} The translated string
193 | */
194 | npgettext: function (
195 | context: string,
196 | msgid: TSingular,
197 | plural: TPlural,
198 | n: number,
199 | parameters?: Parameters & Parameters,
200 | disableHtmlEscaping = false,
201 | ) {
202 | return this.getTranslation(msgid, n, context, plural, undefined, parameters, disableHtmlEscaping);
203 | },
204 | });
205 |
206 | export default translate;
207 |
--------------------------------------------------------------------------------
/src/typeDefs.ts:
--------------------------------------------------------------------------------
1 | import { IJsExtractorOptions } from "gettext-extractor/dist/js/extractors/common";
2 | import { App, UnwrapRef, WritableComputedRef } from "vue";
3 | import type { Component as ComponentType } from "./component";
4 | import directive from "./directive";
5 |
6 | export type TranslateComponent = typeof ComponentType;
7 | export type TranslateDirective = ReturnType;
8 |
9 | export const GetTextSymbol = Symbol("GETTEXT");
10 |
11 | export type Message = string | string[];
12 |
13 | export type MessageContext = {
14 | [context: string]: Message;
15 | };
16 |
17 | export type LanguageData = {
18 | [messageId: string]: Message | MessageContext;
19 | };
20 |
21 | export type Translations = {
22 | [language: string]: LanguageData;
23 | };
24 |
25 | export interface GetTextOptions {
26 | availableLanguages: { [key: string]: string };
27 | defaultLanguage: string;
28 | sourceCodeLanguage?: string; // if set, use it to calculate plural form when a msgid is not translated.
29 | mutedLanguages: Array;
30 | silent: boolean;
31 | translations: Translations;
32 | setGlobalProperties: boolean;
33 | globalProperties: {
34 | language?: Array;
35 | gettext?: Array;
36 | pgettext?: Array;
37 | ngettext?: Array;
38 | npgettext?: Array;
39 | interpolate?: Array;
40 | };
41 | provideDirective: boolean;
42 | provideComponent: boolean;
43 | }
44 |
45 | type ParameterKeys = TString extends `${infer _}%{${infer Key}}${infer Rest}`
46 | ? Key | ParameterKeys
47 | : never;
48 |
49 | export type Parameters = Record, string>;
50 |
51 | export type Language = UnwrapRef<{
52 | available: GetTextOptions["availableLanguages"];
53 | muted: GetTextOptions["mutedLanguages"];
54 | silent: GetTextOptions["silent"];
55 | translations: WritableComputedRef;
56 | current: string;
57 | sourceCodeLanguage?: string; // if set, use it to calculate plural form when a msgid is not translated.
58 | $gettext: (
59 | msgid: TString,
60 | parameters?: Parameters,
61 | disableHtmlEscaping?: boolean,
62 | ) => string;
63 | $pgettext: (
64 | context: string,
65 | msgid: TString,
66 | parameters?: Parameters,
67 | disableHtmlEscaping?: boolean,
68 | ) => string;
69 | $ngettext: (
70 | msgid: TSingular,
71 | plural: TPlural,
72 | n: number,
73 | parameters?: Parameters & Parameters,
74 | disableHtmlEscaping?: boolean,
75 | ) => string;
76 | $npgettext: (
77 | context: string,
78 | msgid: TSingular,
79 | plural: TPlural,
80 | n: number,
81 | parameters?: Parameters & Parameters,
82 | disableHtmlEscaping?: boolean,
83 | ) => string;
84 | interpolate: (msgid: string, context: object, disableHtmlEscaping?: boolean) => string;
85 | install: (app: App) => void;
86 | directive: TranslateDirective;
87 | component: TranslateComponent;
88 | }>;
89 |
90 | export interface GettextConfig {
91 | input: {
92 | /** only files in this directory are considered for extraction */
93 | path: string;
94 | /** glob patterns to select files for extraction */
95 | include: string[];
96 | /** glob patterns to exclude files from extraction */
97 | exclude: string[];
98 | /** js extractor options, for custom extractor keywords */
99 | jsExtractorOpts?: {
100 | keyword: string;
101 | options: IJsExtractorOptions;
102 | }[];
103 | compileTemplate: boolean;
104 | };
105 | output: {
106 | path: string;
107 | locales: string[];
108 | potPath: string;
109 | jsonPath: string;
110 | flat: boolean;
111 | linguas: boolean;
112 | splitJson: boolean;
113 | };
114 | }
115 |
116 | export interface GettextConfigOptions {
117 | input?: Partial;
118 | output?: Partial;
119 | }
120 |
121 | declare module "@vue/runtime-core" {
122 | interface ComponentCustomProperties extends Pick {
123 | $language: Language;
124 | $gettextInterpolate: Language["interpolate"];
125 | }
126 |
127 | interface GlobalComponents {
128 | translate: TranslateComponent;
129 | }
130 |
131 | interface GlobalDirectives {
132 | vTranslate: TranslateDirective;
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/utilities.ts:
--------------------------------------------------------------------------------
1 | import { inject } from "vue";
2 | import { GetTextOptions, GetTextSymbol, Language, LanguageData, Translations } from "./typeDefs";
3 |
4 | export function normalizeTranslationKey(key: string) {
5 | return key
6 | .replace(/\r?\n|\r/, "")
7 | .replace(/\s\s+/g, " ")
8 | .trim();
9 | }
10 |
11 | export function normalizeTranslations(translations: GetTextOptions["translations"]) {
12 | const newTranslations: Translations = {};
13 | Object.keys(translations).forEach((lang) => {
14 | const langData = translations[lang];
15 | const newLangData: LanguageData = {};
16 | Object.keys(langData).forEach((key) => {
17 | newLangData[normalizeTranslationKey(key)] = langData[key];
18 | });
19 | newTranslations[lang] = newLangData;
20 | });
21 | return newTranslations;
22 | }
23 |
24 | export const useGettext = (): Language => {
25 | const gettext = inject(GetTextSymbol, null) as Language | null;
26 | if (!gettext) {
27 | throw new Error("Failed to inject gettext. Make sure vue3-gettext is set up properly.");
28 | }
29 | return gettext;
30 | };
31 |
--------------------------------------------------------------------------------
/tests/component.spec.ts:
--------------------------------------------------------------------------------
1 | import { mountWithPlugin } from "./utils";
2 | import translations from "./json/component";
3 | import { nextTick } from "vue";
4 |
5 | const mount = mountWithPlugin({
6 | availableLanguages: {
7 | en_US: "American English",
8 | fr_FR: "Français",
9 | },
10 | defaultLanguage: "en_US",
11 | translations,
12 | setGlobalProperties: true,
13 | });
14 |
15 | describe("translate component tests", () => {
16 | it("works on empty strings", async () => {
17 | const wrapper = mount({
18 | template: "
",
19 | });
20 | expect(wrapper.element.innerHTML.trim()).toBe(" ");
21 | });
22 |
23 | it("returns an unchanged string when no translation is available for a language", async () => {
24 | const warnSpy = vi.spyOn(console, "warn");
25 | const wrapper = mount({ template: "Unchanged string
" });
26 | const vm = wrapper.vm as any;
27 | vm.$language.current = "fr_BE";
28 | await vm.$nextTick();
29 | expect(vm.$el.innerHTML.trim()).toEqual("Unchanged string ");
30 | expect(warnSpy).toHaveBeenCalledTimes(1);
31 | warnSpy.mockRestore();
32 | });
33 |
34 | it("returns an unchanged string when no translation key is available", async () => {
35 | const warnSpy = vi.spyOn(console, "warn");
36 | const wrapper = mount({ template: "Untranslated string
" });
37 | const vm = wrapper.vm as any;
38 | await vm.$nextTick();
39 | expect(vm.$el.innerHTML.trim()).toEqual("Untranslated string ");
40 | expect(warnSpy).toHaveBeenCalledTimes(1);
41 | warnSpy.mockRestore();
42 | });
43 |
44 | it("translates known strings", async () => {
45 | const wrapper = mount({ template: "Pending
" });
46 | const vm = wrapper.vm as any;
47 | vm.$language.current = "fr_FR";
48 | await vm.$nextTick();
49 | expect(vm.$el.innerHTML.trim()).toEqual("En cours ");
50 | });
51 |
52 | it("translates strings no matter the number of spaces", async () => {
53 | const wrapper = mount({
54 | template: `A lot of lines
`,
55 | });
56 | const vm = wrapper.vm as any;
57 | vm.$language.current = "fr_FR";
58 | await vm.$nextTick();
59 | expect(vm.$el.innerHTML.trim()).toEqual(`Plein de lignes
`);
60 | });
61 |
62 | it("renders translation in custom html tag", async () => {
63 | const wrapper = mount({ template: 'Pending
' });
64 | const vm = wrapper.vm as any;
65 | vm.$language.current = "fr_FR";
66 | await vm.$nextTick();
67 | expect(vm.$el.innerHTML.trim()).toEqual("En cours ");
68 | });
69 |
70 | it("translates known strings according to a given translation context", async () => {
71 | let wrapper = mount({ template: 'Answer
' });
72 | let vm = wrapper.vm as any;
73 | await vm.$nextTick();
74 | expect(vm.$el.innerHTML.trim()).toEqual("Answer (verb) ");
75 | wrapper = mount({ template: 'Answer
' });
76 | vm = wrapper.vm as any;
77 | vm.$language.current = "en_US";
78 | await vm.$nextTick();
79 | expect(vm.$el.innerHTML.trim()).toEqual("Answer (noun) ");
80 | });
81 |
82 | it("allows interpolation", async () => {
83 | const wrapper = mount({
84 | template: "Hello %{ name }
",
85 | data() {
86 | return { name: "John Doe" };
87 | },
88 | });
89 | const vm = wrapper.vm as any;
90 | vm.$language.current = "fr_FR";
91 | await vm.$nextTick();
92 | expect(vm.$el.innerHTML.trim()).toEqual("Bonjour John Doe ");
93 | });
94 |
95 | it("allows interpolation with computed property", async () => {
96 | const wrapper = mount({
97 | template: "Hello %{ name }
",
98 | computed: {
99 | name() {
100 | return "John Doe";
101 | },
102 | },
103 | });
104 | const vm = wrapper.vm as any;
105 | vm.$language.current = "fr_FR";
106 | await vm.$nextTick();
107 | expect(vm.$el.innerHTML.trim()).toEqual("Bonjour John Doe ");
108 | });
109 |
110 | it("allows custom params for interpolation", async () => {
111 | const wrapper = mount({
112 | template: 'Hello %{ name }
',
113 | data() {
114 | return {
115 | someNewNameVar: "John Doe",
116 | };
117 | },
118 | });
119 | const vm = wrapper.vm as any;
120 | vm.$language.current = "fr_FR";
121 | await vm.$nextTick();
122 | expect(vm.$el.innerHTML.trim()).toEqual("Bonjour John Doe ");
123 | });
124 |
125 | it("allows interpolation within v-for with custom params", async () => {
126 | const wrapper = mount({
127 | template: 'Hello %{ name }
',
128 | data() {
129 | return {
130 | names: ["John Doe", "Chester"],
131 | };
132 | },
133 | });
134 | const vm = wrapper.vm as any;
135 | vm.$language.current = "fr_FR";
136 | await vm.$nextTick();
137 | expect(vm.$el.innerHTML.trim()).toEqual("Bonjour John Doe Bonjour Chester ");
138 | });
139 |
140 | it("translates plurals", async () => {
141 | const wrapper = mount({
142 | template: `
143 | %{ count } car
144 |
`,
145 | data() {
146 | return { count: 2 };
147 | },
148 | });
149 | const vm = wrapper.vm as any;
150 | vm.$language.current = "fr_FR";
151 | await vm.$nextTick();
152 | expect(vm.$el.innerHTML.trim()).toEqual("2 véhicules ");
153 | });
154 |
155 | it("translates plurals with computed property", async () => {
156 | const wrapper = mount({
157 | template: `
158 | %{ count } car
159 |
`,
160 | computed: {
161 | count() {
162 | return 2;
163 | },
164 | },
165 | });
166 | const vm = wrapper.vm as any;
167 | vm.$language.current = "fr_FR";
168 | await vm.$nextTick();
169 | expect(vm.$el.innerHTML.trim()).toEqual("2 véhicules ");
170 | });
171 |
172 | it("updates a plural translation after a data change", async () => {
173 | const wrapper = mount({
174 | template: `
175 | %{ count } car
176 |
`,
177 | data() {
178 | return { count: 10 };
179 | },
180 | });
181 | const vm = wrapper.vm as any;
182 | vm.$language.current = "fr_FR";
183 | await vm.$nextTick();
184 | expect(vm.$el.innerHTML.trim()).toEqual("10 véhicules ");
185 | vm.count = 8;
186 | await vm.$nextTick();
187 |
188 | expect(vm.$el.innerHTML.trim()).toEqual("8 véhicules ");
189 | });
190 |
191 | it("updates a translation after a language change", async () => {
192 | const wrapper = mount({ template: "Pending
" });
193 | const vm = wrapper.vm as any;
194 | vm.$language.current = "fr_FR";
195 | await vm.$nextTick();
196 | expect(vm.$el.innerHTML.trim()).toEqual("En cours ");
197 | vm.$language.current = "en_US";
198 | await vm.$nextTick();
199 | expect(vm.$el.innerHTML.trim()).toEqual("Pending ");
200 | });
201 |
202 | it("thrown errors if you forget to add a `translate-plural` attribute", async () => {
203 | try {
204 | mount({
205 | template: '%{ n } car ',
206 | data() {
207 | return { n: 2 };
208 | },
209 | });
210 | } catch (e: any) {
211 | expect(e.message).toBe("`translate-n` and `translate-plural` attributes must be used together: %{ n } car.");
212 | }
213 | });
214 |
215 | it("thrown errors if you forget to add a `translate-n` attribute", async () => {
216 | try {
217 | mount({
218 | template: '%{ n } car
',
219 | });
220 | } catch (e: any) {
221 | expect(e.message).toBe("`translate-n` and `translate-plural` attributes must be used together: %{ n } car.");
222 | }
223 | });
224 |
225 | it("supports conditional rendering such as v-if, v-else-if, v-else", async () => {
226 | const wrapper = mount({
227 | template: `
228 | Pending
229 | Hello %{ name }
230 | `,
231 | data() {
232 | return { show: true, name: "John Doe" };
233 | },
234 | });
235 | const vm = wrapper.vm as any;
236 | vm.$language.current = "en_US";
237 | await vm.$nextTick();
238 | expect(vm.$el.innerHTML).toEqual("Pending");
239 | vm.show = false;
240 | await vm.$nextTick();
241 | expect(vm.$el.innerHTML).toEqual("Hello John Doe");
242 | });
243 | });
244 |
245 | describe("translate component tests for interpolation", () => {
246 | it("goes up the parent chain of a nested component to evaluate `name`", async () => {
247 | const wrapper = mount({
248 | template: `Hello %{ name }
`,
249 | data() {
250 | return {
251 | name: "John Doe",
252 | };
253 | },
254 | components: {
255 | "inner-component": {
256 | template: `
`,
257 | },
258 | },
259 | });
260 | const vm = wrapper.vm as any;
261 | vm.$language.current = "fr_FR";
262 | await nextTick();
263 | expect(vm.$el.innerHTML.trim()).toEqual("Bonjour John Doe
");
264 | });
265 |
266 | it("goes up the parent chain of a nested component to evaluate `user.details.name`", async () => {
267 | const warnSpy = vi.spyOn(console, "warn");
268 | const wrapper = mount({
269 | template: `Hello %{ user.details.name }
`,
270 | data() {
271 | return {
272 | user: {
273 | details: {
274 | name: "Jane Doe",
275 | },
276 | },
277 | };
278 | },
279 | components: {
280 | "inner-component": {
281 | template: `
`,
282 | },
283 | },
284 | });
285 | const vm = wrapper.vm as any;
286 | vm.$language.current = "fr_FR";
287 | await nextTick();
288 | expect(vm.$el.innerHTML.trim()).toEqual("Bonjour Jane Doe
");
289 | expect(warnSpy).not.toHaveBeenCalled;
290 | warnSpy.mockRestore();
291 | });
292 |
293 | it("goes up the parent chain of 2 nested components to evaluate `user.details.name`", async () => {
294 | const warnSpy = vi.spyOn(console, "warn");
295 | const wrapper = mount({
296 | template: `Hello %{ user.details.name }
`,
297 | data() {
298 | return {
299 | user: {
300 | details: {
301 | name: "Jane Doe",
302 | },
303 | },
304 | };
305 | },
306 | components: {
307 | "first-component": {
308 | template: `
`,
309 | components: {
310 | "second-component": {
311 | template: ` `,
312 | },
313 | },
314 | },
315 | },
316 | });
317 | const vm = wrapper.vm as any;
318 | vm.$language.current = "fr_FR";
319 | await nextTick();
320 | expect(vm.$el.innerHTML.trim()).toEqual("Bonjour Jane Doe
");
321 | expect(console.warn).not.toHaveBeenCalled;
322 | warnSpy.mockRestore();
323 | });
324 | });
325 |
--------------------------------------------------------------------------------
/tests/config.test.ts:
--------------------------------------------------------------------------------
1 | import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "fs/promises";
2 | import { join } from "path";
3 | import { tmpdir } from "os";
4 | import { cwd } from "process";
5 | import { execSync } from "child_process";
6 |
7 | describe("config format tests", () => {
8 | type WithTempDirTest = (tmpDir: string) => Promise;
9 |
10 | const withTempDir = async (testFunc: WithTempDirTest) => {
11 | let tmpDir;
12 | try {
13 | tmpDir = await mkdtemp(join(tmpdir(), "vue3-gettext-"));
14 | await testFunc(tmpDir);
15 | } finally {
16 | if (tmpDir) {
17 | await rm(tmpDir, { recursive: true, force: true });
18 | }
19 | }
20 | };
21 |
22 | const testConfigWithExtract = async (tmpDir: string, config: string, configFileName: string, isModule: boolean) => {
23 | const packageJson = {
24 | name: "test",
25 | version: "0.0.1",
26 | type: isModule ? "module" : "commonjs",
27 | };
28 | for (const d of ["src", "scripts", "node_modules"]) {
29 | await symlink(join(cwd(), d), join(tmpDir, d));
30 | }
31 | await writeFile(join(tmpDir, "package.json"), JSON.stringify(packageJson));
32 | await writeFile(join(tmpDir, configFileName), config);
33 | await mkdir(join(tmpDir, "srctest", "lang"), { recursive: true });
34 | await writeFile(
35 | join(tmpDir, "srctest", "example.js"),
36 | `
37 | const { $gettext } = useGettext();
38 | $gettext('Translate me');
39 | `,
40 | );
41 | execSync(`sh -c 'cd ${tmpDir}; ts-node ./scripts/gettext_extract.ts'`);
42 | const appEnPo = (await readFile(join(tmpDir, "srctest", "lang", "en", "app.po"))).toString();
43 | const appEnPoLines = appEnPo.trim().split("\n");
44 | expect(appEnPoLines).toContain('msgid "Translate me"');
45 | expect(appEnPoLines[appEnPoLines.length - 1]).toEqual('msgstr "Translate me"');
46 | const appFrPo = (await readFile(join(tmpDir, "srctest", "lang", "fr", "app.po"))).toString();
47 | const appFrPoLines = appFrPo.trim().split("\n");
48 | expect(appFrPoLines).toContain('msgid "Translate me"');
49 | expect(appFrPoLines[appFrPoLines.length - 1]).toEqual('msgstr ""');
50 | };
51 |
52 | it("load a commonjs format", async () => {
53 | await withTempDir(
54 | async (tmpDir) =>
55 | await testConfigWithExtract(
56 | tmpDir,
57 | `
58 | module.exports = {
59 | input: {
60 | path: './srctest',
61 | },
62 | output: {
63 | path: './srctest/lang',
64 | locales: ['en', 'fr'],
65 | },
66 | };`,
67 | "gettext.config.js",
68 | false,
69 | ),
70 | );
71 | });
72 | it("load an ESM format", async () => {
73 | await withTempDir(
74 | async (tmpDir) =>
75 | await testConfigWithExtract(
76 | tmpDir,
77 | `
78 | export default {
79 | input: {
80 | path: './srctest',
81 | },
82 | output: {
83 | path: './srctest/lang',
84 | locales: ['en', 'fr'],
85 | },
86 | };`,
87 | "gettext.config.js",
88 | true,
89 | ),
90 | );
91 | });
92 | });
93 |
--------------------------------------------------------------------------------
/tests/directive.arabic.spec.ts:
--------------------------------------------------------------------------------
1 | import translations from "./json/directive.arabic";
2 | import { mountWithPlugin } from "./utils";
3 |
4 | const mount = mountWithPlugin({
5 | availableLanguages: {
6 | en_US: "American English",
7 | ar: "Arabic",
8 | },
9 | defaultLanguage: "ar",
10 | translations: translations,
11 | setGlobalProperties: true,
12 | });
13 |
14 | describe("translate arabic directive tests", () => {
15 | it("translates singular", () => {
16 | const vm = mount({
17 | template: "Orange
",
18 | data() {
19 | return { count: 1 };
20 | },
21 | }).vm;
22 | expect(vm.$el.innerHTML).toEqual("البرتقالي");
23 | });
24 |
25 | it("translates plural form 0", async () => {
26 | const count = 0;
27 | const vm = mount({
28 | template: '%{ count } day
',
29 | data() {
30 | return { count };
31 | },
32 | }).vm;
33 | await vm.$nextTick();
34 | expect(vm.$el.innerHTML).toEqual(`${count}أقل من يوم`);
35 | });
36 |
37 | it("translates plural form 1", () => {
38 | const count = 1;
39 | const vm = mount({
40 | template: '%{ count } day
',
41 | data() {
42 | return { count };
43 | },
44 | }).vm;
45 | expect(vm.$el.innerHTML).toEqual(`${count}يوم واحد`);
46 | });
47 |
48 | it("translates plural form 2", () => {
49 | const count = 2;
50 | const vm = mount({
51 | template: '%{ count } day
',
52 | data() {
53 | return { count };
54 | },
55 | }).vm;
56 | expect(vm.$el.innerHTML).toEqual(`${count}يومان`);
57 | });
58 |
59 | it("translates plural form 3", () => {
60 | const count = 9;
61 | const vm = mount({
62 | template: '%{ count } day
',
63 | data() {
64 | return { count };
65 | },
66 | }).vm;
67 | expect(vm.$el.innerHTML).toEqual(`${count} أيام`);
68 | });
69 |
70 | it("translates plural form 4", () => {
71 | const count = 11;
72 | const vm = mount({
73 | template: '%{ count } day
',
74 | data() {
75 | return { count };
76 | },
77 | }).vm;
78 | expect(vm.$el.innerHTML).toEqual(`${count} يومًا`);
79 | });
80 |
81 | it("translates plural form 5", async () => {
82 | const count = 3000;
83 | const vm = mount({
84 | template: '%{ count } day
',
85 | data() {
86 | return { count };
87 | },
88 | }).vm;
89 | expect(vm.$el.innerHTML).toEqual(`${count} يوم`);
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/tests/directive.spec.ts:
--------------------------------------------------------------------------------
1 | import translations from "./json/directive";
2 | import { mountWithPlugin } from "./utils";
3 |
4 | const mount = mountWithPlugin({
5 | availableLanguages: {
6 | en_US: "American English",
7 | fr_FR: "Français",
8 | },
9 | defaultLanguage: "en_US",
10 | translations: translations,
11 | silent: false,
12 | setGlobalProperties: true,
13 | });
14 |
15 | describe("translate directive tests", () => {
16 | it("works on empty strings", () => {
17 | const vm = mount({ template: "
" }).vm;
18 | expect(vm.$el.innerHTML).toEqual("");
19 | });
20 |
21 | it("returns an unchanged string when no translation is available for a language", () => {
22 | const warnSpy = vi.spyOn(console, "warn");
23 | const vm = mount({ template: "Unchanged string
" }).vm;
24 | (vm as any).$language.current = "fr_BE";
25 | expect(vm.$el.innerHTML).toEqual("Unchanged string");
26 | expect(warnSpy).toHaveBeenCalledTimes(1);
27 | warnSpy.mockRestore();
28 | });
29 |
30 | it("returns an unchanged string when no translation key is available", () => {
31 | const warnSpy = vi.spyOn(console, "warn");
32 | const vm = mount({ template: "Untranslated string
" }).vm;
33 | expect(vm.$el.innerHTML).toEqual("Untranslated string");
34 | expect(warnSpy).toHaveBeenCalledTimes(1);
35 | warnSpy.mockRestore();
36 | });
37 |
38 | it("translates known strings", async () => {
39 | const vm = mount({ template: "Pending
" }).vm;
40 | (vm as any).$language.current = "fr_FR";
41 | await vm.$nextTick();
42 | expect(vm.$el.innerHTML).toEqual("En cours");
43 | });
44 |
45 | it("translates known strings when surrounded by one or more tabs and spaces", async () => {
46 | const vm = mount({ template: "\tPending\t\t \t\r\n\t\f\v
" }).vm;
47 | (vm as any).$language.current = "fr_FR";
48 | await vm.$nextTick();
49 | expect(vm.$el.innerHTML).toEqual("En cours");
50 | });
51 |
52 | it("translates known strings according to a given translation context", async () => {
53 | let vm = mount({ template: 'Answer
' }).vm;
54 | (vm as any).$language.current = "en_US";
55 | await vm.$nextTick();
56 | expect(vm.$el.innerHTML).toEqual("Answer (verb)");
57 | vm = mount({ template: 'Answer
' }).vm;
58 | expect(vm.$el.innerHTML).toEqual("Answer (noun)");
59 | });
60 |
61 | it("works with text content", () => {
62 | const vm = mount({ template: "This is sparta!
" }).vm;
63 | expect(vm.$el.innerHTML).toEqual("This is sparta!");
64 | });
65 |
66 | it("works with HTML content", () => {
67 | const vm = mount({
68 | template: 'This is sparta !
',
69 | }).vm;
70 | expect(vm.$el.innerHTML).toEqual('This is sparta !');
71 | });
72 |
73 | it("allows interpolation", async () => {
74 | const vm = mount({
75 | template: "Hello %{ name }
",
76 | data() {
77 | return { name: "John Doe" };
78 | },
79 | }).vm;
80 | (vm as any).$language.current = "fr_FR";
81 | await vm.$nextTick();
82 | expect(vm.$el.innerHTML).toEqual("Bonjour John Doe ");
83 | });
84 |
85 | it("escapes HTML in variables by default", async () => {
86 | const vm = mount({
87 | template: "Hello %{ openingTag }%{ name }%{ closingTag }
",
88 | data() {
89 | return {
90 | name: "John Doe",
91 | openingTag: "",
92 | closingTag: " ",
93 | };
94 | },
95 | }).vm;
96 | (vm as any).$language.current = "fr_FR";
97 | await vm.$nextTick();
98 | expect(vm.$el.innerHTML).toEqual("Bonjour <b>John Doe</b>");
99 | });
100 |
101 | it("forces HTML rendering in variables (with the `render-html` attribute set to `true`)", async () => {
102 | const vm = mount({
103 | template: 'Hello %{ openingTag }%{ name }%{ closingTag }
',
104 | data() {
105 | return {
106 | name: "John Doe",
107 | openingTag: "",
108 | closingTag: " ",
109 | };
110 | },
111 | }).vm;
112 | (vm as any).$language.current = "fr_FR";
113 | await vm.$nextTick();
114 | expect(vm.$el.innerHTML).toEqual("Bonjour John Doe ");
115 | });
116 |
117 | it("allows interpolation with computed property", async () => {
118 | const vm = mount({
119 | template: "Hello %{ name }
",
120 | computed: {
121 | name() {
122 | return "John Doe";
123 | },
124 | },
125 | }).vm;
126 | (vm as any).$language.current = "fr_FR";
127 | await vm.$nextTick();
128 | expect(vm.$el.innerHTML).toEqual("Bonjour John Doe ");
129 | });
130 |
131 | it("allows custom params for interpolation", async () => {
132 | const vm = mount({
133 | template: 'Hello %{ name }
',
134 | data() {
135 | return {
136 | someNewNameVar: "John Doe",
137 | };
138 | },
139 | }).vm;
140 | (vm as any).$language.current = "fr_FR";
141 | await vm.$nextTick();
142 | expect(vm.$el.innerHTML.trim()).toEqual("Bonjour John Doe ");
143 | });
144 |
145 | it("allows interpolation within v-for with custom params", async () => {
146 | let names = ["John Doe", "Chester"];
147 | const vm = mount({
148 | template: 'Hello %{ name }
',
149 | data() {
150 | return {
151 | names,
152 | };
153 | },
154 | }).vm;
155 | (vm as any).$language.current = "fr_FR";
156 | await vm.$nextTick();
157 | const html = vm.$el.innerHTML.trim();
158 | const missedName = names.some((name) => {
159 | if (html.indexOf(name) === -1) {
160 | return true;
161 | }
162 | });
163 | expect(missedName).toEqual(false);
164 | });
165 |
166 | it("logs a warning in the console if translate-params is used", async () => {
167 | const warnSpy = vi.spyOn(console, "warn");
168 | const vm = mount({
169 | template: 'Hello %{ name }
',
170 | data() {
171 | return {
172 | someNewNameVar: "John Doe",
173 | };
174 | },
175 | }).vm;
176 | (vm as any).$language.current = "fr_FR";
177 | await vm.$nextTick();
178 | expect(vm.$el.innerHTML.trim()).toEqual("Bonjour name ");
179 | expect(console.warn).toHaveBeenCalled;
180 | warnSpy.mockRestore();
181 | });
182 |
183 | it("updates a translation after a data change", async () => {
184 | const vm = mount({
185 | template: 'Hello %{ name }
',
186 | data() {
187 | return { name: "John Doe" };
188 | },
189 | }).vm;
190 | (vm as any).$language.current = "fr_FR";
191 | await vm.$nextTick();
192 | expect(vm.$el.innerHTML).toEqual("Bonjour John Doe ");
193 | (vm as any).name = "Kenny";
194 | await vm.$nextTick();
195 | expect(vm.$el.innerHTML).toEqual("Bonjour Kenny ");
196 | });
197 |
198 | it("translates plurals", async () => {
199 | const vm = mount({
200 | template:
201 | '%{ count } car
',
202 | data() {
203 | return { count: 2 };
204 | },
205 | }).vm;
206 | (vm as any).$language.current = "fr_FR";
207 | await vm.$nextTick();
208 | expect(vm.$el.innerHTML).toEqual("2 véhicules");
209 | });
210 |
211 | it("translates plurals with computed property", async () => {
212 | const vm = mount({
213 | template:
214 | '%{ count } car
',
215 | computed: {
216 | count() {
217 | return 2;
218 | },
219 | },
220 | }).vm;
221 | (vm as any).$language.current = "fr_FR";
222 | await vm.$nextTick();
223 | expect(vm.$el.innerHTML).toEqual("2 véhicules");
224 | });
225 |
226 | it("updates a plural translation after a data change", async () => {
227 | const vm = mount({
228 | template:
229 | '%{ count } %{ brand } car
',
230 | data() {
231 | return { count: 1, brand: "Toyota" };
232 | },
233 | }).vm;
234 | (vm as any).$language.current = "fr_FR";
235 | await vm.$nextTick();
236 | expect(vm.$el.innerHTML).toEqual("1 Toyota véhicule");
237 | (vm as any).count = 8;
238 | await vm.$nextTick();
239 | expect(vm.$el.innerHTML).toEqual("8 Toyota véhicules");
240 | });
241 |
242 | it("updates a translation after a language change", async () => {
243 | const vm = mount({ template: "Pending
" }).vm;
244 | (vm as any).$language.current = "fr_FR";
245 | await vm.$nextTick();
246 | expect(vm.$el.innerHTML).toEqual("En cours");
247 | (vm as any).$language.current = "en_US";
248 | await vm.$nextTick();
249 | expect(vm.$el.innerHTML).toEqual("Pending");
250 | });
251 |
252 | it("supports conditional rendering such as v-if, v-else-if, v-else", async () => {
253 | const vm = mount({
254 | template: `
255 | Pending
256 | Hello %{ name }
257 | `,
258 | data() {
259 | return { show: true, name: "John Doe" };
260 | },
261 | }).vm;
262 | (vm as any).$language.current = "en_US";
263 | await vm.$nextTick();
264 | expect(vm.$el.innerHTML).toEqual("Pending");
265 | (vm as any).show = false;
266 | await vm.$nextTick();
267 | expect(vm.$el.innerHTML).toEqual("Hello John Doe ");
268 | });
269 | });
270 |
--------------------------------------------------------------------------------
/tests/interpolate.spec.ts:
--------------------------------------------------------------------------------
1 | import rawInterpolate from "../src/interpolate";
2 | import translations from "./json/translate";
3 | import { mountWithPlugin } from "./utils";
4 | import type { Language } from "../src/typeDefs";
5 |
6 | const mount = mountWithPlugin({
7 | translations: translations,
8 | silent: true,
9 | setGlobalProperties: true,
10 | });
11 |
12 | const wrapper = mount({ template: "
" });
13 | const plugin = wrapper.vm.$.appContext.config.globalProperties.$language as Language;
14 |
15 | const interpolate = rawInterpolate(plugin);
16 |
17 | describe("Interpolate tests", () => {
18 | it("without placeholders", () => {
19 | const msgid = "Foo bar baz";
20 | const interpolated = interpolate(msgid);
21 | expect(interpolated).toEqual("Foo bar baz");
22 | });
23 |
24 | it("with a placeholder", () => {
25 | const msgid = "Foo %{ placeholder } baz";
26 | const context = { placeholder: "bar" };
27 | const interpolated = interpolate(msgid, context);
28 | expect(interpolated).toEqual("Foo bar baz");
29 | });
30 |
31 | it("with HTML in var (should be escaped)", () => {
32 | const msgid = "Foo %{ placeholder } baz";
33 | const context = { placeholder: "bar
" };
34 | const interpolated = interpolate(msgid, context);
35 | expect(interpolated).toEqual("Foo <p>bar</p> baz");
36 | });
37 |
38 | it("with HTML in var (should NOT be escaped)", () => {
39 | const msgid = "Foo %{ placeholder } baz";
40 | const context = { placeholder: "bar
" };
41 | const disableHtmlEscaping = true;
42 | const interpolated = interpolate(msgid, context, disableHtmlEscaping, undefined);
43 | expect(interpolated).toEqual("Foo bar
baz");
44 | });
45 |
46 | it("with multiple spaces in the placeholder", () => {
47 | const msgid = "Foo %{ placeholder } baz";
48 | const context = { placeholder: "bar" };
49 | const interpolated = interpolate(msgid, context);
50 | expect(interpolated).toEqual("Foo bar baz");
51 | });
52 |
53 | it("with the same placeholder multiple times", () => {
54 | const msgid = "Foo %{ placeholder } baz %{ placeholder } foo";
55 | const context = { placeholder: "bar" };
56 | const interpolated = interpolate(msgid, context);
57 | expect(interpolated).toEqual("Foo bar baz bar foo");
58 | });
59 |
60 | it("with multiple placeholders", () => {
61 | const msgid = "%{foo}%{bar}%{baz}%{bar}%{foo}";
62 | const context = { foo: 1, bar: 2, baz: 3 };
63 | const interpolated = interpolate(msgid, context);
64 | expect(interpolated).toEqual("12321");
65 | });
66 |
67 | it("with new lines", () => {
68 | const msgid = "%{ \n \n\n\n\n foo} %{bar}!";
69 | const context = { foo: "Hello", bar: "world" };
70 | const interpolated = interpolate(msgid, context);
71 | expect(interpolated).toEqual("Hello world!");
72 | });
73 |
74 | it("with an object", () => {
75 | const msgid = "Foo %{ foo.bar } baz";
76 | const context = {
77 | foo: {
78 | bar: "baz",
79 | },
80 | };
81 | const interpolated = interpolate(msgid, context);
82 | expect(interpolated).toEqual("Foo baz baz");
83 | });
84 |
85 | it("with an array", () => {
86 | const msgid = "Foo %{ foo[1] } baz";
87 | const context = {
88 | foo: ["bar", "baz"],
89 | };
90 | const interpolated = interpolate(msgid, context);
91 | expect(interpolated).toEqual("Foo baz baz");
92 | });
93 |
94 | it("with a multi level object", () => {
95 | const msgid = "Foo %{ a.b.x } %{ a.c.y[1].title }";
96 | const context = {
97 | a: {
98 | b: {
99 | x: "foo",
100 | },
101 | c: {
102 | y: [{ title: "bar" }, { title: "baz" }],
103 | },
104 | },
105 | };
106 | const interpolated = interpolate(msgid, context);
107 | expect(interpolated).toEqual("Foo foo baz");
108 | });
109 |
110 | it("with a failing expression", () => {
111 | const msgid = 'Foo %{ alert("foobar") } baz';
112 | const context = {
113 | foo: "bar",
114 | };
115 | const warnSpy = vi.spyOn(console, "warn");
116 | interpolate(msgid, context);
117 | expect(warnSpy).toHaveBeenCalledTimes(1);
118 | expect(warnSpy).toHaveBeenCalledWith('Cannot evaluate expression: alert("foobar")');
119 | warnSpy.mockRestore();
120 | });
121 |
122 | it("should warn of the usage of mustache syntax", () => {
123 | const msgid = "Foo {{ foo }} baz";
124 | const context = {
125 | foo: "bar",
126 | };
127 | const warnSpy = vi.spyOn(console, "warn");
128 | interpolate(msgid, context);
129 | expect(warnSpy).not.toHaveBeenCalled;
130 | plugin.silent = false;
131 | interpolate(msgid, context);
132 | expect(warnSpy).toHaveBeenCalledTimes(1);
133 | warnSpy.mockRestore();
134 | });
135 | });
136 |
--------------------------------------------------------------------------------
/tests/json/component.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | en_US: {
3 | Answer: {
4 | Noun: "Answer (noun)",
5 | Verb: "Answer (verb)",
6 | },
7 | "Hello %{ name }": "Hello %{ name }",
8 | "Hello %{ user.details.name }": "Hello %{ user.details.name }",
9 | Pending: "Pending",
10 | "%{ count } car": ["1 car", "%{ count } cars"],
11 | "A lot of lines": "A lot of lines",
12 | },
13 | fr_FR: {
14 | Answer: {
15 | Noun: "Réponse (nom)",
16 | Verb: "Réponse (verbe)",
17 | },
18 | "Hello %{ name }": "Bonjour %{ name }",
19 | "Hello %{ user.details.name }": "Bonjour %{ user.details.name }",
20 | Pending: "En cours",
21 | "%{ count } car": ["1 véhicule", "%{ count } véhicules"],
22 | "A lot of lines": "Plein de lignes",
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/tests/json/directive.arabic.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | ar: {
3 | Orange: "البرتقالي",
4 | "%{ count } day": [
5 | "%{ count }أقل من يوم",
6 | "%{ count }يوم واحد",
7 | "%{ count }يومان",
8 | "%{ count } أيام",
9 | "%{ count } يومًا",
10 | "%{ count } يوم",
11 | ],
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/tests/json/directive.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | en_US: {
3 | Answer: {
4 | Noun: "Answer (noun)",
5 | Verb: "Answer (verb)",
6 | },
7 | "Hello %{ name } ": "Hello %{ name } ",
8 | "Hello %{ openingTag }%{ name }%{ closingTag }": "Hello %{ openingTag }%{ name }%{ closingTag }",
9 | Pending: "Pending",
10 | "%{ count } car": ["1 car", "%{ count } cars"],
11 | "%{ count } %{ brand } car": [
12 | "1 %{ brand } car",
13 | "%{ count } %{ brand } cars",
14 | ],
15 | },
16 | fr_FR: {
17 | Answer: {
18 | Noun: "Réponse (nom)",
19 | Verb: "Réponse (verbe)",
20 | },
21 | "Hello %{ openingTag }%{ name }%{ closingTag }": "Bonjour %{ openingTag }%{ name }%{ closingTag }",
22 | "Hello %{ name } ": "Bonjour %{ name } ",
23 | Pending: "En cours",
24 | "%{ count } car": ["1 véhicule", "%{ count } véhicules"],
25 | "%{ count } %{ brand } car": [
26 | "1 %{ brand } véhicule",
27 | "%{ count } %{ brand } véhicules",
28 | ],
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/tests/json/plugin.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | en_US: {
3 | Foo: "Foo en_US",
4 | },
5 | fr_FR: {
6 | Foo: "Foo fr_FR",
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/tests/json/translate.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | en_US: {
3 | Answer: {
4 | Noun: "Answer (noun)",
5 | Verb: "Answer (verb)",
6 | },
7 | "Hello %{ name }": "Hello %{ name }",
8 | May: "May",
9 | Pending: "Pending",
10 | "%{ carCount } car": ["1 car", "%{ carCount } cars"],
11 | "%{ carCount } car (noun)": {
12 | Noun: ["%{ carCount } car (noun)", "%{ carCount } cars (noun)"],
13 | },
14 | "%{ carCount } car (verb)": {
15 | Verb: ["%{ carCount } car (verb)", "%{ carCount } cars (verb)"],
16 | },
17 | "%{ carCount } car (multiple contexts)": {
18 | "": ["1 car", "%{ carCount } cars"],
19 | Context: ["1 car with context", "%{ carCount } cars with context"],
20 | },
21 | Object: {
22 | "": "Object",
23 | Context: "Object with context",
24 | },
25 |
26 | "%{ orangeCount } orange": ["", "%{ orangeCount } oranges"],
27 | "%{ appleCount } apple": ["1 apple", ""],
28 | },
29 | fr: {
30 | Answer: {
31 | Noun: "Réponse (nom)",
32 | Verb: "Réponse (verbe)",
33 | },
34 | "Hello %{ name }": "Bonjour %{ name }",
35 | May: "Pourrait",
36 | Pending: "En cours",
37 | "%{ carCount } car": ["1 véhicule", "%{ carCount } véhicules"],
38 | "%{ carCount } car (noun)": {
39 | Noun: ["%{ carCount } véhicule (nom)", "%{ carCount } véhicules (nom)"],
40 | },
41 | "%{ carCount } car (verb)": {
42 | Verb: ["%{ carCount } véhicule (verbe)", "%{ carCount } véhicules (verbe)"],
43 | },
44 | "%{ carCount } car (multiple contexts)": {
45 | "": ["1 véhicule", "%{ carCount } véhicules"],
46 | Context: ["1 véhicule avec contexte", "%{ carCount } véhicules avec contexte"],
47 | },
48 | Object: {
49 | "": "Objet",
50 | Context: "Objet avec contexte",
51 | },
52 | },
53 | };
54 |
--------------------------------------------------------------------------------
/tests/plurals.spec.ts:
--------------------------------------------------------------------------------
1 | import plurals from "../src/plurals";
2 |
3 | describe("Translate plurals tests", () => {
4 | it("plural form of singular english is 0", () => {
5 | expect(plurals.getTranslationIndex("en", 1)).toEqual(0);
6 | });
7 |
8 | it("plural form of plural english is 1", () => {
9 | expect(plurals.getTranslationIndex("en", 2)).toEqual(1);
10 | });
11 |
12 | it("plural form of Infinity in english is 1", () => {
13 | expect(plurals.getTranslationIndex("en", Infinity)).toEqual(1);
14 | });
15 |
16 | it("plural form of zero in english is 1", () => {
17 | expect(plurals.getTranslationIndex("en", 0)).toEqual(1);
18 | });
19 |
20 | it("plural form of singular dutch is 0", () => {
21 | expect(plurals.getTranslationIndex("nl", 1)).toEqual(0);
22 | });
23 |
24 | it("plural form of plural dutch is 1", () => {
25 | expect(plurals.getTranslationIndex("nl", 2)).toEqual(1);
26 | });
27 |
28 | it("plural form of zero in dutch is 1", () => {
29 | expect(plurals.getTranslationIndex("nl", 0)).toEqual(1);
30 | });
31 |
32 | it("plural form of singular french is 0", () => {
33 | expect(plurals.getTranslationIndex("fr", 1)).toEqual(0);
34 | });
35 |
36 | it("plural form of plural french is 1", () => {
37 | expect(plurals.getTranslationIndex("fr", 2)).toEqual(1);
38 | });
39 |
40 | it("plural form of zero in french is 0", () => {
41 | expect(plurals.getTranslationIndex("fr", 0)).toEqual(0);
42 | });
43 |
44 | it("plural form of 27 in arabic is 4", () => {
45 | expect(plurals.getTranslationIndex("ar", 27)).toEqual(4);
46 | });
47 |
48 | it("plural form of 23 in kashubian is 1", () => {
49 | expect(plurals.getTranslationIndex("csb", 23)).toEqual(1);
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/tests/translate.spec.ts:
--------------------------------------------------------------------------------
1 | import type { Language } from "../src/typeDefs";
2 | import translateRaw from "../src/translate";
3 | import { mountWithPlugin } from "./utils";
4 | import translations from "./json/translate";
5 |
6 | const mount = mountWithPlugin({
7 | availableLanguages: {
8 | en_US: "American English",
9 | fr_FR: "Français",
10 | },
11 | defaultLanguage: "en_US",
12 | sourceCodeLanguage: "en_US",
13 | translations: translations,
14 | setGlobalProperties: true,
15 | });
16 |
17 | const wrapper = mount({ template: "
" });
18 | let plugin = wrapper.vm.$.appContext.config.globalProperties.$language as Language;
19 | const setLanguage = (lang: string) => (plugin.current = lang);
20 |
21 | const translate = translateRaw(plugin);
22 |
23 | describe("Translate tests", () => {
24 | let translated;
25 | beforeEach(async () => {
26 | plugin = wrapper.vm.$.appContext.config.globalProperties.$language as Language;
27 | setLanguage("en_US");
28 | });
29 |
30 | it("tests the getTranslation() method", () => {
31 | translated = translate.getTranslation("", 1, null, null, "fr_FR");
32 | expect(translated).toEqual("");
33 |
34 | translated = translate.getTranslation("Unexisting language", null, null, null, "be_FR");
35 | expect(translated).toEqual("Unexisting language");
36 |
37 | translated = translate.getTranslation("Untranslated key", null, null, null, "fr_FR");
38 | expect(translated).toEqual("Untranslated key");
39 |
40 | translated = translate.getTranslation("Pending", 1, null, null, "fr_FR");
41 | expect(translated).toEqual("En cours");
42 |
43 | translated = translate.getTranslation("%{ carCount } car", 2, null, null, "fr_FR");
44 | expect(translated).toEqual("%{ carCount } véhicules");
45 |
46 | translated = translate.getTranslation("Answer", 1, "Verb", null, "fr_FR");
47 | expect(translated).toEqual("Réponse (verbe)");
48 |
49 | translated = translate.getTranslation("Answer", 1, "Noun", null, "fr_FR");
50 | expect(translated).toEqual("Réponse (nom)");
51 |
52 | translated = translate.getTranslation("Pending", 1, null, null, "en_US");
53 | expect(translated).toEqual("Pending");
54 |
55 | // If no translation exists, display the untranslated message (if n==1 then use singular form).
56 | translated = translate.getTranslation("Untranslated %{ n } item", 1, null, "Untranslated %{ n } items", "fr_FR");
57 | expect(translated).toEqual("Untranslated %{ n } item");
58 |
59 | // If no translation exists, display the untranslated message (if n!=1 then use plural form).
60 | translated = translate.getTranslation("Untranslated %{ n } item", 0, null, "Untranslated %{ n } items", "fr_FR");
61 | expect(translated).toEqual("Untranslated %{ n } items");
62 |
63 | // If no translation exists, display the untranslated message (if n!=1 then use plural form).
64 | translated = translate.getTranslation("Untranslated %{ n } item", 10, null, "Untranslated %{ n } items", "fr_FR");
65 | expect(translated).toEqual("Untranslated %{ n } items");
66 |
67 | // Test that it works when a msgid exists with and without a context, see #32.
68 | translated = translate.getTranslation("Object", null, null, null, "fr_FR");
69 | expect(translated).toEqual("Objet");
70 | translated = translate.getTranslation("Object", null, "Context", null, "fr_FR");
71 | expect(translated).toEqual("Objet avec contexte");
72 |
73 | // Ensure that pluralization is right in English when there are no English translations.
74 | translated = translate.getTranslation("Untranslated %{ n } item", 0, null, "Untranslated %{ n } items", "en_US");
75 | expect(translated).toEqual("Untranslated %{ n } items");
76 | translated = translate.getTranslation("Untranslated %{ n } item", 1, null, "Untranslated %{ n } items", "en_US");
77 | expect(translated).toEqual("Untranslated %{ n } item");
78 | translated = translate.getTranslation("Untranslated %{ n } item", 2, null, "Untranslated %{ n } items", "en_US");
79 | expect(translated).toEqual("Untranslated %{ n } items");
80 |
81 | // Ensure that pluralization does not fail if the translation have empty strings for singural or plural form.
82 | translated = translate.getTranslation("%{ orangeCount } orange", 1, null, "%{ orangeCount } oranges", "en_US");
83 | expect(translated).toEqual("%{ orangeCount } orange");
84 | translated = translate.getTranslation("%{ orangeCount } orange", 2, null, "%{ orangeCount } oranges", "en_US");
85 | expect(translated).toEqual("%{ orangeCount } oranges");
86 |
87 | translated = translate.getTranslation("%{ appleCount } apple", 1, null, "%{ appleCount } apples", "en_US");
88 | expect(translated).toEqual("1 apple");
89 | translated = translate.getTranslation("%{ appleCount } apple", 2, null, "%{ appleCount } apples", "en_US");
90 | expect(translated).toEqual("%{ appleCount } apples");
91 |
92 | // Test plural message with multiple contexts (default context and 'Context'')
93 | translated = translate.getTranslation("%{ carCount } car (multiple contexts)", 1, null, null, "en_US");
94 | expect(translated).toEqual("1 car");
95 | translated = translate.getTranslation("%{ carCount } car (multiple contexts)", 2, null, null, "en_US");
96 | expect(translated).toEqual("%{ carCount } cars");
97 | translated = translate.getTranslation("%{ carCount } car (multiple contexts)", 1, "Context", null, "en_US");
98 | expect(translated).toEqual("1 car with context");
99 | translated = translate.getTranslation("%{ carCount } car (multiple contexts)", 2, "Context", null, "en_US");
100 | expect(translated).toEqual("%{ carCount } cars with context");
101 | });
102 |
103 | it("tests the gettext() method", () => {
104 | const gettext = translate.gettext.bind(translate) as Language["$gettext"];
105 |
106 | setLanguage("fr_FR");
107 | expect(gettext("Pending")).toEqual("En cours");
108 |
109 | setLanguage("en_US");
110 | expect(gettext("Pending")).toEqual("Pending");
111 |
112 | expect(gettext("Interpolated: %{param1}", { param1: "success" })).toEqual("Interpolated: success");
113 | expect(gettext("Interpolated escaped: %{param1}", { param1: "success " })).toEqual(
114 | "Interpolated escaped: <b>success</b>",
115 | );
116 | expect(gettext("Interpolated unescaped: %{param1}", { param1: "success " }, true)).toEqual(
117 | "Interpolated unescaped: success ",
118 | );
119 | });
120 |
121 | it("tests the pgettext() method", () => {
122 | const undetectablePgettext = translate.pgettext.bind(translate) as Language["$pgettext"]; // Hide from gettext-extract.
123 |
124 | setLanguage("fr_FR");
125 | expect(undetectablePgettext("Noun", "Answer")).toEqual("Réponse (nom)");
126 |
127 | setLanguage("en_US");
128 | expect(undetectablePgettext("Noun", "Answer")).toEqual("Answer (noun)");
129 |
130 | expect(undetectablePgettext("ctx", "Interpolated: %{param1}", { param1: "success" })).toEqual(
131 | "Interpolated: success",
132 | );
133 | expect(undetectablePgettext("ctx", "Interpolated escaped: %{param1}", { param1: "success " })).toEqual(
134 | "Interpolated escaped: <b>success</b>",
135 | );
136 | expect(
137 | undetectablePgettext("ctx", "Interpolated unescaped: %{param1}", { param1: "success " }, true),
138 | ).toEqual("Interpolated unescaped: success ");
139 | });
140 |
141 | it("tests the ngettext() method", () => {
142 | const undetectableNgettext = translate.ngettext.bind(translate) as Language["$ngettext"]; // Hide from gettext-extract.
143 |
144 | setLanguage("fr_FR");
145 | expect(undetectableNgettext("%{ carCount } car", "%{ carCount } cars", 2)).toEqual("%{ carCount } véhicules");
146 |
147 | setLanguage("en_US");
148 | expect(undetectableNgettext("%{ carCount } car", "%{ carCount } cars", 2)).toEqual("%{ carCount } cars");
149 |
150 | // If no translation exists, display the untranslated message (if n==1 then use singular form).
151 | setLanguage("fr_FR");
152 | expect(undetectableNgettext("Untranslated %{ n } item", "Untranslated %{ n } items", 1)).toEqual(
153 | "Untranslated %{ n } item",
154 | );
155 |
156 | // If no translation exists, display the untranslated message (if n!=1 then use plural form).
157 | setLanguage("fr_FR");
158 | expect(undetectableNgettext("Untranslated %{ n } item", "Untranslated %{ n } items", -1)).toEqual(
159 | "Untranslated %{ n } items",
160 | );
161 |
162 | expect(
163 | undetectableNgettext("Interpolated: %{param1}", "Interpolated plural: %{param1}", 1, { param1: "success" }),
164 | ).toEqual("Interpolated: success");
165 | expect(
166 | undetectableNgettext("Interpolated escaped: %{param1}", "Interpolated escaped plural: %{param1}", 1, {
167 | param1: "success ",
168 | }),
169 | ).toEqual("Interpolated escaped: <b>success</b>");
170 | expect(
171 | undetectableNgettext(
172 | "Interpolated unescaped: %{param1}",
173 | "Interpolated unescaped plural: %{param1}",
174 | 1,
175 | { param1: "success " },
176 | true,
177 | ),
178 | ).toEqual("Interpolated unescaped: success ");
179 | });
180 |
181 | it("tests the npgettext() method", () => {
182 | const undetectableNpgettext = translate.npgettext.bind(translate) as Language["$npgettext"]; // Hide from gettext-extract
183 |
184 | setLanguage("fr_FR");
185 | expect(undetectableNpgettext("Noun", "%{ carCount } car (noun)", "%{ carCount } cars (noun)", 2)).toEqual(
186 | "%{ carCount } véhicules (nom)",
187 | );
188 |
189 | setLanguage("en_US");
190 | expect(undetectableNpgettext("Verb", "%{ carCount } car (verb)", "%{ carCount } cars (verb)", 2)).toEqual(
191 | "%{ carCount } cars (verb)",
192 | );
193 |
194 | setLanguage("fr_FR");
195 | expect(undetectableNpgettext("Noun", "%{ carCount } car (noun)", "%{ carCount } cars (noun)", 1)).toEqual(
196 | "%{ carCount } véhicule (nom)",
197 | );
198 |
199 | setLanguage("en_US");
200 | expect(undetectableNpgettext("Verb", "%{ carCount } car (verb)", "%{ carCount } cars (verb)", 1)).toEqual(
201 | "%{ carCount } car (verb)",
202 | );
203 |
204 | // If no translation exists, display the default singular form (if n < 2).
205 | setLanguage("fr_FR");
206 | expect(
207 | undetectableNpgettext("Noun", "Untranslated %{ n } item (noun)", "Untranslated %{ n } items (noun)", 1),
208 | ).toEqual("Untranslated %{ n } item (noun)");
209 |
210 | // If no translation exists, display the default plural form (if n > 1).
211 | setLanguage("fr_FR");
212 | expect(
213 | undetectableNpgettext("Noun", "Untranslated %{ n } item (noun)", "Untranslated %{ n } items (noun)", 2),
214 | ).toEqual("Untranslated %{ n } items (noun)");
215 |
216 | expect(
217 | undetectableNpgettext("ctx", "Interpolated: %{param1}", "Interpolated plural: %{param1}", 1, {
218 | param1: "success",
219 | }),
220 | ).toEqual("Interpolated: success");
221 | expect(
222 | undetectableNpgettext("ctx", "Interpolated escaped: %{param1}", "Interpolated escaped plural: %{param1}", 1, {
223 | param1: "success ",
224 | }),
225 | ).toEqual("Interpolated escaped: <b>success</b>");
226 | expect(
227 | undetectableNpgettext(
228 | "ctx",
229 | "Interpolated unescaped: %{param1}",
230 | "Interpolated unescaped plural: %{param1}",
231 | 1,
232 | { param1: "success " },
233 | true,
234 | ),
235 | ).toEqual("Interpolated unescaped: success ");
236 | });
237 |
238 | it("works when a msgid exists with and without a context, but the one with the context has not been translated", () => {
239 | expect(plugin.silent).toEqual(false);
240 | const warnSpy = vi.spyOn(console, "warn");
241 |
242 | translated = translate.getTranslation("May", null, null, null, "fr_FR");
243 | expect(translated).toEqual("Pourrait");
244 |
245 | translated = translate.getTranslation("May", null, "Month name", null, "fr_FR");
246 | expect(translated).toEqual("May");
247 |
248 | expect(warnSpy).toHaveBeenCalledTimes(1);
249 | expect(warnSpy).toHaveBeenCalledWith("Untranslated fr_FR key found: May (with context: Month name)");
250 |
251 | warnSpy.mockRestore();
252 | });
253 | });
254 |
--------------------------------------------------------------------------------
/tests/utils.ts:
--------------------------------------------------------------------------------
1 | import { createGettext } from "../src";
2 | import type { GetTextOptions } from "../src/typeDefs";
3 | import { mount } from "@vue/test-utils";
4 |
5 | export const mountWithPlugin = (pluginOptions: Partial) => (componentOptions: any) => {
6 | const gettext = createGettext(pluginOptions);
7 | return mount(componentOptions, {
8 | global: {
9 | plugins: [gettext],
10 | },
11 | });
12 | };
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "esnext",
5 | "sourceMap": false,
6 | "strict": true,
7 | "strictNullChecks": true,
8 | "esModuleInterop": true,
9 | "allowSyntheticDefaultImports": true,
10 | "moduleResolution": "Bundler",
11 | "skipLibCheck": true,
12 | "lib": ["DOM", "ES5", "ES2020.Symbol.WellKnown"],
13 | "rootDir": ".",
14 | "types": ["vitest/globals"]
15 | },
16 | "ts-node": {
17 | "compilerOptions": {
18 | "module": "commonjs"
19 | }
20 | },
21 | "exclude": ["node_modules", "**/node_modules/*"],
22 | "include": ["src/**/*", "scripts/**/*.ts", "tests/**/*.ts"]
23 | }
24 |
--------------------------------------------------------------------------------
/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "tsup";
2 |
3 | export default defineConfig([
4 | {
5 | entry: ["src/index.ts"],
6 | clean: true,
7 | dts: true,
8 | format: ["cjs", "esm"],
9 | },
10 | {
11 | entry: ["scripts/gettext_extract.ts"],
12 | clean: true,
13 | external: ["typescript"],
14 | format: ["cjs"],
15 | },
16 | {
17 | entry: ["scripts/gettext_compile.ts"],
18 | clean: true,
19 | format: ["cjs"],
20 | },
21 | ]);
22 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitest/config";
2 | import vue from "@vitejs/plugin-vue";
3 |
4 | export default defineConfig({
5 | plugins: [vue()],
6 | test: {
7 | globals: true,
8 | environment: "happy-dom",
9 | },
10 | });
11 |
--------------------------------------------------------------------------------