diff --git a/lua/js-i18n/analyzer.lua b/lua/js-i18n/analyzer.lua index f30a32b..e70600e 100644 --- a/lua/js-i18n/analyzer.lua +++ b/lua/js-i18n/analyzer.lua @@ -164,6 +164,7 @@ function M.get_key_at_cursor(bufnr, position) end --- @class GetTDetail +--- @field t_func_name string --- @field namespace string --- @field key_prefix string --- @field scope_node TSNode|nil @@ -174,13 +175,16 @@ end --- @param query vim.treesitter.Query クエリ --- @return GetTDetail|nil local function parse_get_t(target_node, source, query) + local t_func_name = nil local namespace = "" local key_prefix = "" for id, node, _ in query:iter_captures(target_node, source, 0, -1) do local name = query.captures[id] - if name == "i18n.namespace" then + if name == "i18n.t_func_name" then + t_func_name = t_func_name or vim.treesitter.get_node_text(node, source) + elseif name == "i18n.namespace" then namespace = vim.treesitter.get_node_text(node, source) elseif name == "i18n.key_prefix" then key_prefix = vim.treesitter.get_node_text(node, source) @@ -190,6 +194,7 @@ local function parse_get_t(target_node, source, query) local scope_node = M.find_closest_node(target_node, { "statement_block", "jsx_element" }) return { + t_func_name = t_func_name, namespace = namespace, key_prefix = key_prefix, scope_node = scope_node, @@ -197,6 +202,7 @@ local function parse_get_t(target_node, source, query) end --- @class CallTDetail +--- @field t_func_name string --- @field key string --- @field key_node TSNode --- @field key_arg_node TSNode @@ -209,6 +215,7 @@ end --- @param query vim.treesitter.Query クエリ --- @return CallTDetail|nil local function parse_call_t(target_node, source, query) + local t_func_name = nil local key = nil local key_node = nil local key_arg_node = nil @@ -220,7 +227,9 @@ local function parse_call_t(target_node, source, query) -- t関数の呼び出しがネストしている場合があるため、最初に見つかったものを採用する -- そのため key = ke or ... のような形にしている - if name == "i18n.key" then + if name == "i18n.t_func_name" then + t_func_name = t_func_name or vim.treesitter.get_node_text(node, source) + elseif name == "i18n.key" then key = key or vim.treesitter.get_node_text(node, source) key_node = key_node or node elseif name == "i18n.key_arg" then @@ -237,6 +246,7 @@ local function parse_call_t(target_node, source, query) end return { + t_func_name = t_func_name, key = key, key_node = key_node, key_arg_node = key_arg_node, @@ -308,20 +318,36 @@ function M.find_call_t_expressions(source, lib, lang) local query = vim.treesitter.query.parse(language, query_str) - --- @type GetTDetail[] + --- @type table local scope_stack = {} + local function preprocess_t_func_name_for_scope(t_func_name) + if lib == utils.Library.I18Next then + return t_func_name + elseif lib == utils.Library.NextIntl then + return vim.split(t_func_name, ".", { plain = true })[1] + end + + return t_func_name + end + --- @param value GetTDetail local function enter_scope(value) - table.insert(scope_stack, value) + local t_func_name = value.t_func_name or "t" + scope_stack[t_func_name] = scope_stack[t_func_name] or {} + table.insert(scope_stack[t_func_name], value) end - local function leave_scope() - table.remove(scope_stack) + --- @param t_func_name string + local function leave_scope(t_func_name) + table.remove(scope_stack[t_func_name or "t"]) end - local function current_scope() - return scope_stack[#scope_stack] + --- @param t_func_name string + local function current_scope(t_func_name) + local t_func_name = preprocess_t_func_name_for_scope(t_func_name) + local stack = scope_stack[t_func_name or "t"] or {} + return stack[#stack] or { namespace = "", key_prefix = "", @@ -329,6 +355,34 @@ function M.find_call_t_expressions(source, lib, lang) } end + local function is_t_func(t_func_name) + if t_func_name == "t" or scope_stack[t_func_name] ~= nil then + return true + end + + if lib == utils.Library.I18Next then + if t_func_name == "i18next.t" then + return true + end + elseif lib == utils.Library.NextIntl then + -- {t_func_name}.rich や {t_func_name}.markup などの形式も考慮する + local split = vim.split(t_func_name, ".", { plain = true }) + local name = split[1] + local member = split[2] + + local allow_members = { + ["rich"] = true, + ["markup"] = true, + ["raw"] = true, + } + if (name == "t" or scope_stack[name] ~= nil) and allow_members[member] then + return true + end + end + + return false + end + --- @type FindTExpressionResultItem[] local result = {} @@ -336,28 +390,31 @@ function M.find_call_t_expressions(source, lib, lang) local name = query.captures[id] -- 現在のスコープから抜けたかどうかを判定する - local current_scope_node = current_scope().scope_node or root_node - if node:start() > current_scope_node:end_() or node:end_() < current_scope_node:start() then - leave_scope() + for t_func_name in pairs(scope_stack) do + local current_scope_node = current_scope(t_func_name).scope_node or root_node + if node:start() > current_scope_node:end_() or node:end_() < current_scope_node:start() then + leave_scope(t_func_name) + end end if name == "i18n.get_t" then local get_t_detail = parse_get_t(node, source, query) if get_t_detail then -- 同一のスコープ内で get_t が呼ばれた場合はスコープを上書きする形になるように、一度 leave_scope してから enter_scope する - if get_t_detail.scope_node == current_scope().scope_node then - leave_scope() + if get_t_detail.scope_node == current_scope(get_t_detail.t_func_name).scope_node then + leave_scope(get_t_detail.t_func_name) end enter_scope(get_t_detail) end elseif name == "i18n.call_t" then - local scope = current_scope() local call_t_detail = parse_call_t(node, source, query) - if call_t_detail == nil then + if call_t_detail == nil or not is_t_func(call_t_detail.t_func_name) then goto continue end + local scope = current_scope(call_t_detail.t_func_name) + local key_prefix = call_t_detail.key_prefix or scope.key_prefix local key = call_t_detail.key if key_prefix ~= "" then diff --git a/queries/i18next.scm b/queries/i18next.scm index 3802120..d0ab2d7 100644 --- a/queries/i18next.scm +++ b/queries/i18next.scm @@ -1,45 +1,48 @@ ;; getFixedT 関数呼び出し -(call_expression - function: [ - (identifier) - (member_expression) - ] @get_fixed_t_func (#match? @get_fixed_t_func "getFixedT$") - ;; 1: lang, 2: ns, 3: keyPrefix - arguments: (arguments - ( - [ - (string (string_fragment)) - (undefined) - (null) - ] - )? - ( - [ - (string (string_fragment) @i18n.namespace) - (undefined) - (null) - ] - )? - ( - [ - (string (string_fragment) @i18n.key_prefix) - (undefined) - (null) - ] - )? - ) +(variable_declarator + name: (identifier) @i18n.t_func_name + value: + (call_expression + function: [ + (identifier) + (member_expression) + ] @get_fixed_t_func (#match? @get_fixed_t_func "getFixedT$") + ;; 1: lang, 2: ns, 3: keyPrefix + arguments: (arguments + ( + [ + (string (string_fragment)) + (undefined) + (null) + ] + )? + ( + [ + (string (string_fragment) @i18n.namespace) + (undefined) + (null) + ] + )? + ( + [ + (string (string_fragment) @i18n.key_prefix) + (undefined) + (null) + ] + )? + ) + ) ) @i18n.get_t - ;; t 関数呼び出し (call_expression function: [ (identifier) (member_expression) - ] @t_func (#match? @t_func "^(i18next\.)?t$") - arguments: (arguments - (string - (string_fragment) @i18n.key - ) @i18n.key_arg - ) + ] @i18n.t_func_name + arguments: (arguments + (string + (string_fragment) @i18n.key + ) @i18n.key_arg + ) ) @i18n.call_t diff --git a/queries/next-intl.scm b/queries/next-intl.scm index 1df5b39..7187a24 100644 --- a/queries/next-intl.scm +++ b/queries/next-intl.scm @@ -1,12 +1,16 @@ ;; useTranslations 関数呼び出し -(call_expression - function: (identifier) @use_translations (#eq? @use_translations "useTranslations") - arguments: (arguments - [ - (string (string_fragment) @i18n.key_prefix) - (undefined) - ]? - ) +(variable_declarator + name: (identifier) @i18n.t_func_name + value: + (call_expression + function: (identifier) @use_translations (#eq? @use_translations "useTranslations") + arguments: (arguments + [ + (string (string_fragment) @i18n.key_prefix) + (undefined) + ]? + ) + ) ) @i18n.get_t ;; t 関数呼び出し @@ -14,7 +18,7 @@ function: [ (identifier) (member_expression) - ] @t_func (#match? @t_func "^t(\.rich|\.markup|\.raw)?$") + ] @i18n.t_func_name arguments: (arguments (string (string_fragment) @i18n.key diff --git a/queries/react-i18next.scm b/queries/react-i18next.scm index 7800fea..cfe50d7 100644 --- a/queries/react-i18next.scm +++ b/queries/react-i18next.scm @@ -1,23 +1,35 @@ ;; useTranslation 関数呼び出し -(call_expression - function: (identifier) @use_translation (#eq? @use_translation "useTranslation") - arguments: (arguments +(variable_declarator + name: (object_pattern [ - (string (string_fragment) @i18n.namespace) - (undefined) - ]? - (object - (pair - key: (property_identifier) @key_prefix_key (#eq? @key_prefix_key "keyPrefix") - value: (string (string_fragment) @i18n.key_prefix) - )? - )? - ) + (pair_pattern + key: (property_identifier) @use_translation_t (#eq? @use_translation_t "t") + value: (identifier) @i18n.t_func_name + ) + (shorthand_property_identifier_pattern) @i18n.t_func_name + ] + ) + value: + (call_expression + function: (identifier) @use_translation (#eq? @use_translation "useTranslation") + arguments: (arguments + [ + (string (string_fragment) @i18n.namespace) + (undefined) + ]? + (object + (pair + key: (property_identifier) @key_prefix_key (#eq? @key_prefix_key "keyPrefix") + value: (string (string_fragment) @i18n.key_prefix) + )? + )? + ) + ) ) @i18n.get_t ;; Translation コンポーネント -( - jsx_opening_element +(jsx_element + open_tag: (jsx_opening_element name: (identifier) @translation (#eq? @translation "Translation") attribute: (jsx_attribute (property_identifier) @key_prefix_attr (#eq? @key_prefix_attr "keyPrefix") @@ -28,6 +40,17 @@ ) ] )? + ) + (jsx_expression + [ + (arrow_function + parameters: (formal_parameters (_) @i18n.t_func_name) + ) + (function_expression + parameters: (formal_parameters (_) @i18n.t_func_name) + ) + ] + ) ) @i18n.get_t ;; Trans コンポーネント @@ -45,7 +68,9 @@ ) attribute: (jsx_attribute (property_identifier) @attr_t (#eq? @attr_t "t") - (_) + (jsx_expression + (identifier) @i18n.t_func_name + ) ) ) @i18n.call_t ( @@ -62,6 +87,8 @@ ) attribute: (jsx_attribute (property_identifier) @attr_t (#eq? @attr_t "t") - (_) + (jsx_expression + (identifier) @i18n.t_func_name + ) ) ) @i18n.call_t diff --git a/tests/js-i18n/analyzer_spec.lua b/tests/js-i18n/analyzer_spec.lua index ba92c4b..374c1be 100644 --- a/tests/js-i18n/analyzer_spec.lua +++ b/tests/js-i18n/analyzer_spec.lua @@ -196,6 +196,18 @@ describe("analyzer.find_call_t_expressions", function() }) end) + test_analyze_file(get_project, "multiple_t_functions.jsx", function(_, utils) + -- see: tests/projects/i18next/test_analyzer/multiple_t_functions.jsx + utils.assert_items({ + -- stylua: ignore start + { key = "t-prefix.key", key_prefix = "t-prefix", key_arg = "key" }, + { key = "t2-prefix.key", key_prefix = "t2-prefix", key_arg = "key" }, + { key = "t-prefix.key", key_prefix = "t-prefix", key_arg = "key" }, + { key = "t2-prefix.key", key_prefix = "t2-prefix", key_arg = "key" }, + -- stylua: ignore end + }) + end) + -- These should be found local tests = { { text = "t('key')" }, @@ -277,6 +289,20 @@ describe("analyzer.find_call_t_expressions", function() }) end) + test_analyze_file(get_project, "multiple_t_functions.jsx", function(_, utils) + -- see: tests/projects/next-intl/test_analyzer/multiple_t_functions.jsx + utils.assert_items({ + -- stylua: ignore start + { key = "t1-prefix.key", key_prefix = "t1-prefix", key_arg = "key" }, + { key = "t2-prefix.key", key_prefix = "t2-prefix", key_arg = "key" }, + { key = "t1-prefix.key", key_prefix = "t1-prefix", key_arg = "key" }, + { key = "t2-prefix.key", key_prefix = "t2-prefix", key_arg = "key" }, + { key = "t1-prefix.key", key_prefix = "t1-prefix", key_arg = "key" }, + { key = "t2-prefix.key", key_prefix = "t2-prefix", key_arg = "key" }, + -- stylua: ignore end + }) + end) + -- These should be found local tests = { { text = "t('key')" }, diff --git a/tests/projects/i18next/test_analyzer/multiple_t_functions.jsx b/tests/projects/i18next/test_analyzer/multiple_t_functions.jsx new file mode 100644 index 0000000..e97f98f --- /dev/null +++ b/tests/projects/i18next/test_analyzer/multiple_t_functions.jsx @@ -0,0 +1,27 @@ +import { useTranslation } from "react-i18next"; + +export const Component1 = () => { + const { t } = useTranslation("translation", { keyPrefix: "t-prefix" }); + const { t: t2 } = useTranslation("translation", { keyPrefix: "t2-prefix" }); + + return ( +
+ {t("key")} + {t2("key")} +
+ ); +}; + +export const Component2 = () => { + const { t } = useTranslation("translation", { keyPrefix: "t-prefix" }); + + const InnerComponent = () => { + const { t: t2 } = useTranslation("translation", { keyPrefix: "t2-prefix" }); + return ( +
+ {t("key")} + {t2("key")} +
+ ); + }; +} diff --git a/tests/projects/next-intl/test_analyzer/key_prefix.jsx b/tests/projects/next-intl/test_analyzer/key_prefix.jsx index fd8d749..58b3105 100644 --- a/tests/projects/next-intl/test_analyzer/key_prefix.jsx +++ b/tests/projects/next-intl/test_analyzer/key_prefix.jsx @@ -1,7 +1,7 @@ import { useTranslations } from "next-intl"; export const Component1 = () => { - const { t } = useTranslations(); + const t = useTranslations(); return (
@@ -12,13 +12,13 @@ export const Component1 = () => { }; export const Component2 = () => { - const { t } = useTranslations("prefix-1"); + const t = useTranslations("prefix-1"); const key = t("prefix-1-key-1"); // This key prefix is "prefix-1". Because, it is provided in the most recent useTranslations. const InnerComponent1 = () => { - const { t } = useTranslations(); + const t = useTranslations(); return (
{t("no-prefix-key-2")} @@ -28,7 +28,7 @@ export const Component2 = () => { }; const InnerComponent2 = () => { - const { t } = useTranslations("prefix-2"); + const t = useTranslations("prefix-2"); return (
{t("prefix-2-key-1")} diff --git a/tests/projects/next-intl/test_analyzer/multiple_t_functions.jsx b/tests/projects/next-intl/test_analyzer/multiple_t_functions.jsx new file mode 100644 index 0000000..bbe94dc --- /dev/null +++ b/tests/projects/next-intl/test_analyzer/multiple_t_functions.jsx @@ -0,0 +1,39 @@ +import { useTranslations } from "next-intl"; + +export const Component1 = () => { + const t1 = useTranslations('t1-prefix'); + const t2 = useTranslations('t2-prefix'); + + return ( +
+ {t1("key")} + {t2("key")} +
+ ); +}; + +export const Component2 = () => { + const t1 = useTranslations('t1-prefix'); + + const InnerComponent = () => { + const t2 = useTranslations('t2-prefix'); + return ( +
+ {t1("key")} + {t2("key")} +
+ ); + }; +}; + +export const Component3 = () => { + const t1 = useTranslations('t1-prefix'); + const t2 = useTranslations('t2-prefix'); + + return ( +
+ {t1.raw("key")} + {t2.raw("key")} +
+ ); +};