i18n-js 4.0.1 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,196 @@
1
+ import { readFileSync, statSync } from "fs";
2
+ import * as ts from "typescript";
3
+ import { glob } from "glob";
4
+
5
+ type ScopeInfo = {
6
+ type: "default" | "scope" | "base";
7
+ location: string;
8
+ base: string | null;
9
+ full: string;
10
+ scope: string;
11
+ };
12
+
13
+ function location(node: ts.Node, append: string = ":") {
14
+ const sourceFile = node.getSourceFile();
15
+ let { line, character } = sourceFile.getLineAndCharacterOfPosition(
16
+ node.getStart(sourceFile)
17
+ );
18
+
19
+ line += 1;
20
+ character += 1;
21
+ const file = sourceFile.fileName;
22
+ const location = `${file}:${line}:${character}`;
23
+
24
+ return `${location}${append}`;
25
+ }
26
+
27
+ const callExpressions = ["t", "i18n.t", "i18n.translate"];
28
+
29
+ function tsKind(node: ts.Node) {
30
+ const keys = Object.keys(ts.SyntaxKind);
31
+ const values = Object.values(ts.SyntaxKind);
32
+
33
+ return keys[values.indexOf(node.kind)];
34
+ }
35
+
36
+ function getTranslationScopesFromFile(filePath: string) {
37
+ const scopes: ScopeInfo[] = [];
38
+
39
+ const sourceFile = ts.createSourceFile(
40
+ filePath,
41
+ readFileSync(filePath).toString(),
42
+ ts.ScriptTarget.ES2015,
43
+ true
44
+ );
45
+
46
+ inspect(sourceFile);
47
+
48
+ return scopes;
49
+
50
+ function inspect(node: ts.Node) {
51
+ const next = () => {
52
+ ts.forEachChild(node, inspect);
53
+ };
54
+
55
+ if (node.kind !== ts.SyntaxKind.CallExpression) {
56
+ return next();
57
+ }
58
+
59
+ const expr = node.getChildAt(0).getText();
60
+ const text = JSON.stringify(node.getText(sourceFile));
61
+
62
+ if (!callExpressions.includes(expr)) {
63
+ return next();
64
+ }
65
+
66
+ const syntaxList = node.getChildAt(2);
67
+
68
+ if (!syntaxList.getText().trim()) {
69
+ return next();
70
+ }
71
+
72
+ const scopeNode = syntaxList.getChildAt(0) as ts.StringLiteral;
73
+ const optionsNode = syntaxList.getChildAt(2) as ts.ObjectLiteralExpression;
74
+
75
+ if (scopeNode.kind !== ts.SyntaxKind.StringLiteral) {
76
+ return next();
77
+ }
78
+
79
+ if (
80
+ optionsNode &&
81
+ optionsNode.kind !== ts.SyntaxKind.ObjectLiteralExpression
82
+ ) {
83
+ return next();
84
+ }
85
+
86
+ if (!optionsNode) {
87
+ scopes.push({
88
+ type: "scope",
89
+ scope: scopeNode.text,
90
+ base: null,
91
+ full: scopeNode.text,
92
+ location: location(node, ""),
93
+ });
94
+ return next();
95
+ }
96
+
97
+ scopes.push(...getScopes(scopeNode, optionsNode));
98
+ }
99
+
100
+ function mapProperties(node: ts.ObjectLiteralExpression): {
101
+ name: string;
102
+ value: ts.Node;
103
+ }[] {
104
+ return node.properties.map((p) => ({
105
+ name: (p.name as ts.Identifier).escapedText.toString(),
106
+ value: p.getChildAt(2),
107
+ }));
108
+ }
109
+
110
+ function getScopes(
111
+ scopeNode: ts.StringLiteral,
112
+ node: ts.ObjectLiteralExpression
113
+ ): ScopeInfo[] {
114
+ const suffix = scopeNode.text;
115
+
116
+ const result: ScopeInfo[] = [];
117
+ const properties = mapProperties(node);
118
+
119
+ if (
120
+ properties.length === 0 ||
121
+ !properties.some((p) => p.name === "scope")
122
+ ) {
123
+ result.push({
124
+ type: "scope",
125
+ scope: suffix,
126
+ base: null,
127
+ full: suffix,
128
+ location: location(scopeNode, ""),
129
+ });
130
+ }
131
+
132
+ properties.forEach((property) => {
133
+ if (
134
+ property.name === "scope" &&
135
+ property.value.kind === ts.SyntaxKind.StringLiteral
136
+ ) {
137
+ const base = (property.value as ts.StringLiteral).text;
138
+
139
+ result.push({
140
+ type: "base",
141
+ scope: suffix,
142
+ base,
143
+ full: `${base}.${suffix}`,
144
+ location: location(scopeNode, ""),
145
+ });
146
+ }
147
+
148
+ if (
149
+ property.name === "defaults" &&
150
+ property.value.kind === ts.SyntaxKind.ArrayLiteralExpression
151
+ ) {
152
+ const op = property.value as ts.ArrayLiteralExpression;
153
+ const values = op.getChildAt(1);
154
+ const objects = (
155
+ values
156
+ .getChildren()
157
+ .filter(
158
+ (n) => n.kind === ts.SyntaxKind.ObjectLiteralExpression
159
+ ) as ts.ObjectLiteralExpression[]
160
+ ).map(mapProperties);
161
+
162
+ objects.forEach((object) => {
163
+ object.forEach((prop) => {
164
+ if (
165
+ prop.name === "scope" &&
166
+ prop.value.kind === ts.SyntaxKind.StringLiteral
167
+ ) {
168
+ const text = (prop.value as ts.StringLiteral).text;
169
+
170
+ result.push({
171
+ type: "default",
172
+ scope: text,
173
+ base: null,
174
+ full: text,
175
+ location: location(prop.value, ""),
176
+ });
177
+ }
178
+ });
179
+ });
180
+ }
181
+ });
182
+
183
+ return result;
184
+ }
185
+ }
186
+
187
+ const patterns = (
188
+ process.argv[2] ??
189
+ "!(node_modules)/**/*.js:!(node_modules)/**/*.ts:!(node_modules)/**/*.jsx:!(node_modules)/**/*.tsx"
190
+ ).split(":");
191
+ const files = patterns.flatMap((pattern) => glob.sync(pattern));
192
+ const scopes = files
193
+ .filter((filePath) => statSync(filePath).isFile())
194
+ .flatMap((path) => getTranslationScopesFromFile(path));
195
+
196
+ console.log(JSON.stringify(scopes, null, 2));
@@ -8,6 +8,7 @@ module I18nJS
8
8
  def self.listen(
9
9
  config_file: Rails.root.join("config/i18n.yml"),
10
10
  locales_dir: Rails.root.join("config/locales"),
11
+ run_on_start: true,
11
12
  options: {}
12
13
  )
13
14
  return unless Rails.env.development?
@@ -19,7 +20,7 @@ module I18nJS
19
20
 
20
21
  self.started = true
21
22
 
22
- locales_dirs = Array(locales_dir)
23
+ locales_dirs = Array(locales_dir).map {|path| File.expand_path(path) }
23
24
 
24
25
  relative_paths =
25
26
  [config_file, *locales_dirs].map {|path| relative_path(path) }
@@ -27,7 +28,7 @@ module I18nJS
27
28
  debug("Watching #{relative_paths.inspect}")
28
29
 
29
30
  listener(config_file, locales_dirs.map(&:to_s), options).start
30
- I18nJS.call(config_file: config_file)
31
+ I18nJS.call(config_file: config_file) if run_on_start
31
32
  end
32
33
 
33
34
  def self.relative_path(path)
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "schema"
4
+
5
+ module I18nJS
6
+ def self.plugins
7
+ @plugins ||= []
8
+ end
9
+
10
+ def self.register_plugin(plugin)
11
+ plugins << plugin
12
+ plugin.setup
13
+ end
14
+
15
+ def self.plugin_files
16
+ Gem.find_files("i18n-js/*_plugin.rb")
17
+ end
18
+
19
+ def self.load_plugins!
20
+ plugin_files.each do |path|
21
+ require path
22
+ end
23
+ end
24
+
25
+ class Plugin
26
+ # This method is responsible for transforming the translations. The
27
+ # translations you'll receive may be already be filtered by other plugins
28
+ # and by the default filtering itself. If you need to access the original
29
+ # translations, use `I18nJS.translations`.
30
+ #
31
+ # Make sure you always check whether your plugin is active before
32
+ # transforming translations; otherwise, opting out transformation won't be
33
+ # possible.
34
+ def self.transform(translations:, config:) # rubocop:disable Lint/UnusedMethodArgument
35
+ translations
36
+ end
37
+
38
+ # In case your plugin accepts configuration, this is where you must validate
39
+ # the configuration, making sure only valid keys and type is provided.
40
+ # If the configuration contains invalid data, then you must raise an
41
+ # exception using something like
42
+ # `raise I18nJS::Schema::InvalidError, error_message`.
43
+ def self.validate_schema(config:)
44
+ end
45
+
46
+ # This method must set up the basic plugin configuration, like adding the
47
+ # config's root key in case your plugin accepts configuration (defined via
48
+ # the config file).
49
+ #
50
+ # If you don't add this key, the linter will prevent non-default keys from
51
+ # being added to the configuration file.
52
+ def self.setup
53
+ end
54
+
55
+ # This method is called whenever `I18nJS.call(**kwargs)` finishes exporting
56
+ # JSON files based on your configuration.
57
+ #
58
+ # You can use it to further process exported files, or generate new files
59
+ # based on the translations that have been exported.
60
+ #
61
+ # Make sure you always check whether your plugin is active before
62
+ # processing files; otherwise, opting out won't be possible.
63
+ def self.after_export(files:, config:)
64
+ end
65
+ end
66
+ end
@@ -4,14 +4,28 @@ module I18nJS
4
4
  class Schema
5
5
  InvalidError = Class.new(StandardError)
6
6
 
7
- ROOT_KEYS = %i[translations check].freeze
8
- REQUIRED_ROOT_KEYS = %i[translations].freeze
9
- REQUIRED_CHECK_KEYS = %i[ignore].freeze
7
+ REQUIRED_LINT_TRANSLATIONS_KEYS = %i[ignore].freeze
8
+ REQUIRED_LINT_SCRIPTS_KEYS = %i[ignore patterns].freeze
10
9
  REQUIRED_TRANSLATION_KEYS = %i[file patterns].freeze
11
10
  TRANSLATION_KEYS = %i[file patterns].freeze
12
11
 
12
+ def self.root_keys
13
+ @root_keys ||= Set.new(%i[
14
+ translations
15
+ lint_translations
16
+ lint_scripts
17
+ check
18
+ ])
19
+ end
20
+
21
+ def self.required_root_keys
22
+ @required_root_keys ||= Set.new(%i[translations])
23
+ end
24
+
13
25
  def self.validate!(target)
14
- new(target).validate!
26
+ schema = new(target)
27
+ schema.validate!
28
+ I18nJS.plugins.each {|plugin| plugin.validate_schema(config: target) }
15
29
  end
16
30
 
17
31
  attr_reader :target
@@ -23,20 +37,36 @@ module I18nJS
23
37
  def validate!
24
38
  expect_type(:root, target, Hash, target)
25
39
 
26
- expect_required_keys(REQUIRED_ROOT_KEYS, target)
27
- reject_extraneous_keys(ROOT_KEYS, target)
40
+ expect_required_keys(self.class.required_root_keys, target)
41
+ reject_extraneous_keys(self.class.root_keys, target)
28
42
  validate_translations
29
- validate_check
43
+ validate_lint_translations
44
+ validate_lint_scripts
30
45
  end
31
46
 
32
- def validate_check
33
- return unless target.key?(:check)
47
+ def validate_lint_translations
48
+ key = :lint_translations
34
49
 
35
- check = target[:check]
50
+ return unless target.key?(key)
36
51
 
37
- expect_type(:check, check, Hash, target)
38
- expect_required_keys(REQUIRED_CHECK_KEYS, check)
39
- expect_type(:ignore, check[:ignore], Array, check)
52
+ config = target[key]
53
+
54
+ expect_type(key, config, Hash, target)
55
+ expect_required_keys(REQUIRED_LINT_TRANSLATIONS_KEYS, config)
56
+ expect_type(:ignore, config[:ignore], Array, config)
57
+ end
58
+
59
+ def validate_lint_scripts
60
+ key = :lint_scripts
61
+
62
+ return unless target.key?(key)
63
+
64
+ config = target[key]
65
+
66
+ expect_type(key, config, Hash, target)
67
+ expect_required_keys(REQUIRED_LINT_SCRIPTS_KEYS, config)
68
+ expect_type(:ignore, config[:ignore], Array, config)
69
+ expect_type(:patterns, config[:patterns], Array, config)
40
70
  end
41
71
 
42
72
  def validate_translations
@@ -63,6 +93,15 @@ module I18nJS
63
93
  raise InvalidError, "#{error_message}#{node_json}"
64
94
  end
65
95
 
96
+ def expect_enabled_config(config_key, value)
97
+ return if [TrueClass, FalseClass].include?(value.class)
98
+
99
+ actual_type = value.class
100
+
101
+ reject "Expected #{config_key}.enabled to be a boolean; " \
102
+ "got #{actual_type} instead"
103
+ end
104
+
66
105
  def expect_type(attribute, value, expected_type, payload)
67
106
  return if value.is_a?(expected_type)
68
107
 
@@ -94,7 +133,7 @@ module I18nJS
94
133
 
95
134
  def reject_extraneous_keys(allowed_keys, value)
96
135
  keys = value.keys.map(&:to_sym)
97
- extraneous = keys - allowed_keys
136
+ extraneous = keys.to_a - allowed_keys.to_a
98
137
 
99
138
  return if extraneous.empty?
100
139
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module I18nJS
4
- VERSION = "4.0.1"
4
+ VERSION = "4.2.0"
5
5
  end
data/lib/i18n-js.rb CHANGED
@@ -6,9 +6,13 @@ require "yaml"
6
6
  require "glob"
7
7
  require "fileutils"
8
8
  require "optparse"
9
+ require "erb"
10
+ require "set"
11
+ require "digest/md5"
9
12
 
10
13
  require_relative "i18n-js/schema"
11
14
  require_relative "i18n-js/version"
15
+ require_relative "i18n-js/plugin"
12
16
 
13
17
  module I18nJS
14
18
  MissingConfigError = Class.new(StandardError)
@@ -19,19 +23,31 @@ module I18nJS
19
23
  "you must set either `config_file` or `config`"
20
24
  end
21
25
 
22
- config = Glob::SymbolizeKeys.call(config || YAML.load_file(config_file))
26
+ load_plugins!
27
+
28
+ config = Glob::SymbolizeKeys.call(config || load_config_file(config_file))
29
+
23
30
  Schema.validate!(config)
24
31
  exported_files = []
25
32
 
26
33
  config[:translations].each do |group|
27
- exported_files += export_group(group)
34
+ exported_files += export_group(group, config)
35
+ end
36
+
37
+ plugins.each do |plugin|
38
+ plugin.after_export(files: exported_files.dup, config: config)
28
39
  end
29
40
 
30
41
  exported_files
31
42
  end
32
43
 
33
- def self.export_group(group)
44
+ def self.export_group(group, config)
34
45
  filtered_translations = Glob.filter(translations, group[:patterns])
46
+ filtered_translations =
47
+ plugins.reduce(filtered_translations) do |buffer, plugin|
48
+ plugin.transform(translations: buffer, config: config)
49
+ end
50
+
35
51
  output_file_path = File.expand_path(group[:file])
36
52
  exported_files = []
37
53
 
@@ -70,4 +86,9 @@ module I18nJS
70
86
  translations
71
87
  end
72
88
  end
89
+
90
+ def self.load_config_file(config_file)
91
+ erb = ERB.new(File.read(config_file))
92
+ YAML.safe_load(erb.result(binding))
93
+ end
73
94
  end
data/package.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "dependencies": {
3
+ "esbuild": "^0.16.2",
4
+ "glob": "^8.0.3",
5
+ "typescript": "^4.9.4"
6
+ },
7
+ "scripts": {
8
+ "compile": "esbuild lib/i18n-js/lint.ts --bundle --platform=node --outfile=lib/i18n-js/lint.js --target=node16"
9
+ }
10
+ }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: i18n-js
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.1
4
+ version: 4.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nando Vieira
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-08-25 00:00:00.000000000 Z
11
+ date: 2022-12-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: glob
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: 0.4.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: 0.4.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: i18n
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: mocha
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: pry-meta
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -176,6 +190,7 @@ files:
176
190
  - MIGRATING_FROM_V3_TO_V4.md
177
191
  - README.md
178
192
  - Rakefile
193
+ - bin/release
179
194
  - exe/i18n
180
195
  - i18n-js.gemspec
181
196
  - lib/guard/i18n-js.rb
@@ -187,11 +202,20 @@ files:
187
202
  - lib/i18n-js/cli/command.rb
188
203
  - lib/i18n-js/cli/export_command.rb
189
204
  - lib/i18n-js/cli/init_command.rb
205
+ - lib/i18n-js/cli/lint_scripts_command.rb
206
+ - lib/i18n-js/cli/lint_translations_command.rb
207
+ - lib/i18n-js/cli/plugins_command.rb
190
208
  - lib/i18n-js/cli/ui.rb
191
209
  - lib/i18n-js/cli/version_command.rb
210
+ - lib/i18n-js/embed_fallback_translations_plugin.rb
211
+ - lib/i18n-js/export_files_plugin.rb
212
+ - lib/i18n-js/lint.js
213
+ - lib/i18n-js/lint.ts
192
214
  - lib/i18n-js/listen.rb
215
+ - lib/i18n-js/plugin.rb
193
216
  - lib/i18n-js/schema.rb
194
217
  - lib/i18n-js/version.rb
218
+ - package.json
195
219
  homepage: https://github.com/fnando/i18n-js
196
220
  licenses:
197
221
  - MIT
@@ -199,10 +223,10 @@ metadata:
199
223
  rubygems_mfa_required: 'true'
200
224
  homepage_uri: https://github.com/fnando/i18n-js
201
225
  bug_tracker_uri: https://github.com/fnando/i18n-js/issues
202
- source_code_uri: https://github.com/fnando/i18n-js/tree/v4.0.1
203
- changelog_uri: https://github.com/fnando/i18n-js/tree/v4.0.1/CHANGELOG.md
204
- documentation_uri: https://github.com/fnando/i18n-js/tree/v4.0.1/README.md
205
- license_uri: https://github.com/fnando/i18n-js/tree/v4.0.1/LICENSE.md
226
+ source_code_uri: https://github.com/fnando/i18n-js/tree/v4.2.0
227
+ changelog_uri: https://github.com/fnando/i18n-js/tree/v4.2.0/CHANGELOG.md
228
+ documentation_uri: https://github.com/fnando/i18n-js/tree/v4.2.0/README.md
229
+ license_uri: https://github.com/fnando/i18n-js/tree/v4.2.0/LICENSE.md
206
230
  post_install_message:
207
231
  rdoc_options: []
208
232
  require_paths:
@@ -218,7 +242,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
218
242
  - !ruby/object:Gem::Version
219
243
  version: '0'
220
244
  requirements: []
221
- rubygems_version: 3.3.7
245
+ rubygems_version: 3.3.26
222
246
  signing_key:
223
247
  specification_version: 4
224
248
  summary: Export i18n translations and use them on JavaScript.